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

Posted

技术标签:

【中文标题】c 内联汇编在使用 cmpxchg 时出现“操作数大小不匹配”【英文标题】:c inline assembly getting "operand size mismatch" when using cmpxchg 【发布时间】:2018-04-13 18:07:32 【问题描述】:

我正在尝试通过 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 风格。 谢谢

【问题讨论】:

尝试使用内联汇编几乎总是bad idea。您是否考虑过使用内部函数或库? 你也可以看看***.com/a/37825052/2189500 这是一个学校作业,所以我们必须使用它。 谢谢大卫的链接,我会看的 我知道你无法控制老师给你的作业,但内联汇编似乎对学生时间的利用不当。它不能在编译器之间移植,不能在平台之间移植,它非常很难正确处理,即使它“运行”时,你可能仍然会遇到一些难以理解的问题,以后会困扰你。它还扰乱了编译器优化,牺牲了持续使用的机会。当你完成后,你获得的唯一技能就是学习这种特殊的内联汇编是如何工作的,你应该努力避免在生产代码中使用。 【参考方案1】:

正如@prl 指出的那样,您颠倒了操作数,将它们按 Intel 顺序排列 (See Intel's manual entry for cmpxchg)。任何时候你的内联汇编没有组装,你应该look at the asm the compiler was feeding to the assembler 看看你的模板发生了什么。在您的情况下,只需删除 static inline 以便编译器进行独立定义,然后您会得到 (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 $0x0040, %edx

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

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


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

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

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


您过于复杂的代码无法移植到 x86-64。从您对popl 的使用来看,我假设您是在 x86-32 上开发的。你不需要pushf/pop 来获取 ZF 作为整数;这就是setcc 的用途。 cmpxchg example for 64 bit integer 有一个 32 位的例子就是这样工作的(展示他们想要 64 位版本的)。

或者更好的是,使用 GCC6 标志返回语法,这样在循环中使用它可以编译为 cmpxchg / jne 循环而不是 cmpxchg / setz %al / test %al,%al / jnz

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

当然,到目前为止,实际使用的最佳方式是使用 C11 stdatomic 或内置 GCC。 https://gcc.gnu.org/wiki/DontUseInlineAsm 在编译器可以从它“理解”的代码中发出同样好(或更好)的 asm 的情况下,因为内联 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,用于带有 AT&T 输出的 32 位 x86:

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

gcc 是愚蠢的,在将0 复制到eax 之前将其存储在一个reg 中,而不是在循环内重新归零eax。这就是为什么它需要保存/恢复 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,英特尔 CPU 可以使用 xor-zeroing 实现 0 比使用 mov 复制它更便宜,尤其是在 Sandybridge 上(xor-zeroing 消除,但没有 mov-elimination)。

【讨论】:

@DavidWohlferd:这就是 _oversimplified 在函数名称中的用途:P AFAIK 没有适当的 GCC(或任何其他 x86 编译器)选项来告诉它以这种方式编译,或者任何用于执行非原子 cmpxchg(或仅原子 wrt.当前核心/线程中的中断/信号处理程序) @PSkocik:我没有看到一种明显的方法来对内联汇编进行优化。请注意,除了 add/sub (neg/xadd) 之外,它对于 ALU 操作更为重要,例如如果需要旧结果,fetch_or 需要一个 CAS 重试循环。 (除非操作数只有一个位集,在这种情况下你可以bts,但这可能只值得寻找if(__builtin_constant_p(x)) 最后一个问题,现在与您的答案更直接相关:虽然 clang-trunk 接受它,但较老的 clang 似乎不喜欢模板的 intel-syntax 部分,他们抱怨“未知用途没有大小后缀的指令助记符”gcc.godbolt.org/z/jfTa11Td1。知道为什么,甚至可以在那里工作吗? @PSkocik:clang-trunk 现在接受 Intel-syntax inline asm 了吗?这很酷,必须更新 How to set gcc to use intel syntax permanently? 的 clang 部分 - 我以前从未找到任何方法让 clang 使用 Intel 语法。请注意,您在较旧的 clang 中看到的 clang 错误是使用 AT&T 操作数名称和模板中该部分的 Intel 语法操作数顺序:cmpxchg (%rdi), %edx,其操作数向后。【参考方案2】:

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

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

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


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

【讨论】:

以上是关于c 内联汇编在使用 cmpxchg 时出现“操作数大小不匹配”的主要内容,如果未能解决你的问题,请参考以下文章

扩展内联汇编基础

内联汇编操作数约束

使用 char 数组块作为内存输出操作数的内联汇编

8 位旋转的内联汇编大小不匹配

gcc 内联汇编中的 min

使用 cmpxchg8b for unsigned long 未获得预期输出