如何使用 nasm、x86_64、linux 函数修改堆栈(使用 `ret` 关键字)?

问题描述 投票:0回答:1

TL;博士

如何在使用

ret
时修改堆栈或在使用其他东西时实现类似的效果?

世界你好,

我正在尝试为我的语言制作一个编译器, 目前一切都是内联的,它使得 某些步骤的编译速度很慢,所以今天我决定 不过,尝试使用函数来优化它 它一直出现段错误,然后我意识到

这似乎不起作用:

;; 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 全部切换到其他东西?

提前致谢

linux assembly x86-64 nasm stack-memory
1个回答
1
投票

tl;博士

请记住,

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

更多答案

虽然你可能应该正确地学习调用约定是如何工作的,但我将尝试一个答案来简单地“软化”这个想法,并为了学习的乐趣。

抽象地说,为了拥有函数,你必须有一个叫做 栈帧 的东西,否则你会很难管理局部变量并让

ret
工作。在 x86_64 上,堆栈帧几乎由一些按顺序组成。

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

只要执行保持在你的小汇编空间内,从技术上讲,你就可以自由地传递参数,但是你想要1,只要你知道像

call
ret
这样的指令如何操作堆栈。在我看来,最简单的方法是使其基于堆栈,这样您的编译器就不需要担心寄存器分配了2

为了简单起见,我建议使用类似 x86 约定但应用于 x86_64 的内容,因为您似乎使用的是 64 位代码。也就是说,调用者函数会将

push
所有参数放入堆栈(通常以相反的顺序),然后
call
被调用者函数。例如,对于 3 参数函数,您的堆栈最终会看起来像这样(请注意堆栈的顶部实际上位于底部)。

+----------------+
| argument 2     |
+----------------+
| argument 1     |
+----------------+
| argument 0     |
+----------------+
| return address |
+----------------+
| local state    |
| ...            |
+----------------+

此外,我注意到您从未真正使用过

rsp
寄存器。根据编译器的设计,从技术上讲,您可以避免这种情况。无论如何,我相信像 JVM 这样的堆栈机器完全依赖于压入和弹出。只要您的推入和弹出匹配(尤其是
call
ret
,它们充当特殊的推入和弹出),就应该没问题。


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

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

2

goto 1

© www.soinside.com 2019 - 2024. All rights reserved.