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

时间:2015-01-26 04:46:59

标签: assembly concurrency x86 thread-safety x86-64

问题

交叉修改代码可以在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;

在某些处理器的勘误表中明确提到了序列化指令。例如,英特尔酷睿2双核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

     

(使用英特尔符号而不是ATT - 习惯的力量)

     

在跟踪缓存中,没有微操作可以使用ebx更新eax。

     

动态地将“mov eax,ebx”改为“mov ecx,ebx”会使   优化的跟踪缓存,因此onlhy求助是一个GPF。如果   修改doens't无效跟踪缓存然后没有GPF。该   问题是:“我们可以预测跟踪缓存的情况   没有失效“,一般的答案是否定的   微架构不公开。但人们可以猜测,修改   中断指令的单字节操作码 - int3 - 没有   导致无法处理的不一致。这就是英特尔   证实。继续存储int3而无需同步   (即强制刷新跟踪缓存)。

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

  

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

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

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

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

2 个答案:

答案 0 :(得分:4)

考虑一个具有很长instruction pipeline的处理器,其中寄存器和内存仅在最后一个管道阶段被修改。当您为此处理器编写自修改代码并修改管道中已存在的内存中的指令时,修改将不起作用。在这种情况下,程序的行为取决于处理器的流水线的长度。

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

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

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

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

答案 1 :(得分:2)

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

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

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

最好不要浪费你的时间。