【问题标题】:Prevent GCC from using dynamic jumps / functions invocations防止 GCC 使用动态跳转/函数调用
【发布时间】:2020-10-01 07:11:21
【问题描述】:

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

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

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 编译器)?非常感谢您的帮助

【问题讨论】:

    标签: assembly gcc msp430 mspgcc


    【解决方案1】:

    使用-fno-function-cse 不对函数地址执行公共子表达式消除。 GCC manual:

    -fno-function-cse

    不要将函数地址放在寄存器中;使调用常量函数的每条指令都包含该函数的 明确地址。

    此选项会导致代码效率降低,但会出现一些奇怪的 hack 改变汇编器输出的可能会被优化混淆 不使用此选项时执行。

    默认是-ffunction-cse


    如何找到特定的 GCC 选项

    我查看了gcc -O1 -fverbose-asm asm 输出以查看-O1 暗示的所有优化选项(GCC 在 asm cmets 中列出)。 -O1 -fno-... 版本的所有东西都编译成 3 个 call 指令,每个指令都带有符号名称,确认其中一个是我想要的,所以我只需要通过平分 -fno- 选项列表来缩小范围/p>

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

    由于有很多选项,我使用tr ' ' '\n' | sed -e 's/-f/-fno-/' -e '/;/d'-f 选项转换为否定形式。我将整个 asm cmets 块复制/粘贴到终端中的该命令中,然后将结果复制/粘贴到 Godbolt 上的 GCC 选项框中。 (与-O1 一起。-O0 是一种用于一致调试的特殊反优化模式,因此即使使用正确的选项,跨语句优化也可能永远不会在-O0 处于活动状态。这就是为什么我需要否定这些选项尝试没有-O1的积极形式)

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

    这比一次只看一个选项或翻阅手册要快,因为我什至不确定这些特定选项中的任何一个能否控制这一点。


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

    It is a code-size win on x86,其中 5 字节 mov-immediate 或 7 字节 RIP-relative lea (x86-64) 可以设置多个 2 字节 call 指令。

    在 AArch64 或 ARM(Thumb 模式除外)等许多固定指令宽度的 ISA 上,它通常甚至不是代码大小的胜利,其中标准代码模型假设函数在相对分支的范围内 - and-link(调用)指令。因此,调用任何函数都需要一条指令,与任何其他指令的大小相同。

    即使显式启用了-ffunction-cse,GCC 也不会针对 x86-64 或 ARM thumb 进行此优化,即使在它已经使用 GOT 中的函数指针的情况下也是如此。 (Godbolt 上的 x86-64 gcc -Os -fPIE -fno-plt -ffunction-cse。我什至告诉 GCC 优化代码大小;保存/恢复像 RBX 这样的调用保留寄存器以用于 2 字节 call rbx 而不是 6 字节 call [RIP+rel32] 会即使在推送/弹出 RBX(每个 1 字节)并加载到 RBX(一个具有 RIP 相对寻址模式的 mov)所需的额外指令之后,也可以节省大小。)

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

    (AArch64 将函数指针加载到带有 -fPIE -fno-plt 的寄存器中,用于没有“隐藏”可见性的函数,即函数可能只在共享库中。即使使用-fno-function-cse.https://godbolt.org/z/f3MP56.)

    【讨论】:

    • 看起来 aarch64 上的 gcc 确实做到了:godbolt.org/z/f3MP56
    • @NateEldredge:谢谢,我没有详细检查 AArch64。不过,即使-fno-function-cse 也没有阻止它的发生。在-O0 它确实adrp/ldr/blr r0 为每个块分别使用来自 GOT 的函数指针。对于-fPIE -fno-plt,如果它不知道该函数将链接到同一个可执行文件中,则它永远无法使用相对分支,如果事实证明不是,则没有任何东西可以发明 PLT 条目。 (或者它不想依赖链接器)。我没有想过 -fno-plt 在内存间接调用不是 1 insn 的非 x86 ISA 上有多么不同。
    猜你喜欢
    • 2014-01-15
    • 2016-11-18
    • 1970-01-01
    • 1970-01-01
    • 2013-02-22
    • 2017-02-15
    • 1970-01-01
    • 1970-01-01
    • 2018-12-31
    相关资源
    最近更新 更多