【问题标题】:How to write / build C code to avoid conflicts with existing assembly code?如何编写/构建 C 代码以避免与现有汇编代码冲突?
【发布时间】:2018-09-03 13:50:54
【问题描述】:

我需要将一些 C 代码与以汇编语言编写的现有项目集成。该项目出于内部目的使用了许多寄存器,因此我不希望 C 代码覆盖它们。

我可以指定 GCC 可以/不能使用哪些寄存器吗?还是我应该在调用 C 代码之前保存寄存器然后恢复它们?

另外,还有哪些需要注意的注意事项?

【问题讨论】:

  • C 编译器很可能会使用 ABI,这会保留一些寄存器。其他的都是易变的,如果您在呼叫站点有重要数据,请保存并恢复它们。通常一个告诉 gcc 哪些寄存器被破坏了,而不是相反。 IDK 如果可能的话,保存/恢复似乎更容易。

标签: c gcc assembly interop calling-convention


【解决方案1】:

通常标准调用约定是相当合理的,并且将一些寄存器指定为调用破坏,而另一些则指定为调用保留。对希望在函数调用中存活的值使用调用保留寄存器。例如,参见What are the calling conventions for UNIX & Linux system calls on i386 and x86-64 的函数调用约定部分。

标准但描述性较差的术语是“调用者保存”与“被调用者保存”(令人困惑,因为没有人保存调用破坏寄存器是正常的,如果你不需要它就让值消失),或“易失性”与“非易失性”:有点假,因为 volatile 在 C 中已经具有不相关的特定技术含义。

我喜欢 call-preserved 和 call-clobbered,因为它从使用它们的当前函数的角度描述了这两种寄存器。


您可以使用任何您想要的自定义调用约定在手写 asm 函数之间进行调用,并基于每个函数在 cmets 中记录约定。 通常最好尽可能使用您平台的标准调用约定,仅在需要加速时进行自定义。大多数都经过精心设计,在性能和代码大小、有效传递参数等方面取得了良好的平衡。

该规则的一个例外是 i386 32 位调用约定 (在 Linux 上使用)很烂。它传递堆栈上的所有参数,而不是寄存器。您可以自定义调用约定 x86 gcc 将使用 with -mregparm=2 -msseregparm 例如,在 32 位 x86 上传递 eaxedx 中的前 2 个整数参数。 32 位 Windows 经常使用这样的调用约定,例如_vectorcall。如果您使用的是 x86,请参阅 Agner Fog's calling convention guide(和其他 x86 asm 优化指南)。


GCC 确实有 some code-gen options 修改调用约定寄存器。

你可以用-ffixed-reg 告诉 gcc 它不能触及寄存器,例如-ffixed-rbx(例如,它在中断或信号处理程序中仍然具有您的价值)。

或者你可以告诉 gcc 一个寄存器是调用保留的 (-fcall-saved-reg),所以只要它保存/恢复它就可以使用它。如果您只想让 gcc 在完成后将其放回原处,而不会削弱它在拥有额外寄存器值得保存/恢复的情况下释放寄存器的能力,这可能就是您想要的。 (如果该 C 代码回调到您的 asm,它将期望您的 asm 函数遵循您告诉它的相同调用约定。)

有趣的是,-fcall-saved-reg 似乎甚至适用于传递参数的寄存器,因此您可以在不重新加载寄存器的情况下进行多个函数调用。

最后,-fcall-used-reg 告诉编译器可以随意破坏寄存器。

请注意,在返回值寄存器上使用 -fcall-saved 或在堆栈或帧指针上使用 -fcall-used 是错误的,但 gcc 可能会默默地做傻事而不是警告!

将此标志与帧指针或堆栈指针一起使用是错误的。 将此标志用于在机器执行模型中具有固定普遍角色的其他寄存器会产生灾难性的结果。

因此,如果您以愚蠢的方式使用这些高级选项,它们可能无法保护您自己;戴上你的护目镜+安全帽。您已收到警告。


示例:我使用的是 x86-64,但它应该适用于任何其他架构。

// tempt the compiler into using lots of registers
// to keep values across loop iterations.
int foo(int a, int *p, int len) {
    int t1 = a * 2, t2 = a-1, t3 = a>>3;
    int max= p[0];

    for (int i=0 ; i<len ; i++) {
        p[i] *= t1;
        p[i] |= t2;
        p[i] ^= t3;
        max = (p[i] < max) ? max : p[i];
    }

    return max;
}

On Godbolt for x86-64gcc6.3 -O3 -fcall-saved-rdx -fcall-saved-rcx -fcall-saved-rsi -fno-tree-vectorize

foo:        # args in the x86-64 SysV convention: int edi, int *rsi, int edx
    lea     r9d, [rdi+rdi]
    lea     r10d, [rdi-1]
    mov     eax, DWORD PTR [rsi]
    sar     edi, 3
    test    edx, edx          # check if loop runs at least once: len <= 0
    jle     .L10
    push    rsi               # save of normally volatile RSI
    lea     r8d, [rdx-1]
    push    rdx               # and RDX
    lea     r11, [rsi+4+r8*4]
.L3:
    mov     r8d, DWORD PTR [rsi]
    imul    r8d, r9d          # and use of temporaries that require a REX prefix
    or      r8d, r10d
    xor     r8d, edi
    cmp     eax, r8d
    mov     DWORD PTR [rsi], r8d
    cmovl   eax, r8d
    add     rsi, 4            # pointer-increment of RSI as the loop counter
    cmp     r11, rsi
    jne     .L3
    pop     rdx               # and restore RDX + RSI
    pop     rsi
.L10:
    ret

注意使用 r8-r11 作为临时文件。这些寄存器需要一个 REX 前缀才能访问,增加 1 个字节的代码大小,除非您已经需要 32 位操作数大小。所以 gcc 更喜欢使用低 8 位寄存器(eax..ebp)作为暂存寄存器,只有在必须保存/恢复 rbxrbp 时才使用 r8d

没有-fcall-saved-reg 选项的代码生成基本相同,但有不同的寄存器选择并且没有推送/弹出。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-03-22
    • 1970-01-01
    • 1970-01-01
    • 2018-05-07
    • 1970-01-01
    • 2019-10-01
    • 2011-08-27
    相关资源
    最近更新 更多