首页 文章

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

提问于
浏览
13

问题

对于可能在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;

某些处理器的勘误表中明确提到了序列化指令 . 例如,Intel 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并且微操作在跟踪高速缓存中时,原始机器指令边界和微操作之间不再存在直接关联 . 这是由于优化 . 例如(用于说明目的的人工):mov eax,ebx mov memory,eax mov eax,1(使用intel符号而不是ATT - 习惯的力量)在跟踪缓存中,没有微操作可以用ebx更新eax . 动态地将“mov eax,ebx”改为“mov ecx,ebx”会使优化的跟踪缓存无效,因此onlhy求助是一个GPF . 如果修改不会使跟踪缓存无效,则不会使用GPF . 问题是:“我们可以预测跟踪缓存未被无效的情况”,并且由于微架构不公开,因此答案通常是否定的 . 但可以猜测,在中断指令(int3)中修改单字节操作码不会导致无法处理的不一致 . 这就是英特尔证实的 . 继续存储int3而不需要同步(即强制刷新跟踪缓存) .

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

当我们意识到这一点时,我与英特尔的微架构人员进行了长时间的讨论 . 事实证明,这种错误的原因(顺便说一下英特尔不打算修复)是因为跟踪缓存 - 由指令解释产生的微波流 - 不能保证有效 . 在线之间进行读取我认为这个问题的出现是因为在跟踪高速缓存中进行了优化,不再可能识别原始指令边界 . 如果CPU发现跟踪缓存由于不同步的交叉修改而失效,那么将使用GPF中止指令执行 . 与英特尔的进一步讨论表明,用int3替换第一个操作码字节不会受到此错误的影响 .

除了我在这里发布的内容之外,我在互联网上看到的关于这个问题的内容并不多 . 另外,在x86和x86-64系统上使用交叉修改代码时,我没有找到任何人因为未能执行序列化指令而被咬的公开示例 .

我有一台运行英特尔酷睿2双核E6600处理器的计算机,该计算机明确记录为容易出现此问题,而且我无法编写触发此问题的代码 .

编写代码来做到这一点对我来说是个人的好奇心 . 在 生产环境 代码中,我只是遵循规则,但我认为在复制时我可能需要学习一些东西 .

2 回答

  • 2

    可以想象一个具有很长时间的处理器instruction pipeline,其中寄存器和存储器仅在最后一个流水线阶段被修改 . 当您为此处理器编写自修改代码并修改已在内存中的指令时存在于管道中,修改将无效 . 在这种情况下,程序的行为取决于处理器的流水线的长度 .

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

    序列化指令是刷新管道的另一种方法 . 当它到达管道的末尾时,管道被刷新并在序列化指令之后再次开始取出 .

    因此,勘误表实际上是说某些处理器模型不会检查来自其他处理器的写操作是否会覆盖已在其管道中执行的指令 . 该检查仅适用于本地写入,不适用于外部写入 . 但是,如果插入序列化指令,则强制处理器刷新管道,一切都将按预期运行 .

    要重现勘误表中描述的行为,您需要确保从一个处理器修改的代码位于另一个处理器的管道内 . 看一下分支预测(决定管道内的代码路径)和同步原语 .

  • 4

    您可以重现此行为的几率非常接近于零 . 首先要记住,自修改和交叉修改代码并不罕见 . 例如,当您使用调试器并设置断点或修改内存时,每天都会发生 . 或者当DLL被加载并且需要将其重新定位到不同的地址时 .

    即使您故意省略序列化指令,您仍然很难避免修改其他处理器的代码 . 您需要的简单事项,如实现同步或更改页面保护属性,以便您可以修改代码,很可能会通过将序列化的操作系统内的代码路径 .

    此外,您引用的勘误表和FUD电子邮件都很旧,它们的历史可以追溯到多核处理器首次普及的时间 . 英特尔始终记录适用于任何处理器的推荐方法,包括未修复错误的处理器 . 当前模型是否仍然实际需要序列化指令很难发现 .

    最好不要浪费你的时间 .

相关问题