尝试理解gcc选项-fomit-frame-pointer

112
我向Google查询了gcc选项-fomit-frame-pointer的含义,它给出了以下陈述。 -fomit-frame-pointer 对于那些不需要栈帧指针的函数,不在寄存器中保存栈帧指针。这避免了保存、设置和恢复栈帧指针的指令;它还使得在许多函数中有一个额外的寄存器可用。这也使得在某些机器上无法进行调试。
据我对每个函数的了解,激活记录将在进程内存的堆栈中创建,以保留所有本地变量和一些其他信息。我希望栈帧指针指的是函数的激活记录地址。
在这种情况下,哪些类型的函数不需要在寄存器中保存栈帧指针?如果我能获得这些信息,我将尝试基于它设计新的函数(如果可能),因为如果不在寄存器中保存栈帧指针,则二进制文件中将省略一些指令。这将在有许多函数的应用程序中显着改善性能。

10
只需要调试一个使用了这个选项编译的代码的崩溃转储文件,就足以让你从 makefile 中删除这个选项。它不会删除任何指令,但会为优化器提供一个额外的寄存器来进行存储。 - Hans Passant
2
@HansPassant 实际上,对于发布版本而言这是非常有用的。在 Makefile 中有两个目标——ReleaseDebug,实际上非常有用,以这个选项为例。 - Kotauskas
12
我猜你从未需要调试过一个运行于客户端的“Release”构建所导致的崩溃转储文件? - Andreas Magnusson
@MaximMasiutin - 这个问题最初没有指定x86,大多数答案仍适用于大多数ISA。此外,删除[c]标签毫无意义;许多评论中都提到了alloca,一个答案也是如此。 - Peter Cordes
3个回答

86

大多数较小的函数不需要帧指针 - 较大的函数可能需要一个。

这实际上取决于编译器如何跟踪堆栈的使用情况,以及堆栈中的位置(本地变量,传递给当前函数的参数以及正在准备调用的函数的参数)。我认为很难对需要或不需要帧指针的函数进行刻画(从技术上讲,没有任何函数需要帧指针 - 这更多是“如果编译器认为有必要减少其他代码的复杂性”)。

我认为你不应该将“尝试使函数没有帧指针”作为编码策略的一部分 - 就像我说的那样,简单的函数不需要它们,所以使用 -fomit-frame-pointer,您将获得一个额外的寄存器可供寄存器分配器使用,并在进入/退出函数时节省1-3条指令。如果您的函数需要帧指针,则是因为编译器决定这比不使用帧指针更好。目标不是拥有没有帧指针的函数,而是拥有既正确又快速的代码。

请注意,“没有帧指针”应该可以提高性能,但这并不是一种神奇的解决方案,可以获得巨大的改进 - 尤其是在x86-64上,并已经有16个寄存器。在32位x86上,由于仅有8个寄存器,其中一个是堆栈指针,并且占用另一个作为帧指针意味着25%的寄存器空间被占用。将其更改为12.5%是相当大的改进。当然,编译为64位也会有很大帮助。


33
一般编译器可以自己跟踪堆栈深度,不需要帧指针。例外情况是函数使用alloca,该函数将以变量数量移动堆栈指针。省略帧指针确实会使调试更加困难。没有帧指针的情况下,本地变量更难定位,堆栈跟踪也更加难以重建。此外,访问参数可能更加昂贵,因为它们远离堆栈顶部,可能需要更昂贵的寻址模式。 - Raymond Chen
5
是的,所以,假设我们不使用alloca [谁会用呢? - 我99%确定我从未编写过使用alloca的代码]或者可变大小本地数组 [这是 alloca 的现代形式],那么编译器仍然可能决定使用帧指针是更好的选择 - 因为编译器被设计成不盲目遵循给定的选项,而是为您提供最佳选择。 - Mats Petersson
7
VLA与alloca不同:当您离开声明它们的作用域时,VLA会被丢弃,而alloca空间仅在您离开函数时才会被释放。我认为这使得VLA比alloca更容易理解。 - Jens Gustedt
43
值得一提的是,对于x86-64架构,gcc默认开启了“-fomit-frame-pointer”选项。 - zwol
6
@JensGustedt,问题不在于它们何时被丢弃,而是它们的大小(例如alloca分配的空间)在编译时是未知的。通常编译器会使用帧指针来获取局部变量的地址,如果栈帧的大小不变,则可以在栈指针的固定偏移量处定位它们。 - vonbrand
显示剩余3条评论

