为什么我的“=r”(var) 输出没有选择与 "a"(var) 输入相同的寄存器?

Why does my "=r"(var) output not pick the same register as "a"(var) input?

我正在学习如何在 GCC 中使用 __asm__ volatile,但遇到了一个问题。我想实现一个执行原子比较和交换并返回先前存储在目标中的值的函数。

为什么 "=a"(expected) 输出约束有效,但 "=r"(expected) 约束让编译器生成不起作用的代码?

案例一

#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

uint64_t atomic_cas(uint64_t * destination, uint64_t expected, uint64_t value){
    __asm__ volatile (
        "lock cmpxchgq %3, %1":
        "=a" (expected) :
        "m" (*destination), "a" (expected), "r" (value) :
        "memory"
    );

    return expected;
}

int main(void){
    uint64_t v1 = 10;
    uint64_t result = atomic_cas(&v1, 10, 5);
    printf("%" PRIu64 "\n", result);           //prints 10, the value before, OK
    printf("%" PRIu64 "\n", v1);               //prints 5, the new value, OK
}

它按预期工作。现在考虑以下情况:

案例2.

#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

uint64_t atomic_cas(uint64_t * destination, uint64_t expected, uint64_t value){
    __asm__ volatile (
        "lock cmpxchgq %3, %1":
        "=r" (expected) ://<----- I changed a with r and expected GCC understood it from the inputs 
        "m" (*destination), "a" (expected), "r" (value) :
        "memory"
    );

    return expected;
}

int main(void){
    uint64_t v1 = 10;
    uint64_t result = atomic_cas(&v1, 10, 5);
    printf("%" PRIu64 "\n", result);            //prints 5, wrong
    printf("%" PRIu64 "\n", v1);                //prints 5, the new value, OK 
}

我检查了生成的程序集并注意到以下几点:

我。在这两种情况下,功能代码是相同的,看起来像

   0x0000555555554760 <+0>:     mov    rax,rsi
   0x0000555555554763 <+3>:     lock cmpxchg QWORD PTR [rdi],rdx
   0x0000555555554768 <+8>:     ret 

二.当 GCC 内联 atomic_cas 时问题就出现了,所以在后一种情况下正确的值没有传递给 printf 函数。这是 disas main 的相关片段:

0x00000000000005f6 <+38>:    lock cmpxchg QWORD PTR [rsp],rdx
0x00000000000005fc <+44>:    lea    rsi,[rip+0x1f1]        # 0x7f4
0x0000000000000603 <+51>:    mov    rdx,rax ;  <-----This instruction is absent in the Case 2.
0x0000000000000606 <+54>:    mov    edi,0x1
0x000000000000060b <+59>:    xor    eax,eax

问题: 为什么将 rax(a) 替换为任意寄存器 (r) 产生错误的结果?我希望这两种情况都能奏效?

更新。我使用以下标志编译 -Wl,-z,lazy -Warray-bounds -Wextra -Wall -g3 -O3

cmpxchg指令总是把结果放在rax寄存器中。所以你需要使用 a 约束来告诉 GCC 从那个寄存器中移动。在情况 2 中,您通过 r 告诉 GCC 使用任意寄存器,但您没有在该寄存器中放置任何内容。

如果您想使用 r,您必须添加 mov 指令以将结果从 rax 移动到该寄存器 (movq %%rax, %0)。您还必须告诉 GCC rax 寄存器已被指令更改,例如将其添加到 asm 语句的 "clobbers" 部分。对于您的情况,没有理由以这种方式使事情复杂化。

首先,https://gcc.gnu.org/wiki/DontUseInlineAsm. There is basically zero reason to roll your own CAS, vs. using bool __atomic_compare_exchange(type *ptr, type *expected, type *desired, bool weak, int success_memorder, int failure_memorder) https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html。这甚至适用于非 _Atomic 变量。


"=r" 告诉 gcc 它可以在任何它想要的寄存器中请求输出,因此它可以避免 mov 那里的结果本身 。 (就像这里 GCC 希望 RSI 中的输出作为 printf 的 arg)。 And/or 这样它就可以避免破坏它放在同一个寄存器中的输入。这就是 =r 而不是特定寄存器约束的全部要点。

如果您想告诉 GCC 它选择用于输入的寄存器也是输出寄存器,请使用 "+r"。或者在这种情况下,因为您需要它来选择 RAX,请使用 "+a"(expected).

已经有语法让编译器为 2 个约束选择相同的寄存器,输入和输出有单独的变量,特别是匹配约束:"=r"(outvar) : "0"(invar).

如果语法没有让您描述一条非破坏性指令,它可以在与输入不同的寄存器中产生输出,那么这将是一个错过的优化。


您可以在评论中使用约束查看 GCC 实际选择了什么。

请记住,GNU C 内联汇编只是将文本替换到您的模板中。编译器根本不知道 asm 指令的作用,甚至不检查它们是否有效。 (只有当汇编程序读取编译器输出时才会发生这种情况)。

    ...
    asm volatile (
    "lock cmpxchgq %3, %1   # 0 out: %0  |  2 in: %2" 
    : ...
    ...

生成的 asm 非常清楚地显示了问题 (Godbolt GCC7.4):

        lock cmpxchgq %rsi, (%rsp)   # 0 out: %rsi  |  2 in: %rax
        leaq    .LC0(%rip), %rdi
        xorl    %eax, %eax
        call    printf@PLT

(我使用了 AT&T 语法,所以你的 cmpxchgq %reg,mem 会匹配 mem,reg 操作数顺序 documented by Intel,尽管 GAS 和 clang 的内置汇编程序似乎都接受其他顺序,也是。也是因为操作数大小后缀)

GCC 借此机会在 RSI 中请求 "=r"(expected) 输出作为 printf 的 arg。 您的错误是您的模板错误地假设 %0 将扩展为 rax


有很多输入和输出之间缺乏隐式连接的例子恰好使用相同的 C 变量。例如,您可以用一个空的 asm 语句交换 2 个 C 变量,只需使用约束。