这种CMPXCHG16B指令仿真有什么问题?

时间:2016-07-06 08:14:42

标签: multithreading assembly x86 thread-safety atomic

我试图在一个地方运行使用CMPXCHG16B指令的二进制程序,不幸的是我的Athlon 64 X2 3800+并不支持它。这很棒,因为我将其视为编程挑战。这个指令看起来似乎难以用洞穴跳跃来实现,所以这就是我所做的,但是有些东西不起作用,程序只是在一个循环中冻结。也许有人可以告诉我,我是否实施了CMPXCHG16B错误?

首先,我试图模仿的实际机器代码是:

f0 49 0f c7 08                lock cmpxchg16b OWORD PTR [r8]

摘自英特尔手册,描述CMPXCHG16B

  

将RDX:RAX与m128进行比较。如果相等,则设置ZF并将RCX:RBX加载到m128。   否则,清除ZF并将m128加载到RDX:RAX。

首先,我用我的仿真程序跳转到代码洞穴替换指令的所有5个字节,幸运的是,跳转恰好占用了5个字节!跳转实际上是call指令e8,但可以是jmp e9,两者都有效。

e8 96 fb ff ff            call 0xfffffb96(-649)

这是一个相对跳跃,其中32位有符号偏移以二进制补码编码,偏移量指向相对于下一条指令地址的代码洞。

接下来,仿真代码我跳转到:

PUSH R10
PUSH R11
MOV r10, QWORD PTR [r8]
MOV r11, QWORD PTR [r8+8]
TEST R10, RAX
JNE ELSE
TEST R11, RDX
JNE ELSE
MOV QWORD PTR [r8], RBX
MOV QWORD PTR [r8+8], RCX
JMP END
ELSE:
MOV RAX, r10
MOV RDX, r11
END:
POP R11
POP R10
RET

就个人而言,我对它很满意,我认为它与手册中给出的功能规格相符。它将堆栈和两个寄存器r10r11恢复为原始顺序,然后恢复执行。唉它不起作用!这就是代码的工作原理,但该程序的行为似乎在等待小费和燃烧电力。这表明我的模拟并不完美,我无意中打破了它的循环。你觉得它有什么问题吗?

我注意到这是它的原子变体 - 拥有lock前缀。我希望除了争论之外别的什么我做错了。或者有没有办法模仿原子性?

2 个答案:

答案 0 :(得分:5)

无法模仿lock cmpxchg16b 。如果对目标地址的所有访问都与单独的锁同步,但包括所有其他指令(包括对象的一半的非原子存储)和原子读 - 修改 - 写(如{{1} },xchglock cmpxchglock add)与16字节对象的一半(或其他部分)。

你可以模仿lock xadd(没有cmpxchg16b),就像你在这里做的那样,使用来自@ Fifoernik的答案的错误修正。这是一个有趣的学习练习,但在实践中不是很有用,因为使用lock的真实代码总是使用cmpxchg16b前缀。

非原子替换在大多数情况下都会起作用,因为来自另一个核心的缓存行无效到达两个附近指令之间的小时间窗口是很少见的。 这并不意味着它是安全的,只是意味着它偶尔会失败时很难调试。如果你只是想让一个游戏适合你自己的使用,并且可以接受偶尔的锁定/错误,这可能是有用的。对于任何正确性很重要的事情,你都运气不好。

  

MFENCE怎么样?似乎是我需要的。

lock在加载和存储之前,之后或之间不会阻止另一个线程看到半写值(“撕裂”),或者在代码做出决定之后修改数据比较成功,但在它存储之前。它可能会缩小漏洞窗口,但无法关闭它,因为MFENCE仅阻止重新排序我们自己的商店和负载的全局可见性。它不能阻止来自其他核心的商店在我们加载之后但在我们的商店之前变得可见。这需要一个原子读 - 修改 - 写总线周期,这是MFENCE指令的用途。

进行两次8字节的原子比较交换可以解决漏洞窗口问题,但仅针对每一半的问题,留下“撕裂”问题。

Atomic 16B加载/存储解决了撕裂问题,但没有解决加载和存储之间的原子性问题。它是possible with SSE on some hardware,但不能保证x86 ISA the way 8B naturally-aligned loads and stores are是原子的。

Xen的lock仿真:

Xen虚拟机有一个x86仿真器,我想是在一台机器上启动VM并迁移到功能较少的硬件的情况。它通过采用全局锁定来模拟lock cmpxchg16b,因为没有其他方法。如果一种“正确”模仿它的方法,我肯定Xen会这样做。

正如this mailing list thread中所讨论的,当一个内核上的模拟版本访问与另一个内核上的非模拟指令相同的内存时,Xen的解决方案仍然不起作用。 (本机版本不尊重全局锁定。)

另请参阅更改lock cmpxchg16b仿真的this patch on the Xen mailing list以支持lock cmpxchg8block cmpxchg8b

根据lock cmpxchg16b的搜索结果,我还发现KVM的x86模拟器也不支持cmpxchg16b

我认为所有这些都是我的分析正确无误的好证据,并且不可能安全地模仿它。

答案 1 :(得分:3)

我发现您的代码错误地模仿了cmpxchg16b指令:

  • 您需要使用cmp代替test才能获得正确的比较。

  • 您需要保存/恢复除ZF之外的所有标志。手册提到:

      

    CF,PF,AF,SF和OF标志不受影响

本手册包含以下内容:

IF (64-Bit Mode and OperandSize = 64)
    THEN
         TEMP128 ← DEST
         IF (RDX:RAX = TEMP128)
              THEN
                    ZF ← 1;
                    DEST ← RCX:RBX;
              ELSE
                    ZF ← 0;
                    RDX:RAX ← TEMP128;
                    DEST ← TEMP128;
                    FI;
         FI

因此,要真正编写“匹配手动”中给出的功能规范的代码,需要写入 m128 。虽然这个特定的写入是锁定版本lock cmpxchg16b的一部分,但它当然不会对仿真的原子性有任何好处!因此不可能直接模拟lock cmpxchg16b。请参阅@PeterCordes' answer

  

该指令可与LOCK前缀一起使用,以允许指令以原子方式执行。为了简化处理器总线的接口,目标操作数接收写周期而不考虑比较结果

ELSE:
MOV RAX, r10
MOV RDX, r11
MOV QWORD PTR [r8], r10
MOV QWORD PTR [r8+8], r11
END: