【问题标题】:Why does my "=r"(var) output not pick the same register as "a"(var) input?为什么我的 "=r"(var) 输出没有选择与 "a"(var) 输入相同的寄存器?
【发布时间】:2019-10-04 15:06:24
【问题描述】:

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

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

案例 1。

#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

问题: 为什么用任意寄存器 (r) 替换 rax(a) 会产生错误结果?我预计它在这两种情况下都有效?

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

【问题讨论】:

  • 它只是恢复使用的寄存器。它究竟是如何不起作用的?你传递了什么编译器标志?
  • @JL2210 第二种情况不会返回在 CAS 之前存储的正确值。它返回存储的值。最让我困惑的是,函数汇编是一样的,但是inline却导致结果不一样。
  • 您使用的是什么版本的 GCC?我无法重现该程序集。
  • 1011 的区别是故意的吗?
  • 我使用了-fPIC -pie。大概就是这样。

标签: c gcc assembly x86 inline-assembly


【解决方案1】:

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

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

【讨论】:

  • 听起来很合理。
  • mov %%rax, %0 不安全cmpxchg 仍会破坏输入 RAX(失败时),但 "a"(expected) 是只读输入。如果您希望"=r"(oldval) 工作,您必须使用"+r"(expected) 或虚拟"=a" 输出。但这非常复杂,并且在编译器确实选择 RAX 作为输出的情况下,mov %rax,%rax 毫无用处,因此 最好将 mov 生成留给编译器并且使用约束准确地描述尽可能小的 asm 模板。
【解决方案2】:

首先,https://gcc.gnu.org/wiki/DontUseInlineAsm 与使用 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 相比,滚动自己的 CAS 基本上是零理由。这甚至适用于非_Atomic 变量。


"=r" 告诉 gcc 它可以在任何它想要的寄存器中请求输出,因此它可以避免将结果本身放在mov 那里。 (就像这里 GCC 希望 RSI 中的输出作为 printf 的 arg 一样)。和/或这样它可以避免破坏它放在同一个寄存器中的输入。这就是 =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 的内置汇编器似乎也以其他顺序接受它。也是因为操作数-size 后缀)

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


有很多例子表明输入和输出之间缺少隐式连接,而恰好使用相同的 C var。例如,您可以使用空 asm 语句交换 2 个 C 变量,仅使用约束。 How to write a short block of inline gnu extended assembly to swap the values of two integer variables?

【讨论】:

  • "=r" 告诉 gcc 它可以在它想要的任何寄存器中请求输出 但是因为我写了return expected;,所以 GCC 选择的任何寄存器中的值都不应该"=r" (expected) 在返回时移动到 rax?所以atomic_cas 的语义是:“为内联 asm 选择任何你想要的输出寄存器,但是当内联 asm 完成后,从选择的寄存器中返回输出值”。
  • @St.Antario:是的,这就是 GCC 选择 RAX 作为非内联版本的原因。但是内联的部分目的是摆脱调用约定的开销,比如必须将expected 移动到 RAX 中。我不确定你是否认为在 RAX 中留下一个值最终会返回它,即使编译器想要返回一个它选择在不同寄存器中请求的值,但我希望不是因为这没有意义.就像内联汇编在 RAX 中留下一些东西后从非void 函数的末尾掉下来一样,但更糟糕的是,编译器会使用来自其他一些注册的mov 破坏 RAX。
  • @St.Antario:在编译器内部,包括内联在内的优化发生在程序逻辑 (GIMPLE) 的 SSA form 上,而不是在 asm 指令上。在那个级别,寄存器分配甚至还没有发生。因此,当我们到达必须进行寄存器分配的 RTL 通道时,内联已经发生,"=r" 输出可以直接连接到 GCC 在调用者中想要它的位置。
  • 如果它复制到 RAX 并返回,这将是一个错过的优化,如果它从 RAX 而不是复制它选择作为输出的 reg,那将是错误的代码asm 语句。
  • @St.Antario:但是不,写"=r"(expected) 不是“错过的优化”,它是源代码中的一个错误。除此之外,您在上一条评论中陈述的事实是真实的,但不知道您在说什么。 IMO 最清楚的是"+r"(expected),或者使用单独的变量作为返回值"=a"(output)"a"(expected)。当你有一个固定的寄存器时,匹配约束是不必要的。 return expected 很奇怪,因为那时它 不再 是预期值。 (更不用说您还为布尔状态遗漏了标志输出约束)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-27
  • 1970-01-01
  • 2020-10-01
  • 1970-01-01
  • 2016-10-26
  • 2014-12-10
相关资源
最近更新 更多