将寄存器值读入C变量

48

我记得曾经看到一种使用扩展的gcc内嵌汇编来读取寄存器值并将其存储到C变量中的方法。

但是,我无论如何都记不起如何形成asm语句了。


2
当您的C代码开始执行时,您如何知道EBX中包含什么内容? - florin
当内联asm语句运行时,你无法知道编译器生成的代码将在任何寄存器中存储什么值,因此该值通常是无意义的,最好使用调试器在断点处停止并查看寄存器值。对于堆栈指针可能有意义,但是可以使用__builtin_frame_address(0)来获取堆栈地址(如果我没记错的话,这个函数会生成完整的堆栈帧,即使启用了-fomit-frame-pointer,例如默认情况下在x86上启用)。 - Peter Cordes
由于此处的最高投票答案已过时(在clang中无法使用,在GCC中也不受支持),因此将其作为新问答的重复关闭。 (至少在简单情况下,它仍然可以在GCC中使用。)对一个执行“mov %%reg,%0”到一个“= r”(var)输出的asm语句也是安全的,那个答案也很好。 - Peter Cordes
8个回答

36
编辑注:使用本地寄存器-asm变量的方式现在已被GCC记录为“不支持”。虽然它通常在GCC上运行,但会在clang上出现问题。(我想这个文档中的措辞是在发布此答案之后添加的。)
全局固定寄存器变量版本对于32位x86来说具有很大的性能成本,因为它只有7个GP整数寄存器(不包括堆栈指针)。这将把它减少到6个。只有在您拥有所有代码都会频繁使用的全局变量时才考虑这一点。

和其他答案不同,我想走一个不同的方向,因为我不确定你想要什么。

GCC手册第5.40节 指定寄存器中的变量

register int *foo asm ("a5");

Here a5 is the name of the register which should be used…

Naturally the register name is cpu-dependent, but this is not a problem, since specific registers are most often useful with explicit assembler instructions (see Extended Asm). Both of these things generally require that you conditionalize your program according to cpu type.

Defining such a register variable does not reserve the register; it remains available for other uses in places where flow control determines the variable's value is not live.

GCC手册§3.18代码生成约定选项

-ffixed-reg

将名为reg的寄存器视为固定寄存器;生成的代码不应该引用它(除了作为堆栈指针、帧指针或其他固定角色之一)。

这可以以更简单的方式复制Richard的答案。

int main() {
    register int i asm("ebx");
    return i + 1;
}

尽管这没有任何意义,因为你不知道 ebx 寄存器中有什么。
如果将这两个结合在一起,使用 gcc -ffixed-ebx 编译。
#include <stdio.h>
register int counter asm("ebx");
void check(int n) {
    if (!(n % 2 && n % 3 && n % 5)) counter++;
}
int main() {
    int i;
    counter = 0;
    for (i = 1; i <= 100; i++) check(i);
    printf("%d Hamming numbers between 1 and 100\n", counter);
    return 0;
}

你可以确保C变量始终驻留在寄存器中以实现快速访问,并且不会被其他生成的代码破坏。 (方便地, ebx 在通常的x86调用约定下是被调用者保存的,因此即使它被编译为没有使用 -ffixed- * 的其他函数调用所破坏,它也应该恢复。)另一方面,这绝对不是可移植的,通常也不是性能上的优势,因为你限制了编译器的自由。

1
引用当前文档中描述本地寄存器的内容:“此功能的唯一支持用途是在调用扩展汇编时指定输入和输出操作数的寄存器。” 因此,像这样将i放在main()中是不受支持的。并且要强调一点:x86只有有限数量的寄存器。通过全局寄存器变量从通用使用中删除一个可能会减慢代码的其他关键部分。这里有一些讨论here - David Wohlferd
1
我强烈建议不要使用全局寄存器变量,除非只在一个包含一个函数的.c文件中作为一种hack。这将会带来显著的性能成本,特别是在32位x86上。 - Peter Cordes

24

以下是获取 ebx 的方法:

int main()
{
    int i;
    asm("\t movl %%ebx,%0" : "=r"(i));
    return i + 1;
}

结果:
main:
    subl    $4, %esp
    #APP
             movl %ebx,%eax
    #NO_APP
    incl    %eax
    addl    $4, %esp
    ret


编辑:

"=r"(i)"是一个输出约束,告诉编译器第一个输出(%0)是一个应该被放置在变量“i”中的寄存器。在这个优化级别(-O5)下,变量i永远不会被存储到内存中,而是保存在eax寄存器中,这也恰好是返回值寄存器。


