我试图在一个地方运行一个使用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
,但可以是一个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
就我个人而言,我对它很满意,我认为它符合手册中给出的功能规格。它将堆栈和两个寄存器r10
和r11
恢复到原来的顺序,然后继续执行。唉,这行不通!这就是代码的工作原理,但程序的行为就像是在等待提示并燃烧电力。这表明我的模拟并不完美,我无意中打破了它的循环。你觉得有什么不妥吗?
我注意到这是它的一个原子变体——属于lock
前缀。我希望除了争论我做错了什么,还有别的。或者也有一种模仿原子性的方法吗?
无法模拟lock cmpxchg16b
。如果对目标地址的所有访问都与一个单独的锁同步,这是可能的,但这包括所有其他指令,包括对对象的一半的非原子存储,以及对16字节对象的一半(或其他部分)的原子读-修改-写(如xchg
, lock cmpxchg
, lock add
, lock xadd
)。
你可以模仿cmpxchg16b
(没有lock
),就像你在这里做的,从@Fifoernik的答案的错误修复。这是一个有趣的学习练习,但在实践中不是很有用,因为使用cmpxchg16b
的实际代码总是使用lock
前缀。
非原子替换在大多数情况下都是有效的,因为来自另一个核心的cache-line invalidate很少会在两个相邻指令之间的小时间窗口内到达。这并不意味着它是安全的,它只是意味着当它偶尔失败时很难调试。如果你只是想让游戏为你自己使用,并且可以接受偶尔的锁定/错误,这可能是有用的。对于任何对正确性很重要的事情,你就不走运了。
MFENCE呢?似乎正是我所需要的。
MFENCE
在加载和存储之前、之后或之间不会阻止另一个线程看到写入一半的值("撕裂"),或者在代码决定比较成功之后修改数据,但在它执行存储之前。它可能会缩小漏洞的窗口,但它不能关闭它,因为MFENCE
只阻止重新排序我们自己的store和load的全局可见性。它不能阻止来自另一个核心的store在我们加载之后但在我们的store之前对我们可见。这需要一个原子读-修改-写总线周期,这就是lock
ed指令的作用。
进行两次8字节原子比较交换可以解决漏洞窗口问题,但只能分别处理每一半,留下"撕裂"问题。
Atomic 16B load/stores解决了撕裂问题,但没有解决load和stores之间的原子性问题。在某些硬件上使用SSE是可能的,但不能保证x86 ISA像8B自然对齐的加载和存储那样是原子性的。
Xen的lock cmpxchg16b
仿真:
Xen虚拟机有一个x86模拟器,我猜是针对VM在一台机器上启动并迁移到功能较差的硬件的情况。它通过采用全局锁来模拟lock cmpxchg16b
,因为没有其他方法。如果有一种方法可以"正确地"模仿它,我相信Xen会这样做的。
正如在这个邮件列表线程中所讨论的,当一个内核上的模拟版本与另一个内核上的非模拟指令访问相同的内存时,Xen的解决方案仍然不起作用。(原生版本不尊重全局锁)。
请参阅Xen邮件列表中的这个补丁,它更改了lock cmpxchg8b
仿真以支持lock cmpxchg8b
和lock cmpxchg16b
。
我还发现KVM的x86模拟器也不支持cmpxchg16b
,根据emulate cmpxchg16b
的搜索结果。
我认为所有这些都是很好的证据,证明我的分析是正确的,并且不可能安全地模拟它。
我看到你的代码模仿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的回答
该指令可以与LOCK前缀一起使用,以允许该指令自动执行。为了简化与处理器总线的接口,目的操作数接收一个写周期,而不考虑比较
的结果。
ELSE:
MOV RAX, r10
MOV RDX, r11
MOV QWORD PTR [r8], r10
MOV QWORD PTR [r8+8], r11
END: