为什么 RBP 而不是另一个寄存器作为帧指针?

问题描述

我了解 push rbp...pop rbp函数开始和结束时的用法,以保留调用函数rbp 值,因为 rbp注册是被调用者保留的。然后我理解使用 rbp 作为当前正在执行的过程的堆栈帧的当前顶部的“约定”。但与此相关,我有两个问题:

  1. rbp 只是一种惯例吗?我是否可以轻松地使用 r11(或任何其他寄存器甚至堆栈上的 8 个字节)作为堆栈帧的基础? rbp 寄存器有什么特别之处,或者它只是用作基于历史和约定的堆栈框架?
  2. 为什么在离开函数之前将 mov %rbp,%rsp 用作“清理”方法?例如,push/pop 指令通常是对称的,那么 mov %rbp,%rsp 是否只是一种速记方式,有人可以“跳过”执行对称的弹出/添加等操作? mov %rbp,%rsp 在哪里有用的实际用法是什么?几乎所有时间我都会在编译器输出中看到它(启用零优化),它似乎是不必要的或多余的,而且我很难想到它可能真正有用的场景。

解决方法

优化代码根本不使用帧指针,除了像 VLAs / alloca(RSP 的可变大小运动),或者如果您专门使用 -fno-omit-frame-pointer(例如使 {{ 1}} 堆栈采样更有效/可靠)。未优化的代码通常看起来不那么有趣。 How to remove "noise" from GCC/clang assembly output?

所以关于何时/为什么使用帧指针的部分有很多重复。有趣的部分是是否可以选择 RBP 以外的寄存器。


RBP 唯一的特别之处leave 可以紧凑地做 RSP=RBP + pop RBP;还有一个 perf record addressing mode requires an explicit disp8 or disp32(值为 0)。

所以如果你打算使用帧指针,你应该选择 RBP,因为它作为帧指针至少和任何其他 reg 一样好,但比其他的差regs 用于其他一些用途。您永远不需要 (%rbp),只需要其他偏移量。 (R13 具有相同的 always-needs-a-disp8=0 效果,但是每个堆栈访问都始终需要一个 REX 前缀,例如 0(frame_pointer) 没有 RBP。)

此外,所有其他“遗留”寄存器(您可以在没有 REX 的情况下使用,即不是 R8-R15)在 at least one instruction that compilers may actually generate 中至少有一个隐式用途,例如 add -12(%r13),%eax、{{1} }、cmpxchg16bcpuid 或其他任何东西,因此任何其他 reg 作为帧指针都会更糟糕。如果您需要调整一些东西以释放 RBX 用于某些需要它用于不同目的的指令,那么您不能进行简单的未优化(或玩具编译器)代码生成。 (如果您的 shl %cl,%reg 指令指定了这一点,则异常上的堆栈展开也可能依赖于帧指针始终位于特定寄存器中。)

与以前的 x86 模式保持一致将是使用 RBP 的充分理由,使微不足道的人类更容易记住,但如果您打算使用 RBP,仍然有代码大小和其他原因选择 RBP。 (实际上,由于 rep movsb 寻址模式始终需要一个 SIB 字节,因此设置帧指针的指令实际上可以在代码大小方面为大型函数付出代价,尽管不是在指令 / uops 中。)


仍然不相关的原因:

RBP 基地址暗示 SS 段,如 RSP,它在 16 位模式下是相关的,理论上在 32 位(非平面内存模型是可能的),但在 64 位模式下它只影响您从非规范地址获得的异常。所以这部分原因基本上消失了,几乎没有人关心 .cfi_*(%rsp) 那里。

#GP 太慢而无法使用,但如果 R​​SP 尚未指向已保存的 RBP,则 #SS 仍然值得使用,与手动 enter 相比仅花费 1 uop / leave 在 Intel CPU 上,这就是 GCC 所做的。您声称看到了无用的 mov %rbp,%rsp 指令,但这并不是编译器实际执行的操作。

注意pop %rbp(3个字节)小于mov %rbp,%rsp(4个字节),所以如果你使用的是帧指针,你不妨恢复RSP如果它不指向已保存的 RBP,则采用这种方式。 (除非您需要恢复其他寄存器,如果您将它们保存在 RBP 正下方而不是 mov %rbp,%rsp 之后,尽管您可以使用 add $imm8,%rsp 加载而不是弹出来进行恢复。)