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

What is wrong with this emulation of CMPXCHG16B instruction?

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

首先,我要模拟的实际机器代码是这样的:

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

摘自描述 CMPXCHG16B 的英特尔手册:

Compare RDX:RAX with m128. If equal, set ZF and load RCX:RBX into m128. Else, clear ZF and load m128 into RDX:RAX.

首先,我用我的仿真程序将指令的所有 5 个字节替换为跳转至 code cave,幸运的是,跳转恰好占用了 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 前缀。我希望除了争论我做错了之外还有别的原因。或者也有模拟原子性的方法吗?

我发现您的代码在模拟 cmpxchg16b 指令时存在这些问题:

  • 您需要使用 cmp 而不是 test 才能获得正确的比较。

  • 您需要 save/restore 除了 ZF 之外的所有标志。手册提到:

    The CF, PF, AF, SF, and OF flags are unaffected.


手册包含以下内容:

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

因此,要真正编写 "matches the functional specification given in manual" 的代码,需要写入 m128。虽然这个特定的写入是锁定版本 lock cmpxchg16b 的一部分,但它当然不会对仿真的原子性有任何好处!因此,不可能直接模拟 lock cmpxchg16b。参见

This instruction can be used with a LOCK prefix to allow the instruction to be executed atomically. To simplify the interface to the processor’s bus, the destination operand receives a write cycle without regard to the result of the comparison

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

无法模拟 lock cmpxchg16b。如果对目标地址的所有访问都与一个单独的锁同步,那是有可能的,但这包括所有其他指令,包括对对象任意一半的非原子存储,以及原子读-修改-写(如 xchg, lock cmpxchg, lock add, lock xadd) 与 16 字节对象的一半(或其他部分)。

您可以像您在此处所做的那样模拟 cmpxchg16b(没有 lock),使用来自@Fifoernik 的答案的错误修复。这是一个有趣的学习练习,但在实践中不是很有用,因为使用 cmpxchg16b 的实际代码总是使用 lock 前缀。

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


What about MFENCE? Seems to be what I need.

MFENCE 在加载和存储之前、之后或之间不会阻止另一个线程看到写了一半的值 ("tearing"),或者在您的代码完成后修改数据做出比较成功的决定,但在它进行存储之前。它可能会缩小 window 漏洞的范围,但无法关闭它,因为 MFENCE 只会阻止对我们自己的商店和负载的全局可见性进行重新排序。它无法阻止来自另一个核心的商店在我们加载之后但在我们的商店之前对我们可见。这需要一个原子读-修改-写总线周期,这就是 locked 指令的目的。

进行两次 8 字节原子比较交换将解决 window 漏洞问题,但仅针对每一半,留下 "tearing" 问题。

Atomic 16B loads/stores 解决了撕裂问题,但没有解决加载和存储之间的原子性问题。这是 possible with SSE on some hardware, but not guaranteed to be atomic by the x86 ISA .


Xen 的 lock cmpxchg16b 仿真:

Xen 虚拟机有一个 x86 模拟器,我猜是为了虚拟机在一台机器上启动并迁移到功能较弱的硬件的情况。它通过获取全局锁来模拟 lock cmpxchg16b,因为没有其他方法。如果 一种模拟它的方法 "properly",我相信 Xen 会那样做。

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

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

根据 emulate cmpxchg16b.

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

我认为所有这些都很好地证明了我的分析是正确的,并且不可能安全地模拟它。