为什么在FreeBSD中要重置堆栈指针寄存器?

4

我正在尝试掌握FreeBSD中的汇编语言。在开发者手册中关于UNIX过滤器的示例代码中,每次系统调用后都会重置寄存器esp。相关的代码如下:

%include    'system.inc'

section .data
hex db  '0123456789ABCDEF'
buffer  db  0, 0, ' '

section .text
global  _start
_start:
    ; read a byte from stdin
    push    dword 1
    push    dword buffer
    push    dword stdin
    sys.read
    add esp, byte 12        ; <--------- Is this necessary?
    or  eax, eax
    je  .done

    ; convert it to hex
    movzx   eax, byte [buffer]
    mov edx, eax
    shr dl, 4
    mov dl, [hex+edx]
    mov [buffer], dl
    and al, 0Fh
    mov al, [hex+eax]
    mov [buffer+1], al

    ; print it
    push    dword 3
    push    dword buffer
    push    dword stdout
    sys.write
    add esp, byte 12        ; <--------- Is this necessary?
    jmp short _start

.done:
    push    dword 0
    sys.exit

这与文档上一页的示例不同:

链接

 1: %include    'system.inc'
 2:
 3: section .data
 4: hello   db  'Hello, World!', 0Ah
 5: hbytes  equ $-hello
 6:
 7: section .text
 8: global  _start
 9: _start:
10: push    dword hbytes
11: push    dword hello
12: push    dword stdout
13: sys.write               ; <--------- ESP not adjusted after this. Why?
14:
15: push    dword 0
16: sys.exit

为什么这两个例子不同?为什么像add esp, byte 12这样的东西是必要的?系统调用不会弹出值吗?在64位FreeBSD中,因为参数不是通过栈传递的,这还有必要吗?我想象中栈指针会自己处理这些问题。

2
请问您能否引用您所困惑的代码,或者至少提供一个链接? - zwol
也许在阅读完https://en.wikipedia.org/wiki/X86_calling_conventions之后,您会发现需要更详细地说明您的问题。我们无法确切知道您所指的是什么。FreeBSD遵循amd64调用约定-也许您正在谈论那个? - BitTickler
3
在32位代码中,int 0x80被用于系统调用。int 0x80的调用规则需要在栈上推送参数,并且调用者需要自己恢复堆栈。如果你在调用int 0x80时在栈上推送参数,那么空间必须在之后被回收。在64位代码中,系统调用是通过syscall指令完成的。在这种情况下,系统调用参数都通过寄存器传递。在这种情况下,在执行syscall指令后无需调整_RSP_。 - Michael Petch
5
我看了一下FreeBSD文档中的前一页,发现你对他们展示的代码感到困惑的原因所在。事实上,在没有之后调整_ESP_的代码可以被认为是正确的,但它会养成一种不好的习惯。这也使他们的文档不一致。在这些简单的情况下,add esp, byte xxx并不是必需的,因为当完成时它们并没有返回到调用函数。它们正在执行sys.exit系统调用,这将终止该进程。系统退出调用后的代码将永远不会返回,因此堆栈的状态并不重要。 - Michael Petch
1
@MichaelPetch:感谢您确认。 我也认为他们应该把它添加到第一个示例中,以保持一致性。 - Rudy Velthuis
显示剩余14条评论
1个回答

3

FreeBSD使用的是调用约定,即在另一个函数调用后调用者清除堆栈。在调用之后,堆栈包含所有函数参数。通过立即调整其位置以清除函数参数来请求从函数参数中清除堆栈是实现正确堆栈维护的最简单方法,但这并不是唯一的方法。例如,您可以编写以下代码:

; print a first thing
push    dword len1
push    dword buffer1
push    dword stdout
sys.write
; here, 12 bytes in stack aren't retired yet
; print a second thing
push    dword len2
push    dword buffer2
push    dword stdout
sys.write
add esp, byte 24 ; clean stack after _both_ write() calls

这种优化实际上在许多IT技术中都有应用,例如在GCC中。
如果你从函数中返回,那么你应该在所有函数调用之后恢复堆栈位置。但是如何恢复完全取决于你,只要所有的堆栈操作都是正确的即可。
那么,在最后一个示例中(使用sys.exit),有什么特别之处呢?特殊之处在于你不会从中返回。Sys.exit不会返回,它只是停止整个进程。因此,在这里恢复堆栈位置并不重要。

或者,你(或gcc)可以使用mov将存储器存储到已经保留的空间中,而不是用push堆积更多的参数。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接