编写函数 'print_string' 接受一个指向空终止字符串的指针并将其打印到标准输出

Write function 'print_string' which accepts a pointer to a null-terminated string and prints it to stdout

我目前正在翻看 Zhirkov 的书,"Low Level Programming" 为了自学。

我卡在了第二章的结尾,作业。我已经编写了第一个函数 string_length,它接受一个指向字符串的指针和 returns 它的长度。我还创建了一个测试 print_str 函数,它打印出预定义的字节字符串。

我不知道怎么写print_string作者定义的: print_string: accepts a pointer to a null-termianted string and prints it to stdout

section .data
string: db "abcdef", 0

string_str:
    xor rax, rax
    mov rax, 1        ; 'write' syscall
    mov rdi, 1        ; stdout
    mov rsi, string   ; address of string
    mov rdx, 7        ; string length in bytes
    syscall
  ret

string_length:
    xor rax, rax
    .loop
        cmp byte [rdi+rax], 0    ; check if current symbol is null-terminated
        je  .end                 ; jump if null-terminated
        inc rax                  ; else go to next symbol and increment
        jmp .loop
    .end
        ret                      ; rax holds return value

section .text

_start:
    mov rdi, string   ; copy address of string
    call print_str

    mov rax, 60
    xor rdi, rdi
    syscall

到目前为止,我有:

print_string:
    push rdi           ; rdi is the argument for string_length
    call string_length ; call string_length with rdi as arg
    mov rdi, rax       ; rax holds return value from string_legnth
    mov rax, 1         ; 'write' syscall

这是我更新的 print_string 函数,它可以工作。有点。它将字符串打印到标准输出,但后来我遇到了:illegal hardware instruction

print_string:
    push rdi               ; rdi is the first argument for string_length
    call string_length
    mov rdx, rax           ; 'write' wants the byte length in rdx, which is
                           ; returned by string_length in rax
    mov rax, 1             ; 'write'
    mov rdi, 1             ; 'stdout'
    mov rsi, string        ; address of original string. I SUSPECT ERROR HERE
    syscall
  ret

我假设你的这个解决方案的最新版本是:

print_string:
    push rdi               ; rdi is the first argument for string_length
    call string_length
    mov rdx, rax           ; 'write' wants the byte length in rdx, which is
                           ; returned by string_length in rax
    mov rax, 1             ; 'write'
    mov rdi, 1             ; 'stdout'
    mov rsi, string        ; address of original string. I SUSPECT ERROR HERE
    syscall
  ret
  1. 为什么需要push rdi

通过调用约定 [1] 函数在 rdirsi 等中接受它们的参数。它还保证某些寄存器(rbprbx, r11-r15) 如果你调用另一个函数然后 return 将不会改变。其他寄存器可以改,rdi也可以。

push rdi的目的是保存rdi以备后用,因为string_length可以根据需要重写其值。拿走它,string_length 仍然有效,但您可能会永远丢失字符串起始地址。

因此,该指令与将参数传递给 string_length.

无关

函数很少从堆栈中获取参数。它发生在例如当有超过 6 个 integer/pointer 个参数时,或者参数很大(例如 256 字节 宽)。

  1. 为什么它与 pop rsi
  2. 一起工作

让我们这样改变解决方案:

`

print_string:
        push rdi               ; !!! save rdi to stack
        call string_length
        mov rdx, rax           ; 'write' wants the byte length in rdx, which is
                               ; returned by string_length in rax
        mov rax, 1             ; 'write'
        mov rdi, 1             ; 'stdout'      
        pop rsi                ; !!! what was saved in stack is moved into rsi
        syscall
      ret

我们刚刚恢复了一个保存的字符串地址并将其写入rsi。这是一件好事,因为 write 系统调用期望 rsi 准确保存它。

  1. 为什么你的解决方案没有 pop rsi 就崩溃了?

为了理解它,让我们修改一下 callret 的工作方式。

调用print_string时,call print_string后面紧跟的指令地址被放到栈顶。这个地址叫做return地址.

另一方面,ret 将堆栈顶部的值弹出到 rip,允许我们从保存的点继续执行。

因此,将堆栈指针恢复到"vanilla"状态非常重要,以便在执行ret时,将return地址放入rip

在包含一个 push 和零 pop 的示例解决方案中,当执行 print_string 中的 ret 时,堆栈将保存这些值:

|                  ...                           |
|        ^ stack grows this way ^                |
|                  ...                           | 
| string starting address, saved from rdi.       | <- rsp
| return address, to the caller of print_string. |
|                  ...                           |

执行ret时,将push rdi保存的字符串起始地址移入rip,CPU从该地址开始执行指令。显然,这对我们没有好处。添加pop rsi后,执行ret时,堆栈中没有存储额外的信息,所以执行应该进行。

您当然可以手动操作 rsp,例如设置和恢复栈帧以及使用 rbp 恢复 ret 之前的栈基。您将在第 14 章中看到很多这方面的工作。


请注意,关于您的特定版本未覆盖 rdi 的论点可能听起来很有说服力。但调用约定的目的是让程序员可以自由更改任何函数,并确保它不会干扰调用者关于哪些寄存器可以更改哪些不能更改的假设。所以是的,在特定情况下这是可行的,但即便如此,它也使您无法自由更改 string_length 实现。

[^1]:程序员和编译器编写者之间的明确协议,描述了在哪里传递参数,哪些寄存器可以被破坏等。在本书中,使用 GNU/Linux 原生的调用约定。在 System V Application Binary Interface

中有完整描述