通常标准调用约定是相当合理的,并且将一些寄存器指定为调用破坏,而另一些则指定为调用保留。对希望在函数调用中存活的值使用调用保留寄存器。例如,参见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 上传递 eax 和 edx 中的前 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-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)作为暂存寄存器,只有在必须保存/恢复 rbx 或 rbp 时才使用 r8d。
没有-fcall-saved-reg 选项的代码生成基本相同,但有不同的寄存器选择并且没有推送/弹出。