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

标签 c gcc assembly interop calling-convention

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

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

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

最佳答案

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

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

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

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

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

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

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

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

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

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

注意使用-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) 作为暂存寄存器,只使用 r8d如果它必须保存/恢复rbxrbp .

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

关于c - 如何编写/构建 C 代码以避免与现有汇编代码冲突?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49474166/

相关文章:

Windows 64 ABI,如果我不调用 Windows API,正确的寄存器使用?

c - 将文本文件导出到 excel 以使用 'C' 程序绘制图形

c - 如何修复 ' Error: invalid read of size 4'

c - Eclipse 在分配 2D 数组元素期间无消息终止

c++ - 为什么 GCC 在我的机器上创建额外的汇编指令?

assembly - X 在 EAX、EBX、ECX 中是什么意思……在汇编中?

使用 `clone()` 创建子进程。为什么 `wait()` 不等待它终止?

c++ - 当派生类的析构函数是虚拟的而基类的 dtor 不是时代码崩溃

c++ - 在每个平台上包含来自同一文件的 header

c - 如何修复 68HC11 编译器生成无效的 JMP/BRA 代码