32

这篇文章涉及到Intel平台上BP/EBP/RBP寄存器的相关内容。该寄存器默认指向堆栈段(不需要特殊前缀来访问堆栈段)。

对于在堆栈中访问数据结构、变量和动态分配工作空间,EBP是最好的寄存器选择。EBP通常用于相对于堆栈上的固定点而不是相对于当前TOS访问堆栈上的元素。它通常标识为为当前过程建立的当前堆栈帧的基地址。当EBP被用作偏移量计算中的基础寄存器时,偏移量会自动计算在当前堆栈段内(即由SS当前选择的段)。因为SS不必明确指定,在这种情况下指令编码更加有效率。EBP还可用于索引通过其他段寄存器寻址的段。

(来源-http://css.csail.mit.edu/6.858/2017/readings/i386/s02_03.htm

由于在大多数32位平台上,数据段和堆栈段是相同的,所以EBP/RBP与堆栈的关联已经不再是一个问题。同样,在64位平台上也是如此:x86-64架构由AMD在2003年引入时,在64位模式下大大降低了对分段的支持:强制四个段寄存器CS、SS、DS和ES为0。这些情况意味着,在x86 32位和64位平台上,EBP/RBP寄存器可以在访问内存的处理器指令中使用,而无需任何前缀。

因此,您提到的编译器选项允许将BP/EBP/RBP用于其他用途,例如保存本地变量。

“这避免了保存、设置和恢复帧指针的指令”是指避免每个函数进入时以下代码:

push ebp
mov ebp, esp
enter 指令在Intel 80286和80386处理器上非常有用。
在函数返回之前,使用以下代码:
mov esp, ebp
pop ebp 

或者使用 leave 指令。

调试工具可以扫描堆栈数据并使用这些推送的 EBP 寄存器数据来定位 调用点,即按层次结构显示调用函数名称和参数的顺序。

程序员可能会对堆栈帧有疑问,不是广义上(它是在堆栈中仅为一个函数调用提供服务并保留返回地址、参数和本地变量的单个实体)而是狭义上——当编译器选项的上下文中提到 堆栈帧 一词时。从编译器的角度看,堆栈帧只是例行程序的入口和出口代码,它将锚点推到堆栈上——这也可用于调试和异常处理。调试工具可以扫描堆栈数据并使用这些锚点进行回溯,同时在堆栈中定位 调用点,即以与按层次结构调用它们的相同顺序显示函数名称。

这就是为什么理解堆栈帧对于程序员来说在编译器选项方面意味着什么非常重要——因为编译器可以控制是否生成此代码。

在某些情况下,编译器可以省略堆栈帧(例行程序的入口和出口代码),并且变量将直接通过堆栈指针(SP/ESP/RSP)而不是方便的基指针(BP/ESP/RSP)进行访问。 编译器省略某些函数的堆栈帧(例行程序的入口和出口代码)的条件可能有所不同,例如:(1) 函数是叶子函数(即不调用其他函数的末端实体);(2) 不使用异常;(3) 不使用传递参数的例行程序;(4) 函数没有参数。

省略堆栈帧(例行程序的入口和出口代码)可以使代码更小更快。但它们也可能对调试工具追踪堆栈数据并将其显示给程序员的能力产生负面影响。这些是决定函数在何种条件下应满足才能被编译器授予堆栈帧入口和出口代码的编译器选项。例如,编译器可能有选项,以在以下情况下向函数添加此类入口和出口代码:(a) 总是,(b) 从不,(c) 在需要时(指定条件)。

从普遍性返回特殊性:如果您使用 -fomit-frame-pointer GCC 编译器选项,则可能在例行程序的入口和出口代码上都取得胜利,并获得一个附加寄存器(除非它已经默认打开,或者由其他选项隐含地打开,在这种情况下,使用 EBP/RBP 寄存器并且显式指定此选项将不会获得额外的收益)。但请注意,在 16 位和 32 位模式下,BP 寄存器没有像 AX 那样提供访问其 8 位部分(AL 和 AH)的能力。

该选项不仅允许编译器在优化中使用EBP作为通用寄存器,还防止为堆栈帧生成退出和进入代码,从而使调试更加复杂。因此,GCC文档明确声明(强调选项会使某些机器上的调试变得不可能)。

请注意,与调试或优化相关的其他编译器选项可能会隐式打开或关闭-fomit-frame-pointer选项。

我没有在gcc.gnu.org找到关于其他选项如何影响x86平台上的-fomit-frame-pointer的官方信息,只有https://gcc.gnu.org/onlinedocs/gcc-3.4.4/gcc/Optimize-Options.html中写到:

-O还会在不影响调试的情况下在某些机器上打开-fomit-frame-pointer。

因此,从文档本身来看,不清楚是否在x86平台上只使用单个“-O”选项进行编译时是否会打开-fomit-frame-pointer。这可以通过经验测试,但在这种情况下,GCC开发人员没有承诺不会未经通知更改此选项的行为。

但是,Peter Cordes在评论中指出了在x86-16平台和x86-32/64平台之间的默认设置的-fomit-frame-pointer的差异。

这个选项-fomit-frame-pointer也与Intel C ++编译器15.0相关,而不仅仅是GCC:

对于Intel编译器,该选项有一个别名/Oy

以下是Intel对此的描述:

这些选项确定是否在优化中使用EBP作为通用寄存器。选项-fomit-frame-pointer和/Oy允许此使用。选项-fno-omit-frame-pointer和/Oy-禁止它。

一些调试器希望EBP用作堆栈帧指针,如果不这样做,它们就无法生成堆栈回溯。选项-fno-omit-frame-pointer和/Oy-指导编译器为所有函数生成并使用EBP作为堆栈帧指针,以便调试器可以生成堆栈回溯而无需进行以下操作:

对于-fno-omit-frame-pointer选项:在使用-O0选项或-g选项时关闭优化。 对于/Oy-选项:关闭/O1、/O2或/O3优化。 当使用-O1、-O2或-O3选项时,设置-fomit-frame-pointer选项。当使用-O0或-g选项时,设置-fno-omit-frame-pointer选项。

当使用/O1、/O2或/O3选项时,设置/Oy选项。当使用/Od选项时,设置/Oy-选项。

使用-fno-omit-frame-pointer或/Oy-选项会减少1个可用的通用寄存器,并可能导致代码效率稍微降低。

注:针对Linux*系统:GCC 3.2存在异常处理问题,因此,在安装C++和打开异常处理(默认情况下)的情况下,英特尔编译器会忽略此选项。

请注意,上述引文仅适用于英特尔C++ 15编译器,而非GCC。


2
16位代码以及BP默认为SS而不是DS并不真正相关于gcc。gcc -m16 是存在的,但这只是一个奇怪的特例,基本上是使用前缀在16位模式下运行32位代码。同时需要注意的是,在x86 -m32 上默认启用了-fomit-frame-pointer,在x86-64 (-m64) 上更久远。 - Peter Cordes
@PeterCordes - 谢谢你,我已经根据你提出的问题更新了编辑。 - Maxim Masiutin
2
优秀的回答! - Seth Johnson

4

我之前没有听说过"activation record"这个词,但我认为它指的是通常称为"堆栈帧"的东西。那就是当前函数使用的堆栈上的区域。

帧指针是一个寄存器,保存着当前函数堆栈帧的地址。如果使用了帧指针,在进入函数时,旧的帧指针会被保存到堆栈中,并将帧指针设置为堆栈指针。在离开函数时,旧的帧指针会被恢复。

大多数普通函数在其自身操作中不需要帧指针。编译器可以跟踪函数中所有代码路径的堆栈指针偏移量,并相应地生成本地变量访问。

帧指针在某些调试和异常处理情况下可能很重要。然而,随着现代调试和异常处理格式的设计,支持大多数情况下无需帧指针的函数,这种情况越来越少见。

现在主要需要帧指针的时间是,如果函数使用alloca或可变长度数组。在这种情况下,无法静态跟踪堆栈指针的值。


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