使用自修改代码观察x86上过时的指令获取

我已经被告知并且从英特尔的手册中读到了可以将指令写入存储器,但是指令预取队列已经提取了陈旧的指令并且将执行那些旧的指令。 我一直没有注意到这种行为。 我的方法如下。

英特尔软件开发手册从第11.6节中指出

对当前在处理器中caching的代码段中的存储单元的写入导致相关联的一个或多个caching行失效。 这个检查是基于指令的物理地址。 此外,P6系列和奔腾处理器会检查写入代码段是否可以修改已被预取执行的指令。 如果写入影响预取的指令,则预取队列失效。 后面的检查是基于指令的线性地址。

所以,看起来如果我希望执行陈旧的指令,我需要有两个不同的线性地址引用同一个物理页面。 所以,我把内存映射到两个不同的地址。

int fd = open("code_area", O_RDWR | O_CREAT, S_IRWXU | S_IRWXG | S_IRWXO); assert(fd>=0); write(fd, zeros, 0x1000); uint8_t *a1 = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_FILE | MAP_SHARED, fd, 0); uint8_t *a2 = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_FILE | MAP_SHARED, fd, 0); assert(a1 != a2); 

我有一个汇编函数接受一个参数,一个指向我想改变的指令的指针。

 fun: push %rbp mov %rsp, %rbp xorq %rax, %rax # Return value 0 # A far jump simulated with a far return # Push the current code segment %cs, then the address we want to far jump to xorq %rsi, %rsi mov %cs, %rsi pushq %rsi leaq copy(%rip), %r15 pushq %r15 lretq copy: # Overwrite the two nops below with `inc %eax'. We will notice the change if the # return value is 1, not zero. The passed in pointer at %rdi points to the same physical # memory location of fun_ins, but the linear addresses will be different. movw $0xc0ff, (%rdi) fun_ins: nop # Two NOPs gives enough space for the inc %eax (opcode FF C0) nop pop %rbp ret fun_end: nop 

在C中,我将代码复制到内存映射文件。 我从线性地址a1调用函数,但是我传递一个指向a2的指针作为代码修改的目标。

 #define DIFF(a, b) ((long)(b) - (long)(a)) long sz = DIFF(fun, fun_end); memcpy(a1, fun, sz); void *tochange = DIFF(fun, fun_ins); int val = ((int (*)(void*))a1)(tochange); 

如果CPU拿起修改后的代码,val == 1。 否则,如果陈旧的指令被执行(两个nops),val == 0。

我已经在1.7GHz的英特尔酷睿i5(2011年的MacBook空气)和英特尔(R)至强(R)的CPU X3460 @ 2.80GHz运行此。 但是,每次我都看到val == 1表示CPU总是注意到新的指令。

有没有人有我想观察的行为的经验? 我的推理是否正确? 我对手册中提到的P6和奔腾处理器有点困惑,以及缺乏提及我的酷睿i5处理器的问题。 也许其他事情正在进行,导致CPU刷新其指令预取队列? 任何见解都会非常有帮助!

我想,你应该检查CPU的MACHINE_CLEARS.SMC性能计数器( MACHINE_CLEARS事件的一部分)(它可以在你的Air powerbook中使用的Sandy Bridge 1中使用;也可以在你的Xeon上使用Nehalem 2 -search“smc”)。 您可以使用oprofileperf或Intel的Vtune来查找它的值:

http://software.intel.com/sites/products/documentation/doclib/iss/2013/amplifier/lin/ug_docs/GUID-F0FD7660-58B5-4B5D-AA9A-E1AF21DDCA0E.htm

机器清除

度量标准说明

某些事件要求整个stream水线从最后一条退休指令刚刚完成之后清除并重新启动。 这个度量度量了三个这样的事件:内存sorting违规,自修改代码,以及某些非法地址范围的负载。

可能的问题

处理机器清除花费了执行时间的大部分时间。 检查MACHINE_CLEARS事件以确定具体原因。

SMC: http : //software.intel.com/sites/products/documentation/doclib/stdxe/2013/amplifierxe/win/win_reference/snb/events/machine_clears.html

MACHINE_CLEARS事件代码:0xC3 SMC掩码:0x04

自我修改代码(SMC)被检测到。

检测到的自修改代码机器清除次数。

英特尔还介绍了关于smc http://software.intel.com/zh-cn/forums/topic/345561 (链接自英特尔性能瓶颈分析器的分类

当检测到自修改代码时,此事件触发。 这可以通过二进制编辑强制它采取一定的path(如黑客)的人通常使用。 此事件计算程序写入代码段的次数。 自修改代码在所有Intel 64和IA-32处理器中造成严重损失。 修改的高速caching行被写回到L2和LLC高速caching。 而且,指令需要重新加载,从而导致性能损失。

