CSAPP第3版说:
要管理可变大小的堆栈帧,x86-64代码使用寄存器%rbp 服务器作为帧指针。
但是,我很好奇这个%rbp
注册是否真的有必要。虽然编译器不知道它必须为函数的堆栈帧分配多少空间,但它总是可以在调用subq xxxx, %rsp
之后将当前分配的堆栈帧大小保存到任何寄存器,所以它不需要依赖%rbp
来恢复%rsp
的值。这是真的吗?如果是这样,这是否意味着%rbp
根本没有必要,只是一个惯例?
答案 0 :(得分:3)
你是对的。如果你保留在可变大小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
作为帧指针具有一些小优势,尤其是在代码大小中:addressing mode %rsp
作为基址寄存器始终需要SIB字节(Scale / Index / Base),因为意味着(%rsp)
的Mod / RM编码实际上是一个转义序列,表示有一个SIB字节。同样,没有位移意味着(%rbp)
的编码实际上意味着根本没有基址寄存器,因此您总是需要一个disp8
字节,如0(%rbp)
。
例如,mov %eax, 16(%rsp)
比mov %eax, -8(%rbp)
长1B。 Jan Hubicka suggested如果gcc有一个启发式方法可以在函数中启用帧指针,它会保存代码大小而不会导致性能回归,并且认为通常就是这种情况。它还可以保存一些堆栈同步uop,以避免在具有堆栈引擎的Intel CPU上直接使用%e/rsp
(在推/弹或调用之后)。
gcc总是在任何具有C99可变大小数组的函数中使用%rbp
作为帧指针。可能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)来访问[{1}}的[-128,+ 127]字节
但只有在您需要将任何东西泄漏给当地人之前,您才能尽早确定VLA尺寸。所以你有一个复杂的特殊情况让编译器检查,并且在gcc中需要一堆代码生成代码来处理这种特殊情况。
如果溢出VLA大小并在%rsp
urn之前重新加载/使用它,则ret
的值取决于从内存重新加载。乱序执行可能会隐藏额外的延迟,但是有些情况下,额外的延迟确实会延迟使用%rsp
的所有其他内容,包括恢复调用者的寄存器。
这种代码风格可能也会让gcc处理一些极端情况,以制作正确有效的代码。由于它很少使用,因此效率很高"其中一部分可能没有得到太多关注。
很容易理解为什么gcc选择简单地回退到帧指针模式,因为任何情况都不会忽略它。通常情况下,它几乎可以免费获得额外的寄存器,因此即使您参考了很多本地人,也值得放弃代码大小优势。在32位代码中尤其如此,您可以使用6到7个通用寄存器(不包括%rsp
)。在64位代码中,这种差异通常较小,其中14对15的差异要小得多。它仍然将push / mov / pop指令保存在不需要它们的功能中,这是一个单独的好处。 (使用esp
作为通用寄存器仍然需要推送/弹出它。)