从 rsp 比 rbp 开始布局堆栈变量的意义

Posted

技术标签:

【中文标题】从 rsp 比 rbp 开始布局堆栈变量的意义【英文标题】:Significance of laying out stack variables starting nearer rsp than rbp 【发布时间】:2020-12-28 04:59:47 【问题描述】:

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

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

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

在下面的反汇编中,前 0x10 个字节被保留,然后调用叶子的结果通过 eax 到 rbp-0xc,常量值 2 到 rbp-0x8,在 rbp-0x8 和 rbp 之间为变量“q”留出空间”。

我可以想象在另一个方向上做,首先在 rbp 分配一个地址,然后在 rbp-0x4,即在 rbp 到 rsp 的方向上做,然后在 rbp-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

【问题讨论】:

是的,您可以随心所欲地使用本地人。如果由于对齐而分配了更多空间,则可以将填充放在任何地方。 PS:您正在查看未优化的代码,这通常是个坏主意。 @Jester 谢谢,但我不确定在这种情况下查看未优化的代码有什么不好?在 -O2 上 gcc 发出“nop WORD PTR cs:[rax+rax*1+0x0]”,根本不使用堆栈变量,这并不是我真正想要展示的。我大致了解优化级别之间的区别,但鉴于我正在编写程序集而 C 只是一项附加资产,我不清楚在此示例中使用 -O0 有什么注意事项? 您声称 GCC 在保存的 RBP 下方留下了一些空间,但实际上使用了 dword [rbp-0x4]。 (对于q 它看起来像。) 这不是“声明” :-) 它似乎与 x 和 y 无关。但是你是对的,我忘记了 q 并且似乎我的意思是留下了未使用的空间。我将进行编辑以使其更清楚地表明它适用于 q。谢谢。 -O0 表示快速编译而不尝试优化(包括不尝试优化堆栈帧布局)。因此,如果您希望了解有关如何布置本地人的任何信息,那么这不是一个好的开始。 (但就像 Jester 所说的那样;如何布置它们并不重要,除了可能将它们分组以便您可以使用单个 qword 存储同时初始化两个)。 【参考方案1】:

它使差异为零,对必须存在的局部变量执行任何操作(因为您无法将它们优化到寄存器中)。


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

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


重要的事情:

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

代码大小:尽可能多的寻址模式可以使用来自 RBP 的小(有符号 8 位)位移1(或者如果您寻址相对于 RSP 的本地人,例如 gcc -fomit-frame-pointer,则为 RSP) .

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

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

如果您有很多局部变量(或数组),如果在此函数(及其子函数)运行时有一个完整的缓存行可能“冷”,则将它们分组以获得空间局部性。

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

这只对超过 16 字节的边界很重要;你知道一个 16 字节对齐的块中的所有内容都在同一个缓存行中。

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

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

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


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


PS:

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

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

这有点离题,或者至少是一种迂腐的消遣;通常,当它不能在寄存器中时,您只需将相同的内存位置一致地用于同一事物。

【讨论】:

我不希望 GCC 在-O0 级别上优化任何东西,老实说,这个问题与 C 或 GCC 无关,我只需要以某种方式说明我不确定的行为和 GCC只是旁白。我还选择了一个叶子函数来精确地避开红色区域,我已经意识到它的存在,这很好。您的回答一如既往地非常全面,对此我表示感谢。如果您可以直接说明顺序无关紧要(从 rsp 到 rbp 或其他方式),这是我查询的核心,我将非常乐意接受。 关于您的编辑,我知道这不是高级语言使用的意义上的变量。你会建议我用什么来让我在 SO 或其他地方的专业汇编程序员更容易理解自己?只是“内存位置”或“堆栈位置”?装配对我来说不是全职工作,我只是不知道。 @Terry:关于 GCC 和那个例子的讨论占据了你大约 80% 的问题,我认为将我的答案的 25% 用于你问题的那部分是合理的,还有什么更好的方法应该是创造一个更好的例子。 @Terry:回复:直接回答:也许您在 5 分钟的“宽限期”内错过了第一次编辑,该编辑添加了第一段“它使差异为零,随心所欲”。答案真的就这么简单,除非你想让它因为轻微的性能问题而变得复杂(但这就是为什么你首先要在 2020 年手写 asm,所以这就是剩下的答案的全部内容)。 @Terry:假设您有char buf[128],您只在函数开始时使用一次。如果你把它放在你的堆栈帧的顶部,它跨越 3 个或者可能只是 2 个缓存行,并且在函数返回之前,其中的前 2 个将保持不变(连同你的返回地址)。但是,如果您在数组上方有另一个本地,并且您在循环中使用该标量本地,则数组的两端可能必须在缓存中保持热状态,而不是从 L1d 中逐出以为更有价值的东西腾出空间。

以上是关于从 rsp 比 rbp 开始布局堆栈变量的意义的主要内容,如果未能解决你的问题,请参考以下文章

如何正确操作装配中的堆栈?

使用前关于堆栈的假设

rsp rbp 寄存器用途

rsp rbp 寄存器用途

为啥 rbp 和 rsp 被称为通用寄存器?

攻防世界-reverse-666