我想,你会看到一些这样的事件。 如果是,则CPU能够检测到自修改代码的行为,并提出了“机器清除” – pipe道的完全重启。 第一阶段是提取,他们会问二级caching新的操作码。 我对每次执行代码的SMC事件的确切计数非常感兴趣 – 这会给我们一些关于延迟的估计。(SMC被计算在一个单位中,其中1个单位被假定为1.5个cpu周期–B.6.2。 intel优化手册6)

我们可以看到,Intel说“从最后一个退休指令后面重新开始”,所以我认为最后退休的指令是mov ; 而你的nops已经在stream水线了。 但是SMC将在Mov退休时筹集资金,并且会杀死所有正在stream失的东西,包括nops。

这个SMC引起的stream水线重启并不便宜,Agner在Optimizing_assembly.pdf中有一些测量 – “17.10自我修改代码(所有处理器)”(我认为任何Core2X / CoreiX都像PM一样):

修改后立即执行一段代码的惩罚大约是19个时钟,P1为31,PMMX为31,PPro,P2,P3,PM为150-300。 P4将自修改代码后清除整个跟踪caching。 80486及更早版本的处理器需要在修改代码和修改代码之间跳转,以刷新代码caching。 …

自修改代码不被认为是良好的编程习惯。 只有在速度增益很大且修改后的代码被执行了很多次以至于使用自修改代码的优势大于惩罚时才应该使用它。

使用不同的线性地址失败SMC检测器在这里推荐: https : //stackoverflow.com/a/10994728/196561 – 我会尽力find真正的英特尔文档…现在不能真正回答你真正的问题。

这里可能有一些提示: 优化手册,248966-026,2012年4月 “3.6.9混合代码和数据”:

在代码段中放置可写数据可能无法与自修改代码区分开来。 代码段中的可写数据可能会遭受与自修改代码相同的性能损失。

和下一节

软件应避免写入正在执行的相同1 KB子页面中的代码页,或者在正在写入的相同2 KB子页面中获取代码。 另外,将包含直接或推测性执行的代码的页面与另一个处理器共享为数据页面可触发SMC条件,从而导致机器的整个stream水线和跟踪caching被清除。 这是由于自修改代码条件。

所以,可能有一些控制可写和可执行子页面的交集的原理图。

您可以尝试从其他线程(交叉修改代码)进行修改 – 但是需要非常小心的线程同步和stream水线刷新(您可能希望包含一些在写入线程中的强制延迟;在同步之后的CPUID是期望的)。 但是你应该知道,他们已经用“ 核武器 ”来解决这个问题 – 查看US6857064专利。

我对手册中提到P6和奔腾处理器有点困惑

这是可能的,如果你已经提取,解码和执行一些陈旧的英特尔的指导手册版本。 您可以重置pipe道并检查此版本: 订单号:325462-047US,2013年6月 “11.6自我修改代码”。 这个版本仍然没有提到任何关于较新的CPU,但提到,当你使用不同的虚拟地址进行修改时,这种行为在微架构之间可能不兼容(可能在你的Nehalem / Sandy Bridge上工作,可能无法在Skymont上运行)

11.6自我修改代码写入当前在处理器中caching的代码段中的内存位置会导致关联的caching行(或行)无效。 这个检查是基于指令的物理地址。 此外,P6系列和奔腾处理器会检查写入代码段是否可以修改已被预取执行的指令。 如果写入影响预取的指令,则预取队列失效。 后面的检查是基于指令的线性地址。 对于Pentium 4和Intel Xeon处理器,目标指令已经解码并驻留在跟踪caching中的代码段中的指令的写入或窥探会使整个跟踪caching失效。 后一种行为意味着在奔腾4和英特尔至强处理器上运行时,自修改代码的程序可能导致性能严重下降。

实际上,对线性地址的检查不应该在IA-32处理器之间产生兼容性问题。 包含自修改代码的应用程序使用相同的线性地址来修改和读取指令。

诸如debugging器之类的系统软件可能修改使用与用于取指令的线性地址不同的线性地址的指令,将在执行修改的指令之前执行序列化操作,诸如CPUID指令,其将自动地重新同步指令caching和预取队列。 (有关使用自修改代码的更多信息,请参见第8.1.3节“处理自交叉和修改代码”。)

对于Intel486处理器,对高速caching中的指令的写入将在高速caching和内存中对其进行修改,但是如果在写入之前指令被预取,则旧版本的指令可能是执行的指令。 为了防止执行旧的指令,在修改指令的任何写入之后立即编写跳转指令来刷新指令预取单元

