如何使用 gcc 风格的内联汇编打印寄存器号?
How to print the register number with gcc-style inline assembly?
灵感来自 。
gcc 风格的内联汇编的一个用例是对编译器和汇编器都不知道的指令进行编码。例如,我给出了 如何在太旧而无法支持的工具链上使用 rdrand
指令:
/* "rdrand %%rax ; setc %b1" */
asm volatile (".byte 0x48, 0x0f, 0xc7, 0xf0; setc %b1"
: "=a"(result), "=qm"(success) :: "cc");
不幸的是,对指令进行硬编码意味着您还需要对其使用的寄存器进行硬编码,从而大大降低了编译器执行寄存器分配的自由度。
在某些体系结构(如 RISC-V 及其 .insn
指令)上,汇编器提供了一种系统地构建原始指令的方法,但这似乎是例外。
一个简单的解决方案是有一种方法来获取寄存器的未修饰数字,以手动将其编码到指令中。例如,假设存在模板修饰符 X
来打印所选寄存器的编号。然后,上面的例子可以变得更加灵活:
/* "rdrand %0 ; setc %b1" */
asm volatile (".byte 0x48 | (%X0 >> 3), 0x0f, 0xc7, 0xf0 | (%X0 & 7); setc %b1"
: "=r"(result), "=qm"(success) :: "cc");
同样,如果有办法让 gcc 在 ARM64 上为 SIMD 寄存器 12 打印 12
而不是 v12
,则可以执行如下操作:
float32x4_t add3(float32x4_t a, float32x4_t b)
{
float32x4_t c;
/* fadd %0, %1, %2 */
asm (".inst 0x4e20d40 + %X0 + (%X1<<5) + (%X2<<16)" : "=w"(c) : "w"(a), "w"(b));
return c;
}
有没有办法获取注册号?如果不是,还有哪些其他选项可以对编译器和汇编程序都不知道的指令进行编码,而不必对寄存器编号进行硬编码?
我实际上遇到了同样的问题并提出了以下解决方案。
#define REG_CONST(n) asm(".equ .L__reg_const__v" #n ", " #n);
REG_CONST(0)
REG_CONST(1)
REG_CONST(2)
REG_CONST(3)
// ... repeat this for all register numbers ...
REG_CONST(27)
REG_CONST(28)
REG_CONST(29)
REG_CONST(30)
float32x4_t add3(float32x4_t a, float32x4_t b) {
float32x4_t c;
// fadd %0, %1, %2
asm(".inst 0x4e20d40 | .L__reg_const__%0 | (.L__reg_const__%1 << 5) + (.L__reg_const__%2 << 16)" : "=w"(c) : "w"(a), "w"(b));
return c;
}
这是如何工作的?
- 请记住,像
%X1
这样的占位符将由编译器 before 填充寄存器名称和简单的字符串替换,然后再将结果传递给汇编程序。
- 在汇编文件中,我们可以使用
.equ
指令来定义符号来表示整数。 (以 .L
开头的符号在生成的目标文件中将不可见,因此我们不会不必要地混淆符号 table)
REG_CONST
宏的每次调用都会定义一个(本地)符号:.L__reg_const__v0
等于 0,.L__reg_const__v1
等于 1,.L__reg_const__v2
到 2,依此类推。
- 宏有意放在文件的顶部,在任何函数之外,因为生成的
asm(".equ .L__reg_const__v0 0")
表达式应该放在程序集文件的顶部。
- 在
add3
函数内的 asm(".inst ...")
模板中,%X0
、%X1
、%X2
将被编译器选择的任何寄存器替换a
、b
和 c
。
- 由于我们偷偷在
.L__reg_const__
表达式后直接写了没有任何 space 的占位符,替换将把它变成像 .L__reg_const__v7
. 这样的表达式
- 但这正好对应我们在顶部定义的整数符号的名称!所以汇编程序实际上会将其作为符号并将其替换为我们定义的整数值。
- 对符号求值后,结果是一个纯数字表达式,汇编器会愉快地将整数值“或”在一起,产生所需的操作码。
灵感来自
gcc 风格的内联汇编的一个用例是对编译器和汇编器都不知道的指令进行编码。例如,我给出了 rdrand
指令:
/* "rdrand %%rax ; setc %b1" */
asm volatile (".byte 0x48, 0x0f, 0xc7, 0xf0; setc %b1"
: "=a"(result), "=qm"(success) :: "cc");
不幸的是,对指令进行硬编码意味着您还需要对其使用的寄存器进行硬编码,从而大大降低了编译器执行寄存器分配的自由度。
在某些体系结构(如 RISC-V 及其 .insn
指令)上,汇编器提供了一种系统地构建原始指令的方法,但这似乎是例外。
一个简单的解决方案是有一种方法来获取寄存器的未修饰数字,以手动将其编码到指令中。例如,假设存在模板修饰符 X
来打印所选寄存器的编号。然后,上面的例子可以变得更加灵活:
/* "rdrand %0 ; setc %b1" */
asm volatile (".byte 0x48 | (%X0 >> 3), 0x0f, 0xc7, 0xf0 | (%X0 & 7); setc %b1"
: "=r"(result), "=qm"(success) :: "cc");
同样,如果有办法让 gcc 在 ARM64 上为 SIMD 寄存器 12 打印 12
而不是 v12
,则可以执行如下操作:
float32x4_t add3(float32x4_t a, float32x4_t b)
{
float32x4_t c;
/* fadd %0, %1, %2 */
asm (".inst 0x4e20d40 + %X0 + (%X1<<5) + (%X2<<16)" : "=w"(c) : "w"(a), "w"(b));
return c;
}
有没有办法获取注册号?如果不是,还有哪些其他选项可以对编译器和汇编程序都不知道的指令进行编码,而不必对寄存器编号进行硬编码?
我实际上遇到了同样的问题并提出了以下解决方案。
#define REG_CONST(n) asm(".equ .L__reg_const__v" #n ", " #n);
REG_CONST(0)
REG_CONST(1)
REG_CONST(2)
REG_CONST(3)
// ... repeat this for all register numbers ...
REG_CONST(27)
REG_CONST(28)
REG_CONST(29)
REG_CONST(30)
float32x4_t add3(float32x4_t a, float32x4_t b) {
float32x4_t c;
// fadd %0, %1, %2
asm(".inst 0x4e20d40 | .L__reg_const__%0 | (.L__reg_const__%1 << 5) + (.L__reg_const__%2 << 16)" : "=w"(c) : "w"(a), "w"(b));
return c;
}
这是如何工作的?
- 请记住,像
%X1
这样的占位符将由编译器 before 填充寄存器名称和简单的字符串替换,然后再将结果传递给汇编程序。 - 在汇编文件中,我们可以使用
.equ
指令来定义符号来表示整数。 (以.L
开头的符号在生成的目标文件中将不可见,因此我们不会不必要地混淆符号 table) REG_CONST
宏的每次调用都会定义一个(本地)符号:.L__reg_const__v0
等于 0,.L__reg_const__v1
等于 1,.L__reg_const__v2
到 2,依此类推。- 宏有意放在文件的顶部,在任何函数之外,因为生成的
asm(".equ .L__reg_const__v0 0")
表达式应该放在程序集文件的顶部。 - 在
add3
函数内的asm(".inst ...")
模板中,%X0
、%X1
、%X2
将被编译器选择的任何寄存器替换a
、b
和c
。 - 由于我们偷偷在
.L__reg_const__
表达式后直接写了没有任何 space 的占位符,替换将把它变成像.L__reg_const__v7
. 这样的表达式
- 但这正好对应我们在顶部定义的整数符号的名称!所以汇编程序实际上会将其作为符号并将其替换为我们定义的整数值。
- 对符号求值后,结果是一个纯数字表达式,汇编器会愉快地将整数值“或”在一起,产生所需的操作码。