RBP/EBP 寄存器是否真的需要支持可变大小堆栈帧?



CSAPP 第 3 版 说:

若要管理可变大小的堆栈帧,x86-64 代码使用寄存器 %rbp 作为帧指针服务器。

但是,我很好奇这个%rbp寄存器是否真的有必要。虽然编译器不知道它必须为函数的堆栈帧分配多少空间,但它始终可以在调用subq xxxx, %rsp后将当前分配的堆栈帧大小保存到任何寄存器,因此它不需要依赖%rbp来恢复%rsp的值。 这是真的吗?如果是这样,这是否意味着%rbp根本不必要,而只是一个公约?

你是对的。 如果保留在可变大小sub xxx, %rsp中使用的大小,则可以在末尾使用add(或使用lea fixed_size(%rsp,%rdi,4), %rsp来反转它,以同时释放任何固定大小的堆栈空间预留。

正如@Ross指出的那样,这不能很好地扩展到同一函数中的多个可变长度分配。 即使使用单个VLA,它也不会比函数结束时的mov %rbp, %rsp(或leave)快。 它将允许编译器溢出大小,并有 15 个空闲寄存器,而不是函数部分的 14 个,当将其用作帧指针时,它永远不会选择与%rbp有关。 无论如何,这意味着 gcc 仍然希望回退到在复杂情况下使用帧指针。 (默认值是-fomit-frame-pointer,但不要担心它不会强制 gcc 从不使用一个)。

%rbp作为帧指针有一些次要的优点,尤其是在代码大小方面:以%rsp作为基本寄存器的寻址模式始终需要一个SIB字节(Scale/Index/Base),因为意味着(%rsp)的Mod/RM编码实际上是一个转义序列,用于指示存在SIB字节。 同样,表示没有位移(%rbp)的编码实际上意味着根本没有基本寄存器,因此您始终需要像0(%rbp)这样的disp8字节。

例如,mov %eax, 16(%rsp)mov %eax, -8(%rbp)长 1B。 Jan Hubicka 建议,如果 gcc 有一个启发式方法,在函数中启用帧指针,在不导致性能回归的情况下保存代码大小,那就太好了,并认为这种情况很常见。 它还可以节省一些堆栈同步 uop,以避免在具有堆栈引擎的英特尔 CPU 上直接使用%e/rsp(在推送/弹出或调用之后)。

gcc 始终使用%rbp作为具有 C99 可变大小数组的任何函数中的帧指针。 可能 gcc 开发人员发现,不值得弄清楚这样的函数何时在没有帧指针的情况下仍然同样高效,并且在 gcc 中为那些罕见的特殊情况提供了大量代码。


但是,如果我们真的想避免在带有 VLA 的函数中使用帧指针怎么办?

第 7 个及以后的整数参数(在 SysV ABI 中,请参阅 x86 标记 wiki)将位于返回地址上方的堆栈上。 通过disp(%rsp)访问它们是不可能的,因为在编译时不知道置换。

disp(%rsp, %rcx, 1)是可能的,其中%rcx保存可变长度数组大小。 (或所有 VLA 的总大小)。 这不会花费任何额外的代码大小disp(%rsp)因为将%rsp作为基本寄存器的寻址模式已经必须使用 SIB 字节。 但这意味着VLA大小需要全职保持在寄存器中,使用帧指针不会给我们带来任何好处。 (并且代码大小丢失)。

另一种方法是将标量/固定大小的局部变量保持在任何可变长度分配之下,因此我们始终可以使用相对于%rsp的固定位移来访问它们。 这对代码大小有好处,因为我们可以使用disp8(1B)而不是disp32(4B)来访问%rsp的[-128,+127]字节。

但是,只有在您需要将任何东西洒给当地人之前,您可以尽早确定VLA大小,它才有效。 因此,编译器再次需要一个复杂的特殊情况来检查,对于该特殊情况,它需要gcc中的一堆代码生成代码。

如果溢出 VLA 大小并在return 之前重新加载/使用它,则%rsp的值取决于从内存重新加载。 乱序执行可能会隐藏额外的延迟,但在某些情况下,这种额外的延迟确实会延迟使用%rsp的其他所有内容,包括恢复调用方的寄存器。

这种风格的代码生成可能也有一些极端情况需要 gcc 来处理,以制作正确和高效的代码。 由于它很少使用,因此其中的"高效"部分可能不会引起太多关注。

很容易理解为什么 gcc 选择简单地回退到帧指针模式,因为任何情况下它都无法省略它。 通常,它几乎免费为您提供额外的寄存器,因此即使您确实引用了很多当地人,也值得放弃代码大小优势。 在 32 位代码中尤其如此,从 6 到 7 个通用寄存器(不包括esp)。 这种差异在 64 位代码中通常较小,其中 14 与 15 的差异要小得多。 它仍然将推送/移动/弹出指令保存在不需要它们的函数中,这是一个单独的好处。 (使用%rbp作为通用寄存器仍然需要推送/弹出它。

最新更新