首页 文章

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

提问于
浏览
21

我被告知并且从英特尔的手册中读到可以将指令写入内存,但是指令预取队列已经获取了陈旧的指令并将执行那些旧的指令 . 我没有成功观察到这种行为 . 我的方法如下 .

英特尔软件开发手册从第11.6节开始说明

写入当前在处理器中高速缓存的代码段中的存储器位置会导致关联的高速缓存行(或多个行)无效 . 此检查基于指令的物理地址 . 此外,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 Intel Core i5(2011 macbook air)和Intel(R)Xeon(R)CPU X3460 @ 2.80GHz上运行 . 但是,每次都看到val == 1表示CPU始终注意到新指令 .

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

2 回答

  • 23

    我想,你应该检查CPU的 MACHINE_CLEARS.SMC 性能计数器( MACHINE_CLEARS 事件的一部分)(它可以在你的Air powerbook中使用的Sandy Bridge 1中找到;也可以在你的Xeon上使用,这是Nehalem 2 - 搜索"smc" ) . 您可以使用 oprofile ,_ perf 或Intel的 Vtune 来查找其值:

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

    机器清除度量标准描述某些事件需要在最后一条退役指令之后清除并重新启动整个管道 . 此度量标准测量三种此类事件:内存排序违规,自修改代码以及对非法地址范围的某些加载 . 可能的问题执行时间的很大一部分用于处理机器清除 . 检查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/en-us/forums/topic/345561(链接自Intel Performance Bottleneck Analyzer's taxonomy

    检测到自修改代码时会触发此事件 . 这通常可以由进行二进制编辑以强制它采取某些路径(例如黑客)的人使用 . 此事件计算程序写入代码段的次数 . 自修改代码会导致所有Intel 64和IA-32处理器严重受损 . 修改后的高速缓存行被写回L2和LLC高速缓存 . 此外,需要重新加载指令,从而导致性能损失 .

    我想,你会看到一些这样的事件 . 如果是,那么CPU就能够检测到自动修改代码的行为,并提出“机器清除” - 完全重启管道 . 第一阶段是Fetch,他们会向L2缓存询问新的操作码 . 我对每次执行代码的SMC事件的确切计数非常感兴趣 - 这将给我们一些关于延迟的估计..(SMC计算在一些单位中,假设1个单位是1.5个cpu周期--B.6.2 . 英特尔优化手册6)

    我们可以看到英特尔说"restarted from just after the last retired instruction.",所以我认为最后退休的指令将是 mov ;你的nops已经在管道中了 . 但SMC将在mov退休时被提升,它将杀死管道中的所有东西,包括nops .

    这个SMC引起的管道重启并不便宜,Agner在_1181309中有一些测量 - "17.10 Self-modifying code (All processors)"(我认为任何Core2 / CoreiX都像PM一样):

    修改后立即执行一段代码的代价是P1约为19个时钟,PMMX约为31个时钟,PPro,P2,P3,PM为150-300 . P4将在自修改代码后清除整个跟踪缓存 . 80486及更早版本的处理器需要在修改代码和修改代码之间跳转才能刷新代码缓存 . ...自修改代码不被认为是良好的编程习惯 . 只有当速度增益很大并且修改后的代码执行了很多次才能使用它时,才能使用它,这有利于超过使用自修改代码的惩罚 .

    这里建议使用不同的线性地址来使SMC检测器失效:https://stackoverflow.com/a/10994728/196561 - 我实际上回答了你真正的问题 .

    这里可能有一些提示:Optimization manual, 248966-026, April 2012 "3.6.9 Mixing Code and Data":

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

    和下一节

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

    因此,可能有一些原理图控制可写子页面和可执行子页面的交叉点 .

    您可以尝试从其他线程(交叉修改代码)进行修改 - 但需要非常仔细的线程同步和管道刷新(您可能希望在编写器线程中包含一些强制延迟;同步后的CPUID是理想的) . 但你应该知道他们已经使用“ nukes ”解决了这个问题 - 检查US6857064专利 .

    我对提到P6和奔腾处理器的手册感到有些困惑

    如果你已经获取,解码并执行了一些陈旧版本的英特尔指导手册,这是可能的 . 您可以重置管道并检查此版本:Order Number: 325462-047US, June 2013 "11.6 SELF-MODIFYING CODE" . 此版本仍未提及有关较新CPU的任何内容,但提到当您使用不同的虚拟地址进行修改时,微架构之间的行为可能不兼容(它可能适用于您的Nehalem / Sandy Bridge并且可能无法正常运行... Skymont)

    11.6自修改代码对当前在处理器中高速缓存的代码段中的内存位置的写入会导致关联的高速缓存行(或多个行)无效 . 此检查基于指令的物理地址 . 此外,P6系列和奔腾处理器检查对代码段的写入是否可以修改已经预取执行的指令 . 如果写入影响预取指令,则预取队列无效 . 后一种检查基于指令的线性地址 . 对于Pentium 4和Intel Xeon处理器,代码段中的指令的写入或窥探(其中目标指令已经被解码并驻留在跟踪高速缓存中)使整个跟踪高速缓存无效 . 后一种行为意味着在Pentium 4和Intel Xeon处理器上运行时,自我修改代码的程序可能会导致性能严重下降 . 实际上,检查线性地址不应该在IA-32处理器之间产生兼容性问题 . 包含自修改代码的应用程序使用相同的线性地址来修改和获取指令 . 使用与用于获取指令的线性地址不同的线性地址修改指令的系统软件(例如调试器)将在执行修改的指令之前执行序列化操作,例如CPUID指令,这将自动重新同步指令缓存和预取队列 . (有关使用自修改代码的更多信息,请参见第8.1.3节“处理自身和交叉修改代码” . )对于Intel486处理器,写入缓存中的指令将在缓存中修改它和内存,但如果在写入之前预取了指令,那么旧版本的指令可能是执行的指令 . 为防止执行旧指令,请通过编码a来刷新指令预取单元在任何修改指令的写入之后立即跳转指令

    REAL Update ,用Google搜索 "SMC Detection" (带引号)并且有一些细节如何现代Core2 / Core iX检测SMC以及许多带有Xeons和Pentiums的勘误列表挂在SMC探测器中:

    根据专利US6237088(图5,摘要),存在“行地址缓冲器”(具有许多线性地址,每个取出指令一个地址 - 或者换句话说,缓冲器充满具有高速缓存行精度的取出IP) . 每个商店,或每个商店的更精确的“商店地址”阶段将被送入并行比较器进行检查,将与任何当前正在执行的指令存储交叉 .

    这两项专利都没有明确说明,他们是否会在SMC逻辑中使用物理或逻辑地址...... Sandy桥中的L1i是VIPT(Virtually indexed, physically tagged,索引的虚拟地址和标签中的物理地址 . ),因此我们有了_118324_ L1缓存返回数据时的物理地址 . 我认为英特尔可能会在SMC检测逻辑中使用物理地址 .

    更重要的是,http://www.google.com/patents/US6594734 @ 1999(2003年出版,只记得CPU设计周期大约3 - 5年)在"Summary"部分说SMC现在在TLB并使用物理地址(换句话说 - 请不要试试愚弄SMC探测器):

    使用转换后备缓冲器来检测自修改代码 . [其]存储有物理页面地址,在该物理页面地址上可以使用存储器的物理存储器地址来执行窥探 . ...为了提供比一页地址更精细的粒度,FINE HIT位包含在高速缓存中的每个条目中,将高速缓存中的信息与存储器内的页面部分相关联 .

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

    然后他们说

    因此,由存储指令触发到存储器中的窥探可以通过将存储在指令高速缓存内的所有指令的物理地址与存储在相关页面或存储器页面内的所有指令的地址进行比较来执行SMC检测 . 如果存在地址匹配,则表示已修改内存位置 . 在地址匹配的情况下,指示SMC条件,退出单元刷新指令高速缓存和指令流水线,并从存储器中取出新指令以存储到指令高速缓冲存储器中 . 因为用于SMC检测的窥探是物理的,并且ITLB通常接受将线性地址转换为物理地址作为输入,所以ITLB另外形成为物理地址上的内容可寻址存储器并且包括附加的输入比较端口(参考作为窥探端口或反向转换端口)

    • 因此,为了检测SMC,它们强制商店通过窥探将物理地址转发回指令缓冲区(类似的窥探将从其他核心/ cpus或从DMA写入到我们的缓存......),如果是snoop的phys . 解决与缓存行冲突的问题,存储在指令缓冲区中,我们将通过从iTLB传送到退出单元的SMC信号重新启动流水线 . 可以想象在dTLB通过iTLB和退休单元这样的窥探循环中会浪费多少CPU时钟(它不能退出下一个“nop”指令,尽管它早于mov执行并且没有副作用) . 但是WAT? ITLB具有物理地址输入和第二个CAM(大而热),仅用于支持和防御疯狂和欺骗自修改代码 .

    PS:如果我们将使用大页面(4M或可能是1G)怎么办? L1TLB有很大的页面条目,并且可能有很多错误的SMC检测到1/4的4 MB页面...

    PPS:有一种变体,只有早期的P6 / Ppro / P2存在错误处理具有不同线性地址的SMC ...

  • 7

    我已经被告知并且已经从英特尔的手册中读到可以将指令写入存储器,但是指令预取队列已经[可能]已经获取过时指令并且[可能]将执行那些旧指令 . 我没有成功观察到这种行为 .

    是的,你会的 .

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

    他们根据物理地址窥探管道,而不仅仅是线性 .

    允许处理器实现比手册更严格 .

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

    或者......因为遵守架构规范的最简单方法(在SMC的情况下,以前正式“直到下一个序列化指令”,但在实践中,对于遗留代码,“直到下一个采用的分支,超过???字节距离“)可能更严格 .

相关问题