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 时出现“操作数大小不匹配”的主要内容,如果未能解决你的问题,请参考以下文章