布局堆栈变量的意义比 rbp 更接近 rsp

Significance of laying out stack variables starting nearer rsp than rbp

这个问题是关于 x86 汇编的,但我提供了一个 C 语言的例子,因为我试图检查 GCC 在做什么。

当我遵循各种汇编指南时,我注意到人们,至少是我阅读过的少数人,似乎习惯于将堆栈变量分配给更接近 rsp 而不是 rbp。

然后我检查了 GCC 会做什么,它似乎是一样的。

在下面的反汇编中,首先保留 0x10 个字节,然后调用 leaf 的结果通过 eax 到 rbp-0xc,常量值 2 到 rbp-0x8,在 rbp-0x8 和 rbp 之间留出空间用于变量"q".

我可以想象在另一个方向上做,首先在 rbp 分配一个地址,然后在 rbp-0x4,即在 rbp 到 rsp 的方向上做,然后在 rbp 之间留下一些 space -0x8 和 rsp 代表“q”。

我不确定我所观察到的是否是因为某些我更好地了解和遵守的架构限制而应该出现的情况,或者它是否纯粹是此特定实现的产物和表现形式我阅读其代码的人的习惯,我不应该赋予任何意义,例如这需要在一个方向或另一个方向上完成,只要保持一致,哪个方向都没有关系。

或者也许我现在只是在阅读和编写琐碎的代码,当我在一段时间内获得更重要的东西时,这将是双向的?

我只想知道在我自己的汇编代码中应该如何处理它。

所有这些都在 Linux 64 位,GCC 版本 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04) 上。谢谢

00000000000005fa <leaf>:
 5fa:   55                      push   rbp
 5fb:   48 89 e5                mov    rbp,rsp
 5fe:   b8 01 00 00 00          mov    eax,0x1
 603:   5d                      pop    rbp
 604:   c3                      ret    

0000000000000605 <myfunc>:
 605:   55                      push   rbp
 606:   48 89 e5                mov    rbp,rsp
 609:   48 83 ec 10             sub    rsp,0x10
 60d:   b8 00 00 00 00          mov    eax,0x0
 612:   e8 e3 ff ff ff          call   5fa <leaf>
 617:   89 45 f4                mov    DWORD PTR [rbp-0xc],eax   ; // <--- This line
 61a:   c7 45 f8 02 00 00 00    mov    DWORD PTR [rbp-0x8],0x2   ; // <--  And this too
 621:   8b 55 f4                mov    edx,DWORD PTR [rbp-0xc]
 624:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
 627:   01 d0                   add    eax,edx
 629:   89 45 fc                mov    DWORD PTR [rbp-0x4],eax
 62c:   8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
 62f:   c9                      leave  
 630:   c3                      ret 

这是C代码:

int leaf() {
   return 1;
}

int myfunc() {
   int x = leaf(); // <--- This line
   int y = 2;      // <--  And this too
   int q = x + y;
   return q;
}

int main(int argc, char *argv[]) {
   return myfunc();
}

我是如何编译的:

gcc -O0 main.c -o main.bin

我是怎么拆解的:

objdump -d -j .text -M intel main.bin

零差异,对于必须存在的局部变量,随心所欲(因为您无法将它们优化到寄存器中)。


GCC 所做的事情意义为零;未使用的间隙在哪里并不重要(由于堆栈对齐而存在)。在这种情况下,它是 [rsp] 处的 4 个字节,又名 [rbp - 0x10].
[rbp - 4] 处的 4 个字节用于 q.

此外,您没有告诉 GCC 进行优化,因此没有理由期望它的选择甚至是最佳的或有用的学习指南。 -O3volatile int 当地人会更有意义。 (但由于没有什么重要的事情发生,实际上仍然没有帮助。)


重要的事情:

  • 局部变量应该自然对齐(双字值至少对齐 4 字节)。 C ABI 要求:alignof(int) = 4。调用前的 RSP 将按 16 字节对齐,因此函数入口 RSP-8 是按 16 字节对齐的。

  • 代码大小:尽可能多的寻址模式可以使用小的(带符号的 8 位)位移1 来自 RBP(如果您寻址你当地人相对于 RSP 喜欢 gcc -fomit-frame-pointer).

    当您只有几个标量局部变量时,这种情况很常见,远不及 128 个字节。

  • 您可以一起操作的任何局部变量都是相邻的,最好不要跨越对齐边界,因此您可以最有效地初始化它们/全部使用一个 qword 或 XMM 存储。

    如果您有很多局部变量(或数组),如果整个缓存行可能是“冷的”而此函数(及其子项)是 运行,则将它们分组以用于空间局部性。

  • 空间局部性:您在函数中较早使用的变量在堆栈帧中应该更高(更接近存储的return地址通过 call 到这个函数)。堆栈通常在高速缓存中很热,但是如果它在较早 loads/stores 之后完成,随着它的增长接触堆栈内存的新高速缓存行将略微影响。 Out-of-order exec 希望可以尽快获取那些稍后的存储指令,并将 cache-miss 存储到管道中以尽早启动 RFO(读取所有权),从而最大限度地减少早期加载阻塞存储缓冲区所花费的时间。

    这只影响大于 16 字节的边界;您知道一个 16 字节对齐块中的所有内容都在同一缓存行中。

    一个缓存行中的递减访问模式可能会触发向下预取下一个缓存行,但我不确定这是否发生在真实的 CPU 中。如果是这样,这可能是 这样做的原因,并且倾向于首先存储到堆栈帧的底部(在 RSP 处,或者最低的 red-zone 地址你'我会实际使用)。

如果在另一个 call 之前有未使用的 space 堆栈对齐,通常最多只有 8 个字节。这比缓存行小得多,因此对局部变量的空间局部性没有任何重大影响。您知道堆栈指针相对于 16 字节边界的对齐方式,因此选择在堆栈帧的顶部或底部保留填充不会影响是否触及新缓存缓存行。

如果您将指向本地变量的指针传递给不同的 线程,请注意错误共享:可能将这些本地变量分开至少 64 字节,以便它们位于不同的缓存行中,甚至更好 128 字节(L2 空间预取器可以在相邻缓存行之间产生“破坏性干扰”)。


脚注 1:x86 sign-extended 8 位对比 sign-extended 32 位寻址模式中的位移,例如 [rsp + disp8] x86-64 System V ABI 在 RSP 下面选择了一个 128 字节的 :它最多给出了一个 ~256 字节的区域,可以用更紧凑的 code-size 访问,包括 red-zone 加保留 space 以上 RSP.


PS:

请注意,您不必在函数的每一点都对相同的high-level“变量”使用相同的内存位置。您可以 spill/reload 某些东西到函数的一部分中的一个位置,然后再到函数的另一个位置。 IDK 为什么你会,但如果你已经浪费了 space 对齐,这是你 可以 做的事情。可能如果你希望一个缓存行在早期是热的(例如,在函数入口的堆栈帧顶部附近),而另一个缓存行稍后是热的(靠近一些其他被大量使用的变量)。

“变量”是一个 high-level 概念,您可以随心所欲地实施。这不是 C,没有要求它有地址,或者有相同的地址。 (如果地址未被占用,或者内联后未转义函数,C 编译器实际上会将变量优化到寄存器中。)

这有点 off-topic 或至少是迂腐的转移;通常你只是简单地使用相同的内存 l当它不能在寄存器中时,始终对同一事物进行阳离子。