作者:左边我们画圈圈 | 来源:互联网 | 2023-09-18 10:40
我理解在函数的开头和结尾使用push rbp
...pop rbp
来保留rbp
调用函数的值,因为rbp
寄存器是被调用者保留的。然后我理解使用rbp
作为当前正在执行的过程的堆栈帧的当前顶部的“约定” 。但与此相关,我有两个问题:
- 是否
rbp
只是一个约定?我可以像r11
堆栈帧的基础一样轻松地使用(或任何其他寄存器甚至堆栈上的 8 个字节)吗?rbp
寄存器有什么特别之处,或者它只是用作基于历史和约定的堆栈框架?
- 为什么
mov %rbp, %rsp
在离开函数之前用作“清理”方法?例如,push/pop
指令通常是对称的,所以mov %rbp, %rsp
只是一种速记方式,有人可以“跳过”执行对称的弹出/添加等操作?什么mov %rbp, %rsp
是有用的实际用途?几乎所有时候我在编译器输出中看到它(启用零优化),它似乎是不必要的或多余的,而且我很难想到它实际上可能有用的场景。
回答
优化的代码根本不使用帧指针,除了像 VLA/ alloca
(RSP 的可变大小移动)之类的东西,或者如果您专门使用-fno-omit-frame-pointer
(例如,使perf record
堆栈采样更有效/可靠)。未优化的代码通常看起来不那么有趣。如何从 GCC/clang 程序集输出中去除“噪音”?
- x86_64:堆栈帧指针几乎没用吗?
- 为什么使用 ebp 比使用 esp 寄存器更好地定位堆栈上的参数?(仅适用于代码大小)
- 帧指针的优点是什么?
因此,关于何时/为什么使用帧指针的部分有很多重复。有趣的部分是是否可以选择 RBP 以外的寄存器。
RBP 唯一的特别之处是leave
可以紧凑地做 RSP=RBP + pop RBP;并且(%rbp)
寻址模式需要显式disp8
或disp32
(值为 0)。
因此,如果您打算使用帧指针,则应该选择 RBP,因为它在作为帧指针方面至少与任何其他 reg 一样好,但在某些其他用途方面比其他 reg 差。 你永远不需要0(frame_pointer)
,只需要其他偏移量。(R13 具有相同的 always-needs-a-disp8=0 效果,但是每个堆栈访问都需要一个 REX 前缀,就像add -12(%r13), %eax
RBP 不一样。)
此外,所有其他的“传统”寄存器(你可以不用REX使用,即不R8-R15)具有至少一个隐式使用至少一个指令编译器实际上可能产生,比如cmpxchg16b
,cpuid
,shl %cl, %reg
,rep movsb
或什么的,所以任何其他reg 作为帧指针会更糟。如果您需要调整一些东西以释放 RBX 用于某些需要它用于不同目的的指令,则不能进行简单的未优化(或玩具编译器)代码生成。(异常堆栈展开也可能依赖于帧指针始终位于特定寄存器中,如果您的.cfi_*
指令指定了这一点。)
与以前的 x86 模式保持一致将是使用 RBP 的充分理由,使微不足道的人类更容易记住,但如果您打算使用 RBP,仍然有代码大小和其他原因选择 RBP。(实际上,由于(%rsp)
寻址模式总是需要一个 SIB 字节,因此设置帧指针的指令实际上可以在代码大小方面为大型函数付出代价,尽管不是在指令/微指令中。)
仍然不相关的原因:
RBP 基地址暗示 SS 段,如 RSP,它在 16 位模式下是相关的,理论上在 32 位(非平面内存模型是可能的),但在 64 位模式下它只影响你的异常从非规范地址获取。所以这部分原因基本上消失了,几乎没有人关心#GP
与#SS
那里。
enter
太慢而无法使用,但leave
如果 RSP 尚未指向保存的 RBP,则仍然值得使用,与手动mov %rbp, %rsp
/pop %rbp
在 Intel CPU 上相比,仅花费 1 额外 uop ,所以这就是 GCC 所做的。您声称看到了无用的mov %rbp, %rsp
指令,但这并不是编译器实际所做的。
请注意,mov %rbp, %rsp
(3 bytes) 小于add $imm8, %rsp
(4 bytes),因此如果您使用的是帧指针,那么如果 RSP 未指向已保存的 RBP,则最好以这种方式恢复 RSP。(除非您需要恢复其他寄存器,如果您将它们保存在 RBP 下方而不是 a 之后sub $imm, %rsp
,尽管您可以使用mov
加载而不是弹出来进行恢复。)