x86_64:堆栈帧指针几乎没用?


  • Linux x86_64。
  • gcc 5.x

我正在研究两个代码的输出,使用-fomit-frame-pointer和without(gcc at“-O3”默认启用该选项)。

pushq %rbp movq %rsp, %rbp ... popq %rbp 

我的问题是:

如果我全局禁用该选项,即使是在极端情况下编译操作系统,是否有一个问题?

我知道中断使用该信息,那么该选项仅适用于用户空间吗?

编译器总是生成自洽的代码,因此只要不使用外部/手工制作的代码(例如依靠rbp的值),禁用帧指针就可以了。

中断不使用帧指针信息,它们可以使用当前堆栈指针来保存最小上下文,但这取决于中断和OS的类型(硬件中断可能使用Ring 0堆栈)。
您可以查看英特尔手册以获取更多相关信息。

关于帧指针的用处:
几年前,在编译了几个简单的例程并查看生成的64位汇编代码后,我遇到了同样的问题。
如果你不介意读我自己为自己写的那些笔记,那么它们就是。

注意 :询问某事物的有用性是有点相对的。 编写当前主要64位ABI的汇编代码我发现自己使用堆栈帧越来越小。 然而,这只是我的编码风格和意见。


我喜欢使用帧指针,编写函数的序言和结尾,但我也喜欢直接不舒服的答案,所以我在这里看到它:

是的,帧指针在x86_64中几乎没用

要注意它并非完全没用,特别是对人类而言,但编译器不再需要它了。 为了更好地理解为什么我们首先有一个帧指针,最好回忆一下历史。

回到真实模式(16位)

当Intel CPU仅支持“16位模式”时,对如何访问堆栈有一些限制,特别是这条指令是(并且仍然是)非法的

 mov ax, WORD [sp+10h] 

因为sp不能用作基址寄存器。 只有少数指定的寄存器可用bx目的,例如bx或更着名的bp
现在这并不是每个人都关注的细节,但是bp比其他基址寄存器具有优势,它隐含地暗示使用ss作为段/选择器寄存器,就像sp一样。
即使你的程序散布在整个内存中,每个段寄存器指向不同的区域, bpsp也是相同的,毕竟这是设计者的意图。

因此通常需要堆栈帧,因此需要帧指针。
bp有效地将堆栈分为三个部分: 参数区域, 旧bp区域(只是一个WORD)和局部变量区域。 每个区域由用于访问它的偏移量标识:参数为正,旧bp为零,局部变量为负。

扩展有效地址

随着英特尔CPU的不断发展,增加了更多指令,并且更多指令也增加了更广泛的寻址模式。
特别是可以使用任何寄存器作为基址寄存器,这包括使用esp
像这样的指示

 mov eax, DWORD [esp+10h] 

现在有效,堆栈框架和框架指针的使用似乎注定要结束。
可能情况并非如此,至少在开始阶段。
确实,现在可以完全使用esp但是在所述三个区域中堆叠的分离仍然是有用的,特别是对于人类而言。

如果没有框架指针,push或pop会改变相对于esp的参数或局部变量偏移量,将表单提供给初看起来不直观的代码,考虑如何使用cdecl调用约定实现以下C例程:

 void my_routine(int a, int b) { return my_add(a, b); } 

没有和有框架

 my_routine: push DWORD [esp+08h] push DWORD [esp+08h] call my_add ret my_routine: push ebp mov ebp, esp push DWORD [ebp+10h] push DWORD [ebp+08h] call my_add pop ebp ret 

乍一看似乎第一个版本推送相同的值两次,你添加本地变量(特别是很多)比情况变得很快难以阅读: mov eax, [esp+0cah]指的是局部var或者争论?
使用堆栈帧,我们可以为参数和本地变量修复偏移量。

甚至编译器最初仍然偏好使用堆栈指针给出的固定偏移量。 我看到这种行为首先用gcc改变。
在调试构建中,堆栈框架有效地增加了代码的清晰度,并使(熟练的)程序员可以轻松地跟踪正在发生的事情,并且如注释中所指出的那样,使堆栈调用的恢复更容易。
然而,现代编译器擅长数学运算,并且可以轻松保持堆栈指针移动的计数,并生成适当的偏移量,省略堆栈帧以加快执行速度。

当CISC需要数据对齐时

在引入SSE指令之前,与RISC兄弟相比,英特尔处理器从未对程序员提出太多要求。
特别是他们从未要求数据对齐,我们可以在3的地址倍数上访问32位数据而没有重大抱怨(取决于DRAM数据宽度,这可能导致延迟增加)。
SSE使用需要在16字节边界上访问的16字节操作数,因为SIMD范例在硬件中有效地实现并且变得更加流行,16字节边界上的对齐变得重要。

主要的64位ABI现在需要它,堆栈必须在段落上对齐。
现在,我们通常被称为在序言之后,堆栈是对齐的,但是假设我们没有幸运的保证,我们需要做其中一个

 push rbp push rbp mov rbp, rsp mov rbp, rsp and spl, 0f0h sub rsp, xxx sub rsp, 10h*k and spl, 0f0h 

在这些序言之后,堆栈是这样或那样的对齐,但是我们不能再使用rbp的负偏移来访问需要对齐的局部变量,因为帧指针本身没有对齐。
我们需要使用rsp ,我们可以安排一个序言,其rbp指向本地变量的对齐区域的顶部,但随后参数将处于未知偏移量。
我们可以安排一个复杂的堆栈帧(可能有更多的一个指针),但旧式堆栈指针的关键是简单。

因此,我们可以使用帧指针访问堆栈上的参数和本地变量的堆栈指针。
唉,参数传递的堆栈的作用已经减少,并且对于少量参数(目前为四个),它甚至没有被使用,并且在将来它可能会用得较少。

所以我们不使用框架指针(主要是),也不使用参数(大多数),我们使用它的是什么?

  1. 它保存了原始rsp的副本,所以要在函数出口恢复堆栈指针,一个mov就足够了。 如果堆栈与一个and不可逆的对齐,则需要原始副本。

  2. 实际上,一些ABI保证在标准序言之后,堆栈被对齐,从而允许我们像往常一样使用帧指针。

  3. 某些变量不需要对齐,可以使用未对齐的帧指针访问,这通常适用于手工制作的代码。

  4. 某些function需要四个以上的参数。

摘要

帧指针是16位程序的残留范例,但由于其在访问本地变量和参数时的简单性和清晰性,已经certificate它在32位机器上仍然有用。
然而,在64位机器上,严格的要求消除了大多数这些简单性和清晰度,但帧指针仍处于调试模式。


事实上,帧指针可以用来制作有趣的东西:这是真的,我想,我从来没有见过这样的代码,但我可以想象它是如何工作的。
然而,我专注于帧指针的管家角色,因为这是我一直看到它的方式。
所有疯狂的事情都可以通过设置为帧指针的相同值的任何指针来完成,我给后者一个更“特殊”的角色。
例如VS2013有时使用rdi作为“帧指针”,但如果它不使用rbp/ebp/bp ,我不认为它是真正的帧指针。
对我来说,使用rdi意味着帧指针省略优化:)