如何使用 nasm、x86_64、linux 函数(使用 `ret` 关键字)修改堆栈?
How can I modify the stack with nasm, x86_64, linux functions (using `ret` keyword)?
TL;DR
How can I modify the stack while using ret
or achieving similar effect while using something else?
你好,
我正在尝试为我的语言制作一个编译器,
目前一切都是内联的,它使
编译慢了一些步骤所以今天我决定
尝试使用函数优化它,虽然
它不断出现段错误,然后我意识到
这似乎不起作用:
;; main.s
BITS 64
segment .text
global _start
exit:
mov rax, 60 ;; Linux syscall number for exit
pop rdi ;; Exit code
syscall
ret
write:
mov rax, 1 ;; Linux syscall number for write
mov rdi, 1 ;; File descriptor (1 = stdout)
pop rsi ;; Pointer to string
pop rdx ;; String length
syscall
ret
_start:
mov rax, msg_len
push rax
mov rax, msg
push rax
call write
mov rax, 0
push rax
call exit
segment .data
msg: db "Hello, world!", 10
msg_len: equ $-msg
我的输出是....有问题的:
$ nasm -felf64 main.s
$ ld -o main main.s
$ ./main
PHello, world!
@ @ @$@ @+ @2 @main.sexitwritemsgmsg_len__bss_start_edata_end.symtab.strtab.shstrtab.text.data9! @ !77!'Segmentation fault
$?
(退出代码)是139
(段错误)
虽然所有内联都有效:
;; main1.s
BITS 64
segment .text
global _start
_start:
mov rax, msg_len
push rax
mov rax, msg
push rax
mov rax, 1 ;; Linux syscall number for write
mov rdi, 1 ;; File descriptor (1 = stdout)
pop rsi ;; Pointer to string
pop rdx ;; String length
syscall
mov rax, 0
push rax
mov rax, 60 ;; Linux syscall number for exit
pop rdi ;; Exit code
syscall
segment .data
msg: db "Hello, world!", 10
msg_len: equ $-msg
我的输出完全正常:
$ nasm -felf64 main1.s
$ ld -o main1 main1.o
$ ./main1
Hello, world!
$?
(退出码)是0
(在汇编中指定,表示成功)
所以现在我很困惑,因为我是新手
在集会上做什么,即使我发现相关
等解决方案
- NASM push before ret
我仍然很困惑如何接受它...
有什么办法可以做到,还是我坚持使用内联?我是否应该将所有汇编器从 nasm 切换到其他东西?
提前致谢
tl;博士
请记住,call
在技术上 是 push rip
,而 ret
在技术上 pop rip
,所以你在你的例子中几乎搞砸了你的堆栈,因为你无意中将它弹出到错误的位置。
更多答案
虽然您可能应该正确了解调用约定的工作原理,但我将尝试一个答案来简要地“软化”这个想法,并且为了学习的乐趣。
抽象地说,为了拥有函数,你必须有一个叫做堆栈框架的东西,否则你将很难管理局部变量并获得ret
工作。在 x86_64 上,堆栈框架几乎由几件事按顺序组成。
- 函数参数,如果有0,
- 如果一些参数是在寄存器中传递的,则可以省略。
- return 地址,
call
指令会将其压入堆栈。
- 您有责任确保
ret
指令将其从堆栈中弹出。
- 可选 帧指针,
- 如果您的堆栈按动态数量增长,这可以跟踪帧的开始。
- 否则,如果您提前知道堆栈大小,则它是可选的。
- 然后是堆栈上的本地状态。
只要执行保持在您的小程序集中 space,您在技术上可以自由地传递参数,但是您想要1 只要您知道指令是如何进行的像 call
和 ret
操纵堆栈。在我看来,最简单的方法是将其设置为 stack-based,这样您的编译器就不必像 2.[=26= 那样担心寄存器分配了。 ]
为简单起见,我建议使用类似于 x86 约定但适用于 x86_64 的内容,因为您似乎使用的是 64 位代码。也就是说,调用函数会将其所有参数 push
入栈(通常以相反的顺序),然后 call
被调用函数。例如,对于一个 3 参数函数,您的堆栈最终会看起来像这样(注意堆栈的顶部实际上在底部)。
+----------------+
| argument 2 |
+----------------+
| argument 1 |
+----------------+
| argument 0 |
+----------------+
| return address |
+----------------+
| local state |
| ... |
+----------------+
此外,我注意到您从未真正使用过 rsp
寄存器。根据编译器的设计,从技术上讲,您可以避免这种情况。无论如何,我相信像 JVM 这样的堆栈机器完全依赖于 push 和 pops。只要你的 push 和 pops 匹配(尤其是 call
和 ret
,它们充当特殊的 push 和 pop),你应该没问题。
0 Windows 实际上在这里至少额外分配了 32 个字节用于参数溢出,但在这种情况下你可以忽略它。
1 有特定的 调用约定 规定参数如何从调用者传递到被调用者并返回。除了您的编程练习之外,我强烈建议您阅读它们的工作原理,以便您的编译器可以输出可以轻松调用的代码,并轻松调用编译器未发出的函数,或者像 Nate 提到的那样采用 Forth 方式。
2 goto 1
TL;DR
How can I modify the stack while using
ret
or achieving similar effect while using something else?
你好,
我正在尝试为我的语言制作一个编译器, 目前一切都是内联的,它使 编译慢了一些步骤所以今天我决定 尝试使用函数优化它,虽然 它不断出现段错误,然后我意识到
这似乎不起作用:
;; main.s
BITS 64
segment .text
global _start
exit:
mov rax, 60 ;; Linux syscall number for exit
pop rdi ;; Exit code
syscall
ret
write:
mov rax, 1 ;; Linux syscall number for write
mov rdi, 1 ;; File descriptor (1 = stdout)
pop rsi ;; Pointer to string
pop rdx ;; String length
syscall
ret
_start:
mov rax, msg_len
push rax
mov rax, msg
push rax
call write
mov rax, 0
push rax
call exit
segment .data
msg: db "Hello, world!", 10
msg_len: equ $-msg
我的输出是....有问题的:
$ nasm -felf64 main.s
$ ld -o main main.s
$ ./main
PHello, world!
@ @ @$@ @+ @2 @main.sexitwritemsgmsg_len__bss_start_edata_end.symtab.strtab.shstrtab.text.data9! @ !77!'Segmentation fault
$?
(退出代码)是139
(段错误)
虽然所有内联都有效:
;; main1.s
BITS 64
segment .text
global _start
_start:
mov rax, msg_len
push rax
mov rax, msg
push rax
mov rax, 1 ;; Linux syscall number for write
mov rdi, 1 ;; File descriptor (1 = stdout)
pop rsi ;; Pointer to string
pop rdx ;; String length
syscall
mov rax, 0
push rax
mov rax, 60 ;; Linux syscall number for exit
pop rdi ;; Exit code
syscall
segment .data
msg: db "Hello, world!", 10
msg_len: equ $-msg
我的输出完全正常:
$ nasm -felf64 main1.s
$ ld -o main1 main1.o
$ ./main1
Hello, world!
$?
(退出码)是0
(在汇编中指定,表示成功)
所以现在我很困惑,因为我是新手 在集会上做什么,即使我发现相关
等解决方案- NASM push before ret
我仍然很困惑如何接受它... 有什么办法可以做到,还是我坚持使用内联?我是否应该将所有汇编器从 nasm 切换到其他东西?
提前致谢
tl;博士
请记住,call
在技术上 是 push rip
,而 ret
在技术上 pop rip
,所以你在你的例子中几乎搞砸了你的堆栈,因为你无意中将它弹出到错误的位置。
更多答案
虽然您可能应该正确了解调用约定的工作原理,但我将尝试一个答案来简要地“软化”这个想法,并且为了学习的乐趣。
抽象地说,为了拥有函数,你必须有一个叫做堆栈框架的东西,否则你将很难管理局部变量并获得ret
工作。在 x86_64 上,堆栈框架几乎由几件事按顺序组成。
- 函数参数,如果有0,
- 如果一些参数是在寄存器中传递的,则可以省略。
- return 地址,
call
指令会将其压入堆栈。- 您有责任确保
ret
指令将其从堆栈中弹出。
- 可选 帧指针,
- 如果您的堆栈按动态数量增长,这可以跟踪帧的开始。
- 否则,如果您提前知道堆栈大小,则它是可选的。
- 然后是堆栈上的本地状态。
只要执行保持在您的小程序集中 space,您在技术上可以自由地传递参数,但是您想要1 只要您知道指令是如何进行的像 call
和 ret
操纵堆栈。在我看来,最简单的方法是将其设置为 stack-based,这样您的编译器就不必像 2.[=26= 那样担心寄存器分配了。 ]
为简单起见,我建议使用类似于 x86 约定但适用于 x86_64 的内容,因为您似乎使用的是 64 位代码。也就是说,调用函数会将其所有参数 push
入栈(通常以相反的顺序),然后 call
被调用函数。例如,对于一个 3 参数函数,您的堆栈最终会看起来像这样(注意堆栈的顶部实际上在底部)。
+----------------+
| argument 2 |
+----------------+
| argument 1 |
+----------------+
| argument 0 |
+----------------+
| return address |
+----------------+
| local state |
| ... |
+----------------+
此外,我注意到您从未真正使用过 rsp
寄存器。根据编译器的设计,从技术上讲,您可以避免这种情况。无论如何,我相信像 JVM 这样的堆栈机器完全依赖于 push 和 pops。只要你的 push 和 pops 匹配(尤其是 call
和 ret
,它们充当特殊的 push 和 pop),你应该没问题。
0 Windows 实际上在这里至少额外分配了 32 个字节用于参数溢出,但在这种情况下你可以忽略它。
1 有特定的 调用约定 规定参数如何从调用者传递到被调用者并返回。除了您的编程练习之外,我强烈建议您阅读它们的工作原理,以便您的编译器可以输出可以轻松调用的代码,并轻松调用编译器未发出的函数,或者像 Nate 提到的那样采用 Forth 方式。
2 goto 1