我正在学习如何在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
}
我检查了生成的程序集并注意到以下内容:
I。在这两种情况下,功能代码都是相同的,看起来像
0x0000555555554760 <+0>: mov rax,rsi
0x0000555555554763 <+3>: lock cmpxchg QWORD PTR [rdi],rdx
0x0000555555554768 <+8>: ret
II。当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
)会导致错误的结果?我希望这两种情况都能奏效?
UPD。我使用以下标志-Wl,-z,lazy -Warray-bounds -Wextra -Wall -g3 -O3
答案 0 :(得分:5)
cmpxchg
指令始终将结果放入rax
寄存器中。因此,您需要使用a
约束来告诉GCC从该寄存器中移出。在情况2中,您通过使用r
来告诉GCC使用任意寄存器,但是您没有在该寄存器中放入任何内容。
如果要使用r
,则必须添加一条mov
指令才能将结果从rax
移到该寄存器(movq %%rax, %0
)。您还必须告诉GCC指令更改了rax寄存器,例如,将其添加到asm
语句的“ clobbers”部分。对于您的情况,没有理由以这种方式使事情复杂化。
答案 1 :(得分:4)
首先, 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
本身{strong} 。 (就像这里的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的内置汇编程序似乎也都接受了其他顺序。也是由于操作数大小的后缀)
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?