对于下面的 C 代码,来自 Compiler Explorer 的 GCC x86-64 10.2发出我在下面进一步粘贴的程序集。
一条指令是 subq $40, %rsp
.问题是,如何从 %rsp
中减去 40 个字节?不会使堆栈错位?
我的理解是:
call foo
, 栈是 16 字节对齐的; call foo
在堆栈上放置一个 8 字节的返回地址,因此堆栈未对齐; pushq %rbp
在 foo
的 start 在堆栈上放置另外 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
不会完全了解其对齐要求。事实上,如果你注释掉 bar
在godbolt中定义行(有效隐藏定义),您将看到行更改:// int bar(int i) { return i; }
--> subq $48, %rsp ; no longer $40
顺便说一句,虽然在这种情况下 16 字节对齐在技术上不是必需的,但我认为这可能会使
gcc
的说法无效。使用 System V AMD64 ABI。该 ABI 中似乎没有任何内容允许这种偏差,文本 ( PDF ) 指出(略有解释,并带有我的粗体):The end of the input argument area shall be aligned on a 16 (or 32 if
__m256
is passed on stack) byte boundary. In other words, the value%rsp + 8
is always a multiple of 16 (or 32) when control is transferred to the function entry point. The stack pointer%rsp
always points to the end of the latest allocated stack frame.
以任何方式使观察到的行为兼容,似乎没有什么解释的余地,即使在这种情况下它不会引起问题。
是否有人认为这足够重要而值得担心超出了这个答案的范围,我对这一点不做任何判断:-)
关于assembly - System V ABI - AMD64 - GCC 发出的程序集中的堆栈对齐,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64627897/