88

我让 Google 告诉我gccoption的含义-fomit-frame-pointer,这会将我重定向到以下语句。

-fomit-frame-pointer

不要将帧指针保存在不需要的函数的寄存器中。这避免了保存、设置和恢复帧指针的指令;它还为许多功能提供了额外的寄存器。这也使得在某些机器上无法进行调试。

根据我对每个函数的了解,将在进程内存的堆栈中创建一个激活记录,以保存所有局部变量和更多信息。我希望这个帧指针表示一个函数的激活记录的地址。

在这种情况下,哪些函数类型不需要将帧指针保存在寄存器中?如果我得到这个信息,我会尝试基于它设计新函数(如果可能的话),因为如果帧指针没有保存在寄存器中,一些指令将在二进制中被省略。在有许多功能的应用程序中,这将真正显着提高性能。

4

3 回答 3

70

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

这实际上是关于编译器如何管理跟踪堆栈的使用方式以及堆栈上的位置(局部变量、传递给当前函数的参数以及为即将调用的函数准备的参数)。我认为描述需要或不需要帧指针的函数并不容易(从技术上讲,没有函数必须有一个帧指针 - 它更像是“如果编译器认为有必要降低复杂性其他代码”)。

我认为你不应该“尝试让函数没有帧指针”作为你编码策略的一部分——就像我说的,简单的函数不需要它们,所以使用-fomit-frame-pointer,你会得到一个可用的寄存器用于寄存器分配器,并保存 1-3 条关于进入/退出函数的指令。如果你的函数需要一个帧指针,那是因为编译器认为这是一个比不使用帧指针更好的选择。拥有没有帧指针的函数不是目标,目标是拥有既正确又快速运行的代码。

请注意,“没有帧指针”应该会提供更好的性能,但这并不是什么神奇的子弹可以带来巨大的改进——尤其是在 x86-64 上,它已经有 16 个寄存器开始。在 32 位 x86 上,因为它只有 8 个寄存器,其中一个是堆栈指针,占用另一个作为帧指针意味着占用了 25% 的寄存器空间。将其更改为 12.5% 是一个相当大的改进。当然,为 64 位编译也会有很大帮助。

于 2013-02-02T21:29:47.403 回答
27

这是关于英特尔平台上的 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 位平台上也是如此:AMD 于 2003 年推出的 x86-64 架构在很大程度上放弃了对 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寄存器数据call sites,即,按照它们被分层调用的顺序显示函数的名称和参数。

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

这就是为什么对于程序员来说,从编译器选项的角度理解什么是堆栈帧是至关重要的——因为编译器可以控制是否生成此代码。

在某些情况下,编译器可以省略堆栈帧(例程的入口和出口代码),变量将直接通过堆栈指针(SP/ESP/RSP)而不是方便的基指针(BP/ ESP/RSP)。编译器省略某些函数的堆栈帧的条件可能不同,例如: (1) 该函数是叶函数(即不调用其他函数的终端实体);(2) 不使用任何例外;(3) 在堆栈上没有调用带有传出参数的例程;(4) 函数没有参数。

省略堆栈帧(例程的进入和退出代码)可以使代码更小更快。尽管如此,它们也可能对调试器回溯堆栈数据并将其显示给程序员的能力产生负面影响。这些是编译器选项,用于确定函数应满足哪些条件,以便编译器授予它堆栈帧进入和退出代码。例如,在以下情况下,编译器可以选择将此类进入和退出代码添加到函数中:(a)总是,(b)从不,(c)在需要时(指定条件)。

从一般性到特殊性:如果您使用-fomit-frame-pointerGCC 编译器选项,您可能会在例程的进入和退出代码以及额外的寄存器上获胜(除非它默认情况下已经打开,或者本身或其他选项隐式打开,在这种情况下,您已经从使用 EBP/RBP 寄存器的增益中受益,并且如果该选项已经隐式打开,则不会通过显式指定此选项获得额外增益)。但是请注意,在 16 位和 32 位模式下,BP 寄存器不能像 AX(AL 和 AH)那样提供对 8 位部分的访问。

由于此选项除了允许编译器在优化中使用 EBP 作为通用寄存器外,还可以防止为堆栈帧生成退出和进入代码,这会使调试复杂化——这就是GCC 文档明确指出的原因(通常用粗体强调style) 启用此选项会使某些机器上的调试变得不可能

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

我在 gcc.gnu.org 上没有找到任何关于其他选项如何影响-fomit-frame-pointer x86 平台的官方信息,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在评论中指出,-fomit-frame-pointerx86-16 平台和 x86-32/64 平台之间的默认设置存在差异。

此选项 – -fomit-frame-pointer– 也与 Intel C++ Compiler 15.0 相关,不仅与 GCC 相关:

对于英特尔编译器,此选项有一个别名/Oy

这是英特尔写的:

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

一些调试器期望 EBP 被用作堆栈帧指针,除非这样,否则无法生成堆栈回溯。-fno-omit-frame-pointer 和 /Oy- 选项指示编译器生成维护和使用 EBP 作为所有函数的堆栈帧指针的代码,以便调试器仍然可以在不执行以下操作的情况下生成堆栈回溯:

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

当您指定 /O1、/O2 或 /O3 选项时,将设置 /Oy 选项。选项 /Oy- 在您指定 /Od 选项时设置。

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

注意 对于 Linux* 系统:GCC 3.2 异常处理当前存在问题。因此,当为 C++ 安装 GCC 3.2 并打开异常处理(默认设置)时,英特尔编译器会忽略此选项。

请注意,上述引用仅与 Intel C++ 15 编译器相关,与 GCC 无关。

于 2017-07-14T10:56:24.703 回答
2

我以前没有遇到过“激活记录”这个词,但我认为它指的是通常所说的“堆栈帧”。这是当前函数使用的堆栈区域。

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

大多数普通函数不需要帧指针来进行自己的操作。编译器可以通过函数跟踪所有代码路径上的堆栈指针偏移,并相应地生成局部变量访问。

在某些情况下,帧指针对于调试和异常处理可能很重要。尽管现代调试和异常处理格式旨在支持大多数情况下没有帧指针的函数,但这种情况变得越来越少见。

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

于 2021-10-02T01:18:58.130 回答