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

时间:2013-06-30 22:52:12

标签: c caching x86 self-modifying

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

英特尔软件开发手册从第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刷新其指令预取队列?任何见解都会非常有用!

3 个答案:

答案 0 :(得分:23)

我认为,您应该检查CPU的 MACHINE_CLEARS.SMC 性能计数器(MACHINE_CLEARS事件的一部分)(可在Sandy Bridge 1中找到,在你的Air powerbook中使用;也可以在你的Xeon上使用,这是Nehalem 2 - 搜索" smc")。您可以使用oprofileperf或英特尔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)

我们可以看到英特尔在最后一条退役指令之后重新启动了。#34;所以我认为最后一条退役指令将是mov;你的nops已经在管道中了。但是SMC将在mov退休时被提升,它会杀死管道中的所有东西,包括nops。

这个SMC引起的管道重启并不便宜,Agner在Optimizing_assembly.pdf - " 17.10自修改代码(所有处理器)"中进行了一些测量。 (我认为任何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混合代码和数据":

  

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

和下一节

  

软件应避免在同一个1 KB的子页面中写入代码页   正在执行或获取同一个2 KB的子页面中的代码   书面。此外,共享包含直接或推测性执行的页面   使用另一个处理器作为数据页的代码可以触发导致的SMC条件   要清除机器的整个管道和跟踪缓存。这是由于   自修改代码条件。

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

您可以尝试从其他线程(交叉修改代码)进行修改 - 但是需要非常小心的线程同步和管道刷新(您可能希望在编写器线程中包含一些强制延迟; CPUID只是在需要同步之后)。但你应该知道他们已经使用" nukes " - 检查US6857064专利。

  

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

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

  

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

     

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

     

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

     

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

REAL更新,用Google搜索&#34; SMC Detection&#34; (带引号),并详细说明现代Core2 / Core iX如何检测SMC以及许多勘误表列出了Xeons和Pentiums挂在SMC探测器中:

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

  2. DOI 10.1535 / itj.1203.03(google for it,citeseerx.ist.psu.edu有免费版) - &#34;包含过滤器&#34;在Penryn中添加了更少的错误SMC检测数;现有的包含检测机制&#34;如图9所示

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

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

    这两项专利都没有明确说明,他们是否会在SMC逻辑中使用物理或逻辑地址... Sandy桥中的L1i是VIPT(Virtually indexed, physically tagged,索引的虚拟地址和标签中的物理地址。)根据http://nick-black.com/dankwiki/index.php/Sandy_Bridge所以我们在L1缓存返回数据时有物理地址。我认为英特尔可能会在SMC检测逻辑中使用物理地址。

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

      

    使用转换后备缓冲区检测自修改代码 .. [其中]存储了物理页面地址,可以使用物理执行 snoops 商店的内存地址到内存中。 ...为了提供比一页地址更精细的粒度,缓存中的每个条目都包含FINE HIT位,将缓存中的信息与内存中页面的某些部分相关联。

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

    然后他们说

      

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

         

    由于用于SMC检测的窥探是物理的,并且ITLB通常接受将线性地址转换为物理地址作为输入,因此ITLB还在物理地址上形成为内容可寻址存储器,并包括附加输入比较端口(称为窥探端口或反向转换端口)

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

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

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

答案 1 :(得分:7)

  

我已经被告知并且已经阅读过英特尔手册,这是可能的   向内存写入指令,但是指令预取队列   已经[可能]已经取得陈旧的指示并将[可]   执行那些旧指令。我没有成功观察到这种行为。

是的,你会的。

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

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

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

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

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

答案 2 :(得分:1)

桑迪布里奇家庭(至少是Skylake)仍然具有相同的行为,显然是在监听物理地址。

您的测试有些复杂,。我看不到跳远点,如果您将SMC函数组装(并在必要时链接)为平面二进制文件,则只需打开+ mmap两次即可。创建a1a2函数指针,然后main可以在映射后return a1(a2)

这是一个简单的测试工具,以防万一任何人想在自己的机器上尝试 :(从问题中复制了open / assert / mmap块,感谢您的出发点。)

缺点是,每次必须重建SMC平面二进制文件,因为使用MAP_SHARED映射它实际上对其进行了修改。IDK如何获取相同的物理页面的两个映射不会修改基础文件;写入MAP_PRIVATE会将其COW到另一个物理页面。因此,现在我意识到了这一点,将机器代码写入文件并对其进行映射就变得很有意义。但是我的asm仍然简单得多。)

// smc-stale.c
#include <sys/mman.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>

typedef int (*intfunc_t)(void *);   // __attribute__((sysv_abi))  // in case you're on Windows.

int main() {
    int fd = open("smc-func", O_RDWR);

    assert(fd>=0);
    intfunc_t a1 = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC,
                MAP_FILE | MAP_SHARED, fd, 0);
    intfunc_t a2 = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC,
                MAP_FILE | MAP_SHARED, fd, 0);
    assert(a1 != a2);
    return a1(a2);
}

测试功能的NASM源:

(有关as的{​​{1}} + ld的替代方法,请参见How to generate plain binaries like nasm -f bin with the GNU GAS assembler?

nasm -f

在运行Linux 4.20.3-arch1-1-ARCH的i7-6700k上,我们观察过时的代码提取。用;;build with nasm smc-func.asm -fbin is the default. bits 64 entry: ; rdi = another mapping of the same page that's executing mov byte [rdi+dummy-entry], 0xcc ; trigger any copy-on-write page fault now mov r8, rbx ; CPUID steps on call-preserved RBX cpuid ; serialize for good measure mov rbx, r8 ; mfence ; lfence mov dword [rdi + retmov+1 - entry], 0 ; return 0 for snooping retmov: mov eax, 1 ; opcode + imm32 ; return 1 for stale ret dummy: dd 0xcccccccc 覆盖立即mov的{​​{1}}确实在执行该指令之前对其进行了修改。

1