在x86-64 CPU上复现交叉修改代码的意外行为

12

问题

有哪些跨修改代码的想法可以在x86或x86-x64系统上触发意外行为,其中跨修改代码中所有内容都正确,除了在执行修改后的代码之前在执行处理器上执行序列化指令?

如下所述,我有一台Core 2 Duo E6600处理器可供测试,该处理器明确提到存在与此相关的问题。我将在此机器上测试任何与我分享的想法并提供更新。

背景

在x86和x64系统上,编写跨修改代码的官方指导是进行以下操作:

; Action of Modifying Processor
Store modified code (as data) into code segment;
Memory_Flag ← 1; 

; Action of Executing Processor
WHILE (Memory_Flag ≠ 1)
  Wait for code to update;
ELIHW;
Execute serializing instruction; (* For example, CPUID instruction *)
Begin executing modified code;

序列化指令在某些处理器的勘误表中被明确提及为必要操作。例如,英特尔 Core 2 Duo E6000 系列有以下勘误:(来自http://www.mathemainzel.info/files/intelX6800andintelE6000.pdf)
引起一个处理器或系统总线主机将数据写入第二个处理器当前正在执行的代码段,并希望第二个处理器将该数据作为代码执行的行为称为交叉修改代码(XMC)。不强制第二个处理器在执行新代码之前执行同步指令的 XMC 称为非同步 XMC。
使用非同步 XMC 来修改处理器的指令字节流的软件可能会看到执行修改后代码的处理器出现意外或不可预测的执行行为。
关于为什么如果不使用序列化指令就可能出现意外执行行为的原因,存在一些推测,详见http://linux.kernel.narkive.com/FDc9TB0d/patch-linux-kernel-markers
当i-fetch完成并且micro-ops被存储在跟踪缓存中时,原始机器指令和微操作之间不再有直接的对应关系,这是由于优化所导致的。例如(为了说明人工构造的):mov eax, ebx;mov memory, eax;mov eax, 1。在跟踪缓存中,将不会有任何微操作来更新eax与ebx的值。在运行时将“mov eax, ebx”更改为“mov ecx, ebx”将实效优化的跟踪缓存,因此唯一的解决方案是一个GPF。如果修改不使跟踪缓存失效,则不会发生GPF。问题是:“我们能否预测跟踪缓存没有失效的情况”,一般而言答案是否定的,因为微体系结构不是公开的。但是,可以猜测使用中断指令int3修改单个字节的操作码不会导致无法处理的不一致性。这就是英特尔确认的情况,可以存储int3而无需同步(即强制刷新跟踪缓存)。

此外,https://sourceware.org/ml/systemtap/2005-q3/msg00208.html上还有更多信息:

当我们意识到这个问题时,我和英特尔微架构团队进行了长时间的讨论。事实证明,这个错误(顺便说一句,英特尔不打算修复)的原因是跟踪缓存 - 指令解释产生的微操作流 - 不能保证有效。在行文之间,我认为这个问题出现是因为跟踪缓存中进行了优化,不再可能识别原始指令边界。如果CPU发现由于未同步的交叉修改而使跟踪缓存无效,则指令执行将被中止并显示GPF。与英特尔的进一步讨论表明,用int3替换第一个操作码字节不会受到这个错误的影响。

除了我在互联网上发布的内容外,关于这个问题没有太多其他的信息。此外,在使用x86和x86-64系统上的交叉修改代码时,我没有找到任何公开的人们遇到指令执行失败的例子。

我有一台电脑,运行着Intel Core 2 Duo E6600处理器,这个处理器明确记录了存在这个问题,并且我还没有写出能够触发这个问题的代码。
编写代码是我个人的好奇心。在生产代码中,我只会遵循规则,但我认为通过复现这个问题,可能还有些东西我需要学习。

1
哇,跟踪缓存?你打算在 Pentium 4 上运行吗?如果你能在 Core2 上触发那种情况,我会非常惊讶的。你在哪里看到它被记录为敏感的? - Leeor
2个回答

4

想象一下,有一个非常长的指令流水线的处理器,在这个流水线中,寄存器和内存只在最后一个流水线阶段被修改。当你为这个处理器编写自修改代码并修改已经存在于流水线中的内存中的指令时,修改将没有效果。在这种情况下,程序的行为取决于处理器流水线的长度。

为了使具有更长流水线的新处理器的行为与旧型号完全相同,英特尔处理器包括一种机制,即在检测到此情况时刷新(清空)流水线。刷新后,修改后的代码被获取到流水线中,因此新处理器的行为与旧型号完全相同。

序列化指令是另一种刷新流水线的方法。当它到达流水线的末端时,流水线会被刷新,并在序列化指令之后重新开始获取。

因此,错误报告实质上是说,一些处理器型号不检查其他处理器的写操作是否覆盖已经在其流水线中执行的指令。该检查仅适用于本地写入,而不适用于外部写入。但是,如果插入序列化指令,则可以强制处理器刷新流水线,并且所有内容都将按预期运行。

为了重现勘误中描述的行为,您需要确保您正在修改的代码来自另一个处理器的管道内。请查看分支预测(决定哪些代码路径在管道内)和同步原语。

根据Richard J Moore的两个引用,问题在于CPU确实会检测到跨CPU代码修改,但如果无法确定哪些微操作在跟踪缓存中被修改,则会生成一般保护故障。 - Ross Ridge
@RossRidge OP想在没有跟踪缓存的CPU上重现这个勘误。当CPU执行旧代码,然后突然开始看到新代码时,PC可能不再指向有效指令,寄存器内容可能不是代码所期望的等等。因此,代码可能会生成任何类型的异常,包括通用保护故障。 - Mackie Messer

2
您几乎不可能重现这种行为。首先要记住,自修改和交叉修改代码并不罕见。当您使用调试器设置断点或修改内存时,每天都会发生。或者当加载DLL并且需要将其重定位到不同的地址时也会发生。
即使您故意省略序列化指令,您仍然很难避免对其他处理器的代码进行调整。您需要的简单事情,例如实现同步或更改页面保护属性以便修改代码,很可能通过操作系统内部的代码路径进行序列化。
此外,您引用的勘误表和FUD电子邮件已经过时,它们可以追溯到多核处理器首次普及的时期。Intel始终记录适用于任何处理器(包括未修复错误的处理器)的推荐方法。当前型号是否仍然需要序列化指令很难发现。
最好不要浪费时间在这上面。

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