如何编写/构建 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-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) 用于临时寄存器,仅在必须使用 save/restore rbx
或 rbp
.[= 时才使用 r8d
34=]
code-gen 基本相同,没有 -fcall-saved-reg
选项,但有不同的寄存器选择,没有 push/pop.
我需要将一些 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-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) 用于临时寄存器,仅在必须使用 save/restore rbx
或 rbp
.[= 时才使用 r8d
34=]
code-gen 基本相同,没有 -fcall-saved-reg
选项,但有不同的寄存器选择,没有 push/pop.