gcc 8.2+ 并不总是在调用 x86 之前对齐堆栈?

gcc 8.2+ doesn't always align the stack before a call on x86?

SysV i386 ABI 的当前 (Linux) 版本在调用前需要 16 字节堆栈对齐:

The end of the input argument area shall be aligned on a 16 (32, if __m256 is passed on stack) byte boundary. In other words, the value (%esp + 4) is always a multiple of 16 (32) when control is transferred to the function entry point.

在 GCC 8.1 上,此代码在调用 callee 之前将堆栈对齐到 16 字节边界:(Godbolt)

source # bytes
call 4
push ebp 4
sub esp, 24 24
sub esp, 4 4
push eax 4
push eax 4
push eax 4
Total 48

在 GCC 8.2 及更高版本的所有版本中,它与 4 字节边界对齐:(Godbolt)

source # bytes
call 4
push ebp 4
sub esp, 16 16
push eax 4
push eax 4
push eax 4
Total 36

如果我们 shorten or raise callee 所需的参数数量很容易验证。

改变-mprefered-stack-boundary奇怪地改变了子指令的操作数,但没有改变实际的堆栈对齐:(Godbolt)

那么,呃,是什么原因?

由于您在同一个翻译单元中提供了函数的 定义,显然 GCC 认为该函数不关心堆栈对齐并且不会太在意它.显然,即使在 -O0.

,这种基本的过程间分析/优化 (IPA) 默认情况下也是开启的

当我搜索“ipa”选项时,这个选项甚至有一个明显的名字 in the manual: -fipa-stack-alignment is on by default even at -O0. Manually turning it off with -fno-ipa-stack-alignment results in what you expected, a second sub whose value depends on the number of pushes (Godbolt), making sure ESP is aligned by 16 before a call


或者,如果您将定义更改为只是一个声明,那么生成的 asm 就是预期的,完全符合 -mpreferred-stack-boundary.

void callee(void* a, void* b) {
}

void callee(void* a, void* b);

使用-fPIC还强迫GCC不承担有关Callee的任何事情,因此它确实尊重功能插入的可能性(例如通过LD_PRELOAD)具有适当的选项。

在不为共享库编译的情况下,允许 GCC 假设它看到的任何全局函数定义都是 定义,这要归功于 ISO C 的单一定义规则。


如果您在函数定义上使用 __attribute__((noipa)),那么调用站点将不会根据定义假设任何内容。就像您重命名了定义(这样您仍然可以查看它)并且只提供了调用者使用的名称的声明。

如果您只是想停止内联,您可以改用 __attribute__((noinline,noclone)),以仍然允许调用站点像优化器简单地选择不内联时那样,但仍然可以看到这个定义。这可能是也可能不是你想要的。

另见 回复:编写其 asm 很有趣的函数,以及编译器选项。


顺便说一句,我发现将声明/定义更改为可变参数最简单,因此我可以添加或删除 args,只需更改调用方即可。即使 push 数量随着额外的 arg 发生变化,我仍然能够重现你 not 改变 sub 数量的结果,当有一个定义时,但是不仅仅是声明。

void callee(void* a, ...)  // {}   // comment out a body or not
;