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

3
我正在尝试运行一个二进制程序,其中在一个地方使用了CMPXCHG16B指令,不幸的是我的Athlon 64 X2 3800+不支持它。这很好,因为我把它看作是一个编程挑战。这个指令似乎不难通过一个洞穴跳跃来实现,所以我这样做了,但是有些东西没有起作用,程序只是在一个循环中冻结了。也许有人可以告诉我我是否错误地实现了我的CMPXCHG16B?
首先,我要模拟的实际机器代码片段如下:
f0 49 0f c7 08                lock cmpxchg16b OWORD PTR [r8]

Intel手册中关于CMPXCHG16B的摘录:

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

首先,我用我的仿真程序将指令的所有5个字节替换为跳转到代码洞的指令,幸运的是跳转占据了恰好5个字节!该跳转实际上是一个call指令e8,但也可以是jmpe9,两者都可以使用。

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 前缀。我希望除了争用之外还有其他我做错了的地方。或者有没有一种方法来模拟原子性呢?


好的… 你不能真的忽略争议。此外,“test”并不是英文中的“test”,它是一种非破坏性的AND。你可以考虑使用“cmp”。 - Margaret Bloom
2
我不太清楚你如何在模拟中处理原始的lock cmpxchg16b的原子性问题?(虽然我不确定带有lock前缀的cmpxchg16b是否是原子性的,它在SSE周围有些复杂,我从未深入研究过)。 (编辑:哦,你也说了,我今天有点迷糊) - Ped7g
@PeterCordes 你确定吗?MFENCE呢?看起来是我需要的。 - vlsh
(OP在回复我发布答案后删除的评论) - Peter Cordes
你可以编写但无法调试吗? - Alec Teal
2个回答

6
无法模拟 lock cmpxchg16b 指令。 如果所有对目标地址的访问都与单独的锁同步,那么在这种情况下有点可能,但这包括所有其他指令,包括对对象任一半的非原子存储和带有 16 字节对象的一半(或其他部分)的原子读写修改指令(如 xchg, lock cmpxchg, lock add, lock xadd)。
您可以像在 @Fifoernik 的回答中修复漏洞那样模拟没有 lock 前缀的 cmpxchg16b。这是一个有趣的学习练习,但在实践中并不是非常有用,因为使用 cmpxchg16b 的实际代码总是使用带有 lock 前缀的指令。

非原子替换通常可以工作,因为在两个相邻指令之间的短时间窗口内很少会从另一个核心来的缓存无效。 这并不意味着它是安全的,只是当偶尔失败时很难调试。 如果您只想让游戏在自己的使用中工作,并且可以接受偶尔的锁定/错误,则可能有用。 对于任何重要性正确性的事情,您就没有运气了。


“MFENCE”是什么?看起来正是我需要的。在加载和存储之前、之后或之间使用“MFENCE”无法防止其他线程看到半写入值(“撕裂”),也无法防止在您的代码决定比较成功但在执行存储之前修改数据。它可能缩小了漏洞窗口,但无法关闭它,因为“MFENCE”仅防止我们自己的存储和加载的全局可见性重排。它无法阻止另一个核心的存储在我们的加载之后但在我们的存储之前变得对我们可见。这需要一个原子读取-修改-写入总线周期,这就是“lock”指令的用途。进行两个8字节的原子比较交换将解决漏洞窗口问题,但仅针对每个部分单独进行,留下“撕裂”问题。

16字节原子读/写解决了撕裂问题,但不能解决加载和存储之间的原子性问题。在某些硬件上,可以使用SSE实现此功能(链接1),但不能保证x86 ISA (链接2)像8字节自然对齐的加载和存储一样具有原子性。


Xen的lock cmpxchg16b仿真:

Xen虚拟机有一个x86仿真器,我猜是为了在虚拟机从一台机器迁移到性能较差的硬件时使用。它通过获取全局锁来模拟lock cmpxchg16b,因为没有其他方法。如果有“合适”的模拟方法,我相信Xen会采用。

正如这个邮件列表线程中所讨论的那样,当一个核心上的仿真版本访问与另一个核心上的非仿真指令相同的内存时,Xen的解决方案仍然无法工作。(本地版本不尊重全局锁)。

另请参见Xen邮件列表上的此补丁,它将lock cmpxchg8b仿真更改为同时支持lock cmpxchg8block cmpxchg16b

我还发现,根据搜索结果,KVM的x86模拟器也不支持cmpxchg16b。我认为这一切都是证明我的分析是正确的,并且无法安全地进行模拟。

经过一堵证据墙,我被说服了。MFENCE是最难理解的。我还没有尝试SSE,那些寄存器对于通用目的来说真的很棘手,这是一种完整的艺术形式。我完成了应用程序的其余部分的修补,它几乎加载,但在渲染单个帧后就会崩溃。有时我能看到那个画面。这不是游戏,而是NASA的Eyes,我想看到朱诺模拟。 - vlsh
@vlsh:如果你只对一个程序中的一个特定用途感兴趣,那么使用MFENCE和/或SSE可能会使其更有效。另一个选择是使用自旋锁,如果你可以将所有存储钩到16B对象上,即使你错过了一些存储也可能有所帮助。但无论如何,似乎只能在大多数情况下工作而不是完全安全的解决方案对你来说至少是有趣的。我绝对确定完全安全的仿真是不可能的,但你可以做得更好。甚至可以使用CPUID序列化序列之前的流水线,这可能会有所帮助。 - Peter Cordes

4
我看到你模拟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:

你猜怎么着,这是测试!用CMP替换后,循环结束了,正确与否得看看看。在程序启动之前,我还有几个指令需要替换,然后就可以确定了... 关于标志位,在实践中只有“奇偶标志位”没有被恢复。 - vlsh
@vlsh 和 Fifoernik: 这仍然不能保证原子性。无条件地执行存储操作也没有帮助。没有必要模拟指令的奇怪行为。但是,即使使用 SSE 在单条指令中加载或存储 128b 的数据也不能保证原子性。(实际上,在该 CPU 上可能不支持)。请参阅 https://dev59.com/NWsz5IYBdhLWcg3w0bOl - Peter Cordes
如果您编辑一下,让它清楚地表明您只是在模拟cmpxchg16b而不是lock cmpxchg16b,我会点赞的。我仍然认为关于模拟无条件存储的整个后半部分是没有意义的。我不认为在缓存中污染该行会产生任何可观察的结果,并且额外的非原子读写也不是cmpxchg16b操作的一部分,除非有一个lock。(这段引用仅描述了LOCKed版本)。 - Peter Cordes
嗯,我想我之前误读了。操作部分确实显示写入始终发生,即使在非锁定情况下也是如此。现在我明白为什么他们要在cmpxchg / cmpxchg8b指令描述中提到这一点了。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接