c 内联汇编在使用 cmpxchg 时得到 "operand size mismatch"

c inline assembly getting "operand size mismatch" when using cmpxchg

我正在尝试通过 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 [=11=]x0040, %1\n\t"
                : "+m" (*(int*)addr), "=r" (ret)
                : "r" (expected), "r" (newval)
                : "%eax"
                );
    return ret;
}

这是我第一次使用内联,我不确定是什么导致了这个问题。 我也尝试了 "cmpxchgl",但仍然没有。还尝试移除锁。 我得到 "operand size mismatch"。 我认为这可能与我对 addr 所做的转换有关,但我不确定。我尝试将 int 换成 int,所以不太明白为什么会出现大小不匹配。 这是使用 AT&T 风格。 谢谢

您颠倒了 cmpxchg 指令的操作数顺序。 AT&T 语法最后需要内存目标:

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

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


至于为什么说 "operand size mismatch",我只能说这似乎是一个汇编程序错误,因为它使用了错误的消息。

正如@prl 指出的那样,您颠倒了操作数,将它们按 Intel 顺序排列 (See Intel's manual entry for cmpxchg). Any time your inline asm doesn't assemble, you should to see what happened to your template. In your case, simply remove the static inline so the compiler will make a stand-alone definition, then you get (on the Godbolt compiler explorer):

 # gcc -S output for the original, with cmpxchg operands backwards
    movl %edx , %eax
    lock; cmpxchg (%ecx), %ebx        # error on this line from the assembler
    pushfl
    popl %edx
    and [=10=]x0040, %edx

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

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


您正在丢弃来自 cas 失败案例的加载结果。我想不出 cmpxchg 的任何用例,其中单独加载原子值是不正确的,而不仅仅是低效的,但是 CAS 函数的通常语义是 oldval 被引用并在失败时更新。(至少 C++11 std::atomic 和 C11 stdatomic 使用 bool atomic_compare_exchange_weak( volatile A *obj, C* expected, C desired ); 是这样做的。)

(weak/strong 允许在使用 LL/SC 的目标上为 CAS 重试循环提供更好的代码生成,其中可能由于中断或使用相同的值重写而导致虚假失败。 x86 的 lock cmpxchg 是 "strong")

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


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

或者甚至更好,使用 GCC6 flag-return 语法,因此在循环中使用它可以编译为 cmpxchg / jne 循环而不是 cmpxchg / setz %al / test %al,%al / jnz.

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

当然,到目前为止,实际使用的最佳方法是使用 C11 stdatomic 或 GCC 内置https://gcc.gnu.org/wiki/DontUseInlineAsm 在编译器可以从代码中发出同样好的(或更好)asm 的情况下 "understands",因为内联 asm 限制了编译器。正确/高效地编写和维护也很困难。

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

// 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 }是ASM方言的替代品。对于 x86,它是 {AT&T | Intel}%[newval] 是命名操作数约束;这是另一种保留操作数的方法。 The "=ccz" takes the z condition code as the output value,就像 setz.

Compiles on Godbolt 到此 asm for 32-bit x86 with AT&T output:

cas_retry:
    pushl   %ebx
    movl    8(%esp), %edx      # load the pointer arg.
    movl    , %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

gcc 是愚蠢的,它在将 0 复制到 eax 之前将其存储在一个 reg 中,而不是在循环内将 eax 重新归零。这就是为什么它需要 save/restore EBX。这与我们从避免 inline-asm 中得到的 asm 相同,但是(来自 x86 spinlock using cmpxchg):

// 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));
}

有人应该教 gcc Intel CPU 使用 xor-zeroing 实现 0 比使用 mov 复制它更便宜,尤其是在 Sandybridge 上(xor- 归零消除但没有 mov-消除).