我目前正在阅读Zhirkov的书,"低级编程"为了自学。
我被困在第二章的结尾,即任务。我编写了第一个函数string_length
,它接受一个指向字符串的指针并返回它的长度。我还创建了一个测试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函数,有效。有点。它将字符串打印到stdout,但后来我遇到了: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
答案 0 :(得分:2)
我认为此解决方案的最新版本是:
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
push rdi
? 通过调用约定[1]函数接受rdi
,rsi
等中的参数。它还保证了一些寄存器(rbp
,rbx
,如果您调用另一个函数然后返回,则不会更改r11
- r15
)。其他寄存器可以更改,rdi
也可以。
push rdi
的目的是保存rdi
以供日后使用,因为string_length
可以根据需要重写其值。拿走它,string_length
仍然有效,但你可能永远丢失字符串起始地址。
因此,该指令与将参数传递给string_length
无关。
函数很少从堆栈中获取参数。它发生在例如当有超过6个整数/指针参数,或者参数很大时(例如256 bytes 宽)。
pop rsi
让我们以这种方式改变解决方案:
`
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
完全保留它。
pop rsi
的情况下崩溃? 要理解它,让我们修改call
和ret
的工作方式。
当调用print_string
时,紧跟在call print_string
之后的指令的地址放在堆栈顶部。此地址称为返回地址。
另一方面,ret
将堆栈顶部的值弹出到rip
,允许我们从保存的点继续执行。
因此,将堆栈指针恢复为" vanilla"是非常重要的。 state,以便在执行ret
时,它是放入rip
的返回地址。
在一个push
和零pop
的示例解决方案中,当ret
执行print_string
时,堆栈会保留这些值:
| ... |
| ^ 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
的争论可能听起来令人信服。但是调用约定的目的是让程序员可以自由地改变任何功能并确保它不会干扰呼叫者&#39;关于哪些寄存器可以改变以及哪些寄存器不能改变的假设。所以,是的,在特定的情况下,这是有效的,但即便如此,它也使您无法自由更改string_length
实施。
[^ 1]:程序员和编译器编写者之间的明确协议,描述了传递参数的位置,哪些寄存器可以被破坏等等。在本书中,使用了GNU / Linux原生的调用约定。它在System V Application Binary Interface
中有详细描述