2
我会使用=rm约束而不是=r。编译器的优化器将尝试选择最佳路径。如果内联汇编器恰好处于寄存器饥饿状态,则=r可能会强制其生成不太优化的代码。如果=rm恰好是最佳选择,那么它将给优化器一个使用内存引用的机会。在这个简单的例子中,这不会成为问题,但如果代码处于更复杂的情况下,给编译器提供选项可能会有益。 - Michael Petch
1
@MichaelPetch "=b" 和一个空的模板字符串怎么样? - David Wohlferd
请注意,如果您使用“=rm”,clang通常会选择内存,即使它实际上需要寄存器中的值。 它最终将存储和重新加载。 这是clang内联asm支持中长期未优化的问题。 使用“=b”(i)也应该可以,只需告诉编译器EBX在asm语句之后保存了“i”的值。 如果您在多个地方使用此功能,则可能需要使用“asm volatile”,否则编译器可以假定asm语句始终产生相同的输出(因为输入始终相同:空输入集)。 - Peter Cordes
-O5优化?我读过O3是最大的吗? - Ilan Schemoul
在我的情况下,出现了“warning: implicit declaration of function 'asm'”和“error: expected ')' before ':' token”:https://godbolt.org/z/PbqsMocEn。我错过了什么吗?@IlanSchemoul关于“-O5”的回复:“据我所知,如果N>3,则N=3”。因此,-O5就是-O3。 - pmor
更新。似乎需要使用__asm volatile而不是asm - pmor

6

我不知道gcc,但在VS中是这样的:

int data = 0;   
__asm
{
    mov ebx, 30
    mov data, ebx
}
cout<<data;

基本上,我将ebx中的数据移动到您的变量data中。


当然是仅限于x86。微软的x64和Itanium编译器不支持内联汇编。 - ephemient
我认为汇编代码会被翻译成 mov ebx, 30 mov dword ptr[data], ebx - Sridarshan
2
为什么不直接使用 mov data, 30 呢? - user2742371

5
这将把堆栈指针寄存器移动到sp变量中。
intptr_t sp;
asm ("movl %%esp, %0" : "=r" (sp) );

只需将“esp”替换为您感兴趣的实际寄存器(但确保不要丢失%%),并将“sp”替换为您的变量。



2
#include <stdio.h>

void gav(){
        //rgv_t argv = get();
        register unsigned long long i asm("rax");
        register unsigned long long ii asm("rbx");
        printf("I`m gav - first arguman is: %s - 2th arguman is: %s\n", (char *)i, (char *)ii);
}

int main(void)
{
    char *test = "I`m main";
    char *test1 = "I`m main2";
    printf("0x%llx\n", (unsigned long long)&gav);
    asm("call %P0" : :"i"((unsigned long long)&gav), "a"(test), "b"(test1));
    return 0;
}

