防止 GCC 使用动态跳转/函数调用

Prevent GCC from using dynamic jumps / functions invocations

我正在尝试为 GCC 编译的应用程序编写一个汇编工具模块作为安全框架的一部分。为了提高模块的性能,我需要尽可能减少动态跳转/动态函数调用。这些基本上使用一些动态指针(例如寄存器)来执行跳转或调用函数。

当前的 GCC 编译器,每当它多次调用同一个函数(代码中的某个标签)时,就会将标签加载到寄存器中,然后在需要调用该函数时跳转到该寄存器。这当然是一种比每次都跳转到相同标签(更小的代码和更少的时钟周期)更快的方法,但是,正如我所提到的,这对我的框架来说效率很低。 为了给你一个我想避免的例子,这里有一个代码片段:

MOV #function_label, R10.  #Copy the label to the R10 register
CALL R10
...
...
CALL R10
...
...
CALL R10

虽然我希望 GCC 执行以下操作:

CALL #label_function
...
...
CALL #label_function
...
...
CALL #label_function

请注意,我实际上使用的是 mspgcc,它是用于 MSP430 系列微控制器的 GCC 编译器,但基于 GCC 应该不会有太大区别。

你认为有什么可以做的吗(除了重写 GCC 编译器)?非常感谢您的帮助

使用-fno-function-cse 不对函数地址执行common-subexpression-elimination。 GCC manual:

-fno-function-cse

Do not put function addresses in registers; make each instruction that calls a constant function contain the function’s address explicitly.

This option results in less efficient code, but some strange hacks that alter the assembler output may be confused by the optimizations performed when this option is not used.

The default is -ffunction-cse


如何找到特定的 GCC 选项

我查看了 gcc -O1 -fverbose-asm asm 输出以查看 -O1 暗示的所有优化选项(GCC 在 asm 注释中列出)。 -O1 -fno-... 版本的所有内容仅编译为 3 call 条指令,每条指令上都有符号名称,确认其中一条是我想要的,所以我只需要将 call 列表一分为二来缩小范围=16=] 选项

我使用了带有 MSP430 GCC6.2.1 的 Godbolt 编译器资源管理器,test code + asm。我禁用了“评论”过滤器选项,所以我可以在 asm 输出中看到 pure-comment 行。

因为有很多选项,我用 tr ' ' '\n' | sed -e 's/-f/-fno-/' -e '/;/d'-f 选项变成了否定形式。我 copy/pasted 将整个 asm 注释块放入终端中的该命令,并将结果 copy/pasted 放入 Godbolt 上的 GCC 选项框中。 (与 -O1 一起。-O0 是一种用于一致调试的特殊 anti-optimized 模式,因此即使使用正确的选项,across-statement 优化也可能永远不会在 -O0 处激活. 这就是为什么我需要否定选项而不是尝试没有 -O1)

的肯定形式

然后我选择并删除了一堆选项,看看是否改变了 asm。如果没有,请继续。当我找到一个块时,我知道我想要的选项就在那里,所以我可以撤消 (control-z) 并删除所有其他 -f 选项,然后将其缩小到一个。 (当我在该组中看到名称 -fno-function-cse 时,我认为这听起来像是正确的事情。幸运的是,如果您了解编译器/优化术语,GCC 选项确实具有有意义的名称。)

这比一次查看 1 个选项或费力地阅读手册要快,因为我什至不确定这些特定选项中的任何一个是否会控制它。


顺便说一句,GCC 不会对大多数其他 ISA code-size 进行优化,因为这对他们来说不是性能上的胜利。 Code-size 不是 x86-64 甚至 ARM thumb 上性能的最重要因素;间接跳跃的可能分支错误预测的额外成本(以及分支预测器的额外污染)超过了 code-size 成本。

,其中一个5字节mov-立即数或7字节RIP-relativelea(x86-64)可以设置为多个2字节call 说明。

在许多 fixed-instruction-width ISA 上,例如 AArch64 或 ARM(Thumb 模式除外),通常甚至 code-size 都不会获胜,其中标准代码模型假定函数将在彼此的范围内对于相对 branch-and-link (调用)指令。因此调用任何函数都需要一条指令,其大小与任何其他指令相同。

即使显式启用 -ffunction-cse,GCC 也不会对 x86-64 或 ARM thumb 进行此优化,即使在它已经使用来自 GOT 的函数指针的情况下也是如此。 (Godbolt 上的 x86-64 gcc -Os -fPIE -fno-plt -ffunction-cse。我什至告诉 GCC 优化 code-size;saving/restoring 像 RBX 这样的 call-preserved 寄存器用于 2 字节 call rbx 而不是 6 字节 call [RIP+rel32] 即使在 push/pop RBX(每个 1 个字节)和加载到 RBX(一个具有 RIP-relative 寻址模式的 mov)所需的额外指令之后,也会节省大小。 )

这可能被认为是对 -Os 的优化遗漏,尤其是对于像 -mcpu=cortex-m3 这样的“简单”内核的 ARM Thumb,它甚至可能没有分支预测器。

(AArch64 将 function-pointer 加载到具有 -fPIE -fno-plt 的寄存器中,用于没有“隐藏”可见性的功能,即功能可能只在共享库中。即使 -fno-function-cse 也会发生这种情况。https://godbolt.org/z/f3MP56.)