堆栈分配、填充和对齐

50

我一直在努力深入理解编译器如何生成机器代码,特别是GCC如何处理堆栈。为此,我编写了一些简单的C程序,将它们编译成汇编语言,并尽力理解输出结果。以下是一个简单的程序及其生成的输出:

asmtest.c:

void main() {
    char buffer[5];
}

asmtest.s:

pushl   %ebp
movl    %esp, %ebp
subl    $24, %esp
leave
ret

对我来说令人困惑的是,为什么要为堆栈分配24字节。我知道由于处理器寻址内存的方式,堆栈必须按4字节递增分配,但如果是这种情况,我们只应该将堆栈指针移动8字节,而不是24字节。参考一下,17字节的缓冲区会导致堆栈指针移动40字节,而没有缓冲区会导致堆栈指针移动8字节。大小在1到16字节之间的缓冲区会导致ESP移动24字节。

现在假设8字节是必要的常量(它需要用来做什么?),这意味着我们以16字节的块进行分配。为什么编译器要以这种方式对齐呢?我正在使用x86_64处理器,但即使是64位的字也只需要8字节对齐。为什么会有这种差异?

参考资料:我在运行Mac OS 10.5上编译此代码,并使用gcc 4.0.1且未启用任何优化选项。


1
相关:为什么System V / AMD64 ABI要求16字节的堆栈对齐?,这个理由同样适用于i386 SysV ABI,以及gcc的-mprefered-stack-boundary默认设置,即使在i386 SysV ABI正式更改要求/保证之前,32位代码的默认设置也是16字节。 - Peter Cordes
很奇怪,我已经尝试了相同的代码,使用-mpreferred-stack-boundary = 4,但只有从esp减去16。 - Ta Thanh Dinh
相关:为什么GCC在堆栈上分配比对齐所需的空间更多的空间? - sub $8, %esp 应该重新对齐堆栈,并使这8个字节可用于数组。额外的16是GCC未优化的结果。 - Peter Cordes
6个回答

53
这是GCC的一个特性,由-mpreferred-stack-boundary=n控制,编译器尝试将栈上的项目对齐到2^n。如果您将n更改为2,则它只会在栈上分配8个字节。默认值为n4,即它将尝试对齐到16字节边界。
为什么存在“默认”的8字节,然后是24=8 + 16字节,因为栈已经包含8字节的leaveret,所以编译后的代码必须首先通过8字节来调整栈以使其对齐到2^4=16。

1
"push %ebp" 会使 esp 减少 8 字节吗?再加上 ret 的 8 字节,应该已经对齐到 16 字节了。为什么编译器需要这额外的 8 字节呢? - Joe.Z
1
哦,我明白了。这是一台32位机器。抱歉。应该是ret 4字节+ebp 4字节+对齐8字节+缓冲区16字节。 - Joe.Z
1
i386和x86-64 System V ABIs的当前版本要求16B堆栈对齐(在call指令之前),因此函数允许假设。历史上,i386 ABI仅要求4B对齐。(有关ABI文档的链接,请参见https://stackoverflow.com/tags/x86/info)。即使在叶子函数中(不调用其他函数),当GCC必须保留任何空间时,它也会保持`%esp`对齐,这就是这里正在发生的事情。 - Peter Cordes

12

SSEx指令系列要求128位打包向量对齐到16字节,否则在加载/存储时会导致分段错误。也就是说,如果您想在堆栈上安全地传递用于SSE的16字节向量,则需要始终保持堆栈对齐到16。 GCC默认考虑到这一点。


我可能对此事的经验有限,无法断言你的答案是否错误。但是,难道不是正是使用movupd和类似的不对齐指令来加载/存储不对齐数据吗?据我所知,当尝试在不对齐的数据上使用movapd和类似的指令时,确实可能会出现错误行为,但一般情况下数据不对齐并不是问题。 - andreee
@andreee:即使数据对齐,movups 在 Core2 及更早版本上的速度也较慢。ABI 的设计是在所有 CPU 都是这样的情况下进行的。此外,对齐允许您执行 paddd xmm0,[rsp] 而不需要单独的 movdqu 指令。请参见 为什么 System V / AMD64 ABI 规定堆栈对齐为 16 字节? - Peter Cordes

4
我发现这个网站,页面底部有一些不错的解释,可以说明为什么堆栈可能会更大。将这个概念扩展到64位机器上,可能可以解释你所看到的情况。

3

1

Mac OS X / Darwin x86 ABI 要求堆栈对齐为 16 字节。而在其他 x86 平台(如 Linux、Win32、FreeBSD 等)则不是这样。


1
实际的ABI要求是在函数调用边界处将堆栈对齐到16字节。 - Stephen Canon
2
这是正确的,但由于函数序言/尾声是堆栈指针被改变的唯一位置,因此这几乎等同于说它需要始终对齐。 - Ringding

-1

8个字节是因为第一条指令将%ebp的起始值推送到堆栈上(假设是64位)。


1
返回地址和基指针都被推入堆栈。 - dreamlax

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