c内联汇编在使用cmpxchg时会出现“操作数大小不匹配”

时间:2018-04-13 18:07:32

标签: c gcc x86 inline-assembly att

我正在尝试通过c使用cmpxchg和内联汇编。这是我的代码:

static inline int
cas(volatile void* addr, int expected, int newval) {
    int ret;
    asm volatile("movl %2 , %%eax\n\t"
                "lock; cmpxchg %0, %3\n\t"
                "pushfl\n\t"
                "popl %1\n\t"
                "and $0x0040, %1\n\t"
                : "+m" (*(int*)addr), "=r" (ret)
                : "r" (expected), "r" (newval)
                : "%eax"
                );
    return ret;
}

这是我第一次使用内联,我不确定是什么原因导致了这个问题。 我也试过“cmpxchgl”,但仍然没有。还尝试取下锁。 我得到“操作数大小不匹配”。 我想也许这与我对addr的投射有关,但我不确定。我尝试用int交换int,所以不要真正理解为什么会出现大小不匹配的问题。 这是使用AT& T风格。 感谢

2 个答案:

答案 0 :(得分:2)

您对cmpxchg指令的操作数顺序是相反的。 AT& T语法最后需要内存目标:

    "lock; cmpxchg %3, %0\n\t"

或者您可以使用-masm=intel以原始顺序编译该指令,但您的其余代码是AT& T语法和排序,因此这不是正确的答案。

至于它为什么说“操作数大小不匹配”,我只能说这似乎是一个汇编程序错误,因为它使用了错误的消息。

答案 1 :(得分:2)

正如@prl指出的那样,你颠倒了操作数,将它们置于英特尔顺序(See Intel's manual entry for cmpxchg)。只要你的内联asm没有汇编,你应look at the asm the compiler was feeding to the assembler查看模板发生了什么。在您的情况下,只需删除static inline,以便编译器创建一个独立的定义,然后得到(on the Godbolt compiler explorer):

    movl %edx , %eax
    lock; cmpxchg %ebx, (%ecx)       # error on this line from the assembler
    pushfl
    popl %edx
    and $0x0040, %edx

有时这会在盯着%3%0的情况下扼杀你的眼睛/大脑,特别是在检查instruction-set reference manual entry for cmpxchg并看到内存操作数是目的地(英特尔语法第一操作数,AT& T语法最后一个操作数)。

这是有道理的,因为显式寄存器操作数只是源,而EAX和内存操作数都被读取,然后根据比较的成功写入一个或另一个。 (在语义上,您使用cmpxchg作为内存目标的条件存储。)

您正在丢弃cas-failure案例中的加载结果。我无法想到cmpxchg的任何用例,其中单独加载原子值会不正确,而不仅仅是低效,但 CAS函数的通常语义是{ {1}}通过引用获取并在失败时更新。(至少C ++ 11 std :: atomic和C11 stdatomic如何使用bool atomic_compare_exchange_weak( volatile A *obj, C* expected, C desired );进行更新。)

(弱/强的东西允许在使用LL/SC的目标上使用CAS重试循环的更好的代码,其中由于中断或用相同的值重写可能导致虚假失败.x86' s oldval是"强")

实际上,GCC的遗留lock cmpxchg内置提供了2个独立的CAS函数:一个返回旧值,另一个返回__sync。两者都通过引用获取旧/新值。所以它与C ++ 11使用的API并不相同,但显然它并不是那么可怕,没有人使用它。

您过于复杂的代码无法移植到x86-64。从您使用bool开始,我假设您是在x86-32上开发的。您不需要popl来获得整数ZF;这是setcc的用途。 cmpxchg example for 64 bit integer有一个32位的示例,以这种方式工作(以显示他们想要的64位版本)。

甚至更好,使用GCC6标志返回语法,因此在循环中使用它可以编译为pushf/pop循环而不是cmpxchg / jne / cmpxchg / setz %al / { {1}}。

我们可以解决所有这些问题并改善寄存器分配。 (如果inline-asm语句的第一个或最后一个指令是test %al,%al,则您可能无效地使用约束。)

当然,到目前为止,实际使用的最佳方法是使用C11 stdatomic或GCC内置https://gcc.gnu.org/wiki/DontUseInlineAsm如果编译器可以从代码中发出同样好(或更好)的asm,那么它就会理解",因为内联asm会约束编译器。它也很难正确/有效地编写和维护。

可移植到i386和x86-64,AT& T或Intel语法,适用于任何整数类型宽度的寄存器宽度或更小

jnz

mov是ASM方言的替代品。对于x86,它是// Note: oldVal by reference static inline char CAS_flagout(int *ptr, int *poldVal, int newVal) { char ret; __asm__ __volatile__ ( " lock; cmpxchg {%[newval], %[mem] | %[mem], %[newval]}\n" : "=@ccz" (ret), [mem] "+m" (*ptr), "+a" (*poldVal) : [newval]"r" (newVal) : "memory"); // barrier for compiler reordering around this return ret; // ZF result, 1 on success else 0 } // spinning read-only is much better (with _mm_pause in the retry loop) // not hammering on the cache line with lock cmpxchg. // This is over-simplified so the asm is super-simple. void cas_retry(int *lock) { int oldval = 0; while(!CAS_flagout(lock, &oldval, 1)) oldval = 0; } { foo,bar | bar,foo }是一个命名操作数约束;这是保持操作数的另一种方式。 The "=ccz" takes the z condition code as the output value,与{AT&T | Intel}一样。

Compiles on Godbolt对于具有AT& T输出的32位x86的asm:

%[newval]

gcc是愚蠢的,并且在将setz复制到cas_retry: pushl %ebx movl 8(%esp), %edx # load the pointer arg. movl $1, %ecx xorl %ebx, %ebx .L2: movl %ebx, %eax # xor %eax,%eax would save a lot of insns lock; cmpxchg %ecx, (%edx) jne .L2 popl %ebx ret 之前将0存储在一个注册表中,而不是在循环内重新归零eax。这就是它需要保存/恢复EBX的原因。尽管如此(来自x86 spinlock using cmpxchg),它与我们避免使用inline-asm的结果相同:

eax

有人应该教gcc,英特尔CPU可以通过xor-zeroing更便宜地实现// also omits _mm_pause and read-only retry, see the linked question void spin_lock_oversimplified(int *p) { while(!__sync_bool_compare_and_swap(p, 0, 1)); } ,而不是使用mov复制它,特别是在Sandybridge(0 - 零消除但没有xor }在剔除)。