代码之家  ›  专栏  ›  技术社区  ›  Locke

GCC和Clang奇怪的不必要堆栈使用[重复]

  •  0
  • Locke  · 技术社区  · 1 年前

    我使用编译器资源管理器,注意到GCC和Clang在编译这个简单函数时会发出与堆栈相关的看似不必要的指令( Compiler Explorer ).

    void bar(void);
    
    int foo(void) {
        bar();
        return 42;
    }
    

    这是编译的结果(也可以通过上面的链接在编译器资源管理器中看到)。 -mabi=sysv 对输出程序集没有影响,但我想排除ABI是导致奇怪程序集的原因。

    // Expected output:
    foo:
            call    bar
            mov     eax, 42
            ret
    
    // gcc -O3 -mabi=sysv
    // Why is it reserving unused space in the stack frame?
    foo:
            sub     rsp, 8
            call    bar
            mov     eax, 42
            add     rsp, 8
            ret
    
    // clang -O3 -mabi=sysv
    // Why is it preserving a scratch register then moving it to another unused scratch register?
    foo:
            push    rax
            call    bar@PLT
            mov     eax, 42
            pop     rcx
            ret
    

    为什么尽管函数不使用堆栈,堆栈帧仍被修改?

    我觉得这特别奇怪,因为对于GCC和Clang这样的主要编译器来说,在使用已知的ABI时,这似乎是一个特别容易执行的优化。

    我有几个理论,但我希望得到一些澄清。

    • 也许这样做是为了防止在以下情况下出现无限循环 bar 电话 foo 递归?通过在每次调用中消耗少量的堆栈空间,我们可以确保程序在堆栈空间用完时最终会分段故障。也许clang也在做同样的事情,但它使用 push pop 以便在某些情况下实现更好的流水线?如果是这种情况,是否有任何CLI参数可以用于禁用此行为?然而,这似乎不是问题,因为 call 推动 rip 到x86-64上的堆栈。
    • 也许C或AMD64 System V ABI有一些我不知道的怪癖?
    • 也许我想得太多了,奇怪的程序集只是寄存器/堆栈优化不力的结果。也许在编译过程中的某个时刻使用了堆栈,但在使用优化后,它无法删除堆栈上的值。
    1 回复  |  直到 1 年前
        1
  •  1
  •   Jonathon Reinhart    1 年前

    对齐

    这个 call 指令将8个字节推送到堆栈(返回地址)。因此,优化后的函数再调整8个字节,以确保堆栈指针对齐16个字节。

    我认为这是ABI的一个要求,以确保128位SSE寄存器值可以溢出到自然对齐的地址,这对于避免性能命中或故障很重要,具体取决于CPU配置。和/或使得SSE指令可以用于从适当地址进行优化的块移动。

    clang和gcc的情况实际上是相同的——你并不真正关心写入堆栈插槽的内容,也不关心更新了哪个易失性寄存器,只关心调整了堆栈指针。