对于下面的C代码,编译器资源管理器中的GCC x86-64 10.2会发出我在下面粘贴的程序集。
一条指令是subq $40, %rsp
。问题是,为什么从%rsp
中减去40个字节不会使堆栈错位?我的理解是:
- 就在
call foo
之前,堆栈对齐了16个字节 call foo
在堆栈上放置了一个8字节的返回地址,因此堆栈会错位- 但是
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
16字节对齐的目的是使在当前以下的任何级别调用的函数,如果它们需要对齐的局部变量,就不必担心对齐它们的堆栈。
如果没有ABI保证,每个需要它的函数都必须and
具有某个值的堆栈指针,以确保它正确对齐,比如:
and %rsp, $0xfffffffffffffff0
然而,在这种特殊情况下,之所以有必要这样做,是因为bar()
函数是叶函数,这意味着编译器对其级别或更低级别的任何对齐要求都有充分的了解(它没有局部变量,也不调用函数,因此没有要求(。
foo()
函数也没有以下要求,因为它唯一调用的是bar()
。它似乎还决定,它是自己的本地人也不需要这种级别的对齐。
即使bar()
或foo()
是从即时翻译单元外部调用的(它们可以,因为它们没有标记为static
(,这也不会改变它们不需要对齐的事实。
例如,如果bar
在一个单独的翻译单元中,或者它调用了其他无法确定不需要对齐的函数,情况就会有所不同。
这意味着gcc
不会完全了解其对准要求。事实上,如果你在godbolt中注释掉bar
定义行(实际上隐藏了定义(,你会看到行的变化:
// int bar(int i) { return i; }
--> subq $48, %rsp ; no longer $40
顺便说一句,尽管在这种情况下,16字节对齐在技术上是不必要的,但我认为它可能会使gcc
使用System V AMD64 ABI的说法无效。ABI中似乎没有任何内容允许这种偏差,文本(PDF(指出(稍微转述一下,用我的粗体(:
输入自变量区域的末尾应在16字节边界上对齐(如果在堆栈上传递
__m256
,则为32字节边界(。换句话说,当控制转移到功能入口点时,值%rsp + 8
总是16(或32(的倍数堆栈指针%rsp
始终指向最近分配的堆栈帧的末尾。
在以任何方式解释这一点以使观察到的行为兼容方面似乎都没有什么余地,尽管在这种情况下已知不会造成问题。
是否有人认为这一点足够重要而值得担忧,超出了这个答案的范围,我对这一点没有任何判断:-(