今天和20年前的内存对齐方式

13
在著名的论文“Smashing the Stack for Fun and Profit”中,作者使用了一个C语言函数。
void function(int a, int b, int c) {
  char buffer1[5];
  char buffer2[10];
}

并生成相应的汇编代码输出

pushl %ebp
movl %esp,%ebp
subl $20,%esp

作者解释说,由于计算机按字大小的倍数寻址内存,编译器在堆栈上保留了20个字节(8个字节用于缓冲区1,12个字节用于缓冲区2)。

我尝试重现此示例并获得以下结果:

pushl   %ebp
movl    %esp, %ebp
subl    $16, %esp

有不同的结果!我尝试了各种buffer1和buffer2大小的组合,现代gcc似乎不再将缓冲区大小填充到字大小的倍数。而是遵守-mpreferred-stack-boundary选项。

举个例子--使用论文中的算术规则,对于buffer1[5]和buffer2[13],我会得到在堆栈上保留8+16=24字节。但实际上我得到了32字节。

这篇论文相当古老,之后发生了很多事情。我想知道,究竟是什么促使了这种行为上的改变?是向64位机器的过渡吗?还是其他什么?

编辑

该代码在x86_64机器上使用gcc版本4.8.2(Ubuntu 4.8.2-19ubuntu1)进行编译,如下所示:

$ gcc -S -o example1.s example1.c -fno-stack-protector -m32


4
8加16等于24,不是20。顺便说一句,编译器似乎变得更聪明了,它的本地变量分析通过考虑了两个字符数组,由于字符数组不需要任何对齐,所以它将它们粘在一起,并使得结果的"紧凑"数组对齐。 - The Paramagnetic Croissant
4
对于char值进行对齐有意义吗? - Matti Virkkunen
2
这是在x86还是x86_64上运行的? - rmmh
我不太清楚这个问题,但我对答案很感兴趣。我想知道它是否与为解决此问题而创建的优化或事物的64位性有关。 - Frank V
我在想,如果将那个函数调用改为外部函数,以符合ABI堆栈对齐要求,是否可以使堆栈底部对齐。在叶函数中,对齐可能会被优化掉。(虽然保留一些不使用的堆栈字节通常不是问题,但呃) - Matti Virkkunen
显示剩余3条评论
3个回答

7

发生变化的是SSE, 它需要16字节对齐,这在这篇较旧的gcc文档中有所涵盖-mpreferred-stack-boundary=num,其中提到(我强调):

在奔腾和奔腾Pro上,double和long double值应该对齐到8字节边界(参见-malign-double),否则会遭受显著的运行时性能惩罚。 在奔腾III上,流媒体SIMD扩展(SSE)数据类型__m128如果未对齐到16字节,则会遭受类似的惩罚。

这也得到了论文Smashing The Modern Stack For Fun And Profit的支持,该论文涵盖了其他现代变化,这些变化破坏了Smashing the Stack for Fun and Profit的做法。


2
内存对齐,其中堆栈对齐只是其中一个方面,取决于体系结构。它在语言的应用二进制接口和体系结构(CPU,甚至可能因平台而异)的过程调用标准中部分定义(有时两者合并为单个规范),还取决于编译器/工具链,前两个文档(名称可能不同)大多用于函数之间的外部接口;他们可能会将内部结构留给工具链。但是,这必须与体系结构匹配。通常,硬件需要最小对齐方式,但允许出于性能原因使用更大的对齐方式(例如:字节对齐最小,但这将需要多个总线周期才能读取32位字,因此编译器使用32位对齐)。
通常,编译器(遵循PCS)使用最适合体系结构的对齐方式,并受到优化设置的控制(针对速度或大小进行优化)。 它不仅考虑对象的大小(对齐到其自然边界),还考虑内部总线的大小(例如,32位x86具有内部64或128位总线,ARM CPU具有内部32到128位(可能更宽)位总线),缓存等。 对于局部变量,它还可以考虑访问模式,因此可以将两个相邻的变量并行加载到寄存器对中,而不是进行两个单独的加载或重新排序这些变量。
例如,堆栈指针可能需要更高的对齐方式,以便CPU可以一次推入中断帧中的两个寄存器,推入需要更高对齐方式的向量寄存器等。您可以写一本相当厚的书来讲述这个主题(我打赌,有人已经写过了)。
因此,通常没有单一的对齐方式适用于所有情况。但是,对于结构体和数组包装,C标准确实定义了一些包装/对齐规则,主要是为了保证例如sizeof(type)和数组中的地址的一致性(对于正确的malloc()所需)。即使char数组也可能被对齐以实现最佳缓存布局。请注意,不仅CPU可能具有缓存,而且PCIe桥接器也可能具有缓存,更不用说PCIe传输本身直到DRAM页面。

0

我没有尝试过你报告的特定编译器版本或发行版。我的猜测是16来自于堆栈上的字节对齐要求(即所有堆栈调整都将是x字节对齐,而x可能是你的调用中的16)。

请注意,你似乎已经开始使用的变量对齐与上述略有不同,并且由gcc中变量上的对齐标记控制。尝试使用它们,你应该会看到一个不同之处。


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