PiR*_*cks 3 x86 assembly jit atomic self-modifying
假设我有如下所示的 x86-64 代码(尽管这个问题更普遍地适用于所有代码):
mov rbx,7F0140E5247Dh
jmp rbx
Run Code Online (Sandbox Code Playgroud)
如果目标值未对齐,而该代码可以执行,则覆盖目标常量是否安全?换句话说,我可以观察到部分更新的跳转目标,从而导致跳转到不存在的地址吗?此外,如果目标常量跨越页面或缓存行边界,这是否安全?
编辑:
我只对更改单个指令感兴趣,而不是更改指令边界位置。
仅当写入是原子的时,在 Intel 上通过未对齐的 qword 写入来保证,只要它不跨越缓存行边界,但在 AMD 上则不能保证。最低公分母原子性保证是 8 字节对齐存储是原子的,仅此而已。
使用 anxchg来执行保证原子 RMW。如果常量本身跨越缓存行边界,那将非常慢,但我相信是正确的。(总线锁,不仅仅是缓存锁;速度太慢了,甚至有一个性能计数器,甚至只是用于 split- lock,甚至还有一个 CPU 功能,至少在内核代码中会出现该错误,这样您就可以在虚拟机中找到它的实例。)无论 CPU 是什么,该常量都不会跨越有问题的边界,它应该与对齐的原子操作一样快。
或者,如果您的 CPU 支持 AVX,则 16 字节对齐的 SSE/AVX 存储在具有 AVX 的 CPU 上保证是原子的。(直到最近才记录下来,多年以来人们都知道这种做法在实践中基本上是安全的,但幸运的是,它对所有 AVX CPU 都有追溯力,没有新的功能位。)因此,如果您可以让常量排列不跨越 16 字节边界,你可以这样更新它。(用自身覆盖周围的字节不会导致问题,除非另一个线程也在更新附近的另一个常量。)
如果性能对此很重要(例如,每分钟执行一次以上),可能值得使用一些填充或 NOP 来使常量 8 字节对齐,特别是如果您可以延长早期指令而不需要实际的 NOP ,甚至是它mov r64,imm64本身。(尽管它是 10 个字节,并且一条指令的最大长度是 15。)
在其他情况下,您可能会重写一系列指令,其中指令边界位于不同的位置,这将是一个不同的故事。您说这个问题“更普遍”适用,但仅适用于替换立即数或用相同长度的指令替换整个 4 字节或 8 字节指令。如果另一个线程可能在您正在写入的区域内休眠或使用 RIP 运行,则必须考虑更新后从旧序列中任何可能的 RIP 中提取代码的情况。正如我所说,改变指令边界是有问题的。
但如果您尊重该限制,那么交叉修改代码据我所知是安全的。我认为 Windows 热补丁会停止可能正在运行代码的其他线程,但我不知道为什么,因为它已经确保有一个足够大的指令可供其覆盖。要么他们过于谨慎,要么存在一些我没有意识到的代码获取不尊重存储原子性的风险。也许只是他们不想在未对齐函数的情况下依赖 2 字节存储原子性,即使认为这是正常编译器设置中出于不同原因的默认设置。