2
引用当前文档(https://gcc.gnu.org/onlinedocs/gcc/Local-Register-Variables.html)中的描述本地寄存器变量的内容,“该特性的唯一支持用途是在调用扩展内联汇编时指定输入和输出操作数的寄存器。” 因此,像这样将iii放在gav()中是不受支持的。 - David Wohlferd

2

当您的内联asm语句运行时,您无法知道编译器生成的代码将在任何寄存器中存储什么值,因此该值通常是无意义的,您最好使用调试器在断点处查看寄存器的值。

话虽如此,如果您要执行这个奇怪的任务,那么最好高效地完成它。

在某些目标(例如x86)上,您可以使用特定寄存器输出约束来告诉编译器输出将在哪个寄存器中。使用一个空的asm模板(零条指令)和一个特定寄存器输出约束来告诉编译器,您的asm语句不关心输入的寄存器值,但是给定的C变量将在该寄存器中。

#include <stdint.h>

int foo() {
    uint64_t rax_value;           // type width determines register size
    asm("" : "=a"(rax_value));  // =letter determines which register (or partial reg)

    uint32_t ebx_value;
    asm("" : "=b"(ebx_value));

    uint16_t si_value;
    asm("" : "=S"(si_value) );

    uint8_t sil_value;  // x86-64 required to use the low 8 of a reg other than a-d
       // With -m32:  error: unsupported size for integer register
    asm("# Hi mom, my output constraint picked %0" : "=S"(sil_value) );

    return sil_value + ebx_value;
}

使用 Godbolt 上的 x86-64 clang5.0 编译。请注意,优化掉了两个未使用的输出值,没有#APP / #NO_APP编译器生成的汇编注释对(这些对将汇编器切换到/进入快速解析模式,或者至少曾经是这样)。这是因为我没有使用asm volatile,它们具有输出操作数,因此它们不是隐式的volatile
foo():                                # @foo()
# BB#0:
    push    rbx
    #APP
    #NO_APP
    #DEBUG_VALUE: foo:ebx_value <- %EBX
    #APP
    # Hi mom, my output constraint picked %sil
    #NO_APP
    #DEBUG_VALUE: foo:sil_value <- %SIL
    movzx   eax, sil
    add     eax, ebx
    pop     rbx
    ret
                                    # -- End function
                                    # DW_AT_GNU_pubnames
                                    # DW_AT_external

请注意编译器生成的代码,直接从指定的寄存器中添加两个输出。还要注意推入/弹出RBX,因为在x86-64 System V调用约定中,RBX是一个保存调用的寄存器。(基本上所有32位和64位x86调用约定都是如此)。但我们已经告诉编译器,我们的asm语句在那里写入了一个值。(使用空的asm语句有点像hack;没有语法直接告诉编译器我们只想读取一个寄存器,因为就像我说的,在插入您的asm语句时,您不知道编译器正在使用寄存器做什么。)
编译器将把您的asm语句视为实际写入该寄存器,因此如果它需要稍后使用该值,则在您的asm语句“运行”时,它将把它复制到另一个寄存器(或溢出到内存)。
其他x86寄存器约束包括b(bl/bx/ebx/rbx)、c(.../rcx)、d(.../rdx)、S(sil/si/esi/rsi)和D(.../rdi)。尽管在没有帧指针的函数中bpl/bp/ebp/rbp不是特殊的,但没有特定的约束。(也许是因为使用它会使您的代码无法与-fno-omit-frame-pointer编译。)
你可以使用 register uint64_t rbp_var asm ("rbp"),这样 asm("" : "=r" (rbp_var)); 会确保 "=r" 约束将选择 rbp。对于没有任何显式约束的 r8-r15,也是同样的道理。在一些体系结构中,例如 ARM,asm 寄存器变量是指定要用于 asm 输入/输出约束的唯一方法。(并且请注意,asm 约束是 register asm 变量的唯一支持用法;不能保证变量的值在其他时间在该寄存器中。)
“编译器可以在函数内(或内联后的父函数)的任何位置放置这些asm语句,因此您无法控制从哪里采样寄存器的值。使用`asm volatile`可能会避免一些重新排序,但也许只与其他`volatile`访问有关。您可以检查编译器生成的汇编代码,看看是否得到了所需的结果,但要注意,这可能只是偶然的,而且以后可能会出问题。”
“您可以将一个asm语句放置在其他内容的依赖链中,以控制编译器放置它的位置。使用`+rm`约束来告诉编译器它修改了一些其他变量,该变量实际上用于某些不会被优化掉的东西。”
uint32_t ebx_value;
asm("" : "=b"(ebx_value), "+rm"(some_used_variable) );

其中,some_used_variable 可能是一个函数的返回值,并且(经过一些处理后)作为另一个函数的参数传递。或者在循环中计算,将作为函数的返回值返回。在这种情况下,汇编语句保证在循环结束后的某个时刻出现,在依赖于该变量后续值的任何代码之前。
然而,这将破坏针对该变量的常量传播等优化。https://gcc.gnu.org/wiki/DontUseInlineAsm。编译器不能假设任何关于输出值的事情;它不检查 asm 语句是否有零条指令。
这对于一些寄存器不起作用,因为gcc不允许您将其用作输出操作数或破坏寄存器,例如堆栈指针。
然而,对于堆栈指针,将值读入C变量可能是有意义的,如果您的程序对堆栈执行某些特殊操作。
作为内联汇编的替代方案,可以使用__builtin_frame_address(0)获取堆栈地址。(但是我记得,即使在启用了-x86默认情况下的-fomit-frame-pointer时,该函数也会产生完整的堆栈帧。)
尽管如此,在许多函数中,这几乎是免费的(制作堆栈帧可能对代码大小有好处,因为RBP相对于RSP相对于局部变量的访问具有更小的寻址模式)。
当然,在asm语句中使用mov指令也可以。

-1

这不是你要找的this吗?

语法:

 asm ("fsinx %1,%0" : "=f" (result) : "f" (angle));

1
你应该意识到这将会读取一个变量,计算正弦值并将结果存储在第二个变量中。 - R Samuel Klatchko
1
@Samuel:那只是语法的一个例子。 - Kornel Kisielewicz
1
因为这是GCC中使用扩展汇编的示例,而不是如何将特定寄存器的值放入特定变量中,这正是OP所问的。通过使用“%0”和“%1”指定寄存器,GCC将代表您选择相关寄存器。不能保证它会选择您所希望的寄存器。 - Matthew Cole

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接