今天和 20 年前的内存对齐
Memory alignment today and 20 years ago
在著名论文"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 ,%esp
作者解释说,由于计算机以字长的倍数寻址内存,因此编译器在堆栈上保留了 20 个字节(8 个字节用于 buffer1,12 个字节用于 buffer2)。
我尝试重新创建此示例并得到以下结果
pushl %ebp
movl %esp, %ebp
subl , %esp
不一样的结果!我尝试了缓冲区 1 和缓冲区 2 的各种大小组合,现代 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
我没有尝试过您报告的特定版本的编译器或分发版本。我的猜测是 16 是来自堆栈上的字节对齐要求(即所有堆栈调整将是 x 字节对齐的,并且 x 可能是 16 用于您的调用)。
请注意,您似乎已经开始使用的变量对齐方式与上面的略有不同,并且由 gcc 中变量上的对齐标记控制。尝试使用它们,您应该会看到不同之处。
内存对齐,其中堆栈对齐只是一个方面,取决于架构。它部分定义在语言的应用程序二进制接口和架构的过程调用标准(有时它都在一个规范中)(CPU,它甚至可能因平台而异)并且还取决于compiler/toolchain 以前的文件留有变化的空间。
前两个文件(名称可能不同)主要是函数之间的对外接口;他们可能会将内部结构留给工具链。然而,这必须与架构相匹配。通常硬件需要最小对齐,但出于性能原因允许更大的对齐(例如:字节对齐最小值,但这需要多个总线周期来读取 32 位字,因此编译器使用 32 位对齐)。
通常,编译器(在 PCS 之后)使用最适合体系结构的对齐方式,并在优化设置的控制下(针对速度或大小进行优化)。它不仅考虑对象的大小(与其自然边界对齐),还考虑内部总线的大小(例如,32 位 x86 具有内部 64 或 128 位总线,ARM CPUs 具有内部 32 到128(可能更宽)位总线)、缓存等。对于局部变量,它还可能考虑访问模式,因此两个相邻变量可以并行加载到寄存器对中,而不是两个单独的加载,甚至重新排序变量。
例如,堆栈指针可能需要更高的对齐,因此 CPU 可以同时将两个寄存器压入中断帧,压入需要更高对齐的向量寄存器等。您可以写一本厚书关于这个主题(我敢打赌,已经有人了)。
所以,一般来说,不存在一个一刀切的规则。但是,对于结构和数组打包,C 标准 确实 为 packing/alignment 定义了一些规则,主要是为了保证一致性,例如sizeof(type) 和数组中的地址(正确 malloc()
所必需)。
甚至 char 数组也可能会对齐以获得最佳缓存布局。请注意,不仅 CPU 可能有缓存,还有 PCIe 桥,更不用说 PCIe 将自己向下传输到 DRAM 页面。
发生变化的是 SSE, which requires 16 byte alignment, this is covered in this older gcc document for -mpreferred-stack-boundary=num,它说(强调我的):
On Pentium and PentiumPro, double and long double values should be aligned to an 8 byte boundary (see -malign-double) or suffer significant run time performance penalties. On Pentium III, the Streaming SIMD Extension (SSE) data type __m128 suffers similar penalties if it is not 16 byte aligned.
这也得到了论文 Smashing The Modern Stack For Fun And Profit 的支持,该论文涵盖了另一个打破 Smashing the Stack for Fun and Profit.
的现代变化
在著名论文"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 ,%esp
作者解释说,由于计算机以字长的倍数寻址内存,因此编译器在堆栈上保留了 20 个字节(8 个字节用于 buffer1,12 个字节用于 buffer2)。
我尝试重新创建此示例并得到以下结果
pushl %ebp
movl %esp, %ebp
subl , %esp
不一样的结果!我尝试了缓冲区 1 和缓冲区 2 的各种大小组合,现代 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
我没有尝试过您报告的特定版本的编译器或分发版本。我的猜测是 16 是来自堆栈上的字节对齐要求(即所有堆栈调整将是 x 字节对齐的,并且 x 可能是 16 用于您的调用)。
请注意,您似乎已经开始使用的变量对齐方式与上面的略有不同,并且由 gcc 中变量上的对齐标记控制。尝试使用它们,您应该会看到不同之处。
内存对齐,其中堆栈对齐只是一个方面,取决于架构。它部分定义在语言的应用程序二进制接口和架构的过程调用标准(有时它都在一个规范中)(CPU,它甚至可能因平台而异)并且还取决于compiler/toolchain 以前的文件留有变化的空间。
前两个文件(名称可能不同)主要是函数之间的对外接口;他们可能会将内部结构留给工具链。然而,这必须与架构相匹配。通常硬件需要最小对齐,但出于性能原因允许更大的对齐(例如:字节对齐最小值,但这需要多个总线周期来读取 32 位字,因此编译器使用 32 位对齐)。
通常,编译器(在 PCS 之后)使用最适合体系结构的对齐方式,并在优化设置的控制下(针对速度或大小进行优化)。它不仅考虑对象的大小(与其自然边界对齐),还考虑内部总线的大小(例如,32 位 x86 具有内部 64 或 128 位总线,ARM CPUs 具有内部 32 到128(可能更宽)位总线)、缓存等。对于局部变量,它还可能考虑访问模式,因此两个相邻变量可以并行加载到寄存器对中,而不是两个单独的加载,甚至重新排序变量。
例如,堆栈指针可能需要更高的对齐,因此 CPU 可以同时将两个寄存器压入中断帧,压入需要更高对齐的向量寄存器等。您可以写一本厚书关于这个主题(我敢打赌,已经有人了)。
所以,一般来说,不存在一个一刀切的规则。但是,对于结构和数组打包,C 标准 确实 为 packing/alignment 定义了一些规则,主要是为了保证一致性,例如sizeof(type) 和数组中的地址(正确 malloc()
所必需)。
甚至 char 数组也可能会对齐以获得最佳缓存布局。请注意,不仅 CPU 可能有缓存,还有 PCIe 桥,更不用说 PCIe 将自己向下传输到 DRAM 页面。
发生变化的是 SSE, which requires 16 byte alignment, this is covered in this older gcc document for -mpreferred-stack-boundary=num,它说(强调我的):
On Pentium and PentiumPro, double and long double values should be aligned to an 8 byte boundary (see -malign-double) or suffer significant run time performance penalties. On Pentium III, the Streaming SIMD Extension (SSE) data type __m128 suffers similar penalties if it is not 16 byte aligned.
这也得到了论文 Smashing The Modern Stack For Fun And Profit 的支持,该论文涵盖了另一个打破 Smashing the Stack for Fun and Profit.
的现代变化