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

时间:2015-07-14 21:32:50

标签: c gcc assembly x86-64 stackframe

  • Linux x86_64。
  • gcc 5.x

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

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

我的问题是:

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

我知道中断使用了这些信息,那么这个选项只适用于用户空间吗?

1 个答案:

答案 0 :(得分:19)

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

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

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

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

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

是的,帧指针在x86_64

中几乎无用

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

返回实模式(16位)

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

rbp

因为mov ax, WORD [sp+10h] 不能用作基址寄存器。只有少数指定的寄存器可用于此目的,例如sp或更有名的bx 现在它并不是每个人都关注的细节,但bp优于其他基址寄存器,它隐含地暗示使用bp作为段/选择器寄存器,就像ss一样1}} 即使你的程序遍布整个内存,每个段寄存器指向不同的区域,spbp也是相同的,毕竟这是设计者的意图。

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

扩展有效地址

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

esp

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

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

esp

没有和使用framestack

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 指的是本地var或者争论?
使用堆栈帧,我们可以为参数和本地变量修复偏移量。

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

当CISC需要数据对齐时

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

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

mov eax, [esp+0cah]

在这些序言之后,堆栈被对齐,但是我们不能再使用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指向本地变量的对齐区域的顶部,但参数将是未知的偏移量。
我们可以安排一个复杂的堆栈帧(可能有更多的一个指针),但旧式堆栈指针的关键是简单。

所以我们可以使用框架指针访问堆栈上的参数和本地变量的堆栈指针,这很公平。
唉,参数传递的堆栈角色已经减少,并且对于少量参数(目前为四个),它甚至没有被使用,并且将来它可能会用得较少。

因此,我们不会将帧指针用于本地变量(大多数情况下),也不用于参数(大多数),我们使用它的是什么?

  1. 它保存了原始rbp的副本,因此要在函数出口恢复堆栈指针,rsp就足够了。如果堆栈与mov对齐,而and不可翻转,则需要原始副本。

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

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

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

  5. 摘要

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

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