Jas*_*son 13 concurrency x86 assembly x86-64 thread-safety
对于可能在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;
Run Code Online (Sandbox Code Playgroud)
某些处理器的勘误表中明确提到了序列化指令.例如,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
(使用英特尔符号而不是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处理器的计算机,明确记录为容易出现此问题,我没有编写触发此问题的代码.
编写代码来做到这一点对我来说是个人的好奇心.在生产代码中,我只是遵循规则,但我认为在复制时我可能需要学习一些东西.
想象一个具有很长指令流水线的处理器处理器,其中寄存器和内存仅在最后一个流水线阶段被修改。当您为此处理器编写自修改代码并修改内存中已存在于管道中的指令时,修改将无效。在这种情况下,程序的行为取决于处理器的管道有多长。
为了使具有更长管道的新处理器的行为与旧型号完全相同,英特尔处理器包含一种机制,可以在检测到这种情况时刷新(清空)管道。刷新后,修改后的代码将被提取到管道中,因此新处理器的行为与旧处理器完全相同。
序列化指令是刷新管道的另一种方法。当它到达管道末端时,管道被刷新并在串行化指令之后再次开始获取。
因此,勘误表本质上是说,某些处理器模型不会检查来自其他处理器的写入是否会覆盖已在其管道中执行的指令。该检查仅适用于本地写入,不适用于外部写入。但是,如果插入序列化指令,则会强制处理器刷新管道,一切都会按预期运行。
要重现勘误表中描述的行为,您需要确保从一个处理器修改的代码位于另一个处理器的管道内。看一下分支预测(决定哪个代码路径位于管道内)和同步原语。