为什么我不能在C中获取asm寄存器的值?
我正在尝试获取汇编寄存器rdi, rsi, rdx, rcx, 的值r8,但是我得到了错误的值,所以我不知道我正在做的是获取这些值还是告诉编译器在这些值上写入寄存器,如果是这种情况,我怎么能实现我想要做的事情(将汇编寄存器的值放在 C 变量中)?
当此代码编译时(使用gcc -S test.c)
#include <stdio.h>
void beautiful_function(int a, int b, int c, int d, int e) {
register long rdi asm("rdi");
register long rsi asm("rsi");
register long rdx asm("rdx");
register long rcx asm("rcx");
register long r8 asm("r8");
const long save_rdi = rdi;
const long save_rsi = rsi;
const long save_rdx = rdx;
const long save_rcx = rcx;
const long save_r8 = r8;
printf("%ldn%ldn%ldn%ldn%ldn", save_rdi, save_rsi, save_rdx, save_rcx, save_r8);
}
int main(void) {
beautiful_function(1, 2, 3, 4, 5);
}
它输出以下汇编代码(在函数调用之前):
movl $1, %edi
movl $2, %esi
movl $3, %edx
movl $4, %ecx
movl $5, %r8d
callq _beautiful_function
当我编译和执行它输出这个:
0
0
4294967296
140732705630496
140732705630520
(some undefined values)
我做错了什么 ?我怎么能做到这一点?
回答
您的代码不起作用,因为指定局部变量的寄存器明确告诉您不要执行您所做的操作:
此功能唯一支持的用途是在调用 Extended 时为输入和输出操作数指定寄存器
asm(请参阅Extended Asm)。
除了调用 Extended 时
asm,不保证指定寄存器的内容。因此,明确不支持以下用途。如果它们看起来有效,那只是偶然,并且可能由于周围代码中(看似)无关的更改,甚至是 gcc 未来版本的优化中的微小更改而停止按预期工作:
- 向 Basic 传递参数或从 Basic 传递参数
asm- 在
asm不使用输入或输出操作数的情况下将参数传入或传出扩展。- 使用非标准调用约定将参数传入或传出用汇编器(或其他语言)编写的例程。
要将寄存器的值放在变量中,您可以使用 Extended asm,如下所示:
long rdi, rsi, rdx, rcx;
register long r8 asm("r8");
asm("" : "=D"(rdi), "=S"(rsi), "=d"(rdx), "=c"(rcx), "=r"(r8));
但请注意,即使这样也可能无法满足您的要求:编译器有权将函数的参数复制到其他地方,并在 Extendedasm运行之前将寄存器重用于不同的内容,或者如果您从未阅读过,甚至根本不传递参数通过普通的 C 变量。(事实上,即使我发布的内容在启用优化时也不起作用。)如果你想做你正在做的事情,你应该强烈考虑只在汇编中编写你的整个函数,而不是在 C 函数中编写内联汇编。
回答
即使你有一个有效的方法来做到这一点(这不是),它可能只在没有 inlined的函数的顶部才有意义。所以你可能需要__attribute__((noinline, noclone)). (noclone 是一个 GCC 属性,clang 会警告无法识别;这意味着不要使用较少的实际参数制作函数的替代版本,在其中一些是可以传播到克隆中的已知常量的情况下调用.)
register ... asm除非用作扩展 Asm 语句的操作数,否则不能保证本地变量执行任何操作。如果未初始化,GCC 有时仍会读取命名寄存器,但 clang 不会。(而且看起来你在 Mac 上,gcc命令实际上是 clang,因为很多构建脚本使用gcc而不是cc.)
因此,即使没有优化,您的独立非内联版本beautiful_function在读取您的rdiC 变量时也只是读取未初始化的堆栈空间const long save_rdi = rdi;。(GCC 确实碰巧在此处执行您想要的操作,即使在-Os- 优化但选择不内联您的函数。请参阅Godbolt 上的 clang 和 GCC(针对 Linux),以及 asm + 程序输出。)。
使用asm语句来register asm做某事
(这做你说你想要的(读取寄存器),但由于其他优化,当调用者可以看到定义时,仍然不会产生 1 2 3 4 5 with clang。只有实际的 GCC。可能有一个 clang 选项禁用一些相关的 IPA / IPO 优化,但我没有找到。)
您可以使用asm volatile()带有空模板字符串的语句来告诉编译器这些寄存器中的值现在是这些 C 变量的值。(register ... asm声明强制它为正确的变量选择正确的寄存器)
#include <stdlib.h>
#include <stdio.h>
__attribute__((noinline,noclone))
void beautiful_function(int a, int b, int c, int d, int e) {
register long rdi asm("rdi");
register long rsi asm("rsi");
register long rdx asm("rdx");
register long rcx asm("rcx");
register long r8 asm("r8");
// "activate" the register-asm locals:
// associate register values with C vars here, at this point
asm volatile("nop # asm statement here" // can be empty, nop is just because Godbolt filters asm comments
: "=r"(rdi), "=r"(rsi), "=r"(rdx), "=r"(rcx), "=r"(r8) );
const long save_rdi = rdi;
const long save_rsi = rsi;
const long save_rdx = rdx;
const long save_rcx = rcx;
const long save_r8 = r8;
printf("%ldn%ldn%ldn%ldn%ldn", save_rdi, save_rsi, save_rdx, save_rcx, save_r8);
}
int main(void) {
beautiful_function(1, 2, 3, 4, 5);
}
这使得 asmbeautiful_function确实捕获了寄存器的传入值。(它不内联,并且编译器碰巧在执行任何这些寄存器的 asm 语句之前没有使用任何指令。通常不能保证后者。)
在 Godbolt 上使用 clang -O3 和 gcc -O3
gcc -O3 确实有效,打印出您期望的内容。clang 仍然打印垃圾,因为调用者看到 args 未使用,并决定不设置这些寄存器。(如果您对调用者隐藏了定义,例如在另一个没有 LTO 的文件中,则不会发生这种情况。)
(使用 GCC、noninline、noclone 属性足以禁用此过程间优化,但不能使用 clang。甚至不编译-fPIC使这成为可能。我想这个想法是符号插入来提供它的替代定义beautiful_function确实使用它的args 将违反 C 中的单一定义规则。因此,如果 clang 可以看到函数的定义,则它假定该函数就是这样工作的,即使实际上不允许将其内联。)
随着叮当声:
main:
pushq %rax # align the stack
# arg-passing optimized away
callq beautiful_function@PLT
# indirect through the PLT because I compiled for Linux with -fPIC,
# and the function isn't "static"
xorl %eax, %eax
popq %rcx
retq
但实际的定义beautiful_function正是你想要的:
# clang -O3
beautiful_function:
pushq %r14
pushq %rbx
nop # asm statement here
movq %rdi, %r9 # copying all 5 register outputs to different regs
movq %rsi, %r10
movq %rdx, %r11
movq %rcx, %rbx
movq %r8, %r14
leaq .L.str(%rip), %rdi
xorl %eax, %eax
movq %r9, %rsi # then copying them to printf args
movq %r10, %rdx
movq %r11, %rcx
movq %rbx, %r8
movq %r14, %r9
popq %rbx
popq %r14
jmp printf@PLT # TAILCALL
GCC 浪费的指令更少,例如,从movq %r8, %r9将您的r8C var 作为第 6 个参数移动到 printf 开始。然后movq %rcx, %r8设置第 5 个参数,在读取所有输出寄存器之前覆盖其中一个输出寄存器。叮当声有些过分谨慎。然而,clang 仍然%r12在 asm 语句周围推送/弹出;我不明白为什么。它以尾调用 printf 结束,所以它不是为了对齐。
有关的:
- 如何指定一个特定的寄存器来分配 GCC 内联汇编中的 C 表达式的结果?- 相反的问题:在特定点在特定寄存器中实现 C 变量值。
- 将寄存器值读入 C 变量- 以前的规范问答使用现在不受支持的
register ... asm("regname")方法,就像您尝试的那样。或者使用 register-asm全局变量,否则会影响所有代码的效率,否则会影响它。 - 我忘了我已经回答了那个问答,提出了与此基本相同的观点。还有其他一些要点,例如这不适用于堆栈指针等寄存器。