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
;
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”选项时,这个选项甚至有一个明显的名字 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))
,以仍然允许调用站点像优化器简单地选择不内联时那样,但仍然可以看到这个定义。这可能是也可能不是你想要的。
另见
顺便说一句,我发现将声明/定义更改为可变参数最简单,因此我可以添加或删除 args,只需更改调用方即可。即使 push
数量随着额外的 arg 发生变化,我仍然能够重现你 not 改变 sub
数量的结果,当有一个定义时,但是不仅仅是声明。
void callee(void* a, ...) // {} // comment out a body or not
;