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

Kro*_*oma 13 c assembly gcc x86-64 stack-frame


  • Linux x86_64.
  • gcc 5.x

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

pushq    %rbp
movq     %rsp, %rbp
...
popq     %rbp
Run Code Online (Sandbox Code Playgroud)

我的问题是:

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

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

小智 21

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

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

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

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


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

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

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

回到真实模式(16位)

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

mov ax, WORD [sp+10h]
Run Code Online (Sandbox Code Playgroud)

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

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

扩展有效地址

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

mov eax, DWORD [esp+10h]
Run Code Online (Sandbox Code Playgroud)

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

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

void my_routine(int a, int b)
{  
    return my_add(a, b); 
}
Run Code Online (Sandbox Code Playgroud)

没有和有框架

my_routine:      
  push DWORD [esp+08h]
  push DWORD [esp+08h]
  call my_add
  ret

my_routine:
  push ebp
  mov ebp, esp

  push DWORD [ebp+0Ch]
  push DWORD [ebp+08h]
  call my_add

  pop ebp
  ret 
Run Code Online (Sandbox Code Playgroud)

乍一看似乎第一个版本推送相同的值两次,你添加本地变量(特别是很多)比情况变得很难阅读:esp引用局部变量或参数?
使用堆栈帧,我们可以为参数和本地变量修复偏移量.

甚至编译器最初仍然偏好使用堆栈指针给出的固定偏移量.我看到这种行为首先用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
Run Code Online (Sandbox Code Playgroud)

在这些序言之后,堆栈的一种方式是对齐的,但是我们不能再使用负偏移esp来访问需要对齐的局部变量,因为帧指针本身没有对齐.
我们需要使用esp,我们可以安排一个mov eax, [esp+0CAh]指向本地变量对齐区域顶部的序言,但后面的参数将是未知的偏移量.
我们可以安排一个复杂的堆栈帧(可能有更多的一个指针),但旧式堆栈指针的关键是简单.

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

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

  1. 它保存了原始的副本esp,所以要在功能退出时恢复堆栈指针,这就rbp足够了.如果堆栈与rsp不可逆的堆栈对齐,则需要原始副本.

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

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

  4. 某些功能需要四个以上的参数.

摘要

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


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