System V ABI - AMD64 - GCC 发出的程序集中的堆栈对齐

Nik*_*Nik 4 assembly stack x86-64 calling-convention memory-alignment

对于下面的 C 代码,来自Compiler Explorer 的GCC x86-64 10.2发出我在下面进一步粘贴的程序集。

一个指令是subq $40, %rsp。问题是,为什么减去 40 个字节%rsp不会使堆栈未对齐?我的理解是:

  • 就在 之前call foo,堆栈是 16 字节对齐的;
  • call foo 在堆栈上放置一个 8 字节的返回地址,因此堆栈未对齐;
  • 但是pushq %rbpatfoo的 start 在堆栈上又放置了 8 个字节,因此它再次对齐了 16 个字节;
  • 所以堆栈是 16 个字节对齐的subq $40, %rsp。结果,减少%rsp40个字节一定会破坏对齐吗?

显然,就保持堆栈对齐而言,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);
}
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)

pax*_*blo 5

16 字节对齐的目的是使在当前级别以下的任何级别调用的函数如果需要对齐的局部变量,则不必担心对齐它们的堆栈。

如果没有 ABI 保证,每个需要它的函数都必须and使用一些值指向堆栈指针,以确保它正确对齐,例如:

and %rsp, $0xfffffffffffffff0
Run Code Online (Sandbox Code Playgroud)

但是,在这种特殊情况下没有理由这样做 - 该bar()函数是叶函数,这意味着编译器完全了解其级别或以下级别的任何对齐要求(它没有局部变量,并且不调用任何函数,因此无要求)。

foo()函数在下面也没有要求,因为它唯一调用的是bar(). 它还似乎决定它自己的当地人也不需要这种级别的对齐。

即使bar()foo()是从直接翻译单元外部调用的(它们可以,因为它们没有被标记static),这不会改变不需要对齐的事实。

例如,如果bar在单独的翻译单元中,或者在无法确定不需要对齐的情况下调用其他函数,则情况会有所不同。

这意味着gcc不会完全了解其对齐要求。而且,事实上,如果您bar在 Godbolt 中注释掉定义行(有效地隐藏了定义),您将看到该行发生了变化:

// int bar(int i) { return i; }
   --> subq $48, %rsp             ; no longer $40
Run Code Online (Sandbox Code Playgroud)

顺便说一句,虽然在这种情况下16 字节对齐在技术上不是必需的,但我认为它可能会使gcc使用 System V AMD64 ABI的声明无效。该 ABI 中似乎没有任何内容允许这种偏差,文本 ( PDF ) 指出(略有解释,并带有我的粗体):

输入参数区域的末尾应在 16(或 32,如果__m256在堆栈上传递)字节边界对齐。换句话说,该值%rsp + 8始终16(或32)时控制转移到函数入口点的倍数。堆栈指针%rsp始终指向最新分配的堆栈帧的末尾。

以任何方式使观察到的行为兼容,似乎没有什么解释的余地​​,即使在这种情况下它不会引起问题。

是否有人认为这足够重要而值得担心超出了这个答案的范围,我对这一点不做任何判断:-)

  • 好的,是的,终于设法使用“-O0”在本地重现错误,代码和选项来自 https://godbolt.org/z/3MPv7G,因为 GCC 选择的“-32(%rsp)”恰好可以工作如果直接从 main 调用,而不是通过 foo 调用。无论如何,这并不完全算作违反 ABI;对于 GCC 来说,忽略 ABI 中奇怪且侵入性的align-local-arrays 点是完全明智的。我必须使用 inline-asm 来隐藏 GCC 的对齐要求。数组上的“_Alignas(16)”解决了这个问题。 (2认同)
  • 编译器已经可以创建“私有”函数,并且过程间优化也是众所周知的事情。稍微优化调用约定是一个巧妙的技巧,重要的是从另一个编译单元对任何这些函数的调用*将*完全尊重 x86-64 SysV ABI。从这个意义上说,GCC 仍然“使用”ABI,而不是在 RAX 中传递第一个参数,这样 `bar: ret` 就可以工作。这看起来像是假设规则的应用:外部观察者(合法)看不到的东西不会伤害他们。 (2认同)