【问题标题】:System V ABI - AMD64 - Stack alignment in GCC-emitted assemblySystem V ABI - AMD64 - GCC 发射程序集中的堆栈对齐
【发布时间】:2020-11-01 01:20:00
【问题描述】:

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

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

  • 就在call foo之前,堆栈是16字节对齐的;
  • call foo 在堆栈上放置了一个 8 字节的返回地址,因此堆栈未对齐;
  • 但是pushq %rbpfoo 的开始处又在堆栈上放置了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);,则堆栈将正确对齐。
  • 此外,如果您更改 bar,使其确实需要对齐,例如因为它自己调用另一个函数,编译器也会注意到这一点。
  • 我对 -O0 进行的优化感到好奇。显然,它是 ipa 堆栈对齐的一个功能,这是 GCC 中的默认设置。您可以在 GCC 版本 >= 9.0 中使用 -fipa-stack-alignment-fno-ipa-stack-alignment 打开/关闭它。输出与 GCC 中选项 on/off 的比较:godbolt.org/z/a1YdjG
  • 函数是否可以从外部调用(“上面”)在这里并不真正相关。对齐要求保护 below 当前的函数,并且由于gcc 可以看到 foo 下面的所有函数都没有对齐要求,因此它认为没有必要。

标签: assembly stack x86-64 memory-alignment calling-convention


【解决方案1】:

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总是指向最新分配的堆栈帧的末尾。

在以任何使观察到的行为兼容的方式解释这一点时似乎几乎没有回旋余地,即使已知在这种情况下不会导致问题。

是否有人认为重要到足以担心超出此答案的范围,我对此不做判断:-)

【讨论】:

  • 这种跨过程优化至少可以追溯到 gcc4.1。 godbolt.org/z/66TEne-fno-unit-at-a-time 不会禁用它。 (我的 Godbolt 链接使用 register int args 和 -fomit-frame-pointer-O0 获得更多更简单的asm,并且还可以在-O1-fno-inline 上工作。此外,需要16 字节对齐bar() 内部的操作使整个调用者链都遵循 ABI,例如 volatile __m128 v = _mm_setzero_ps();
  • godbolt.org/z/Y8ETTa 表明像 char arr[24] 这样的本地数组不会导致调用者对齐。 x86-64 SysV ABI 指定全局 和本地 数组如果它们的大小 >=16 或变量,则按 16 对齐。但本案似乎违反了这一点。当然,ABI 应该真正远离函数内部,并且由于某种原因数组最终对齐,所以在asm 语句中的movaps 没有错误...我可以使用 gcc10 在本地复制。 1.0 在 Arch Linux 上使用 -fno-stack-protector,否则它与 sub $8, %rsp 对齐。 (我以后可能会写一个答案。)
  • 好的,是的,终于用-O0godbolt.org/z/3MPv7G 的代码和选项在本地重现了一个错误,因为如果直接从main 调用,GCC 选择的-32(%rsp) 恰好可以工作,但不是通过 foo。无论如何,这并不能完全算作违反 ABI; GCC 完全明智地忽略 ABI 中那个奇怪且侵入性的 align-local-arrays 点。而且我不得不使用 inline-asm 来隐藏 GCC 的对齐要求。阵列上的_Alignas(16) 解决了这个问题。
  • 制作“私有”函数是编译器已经允许做的事情,而且程序间优化也是众所周知的事情。稍微优化调用约定是一个巧妙的技巧,重要的是从另一个编译单元对这些函数中的任何一个的调用完全尊重 x86-64 SysV ABI。从这个意义上说,GCC 仍在“使用”ABI,而不是例如在 RAX 中传递第一个参数,所以 bar: ret 会起作用。这看起来像是假设规则的应用:外部观察者(合法)看不到的东西不会伤害他们。
猜你喜欢
  • 2012-09-18
  • 2021-10-14
  • 1970-01-01
  • 2013-10-08
  • 2017-01-04
  • 1970-01-01
  • 1970-01-01
  • 2011-02-15
相关资源
最近更新 更多