今天和20年前的记忆对齐

在着名的论文“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个字节(缓冲区1为8个字节,缓冲区2为12个字节)。

我试图重新创建这个例子并获得以下内容

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

不同的结果! 我尝试了缓冲区1和缓冲区2的各种大小组合,似乎现代的gcc不再将缓冲区大小填充到字大小的倍数。 相反,它遵循-mpreferred-stack-boundary选项。

作为一个例子 – 使用纸张的算术规则,对于buffer1 [5]和buffer2 [13],我会在堆栈上保留8 + 16 = 24个字节。 但实际上我有32个字节。

这篇论文很古老,自那以后发生了很多事情。 我想知道,究竟是什么推动了这种行为的改变? 这是向64位机器的转变吗? 或者是其他东西?

编辑

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

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

更改的是SSE ,它需要16字节对齐,这在旧的gcc文档中涵盖-mpreferred-stack-boundary = num ,其中说明( 强调我的 ):

在Pentium和PentiumPro上,double和long double值应与8字节边界对齐(请参阅-malign-double)或遭受严重的运行时性能损失。 在Pentium III上,流式SIMD扩展(SSE)数据类型__m128如果不是16字节对齐则会受到类似的惩罚。

这也是Smashing The Modern Stack For Fun和Profit的论文 ,它涵盖了其他现代化的变化,打破了Smashing the Stack for Fun and Profit

堆栈对齐的内存对齐只是一个方面取决于架构。 它部分地定义在该语言的Applicaion二进制接口和一个过程调用标准(有时它只是在一个规范中)的架构(CPU,它甚至可能因平台而异)并且还取决于编译器/工具链在哪里以前的文件留有变化的空间。

前两个文件(名称可能不同)主要用于function之间的外部接口; 他们可能会将内部结构留给工具链。 但是,必须与架构相匹配。 通常硬件需要最小的对齐,但出于性能原因允许更大的对齐(例如:字节对齐最小,但这需要多个总线周期来读取32位字,因此编译器使用32位对齐)。

通常,编译器(在PCS之后)使用对于体系结构最佳的对齐并且在优化设置的控制下(针对速度或大小进行优化)。 它不仅考虑了对象的大小(与其自然边界对齐),还考虑了内部总线的大小(例如,32位x86具有内部64或128位总线,ARM CPU具有内部32至128(可能甚至更宽)对于局部变量,它也可以考虑访问模式,因此两个相邻变量可以并行加载到寄存器对中,而不是两个单独的加载,甚至可以对这些变量进行重新排序。

例如,堆栈指针可能需要更高的对齐,因此CPU可以同时向中断帧推入两个寄存器,推送需要更高对齐的向量寄存器等。你可以写一本关于这个主题的相当厚的书(我打赌,有人已经有了)。

因此,一般来说,没有一个单一对齐适合所有规则。 但是,对于结构和数组打包,C标准确实定义了一些打包/对齐规则,主要是为了保证例如sizeof(type)和数组中的地址(正确的malloc()所需)的一致性。

甚至char数组也可以对齐以实现最佳缓存布局。 请注意,不仅CPU可能具有高速缓存,而且还有PCIe桥,更不用说PCIe将自身传输到DRAM页面。

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

请注意,您似乎已开始使用的变量对齐方式与上述方法略有不同,并且通过gcc中变量上的对齐标记进行控制。 尝试使用这些,你应该看到一个区别。