以下是来自Compiler Explorer上GCC x86-64 10.2的C代码产生的汇编代码:
其中一个指令是
我的理解是:
- 在
显然,GCC生成的汇编代码保持了堆栈对齐的有效性,所以我一定漏掉了什么。
(我尝试用CLANG替换GCC,CLANG生成的代码是
那么,在GCC生成的汇编代码中我漏掉了什么?它是如何保持堆栈16字节对齐的呢?
其中一个指令是
subq $40, %rsp
。问题是,从%rsp
减去40个字节为什么不会使堆栈错位?我的理解是:
- 在
call foo
之前,堆栈的大小为16字节对齐;
- call foo
在堆栈中放置了一个8字节的返回地址,因此堆栈的大小就不再是16字节对齐了;
- 但是,在foo
的开头执行pushq %rbp
可以再次在堆栈上添加另外8个字节,以使其重新16字节对齐;
- 因此,在subq $40, %rsp
之前,堆栈已经恢复到16字节对齐状态;因此,将%rsp
减少40个字节必须会破坏对齐性吧?显然,GCC生成的汇编代码保持了堆栈对齐的有效性,所以我一定漏掉了什么。
(我尝试用CLANG替换GCC,CLANG生成的代码是
subq $48, %rsp
——正如我直觉所期望的那样。)那么,在GCC生成的汇编代码中我漏掉了什么?它是如何保持堆栈16字节对齐的呢?
int bar(int i) { return i; }
int foo(int p0, int p1, int p2, int p3, int p4, int p5, int p6) {
int sum = p0 + p1 + p2 + p3 + p4 + p5 + p6;
return bar(sum);
}
int main() {
return foo(0, 1, 2, 3, 4, 5, 6);
}
bar:
pushq %rbp
movq %rsp, %rbp
movl %edi, -4(%rbp)
movl -4(%rbp), %eax
popq %rbp
ret
foo:
pushq %rbp
movq %rsp, %rbp
subq $40, %rsp
movl %edi, -20(%rbp)
movl %esi, -24(%rbp)
movl %edx, -28(%rbp)
movl %ecx, -32(%rbp)
movl %r8d, -36(%rbp)
movl %r9d, -40(%rbp)
movl -20(%rbp), %edx
movl -24(%rbp), %eax
addl %eax, %edx
movl -28(%rbp), %eax
addl %eax, %edx
movl -32(%rbp), %eax
addl %eax, %edx
movl -36(%rbp), %eax
addl %eax, %edx
movl -40(%rbp), %eax
addl %eax, %edx
movl 16(%rbp), %eax
addl %edx, %eax
movl %eax, -4(%rbp)
movl -4(%rbp), %eax
movl %eax, %edi
call bar
leave
ret
main:
pushq %rbp
movq %rsp, %rbp
pushq $6
movl $5, %r9d
movl $4, %r8d
movl $3, %ecx
movl $2, %edx
movl $1, %esi
movl $0, %edi
call foo
addq $8, %rsp
leave
ret
bar
不需要栈对齐,所以它没有费心。如果你将其改为extern int bar(int i);
,那么栈将会被正确对齐。 - Jesterbar
以便它需要对齐,例如因为它本身调用另一个函数,编译器也会注意到这一点。 - Jester-O0
级别进行的这种优化很好奇。显然,这是GCC中默认的ipa堆栈对齐功能。您可以使用GCC版本>= 9.0中的-fipa-stack-alignment
和-fno-ipa-stack-alignment
打开/关闭它。在GCC中启用/禁用选项的输出比较:https://godbolt.org/z/a1YdjG - Michael Petchgcc
可以看到foo下面的所有函数都没有对齐要求,因此认为它是不必要的。 - paxdiablo