如何编写/构建 C 代码以避免与现有汇编代码发生冲突?

How to write / build C code to avoid conflicts with existing assembly code?

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

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

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

通常标准的调用约定是很合理的,将一些寄存器指定为call-clobbered,一些指定为call-preserved。使用 call-preserved 寄存器来存储您希望在函数调用中保留下来的值。例如,参见 What are the calling conventions for UNIX & Linux system calls on i386 and x86-64 的 function-calling 约定部分。

标准的但描述性较差的术语是 "caller-saved" 与 "callee saved"(令人困惑,因为没有人保存 call-clobbered 寄存器是正常的,如果你让值消失不需要它),或者 "volatile" 与 "non-volatile":有些虚假,因为 volatile 在 C 中已经具有不相关的特定技术含义。

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


对于 hand-written asm 函数之间的调用,您可以使用任何您想要的自定义调用约定,在 per-function 的基础上在注释中记录约定。 通常最好尽可能使用适用于您的平台的标准调用约定,只有在需要加速时才进行自定义。大多数都相当 well-designed 并且在性能和 code-size 之间取得了很好的平衡,有效地传递参数等等。

规则的一个例外是 i386 32 位调用约定 (用于 Linux)糟透了。它传递堆栈上的所有参数,而不是寄存器。您可以自定义 x86 gcc 将使用的调用约定 with -mregparm=2 -msseregparm for example, to pass the first 2 integer args in eax and edx on 32-bit x86. 32-bit Windows often uses a calling convention like this, e.g. _vectorcall. If you're on x86, see Agner Fog's calling convention guide(以及其他 x86 asm 优化指南)。


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

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

或者你可以告诉gcc一个寄存器是call-preserved(-fcall-saved-reg),这样只要[=81=就可以用] 它。如果您只是希望 gcc 在完成后将其放回原处,而不削弱其释放寄存器的能力,以应对拥有额外寄存器值得 saving/restoring 的情况,这可能就是您想要的。 (如果该 C 代码回调到您的 asm,它会期望您的 asm 函数遵循您告诉它的相同调用约定。)

有趣的是 -fcall-saved-reg 似乎甚至对 arg-passing 寄存器也有效,因此您可以在不重新加载寄存器的情况下进行多个函数调用。

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

请注意,在 return-value 寄存器上使用 -fcall-saved 或在堆栈或帧指针上使用 -fcall-used 是错误的,但 gcc 可能会默默地做一些愚蠢的事情而不是发出警告!

It is an error to use this flag with the frame pointer or stack pointer. Use of this flag for other registers that have fixed pervasive roles in the machine’s execution model produces disastrous results.

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


示例:我使用的是 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) 用于临时寄存器,仅在必须使用 save/restore rbxrbp.[= 时才使用 r8d 34=]

code-gen 基本相同,没有 -fcall-saved-reg 选项,但有不同的寄存器选择,没有 push/pop.