繁体   English   中英

布局堆栈变量的意义比 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-0x8 和 rsp 之间留出一些空间用于“q”。

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

或者也许我现在只是阅读和编写琐碎的代码,这将是 go 两种方式,因为我会在一段时间内获得更实质性的东西?

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

所有这些都在 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 locals 会更有意义。 (但由于没有什么重要的事情发生,实际上仍然没有帮助。)


重要的事情:

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

  • 代码大小:尽可能多的寻址模式可以使用来自 RBP 的小(带符号的 8 位)位移1 (或者 RSP,如果您相对于 RSP 解决本地问题,例如gcc -fomit-frame-pointer )。

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

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

    如果你有很多局部变量(或数组),如果在这个 function(及其子项)运行时有一整条高速缓存行可能是“冷的”,则将它们分组以用于空间局部性。

  • 空间局部性:您之前在 function 中使用的变量在堆栈帧中应该更高(更接近通过call此函数存储的返回地址)。 堆栈通常在缓存中很热,但是如果它在较早的加载/存储之后完成,那么随着它的增长接触堆栈 memory 的新缓存行将略微影响。 无序执行有望尽快获得那些较晚的存储指令,并将缓存未命中存储放入管道中,以尽早启动 RFO(读取所有权),从而最大限度地减少早期加载阻塞存储缓冲区所花费的时间。

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

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

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

如果您将指向本地变量的指针传递给不同的线程,请注意虚假共享:可能将这些本地变量分开至少 64 字节,这样它们将位于不同的缓存行中,或者最好分开 128 字节(L2 空间预取器可以创建“破坏性的”相邻高速缓存行之间的干扰”)。


脚注 1 :x86 符号扩展 8 位与符号扩展 32 位位移在诸如[rsp + disp8]的寻址模式中是 x86-64 System V ABI 选择 RSP 下面的 128 字节红色区域的原因:它给出最多约 256 字节可以用更紧凑的代码大小访问,包括红色区域加上 RSP 上方的保留空间。


附言:

请注意,您不必在 function 中的每一点对相同的高级“变量”使用相同的 memory 位置。您可以将某些内容溢出/重新加载到 function 的一部分中的一个位置,以及稍后在另一个位置function。IDK 为什么会这样,但是如果您浪费了 alignment 的空间,那么您可以这样做。 可能如果您希望一个缓存行在早期很热(例如,靠近 function 条目的堆栈帧顶部),而另一个缓存行稍后会很热(靠近一些其他被大量使用的变量)。

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

这是一种题外话,或者至少是一种迂腐的转移; 通常情况下,当它不能在寄存器中时,您只需始终如一地使用相同的 memory 位置。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM