编写函数 '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
- 为什么需要
push rdi
?
通过调用约定 [1] 函数在 rdi
、rsi
等中接受它们的参数。它还保证某些寄存器(rbp
、rbx
, r11
-r15
) 如果你调用另一个函数然后 return 将不会改变。其他寄存器可以改,rdi
也可以。
push rdi
的目的是保存rdi
以备后用,因为string_length
可以根据需要重写其值。拿走它,string_length
仍然有效,但您可能会永远丢失字符串起始地址。
因此,该指令与将参数传递给 string_length
.
无关
函数很少从堆栈中获取参数。它发生在例如当有超过 6 个 integer/pointer 个参数时,或者参数很大(例如 256 字节 宽)。
- 为什么它与
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
后面紧跟的指令地址被放到栈顶。这个地址叫做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
中有完整描述
我目前正在翻看 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
- 为什么需要
push rdi
?
通过调用约定 [1] 函数在 rdi
、rsi
等中接受它们的参数。它还保证某些寄存器(rbp
、rbx
, r11
-r15
) 如果你调用另一个函数然后 return 将不会改变。其他寄存器可以改,rdi
也可以。
push rdi
的目的是保存rdi
以备后用,因为string_length
可以根据需要重写其值。拿走它,string_length
仍然有效,但您可能会永远丢失字符串起始地址。
因此,该指令与将参数传递给 string_length
.
函数很少从堆栈中获取参数。它发生在例如当有超过 6 个 integer/pointer 个参数时,或者参数很大(例如 256 字节 宽)。
- 为什么它与
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
后面紧跟的指令地址被放到栈顶。这个地址叫做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
中有完整描述