考虑这个“Hello world”x86 程序:
bits 64
global main
extern puts
section .text
main:
push rbp
mov rdi, msg
call puts
pop rbp
ret
section .data
msg:db "Hello world!", 10, 0
我理解
push rbp
指令的目的是将外部子程序puts
的堆栈指针与16字节边界对齐。执行此指令后,rsp
将错位 8 个字节,因为 rbp
的大小为 8 个字节。但call
指令会将返回地址压入堆栈,因此rsp
将再次减8。然后在执行puts
子例程中的第一条指令之前,堆栈将被对齐。
还有其他方法可以对齐堆栈指针。例如,this相关问题的最佳答案表明,当我不知道堆栈指针是否对齐时,我可以这样做
and rsp,-16
。上面的程序可能会变成
bits 64
global main
extern puts
section .text
main:
push rbp
mov rbp, rsp
and rsp,-16 ; after this instruction, expect rsp to be divisible by 16 (0b10000)
mov rdi, msg
call puts
mov rsp, rbp ; restore the old stack pointer
pop rbp
ret
section .data
msg:db "Hello world!", 10, 0
令我困惑的是,如果我在
and rsp,-16
指令之前执行call
,然后在call
之后隐式将返回地址压入堆栈,那么在执行puts
中的第一条指令时,堆栈不会错位8个字节吗?子程序?也许我误解了如何正确使用and rsp,-16
。
好吧,我想我明白了我的误解。在 System V ABI 调用约定中,要求在执行
rsp % 16 == 0
指令之前堆栈指针必须是 16 字节对齐(即 call
)。
在我编写的第一个 hello world 示例中,在进入
main
函数时,堆栈指针未对齐 8。呃,为什么?那么,在 Linux 中,程序的真正入口点是 _start
而不是 main
。堆栈指针在 _start
的开头是 16 字节对齐的,但在程序控制从 _start
传递到 main
后,堆栈指针错位了 8 个字节。所以这就是为什么我们需要使用 push rbp
来对齐 puts
子例程的堆栈指针。