我试图清楚地了解谁(调用者或被调用者)对堆栈对齐的反应。 64位汇编的情况相当清楚,它是由调用者。
参考System V AMD64 ABI,第3.2.2节堆栈帧:
输入参数区域的末尾应对齐16(32,如果 __m256在堆栈上传递。字节边界。
换句话说,应该安全地假设,对于被调用函数的每个入口点:
16 | (%rsp + 8)
成立(额外八个是因为call
隐含地在堆栈上推送返回地址。)
它在32位世界中的表现(假设为cdecl)?我注意到gcc
将对齐放置在被调用的函数中,并使用以下结构:
and esp, -16
似乎表明,这是被调用者的责任。
为了更清楚,请考虑遵循NASM代码:
global main
extern printf
extern scanf
section .rodata
s_fmt db "%d %d", 0
s_res db `%d with remainder %d\n`, 0
section .text
main:
start 0, 0
sub esp, 8
mov DWORD [ebp-4], 0 ; dividend
mov DWORD [ebp-8], 0 ; divisor
lea eax, [ebp-8]
push eax
lea eax, [ebp-4]
push eax
push s_fmt
call scanf
add esp, 12
mov eax, [ebp-4]
cdq
idiv DWORD [ebp-8]
push edx
push eax
push s_res
call printf
xor eax, eax
leave
ret
在调用scanf
之前是否需要对齐堆栈?如果是这样,那么在将这两个参数推送到%esp
之前,这需要将scanf
减少四个字节:
4 bytes (return address)
4 bytes (%ebp of previous stack frame)
8 bytes (for two variables)
12 bytes (three arguments for scanf)
= 28
答案 0 :(得分:8)
仅限GCC 在main
中执行此额外堆栈对齐;该功能很特别。如果您查看任何其他功能的代码,您将看不到它,除非您有alignas(32)
或其他地方的本地。
GCC只是采用-m32
采取防御措施,而不是假设使用正确的16B对齐堆栈调用main
。或者,当-mpreferred-stack-boundary=4
只是一个好主意而不是法律时,这种特殊待遇就会遗留下来。
多年来,i386 System V ABI保证/要求ESP + 4在进入功能时进行16B对齐。 (即,在 CALL指令之前,ESP必须是16B对齐,因此堆栈上的args从16B边界开始。这与x86-64 System V相同。)
ABI还保证新的32位进程以ESP在16B边界上对齐开始(例如在_start
,ELF入口点,其中ESP指向argc,而不是返回地址),以及glibc CRT代码保持对齐。
就调用约定而言,EBP只是另一个调用保留寄存器。但是,带有-fno-omit-frame-pointer
的编译器输出确实在其他调用保留寄存器(如EBX)之前关注push ebp
,因此保存的EBP值形成链表。 (因为在推送之后它也会设置一个帧指针的mov ebp, esp
部分。)
也许gcc是防御性的,因为一个非常古老的Linux内核(从i386 ABI修订之前,当所需的对齐只有4B时)可能违反了这个假设,并且它只是在生命中运行一次的额外几个指令 - 进程的时间(假设程序没有递归调用main
)。
与gcc不同,clang假设堆栈在进入main时正确对齐。 (clang也是assumes that narrow args have been sign or zero-extended to 32 bits,即使当前的ABI修订版没有指定行为(还).gcc和clang都发出在调用者端执行的代码,但只有clang在被调用者中依赖于它。这种情况发生在64位代码中,但我没有检查32位。)
如果你很好奇,请查看http://gcc.godbolt.org/上的编译器输出中的main和main之外的函数。
我刚刚更新了x86标记维基中的ABI链接。 http://x86-64.org/仍然死了,似乎没有回来,所以我更新了System V链接以指向HJ Lu的github repo中当前版本的PDF和his page with links。
请注意,last version on SCO's site 不当前版本,并且不包含16B-stack-alignment要求。
我认为某些BSD版本仍然不需要/维护16字节堆栈对齐。