我正在使用 Compiler Explorer,注意到 GCC 和 Clang 在编译这个简单函数时会发出看似不必要的与堆栈相关的指令(Compiler Explorer)。
void bar(void);
int foo(void) {
bar();
return 42;
}
这是编译的结果(也可以通过上面的链接在编译器资源管理器中看到)。
-mabi=sysv
对输出程序集没有影响,但我想排除 ABI 作为奇怪程序集的原因。
// Expected output:
foo:
call bar
mov eax, 42
ret
// gcc -O3 -mabi=sysv
// Why is it reserving unused space in the stack frame?
foo:
sub rsp, 8
call bar
mov eax, 42
add rsp, 8
ret
// clang -O3 -mabi=sysv
// Why is it preserving a scratch register then moving it to another unused scratch register?
foo:
push rax
call bar@PLT
mov eax, 42
pop rcx
ret
我发现这特别奇怪,因为对于 GCC 和 Clang 等主要编译器来说,在使用已知的 ABI 时,这似乎是一个特别容易执行的优化。
我有一些理论,但我希望得到一些澄清。
bar
递归调用 foo
时出现无限循环?通过在每次调用时消耗少量的堆栈空间,我们确保程序在耗尽堆栈空间时最终会出现段错误。也许 clang 正在做同样的事情,但它使用 push
和 pop
在某些情况下允许更好的管道?如果是这种情况,我可以使用任何 CLI 参数来禁用此行为吗?然而,这似乎不是问题,因为 call
在 x86-64 上无论如何都会将 rip
推送到堆栈。对齐。
call
指令将8个字节压入堆栈(返回地址)。因此,优化后的函数又调整了 8 个字节,以确保堆栈指针是 16 字节对齐的。
我相信这是 ABI 的要求,以确保 128 位 SSE 寄存器值可以溢出到自然对齐的地址,这对于避免性能下降或故障非常重要,具体取决于 CPU 配置。
clang 和 gcc 的情况实际上是相同的 - 你并不真正关心写入堆栈槽的内容,或者更新了哪个易失性寄存器,只关心堆栈指针被调整。