如何使用 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

虽然所有内联都有效:

;; 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!

所以现在我很困惑,因为我是新手 在集会上做什么,即使我发现相关

等解决方案

我仍然很困惑如何接受它... 有什么办法可以做到,还是我坚持使用内联?我是否应该将所有汇编器从 nasm 切换到其他东西?

提前致谢

tl;博士

请记住,call 在技术上 push rip,而 ret 在技术上 pop rip,所以你在你的例子中几乎搞砸了你的堆栈,因为你无意中将它弹出到错误的位置。

更多答案

虽然您可能应该正确了解调用约定的工作原理,但我将尝试一个答案来简要地“软化”这个想法,并且为了学习的乐趣。

抽象地说,为了拥有函数,你必须有一个叫做堆栈框架的东西,否则你将很难管理局部变量并获得ret 工作。在 x86_64 上,堆栈框架几乎由几件事按顺序组成。

  • 函数参数,如果有0
    • 如果一些参数是在寄存器中传递的,则可以省略。
  • return 地址,
    • call 指令会将其压入堆栈。
    • 您有责任确保 ret 指令将其从堆栈中弹出。
  • 可选 帧指针,
    • 如果您的堆栈按动态数量增长,这可以跟踪帧的开始。
    • 否则,如果您提前知道堆栈大小,则它是可选的。
  • 然后是堆栈上的本地状态。

只要执行保持在您的小程序集中 space,您在技术上可以自由地传递参数,但是您想要1 只要您知道指令是如何进行的像 callret 操纵堆栈。在我看来,最简单的方法是将其设置为 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 匹配(尤其是 callret,它们充当特殊的 push 和 pop),你应该没问题。


0 Windows 实际上在这里至少额外分配了 32 个字节用于参数溢出,但在这种情况下你可以忽略它。

1 有特定的 调用约定 规定参数如何从调用者传递到被调用者并返回。除了您的编程练习之外,我强烈建议您阅读它们的工作原理,以便您的编译器可以输出可以轻松调用的代码,并轻松调用编译器未发出的函数,或者像 Nate 提到的那样采用 Forth 方式。

2 goto 1