今天和20年前的记忆对齐

时间:2015-05-13 18:03:47

标签: c gcc assembly x86

在着名论文" 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

3 个答案:

答案 0 :(得分:7)

更改的内容是SSE,需要16字节对齐,这个-mpreferred-stack-boundary=num的旧gcc文档中介绍了这一点(强调我的):

  

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

这也得到了论文Smashing The Modern Stack For Fun And Profit的支持,该论文涵盖了其他一些现代化的变化,这些变化打破了粉碎堆栈以获得乐趣和利润

答案 1 :(得分:2)

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

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

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

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

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

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

答案 2 :(得分:0)

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

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