真正的更新 ,search“SMC检测” (带引号),还有一些细节如何现代Core2 / Core iX检测SMC和许多勘误表与SMC检测器悬挂Xeons和Pentiums:

  1. http://www.google.com/patents/US6237088系统和方法用于跟踪pipe道中的飞行中指令@ 2001

  2. DOI 10.1535 / itj.1203.03(谷歌为它,在citeseerx.ist.psu.edu有免费版本) – 在Penryn添加“INCLUSION FILTER”,以减less错误的SMC检测数量; 图9中示出了“现有包含检测机制”

  3. http://www.google.com/patents/US6405307 – 有关SMC检测逻辑的较早专利

根据专利US6237088(图5,总结),存在“线地址缓冲器”(具有许多线性地址,每个取出的指令有一个地址 – 或者换言之,具有caching行精度的取出的IP满缓冲器)。 每个商店的每个商店或更确切的“商店地址”阶段将被送入并行比较器检查,将存储与任何当前正在执行的指令的交集。

这两个专利都没有清楚地说,他们会在SMC逻辑中使用物理地址还是逻辑地址… …桑迪桥中的L1i是VIPT( 虚拟索引,物理标记 , 标记中的索引和物理地址的虚拟地址)根据http ://nick-black.com/dankwiki/index.php/Sandy_Bridge所以我们在L1caching返回数据时有物理地址。 我认为intel可能会在SMC检测逻辑中使用物理地址。

更进一步, http://www.google.com/patents/US6594734 @ 1999(2003年出版,只记得CPUdevise周期大概是3 – 5年),在“摘要”部分中提到,SMC现在在TLB中,并使用物理地址(或换句话说 – 请不要试图愚弄SMC探测器):

使用翻译旁视缓冲器检测自修改代码,其中存储有物理页面地址,通过该页面地址,可以使用存储器物理存储器地址窥探执行到存储器中。 为了提供比一页地址更精细的粒度,FINE HIT位被包括在高速caching中的每个条目中,将高速caching中的信息与存储器中页面的各部分相关联。

(页面的一部分,在专利US6594734中被称为象限,听起来像1K个子页面,不是吗?)

然后他们说

因此,由存储指令触发到存储器中的窥探可以通过将存储在指令高速caching内的所有指令的物理地址与存储在相关联的页面或存储页面内的所有指令的地址进行比较来执行SMC检测。 如果有地址匹配,则表示内存位置已被修改。 在地址匹配的情况下,指示SMC条件,指令高速缓冲存储器和指令stream水线由引退单元清除,并从存储器取出新的指令以存储到指令高速缓冲存储器中。

由于用于SMC检测的侦听是物理的,并且ITLB通常接受将线性地址转换为物理地址作为input,所以ITLB另外形成为物理地址上的内容可寻址存储器,并且包括额外的input比较端口(被称为作为窥探端口或反向翻译端口)

所以,为了检测SMC,他们强制存储器通过snoop将物理地址转发回指令缓冲区(类似的snoops将从其他内核/ cpus或从DMA写入到我们的caching….),如果snoop的phys。 地址与caching行冲突,存储在指令缓冲区中,我们将通过从iTLB发送到退休单元的SMC信号重启pipe道。 可以想象,在dTLB通过iTLB和退休单元的这种监听循环中,CPU时钟将浪费多less(尽pipe它早于mov执行并且没有副作用,它不能退休下一个“nop”指令)。 但是WAT? ITLB具有物理地址input和第二个CAM(大和热),以支持和防止疯狂和欺骗性的自修改代码。

PS:如果我们用大页面(4M或可能是1G)工作呢? L1TLB有大量的页面条目,并且可能有很多错误的SMC检测到四分之一的4 MB页面…

PPS:有一个变种,错误的处理SMC与不同的线性地址只出现在早期的P6 / Ppro / P2 …

我已经被告知并且从英特尔的手册中读到可以将指令写入存储器,但是指令预取队列已经[可能]已经提取了陈旧的指令并且将[可以]执行那些旧的指令。 我一直没有注意到这种行为。

是的,你会的。

所有或几乎所有的现代英特尔处理器都比手册更严格:

他们根据物理地址来窥探pipe道,而不仅仅是线性的。

处理器的实现可以比手册更严格。

他们可能会select这样做,因为他们遇到的代码不符合手册中的规则,他们不想破坏。

或者…因为坚持体系结构规范的最简单的方法(在SMC的情况下,以前正式“直到下一个序列化指令”,但在实践中,对于遗留代码)是“直到下一个被采纳的分支远远超过“字节”)可能会更严格。