EBP帧指针寄存器的目的是什么?

103

我是一名汇编语言的初学者,注意到编译器生成的x86代码即使在发布/优化模式下也保留了帧指针(EBP寄存器),即使可以将其用于其他用途。

我明白帧指针可能会使代码更易于调试,并且如果在函数中调用alloca()可能是必需的。 但是,x86只有非常少的寄存器,使用其中两个来保存堆栈帧的位置,而一个寄存器就足够,这对我来说没有意义。为什么在优化/发布构建中省略帧指针被认为是一个坏主意?


22
如果你认为x86寄存器很少,那么你应该看一下6502处理器 :) - Sedat Kapanoglu
3
相关文章:为什么在函数前后套用 EBP?在函数的 prologue 和 epilogue 中使用 EBP 的原因是它可以方便地访问栈中的局部变量和参数。通过将栈指针 ESP 减去固定偏移量,可以访问特定位置上的局部变量或参数。然而,由于在函数调用过程中 ESP 的值会发生变化,所以使用 EBP 可以保持一个固定的引用点,从而使代码更加简洁明了。在函数的 prologue 中,EBP 被设置为当前栈指针 ESP 的值,然后 ESP 被减去需要分配给局部变量的空间大小。在函数的 epilogue 中,EBP 被恢复到之前的值,以便返回时可以正确地清除栈。 - legends2k
1
C99 可变长度数组也可以从中受益。 - Ciro Santilli OurBigBook.com
2
https://dev59.com/eHM_5IYBdhLWcg3wZSPX - Ciro Santilli OurBigBook.com
1
“框架指针不会使堆栈指针变得多余吗?”(http://stackoverflow.com/a/26279405/264047)。太长没看?重点:1.非平凡的堆栈对齐2.堆栈分配(`alloca`) 3.运行时实现的便捷性:异常处理,沙盒,GC。 - Alexander Malakhov
显示剩余5条评论
5个回答

107
帧指针是一个引用指针,允许调试器通过一个恒定的偏移量知道局部变量或参数的位置。尽管ESP的值在执行过程中会发生变化,但EBP保持不变,因此可以通过相同的偏移量访问相同的变量(例如,第一个参数始终在EBP+8处,而ESP的偏移量可能会发生显著变化,因为你会进行推入/弹出操作)。
为什么编译器不丢弃帧指针?因为有了帧指针,调试器可以使用符号表来确定局部变量和参数的位置,因为它们保证与EBP之间存在恒定的偏移量。否则,在代码的任何位置找到局部变量的方法就不容易。
正如Greg提到的,帧指针还有助于调试器的堆栈展开,因为EBP提供了一个堆栈帧的反向链表,从而让调试器能够确定函数的堆栈帧的大小(局部变量+参数)。
大多数编译器提供了省略帧指针的选项,尽管这会使调试变得非常困难。这个选项不应该在全局范围内使用,即使在发布代码中也是如此。你不知道何时需要调试用户的崩溃。

10
编译器可能知道对ESP做了什么。尽管如此,其他观点是有效的,+1。 - erikkallen
10
现代调试器即使在使用“-fomit-frame-pointer”编译的代码中也可以进行堆栈回溯。这个设置是最近的 gcc 的默认设置。 - Peter Cordes
3
一个数据部分记录必要的信息:http://yosefk.com/blog/getting-the-call-stack-without-a-frame-pointer.html。 - Peter Cordes
3
.eh_frame_hdr 这个区段也用于运行时的异常处理。你可以在大多数 Linux 系统的二进制文件中(使用 objdump -h 命令)找到它。对于 /bin/bash,它大约是 16k,对于 GNU /bin/true,它只有 572B,对于 ffmpeg,它是 108k。GCC 提供了禁用这个区段生成的选项,但它是一个“普通”的数据区段,而不是 strip 默认删除的调试区段。否则,如果一个库函数没有调试符号,你将无法通过回溯定位其位置。这个区段可能比它所替代的 push/mov/pop 指令更加庞大,但它几乎没有运行时成本(例如微操作缓存)。 - Peter Cordes
4
关于"such as first parameter will always be at EBP-4": 在x86架构上,第一个参数不是在EBP-4位置,而是在EBP+8位置。 - Aydin K.
显示剩余4条评论

33

在这些已经很好的回答中,我想再添砖加瓦。

在一个良好的语言体系结构中,有一个堆栈帧链是必要的。BP指向当前帧,其中存储子例程本地变量。(局部变量在负偏移处,参数在正偏移处。)

认为它阻止了优化中可用的寄存器的观点引发了一个问题:何时何地进行优化才是值得的呢?

仅当紧密循环不调用函数且程序计数器花费其时间的重大部分在编译器实际上会看到的代码中(即非库函数)时,优化才是值得的。这通常只占整个代码的一小部分,特别是在大型系统中。

其他代码可以被改变和挤压以消除周期,但它实际上并没有什么影响,因为程序计数器几乎从来不在那里。

我知道你没有提出这个问题,但在我的经验中,99%的性能问题与编译器优化完全无关,而与过度设计有关。


2
取消帧指针还可以在每个函数调用时节省几条指令,这本身就是一个小优化。顺便说一句,你使用“begs the question”的方式是不正确的;你的意思是“引出问题”。 - augurar
@augurar:已修正。谢谢。我自己也是一个语法控。 - Mike Dunlavey
4
语言不断演变,“Begs the question”现在只是指“引出问题”。成为一个对过时用法拘泥不放的规范主义挑剔者并没有任何意义。 - user3364825

10

这取决于编译器,我曾看到x86编译器生成的优化代码自由使用EBP寄存器作为通用寄存器。(尽管我不记得是哪个编译器了。)

编译器可能还会选择保留EBP寄存器来协助堆栈展开处理异常,但这也取决于具体的编译器实现。


大多数编译器在启用优化时默认使用-fomit-frame-pointer(如果ABI允许的话)。我记得GCC、clang、ICC和MSVC都这样做,即使是针对32位Windows。是的,我的回答为什么使用ebp而不是esp寄存器来定位堆栈上的参数?表明即使是32位Windows也可以省略帧指针。32位x86 Linux肯定可以并且已经这样做了。当然,64位ABI从一开始就允许省略帧指针。 - Peter Cordes

4
然而,x86只有很少的寄存器。这只是指操作码只能寻址8个寄存器。处理器本身实际上会有更多的寄存器,并使用寄存器重命名、流水线、推测执行和其他处理器术语来绕过这个限制。维基百科有一个很好的介绍段落,介绍了x86处理器如何克服寄存器限制的方法:http://en.wikipedia.org/wiki/X86#Current_implementations

1
原始问题涉及生成的代码,该代码严格限制了可由操作码引用的寄存器。 - Darron
1
是的,但这就是为什么在优化构建中省略帧指针现在不那么重要的原因。 - Michael
2
寄存器重命名并不完全等同于实际拥有更多可用的寄存器。仍然有许多情况下,寄存器重命名无法帮助,但更多“常规”寄存器则可以。 - jalf

1

在任何现代硬件上,使用堆栈帧已经变得非常便宜。如果您有廉价的堆栈帧,则保存几个寄存器并不重要。我相信快速堆栈帧与更多寄存器之间是一种工程权衡,而快速堆栈帧胜出了。

通过纯粹的寄存器,您可以节省多少?这值得吗?


更多的寄存器受指令编码的限制。x86-64使用REX前缀字节中的位将指令的寄存器指定部分从3位扩展到4位,用于src和dest寄存器。如果有空间,x86-64可能会增加到32个体系结构寄存器,尽管在上下文切换时保存/恢复这么多寄存器开始累积。15比7大了很多,但在大多数情况下,31只是一个更小的改进。(不包括堆栈指针作为通用寄存器)。使push/pop快速不仅对堆栈帧有好处。这不是与寄存器数量的权衡。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接