GCC 将寄存器 args 放置在堆栈上,并在局部变量下方留有间隙?

GCC placing register args on the stack with a gap below local variables?

我试图查看一个非常简单的程序的汇编代码。

int func(int x) {
    int z = 1337;
    return z;
} 

使用 GCC -O0,每个 C 变量都有一个未优化的内存地址,因此 gcc 会溢出其寄存器 arg:(Godbolt, gcc5.5 -O0 -fverbose-asm)

func:
        pushq   %rbp  #
        movq    %rsp, %rbp      #,
        movl    %edi, -20(%rbp) # x, x
        movl    37, -4(%rbp) #, z
        movl    -4(%rbp), %eax  # z, D.2332
        popq    %rbp    #
        ret

函数参数 x 被放置在局部变量下方的堆栈中的原因是什么?为什么不把它放在 -4(%rbp) 和下面的地方呢?

而且当把它放在局部变量下面时,为什么不把它放在-8(%rbp)

为什么要留空隙,使用不必要的 ?这不能触及一个新的缓存行,否则不会触及此叶函数吗?

(首先,不要指望在 -O0 做出有效的决策。事实证明,如果我们使用 volatile 或其他强制编译器分配堆栈的东西 space 否则这个问题就没那么有趣了。)

What is the reason that the function parameter x gets placed on the stack below the local variables?

选择是 100% 任意的,并且取决于编译器内部结构。 GCC 和 clang 都碰巧做出了那个选择,但这基本上是无关紧要的。 args 到达寄存器并且基本上 只是局部变量,所以完全由编译器决定将它们溢出到哪里(或者根本不溢出,如果启用优化)。

But why save it further down the stack later than really necessary?

因为已知(?)GCC 优化错误导致堆栈浪费 space。 例如,Why does GCC allocate more space than necessary on the stack? 演示了 x86-64 GCC -O3 分配 24 个而不是 8 个字节的堆栈 space,其中 clang 分配 8 个字节。(我想我已经看到一个错误报告,关于有时在 GCC 需要时使用额外的 16 个字节的 space移动 RSP(不像这里它只使用红色区域)但是在 GCC bugzilla 上找不到它。)

请注意,x86-64 System V ABI 要求在 call 之前进行 16 字节堆栈对齐。在push %rbp并设置RBP为帧指针后,RBP和RSP是16字节对齐的。 -20(%rbp)-8(%rbp) 位于同一对齐的 16 字节堆栈块 space 中,因此此间隙不会冒触及我们尚未触及的新缓存行或页面的风险。 (自然对齐的内存块不能跨越任何比自身更宽的边界,并且 x86-64 缓存行总是至少 32 字节;现在总是 64 字节。)

但是,如果我们添加第二个参数 int y,这 确实 成为一个错过的优化:gcc5.5(和当前的 gcc9.2 -O0)溢出它到 -24(%rbp) 可能在新的缓存行中。


事实证明,这个错过的优化是 而不是 只是因为您使用了 -O0(快速编译,跳过大多数优化过程,make bad asm)。在 -O0 输出中查找遗漏的优化是没有意义的,除非它们仍然处于任何人关心的优化级别,特别是 -Os-O2-O3.

我们可以用使用 volatile 的代码证明它仍然使 gcc 在 -O3[=92= 为 args/locals 分配堆栈 space ] 另一种选择是将它们的地址传递给另一个函数,但是 GCC 必须保留 space 而不是仅使用 RSP 下面的红色区域。

int *volatile sink;

int func(int x, int y) {
    sink = &x;
    sink = &y;
    int z = 1337;
    sink = &z;
    return z;
}

(Godbolt, gcc9.2)

gcc9.2 -O3  (hand-edited comments)
func(int, int):
        leaq    -20(%rsp), %rax                 # &x
        movq    %rax, sink(%rip)        # tmp84, sink
        leaq    -24(%rsp), %rax                 # &y
        movq    %rax, sink(%rip)        # tmp86, sink
        leaq    -4(%rsp), %rax                  # &z
        movq    %rax, sink(%rip)        # tmp88, sink
        movl    37, %eax     #,
        ret     
sink:
        .zero   8

有趣的事实:clang -O3 在将它们的地址存储到 sink 之前溢出堆栈参数,就像它是地址的 std::atomic 释放存储,另一个线程可能会在之后加载它们的值从 sink 获取指针。但它不会为 z 执行此操作。实际上溢出 xy 只是一个错过的优化,我只能推测 clang 的内部机制的哪一部分可能是罪魁祸首。

无论如何,clang 会在 -4(%rsp) 处分配 z,在 -8 处分配 x,在 -12 处分配 y。因此,无论出于何种原因,clang 还选择将 args 的溢出槽放在局部变量下方。


相关:

  • 讨论了 GCC main 不假设 main.

    入口处的 16 字节对齐
  • 关于 GCC 为变量分配额外堆栈 space 的几个可能的重复,但主要是按照对齐的要求,而不是额外的。