我需要将一些 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-64与
gcc6.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
如果它必须保存/恢复rbx
或 rbp
.没有
-fcall-saved-reg
的代码生成基本相同选项,但有不同的寄存器选择并且没有推送/弹出。
关于c - 如何编写/构建 C 代码以避免与现有汇编代码冲突?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49474166/