什么是“堆栈对齐”?

Posted

技术标签:

【中文标题】什么是“堆栈对齐”?【英文标题】:what is "stack alignment"? 【发布时间】:2010-10-14 22:05:59 【问题描述】:

什么是堆栈对齐? 为什么使用它? 可以通过编译器设置来控制吗?

此问题的详细信息取自尝试将 ffmpeg 库与 msvc 一起使用时遇到的问题,但我真正感兴趣的是对什么是“堆栈对齐”的解释。

细节:

在运行我的 msvc 编译程序时,它链接到 avcodec,我得到了 以下错误:“编译器未对齐堆栈变量。Libavcodec 具有 被错误编译”,然后在 avcodec.dll 中崩溃。 avcodec.dll 不是用 msvc 编译的,所以我看不到里面发生了什么。 运行 ffmpeg.exe 并使用相同的 avcodec.dll 时一切正常。 ffmpeg.exe不是用msvc编译的,是用gcc/mingw编译的(和avcodec.dll一样)

谢谢,

【问题讨论】:

既然其他人已经解释了堆栈对齐是什么以及为什么使用它,我只想添加我的两分钱关于“它可以由编译器设置控制吗?”。见this question 【参考方案1】:

内存中变量的对齐方式(历史很短)。

过去计算机有一个 8 位数据总线。这意味着,每个时钟周期可以处理 8 位信息。那时候还好。

然后是 16 位计算机。由于向下兼容等问题,保留了 8 位字节,引入了 16 位字。每个字为 2 个字节。每个时钟周期可以处理 16 位信息。但这带来了一个小问题。

让我们看一个内存映射:

+----+
|0000| 
|0001|
+----+
|0002|
|0003|
+----+
|0004|
|0005|
+----+
| .. |

在每个地址都有一个可以单独访问的字节。 但是只能在偶数地址处获取单词。因此,如果我们读取 0000 处的字,我们会读取 0000 和 0001 处的字节。但如果我们想读取位置 0001 处的字,则需要两次读取访问。首先是 0000,0001,然后是 0002,0003,我们只保留 0001,0002。

当然,这需要一些额外的时间,这并不值得赞赏。所以这就是他们发明对齐的原因。所以我们在字边界存储字变量,在字节边界存储字节变量。

例如,如果我们有一个包含字节字段 (B) 和字字段 (W) 的结构(以及一个非常幼稚的编译器),我们会得到以下结果:

+----+
|0000| B
|0001| W
+----+
|0002| W
|0003|
+----+

这不好玩。但是当使用单词对齐时,我们发现:

+----+
|0000| B
|0001| -
+----+
|0002| W
|0003| W
+----+

这里为了访问速度牺牲了内存。

您可以想象,当使用双字(4 字节)或四字(8 字节)时,这一点更为重要。这就是为什么在大多数现代编译器中,您可以在编译程序时选择使用哪种对齐方式。

【讨论】:

堆栈对齐的精彩描述! 我正在努力学习组装,但我一直在努力理解对齐方式。这完全回答了我的问题! 总是乐于帮助别人:-)。 这很好地解释了为什么单词数组应该对齐。因为访问特定元素将需要两次读取。但是在带有一个字节和一个单词的示例中:如果您阅读完整的结构,那么在这两种情况下您都必须阅读这两个单词。 @ToonKrijthe " 但是单词只能在偶数地址获取。" 为什么这必须是真的,在你的例子中内存/堆栈指针不能指向 0001然后从那里开始读一个信息?【参考方案2】:

某些 CPU 架构需要对各种数据类型进行特定对齐,如果您不遵守此规则,则会引发异常。在标准模式下,x86 对基本数据类型不要求这样做,但可能会遭受性能损失(查看 www.agner.org 了解低级优化技巧)。

但是,SSE 指令集(通常用于高性能)音频/视频处理具有严格的对齐要求,如果您尝试在未对齐的数据上使用它会抛出异常(除非您在某些处理器上使用,慢得多的未对齐版本)。

您的问题是可能一个编译器期望 caller 保持堆栈对齐,而另一个期望 callee 在以下情况下对齐堆栈必要的。

编辑:至于为什么会发生异常,DLL中的一个例程可能想对一些临时堆栈数据使用SSE指令,并且由于两个不同的编译器不同意调用约定而失败.

【讨论】:

【参考方案3】:

IIRC,堆栈对齐是指将变量放置在堆栈上“对齐”到特定数量的字节。因此,如果您使用 16 位堆栈对齐,堆栈上的每个变量将从一个字节开始,该字节是函数中当前堆栈指针的 2 个字节的倍数。

这意味着如果你使用一个小于 2 字节的变量,例如一个 char(1 字节),它和下一个变量之间会有 8 位未使用的“填充”。这允许基于可变位置的假设进行某些优化。

当调用函数时,将参数传递给下一个函数的一种方法是将它们放在堆栈上(而不是将它们直接放在寄存器中)。这里是否使用对齐很重要,因为调用函数将变量放在堆栈上,以便调用函数使用偏移量读取。如果调用函数对齐变量,而被调用函数期望它们不对齐,那么被调用函数将无法找到它们。

似乎 msvc 编译的代码在变量对齐方面存在分歧。尝试在关闭所有优化的情况下进行编译。

【讨论】:

sizeof(char) 始终为 1 字节,始终至少为 8 位......不是字节。对齐方式取决于编译器平台,并且(无论如何,x86)对于 32 位架构通常是 4 字节,对于 64 位架构通常是 8 字节。 谢谢,在字节大小上确实是个脑残:P。我选择了 16 个字节作为任意示例,但使用较小的示例会更清楚。 不,堆栈对齐是关于保持堆栈指针本身的对齐。堆栈上的单字节局部变量可以位于任何地址。如果只有一个,那么在下一个变量之前会有填充,因为大多数 ABI 会将 int 等基本类型与它们自己的宽度对齐(自然对齐)。只有在堆栈上传递参数时,才会将单字节对象填充到“堆栈宽度”或槽(单个 push 指令的大小)。【参考方案4】:

据我所知,编译器通常不会对齐堆栈上的变量。该库可能取决于您的编译器不支持的某些编译器选项集。正常的解决方法是将需要对齐的变量声明为静态的,但如果你在其他人的代码中这样做,你会想要确保它们有问题的变量稍后在函数中初始化,而不是在声明。

// Some compilers won't align this as it's on the stack...
int __declspec(align(32)) needsToBe32Aligned = 0;
// Change to
static int __declspec(align(32)) needsToBe32Aligned;
needsToBe32Aligned = 0;

或者,找到一个编译器开关来对齐堆栈上的变量。显然我在这里使用的“__declspec”对齐语法可能不是你的编译器使用的。

【讨论】:

编译器将堆栈上的变量与 ABI 中指定的该类型的对齐保证/要求对齐。通常这意味着自然对齐:对齐 = 宽度,因此 4 字节 int 得到 4 字节对齐。为堆栈指针本身保持 16 字节对齐,可以将堆栈上的变量对齐 16、8、4 或 2,而无需额外成本。

以上是关于什么是“堆栈对齐”?的主要内容,如果未能解决你的问题,请参考以下文章

GCC - 如何重新对齐堆栈?

在颤动中与堆栈对齐

正确调用 printf 的堆栈对齐?

gcc x86-32堆栈对齐并调用printf

如何将堆栈对齐到 SRAM 的末尾?

组装 - 堆对齐 - x86 intel