C语言 使用自修改代码观察x86上的陈旧指令提取

ilmyapht  于 2022-12-17  发布在  其他
关注(0)|答案(4)|浏览(164)

我曾被告知并阅读过英特尔的手册,可以向内存写入指令,但指令预取队列已经获取了过时的指令,并将执行这些旧指令。我未能成功观察到这种行为。我的方法如下。
英特尔软件开发手册第11.6节指出
对当前缓存在处理器中的代码段中的内存位置的写入将导致关联的缓存线(或缓存线)。此检查基于指令的物理地址。此外,P6家族和奔腾处理器还检查写入代码段是否会修改已预取执行的指令。如果写入操作影响预取的指令,预取队列无效。后一项检查基于指令的线性地址。
看起来,如果我想执行过时的指令,我需要两个不同的线性地址,指向同一个物理页面,所以我把一个文件内存Map到两个不同的地址。

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语言中,我将代码复制到内存Map文件中,从线性地址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拾取修改的代码,则瓦尔==1。否则,如果执行了过时的指令(两个nop),则val==0。
我在1.7GHz的英特尔酷睿i5(2011 macbook air)和2.80GHz的英特尔(R)至强(R)CPU X3460上运行了这个程序,但是每次我都看到瓦尔==1,表明CPU总是注意到新指令。
有人经历过我想观察的行为吗?我的推理正确吗?我对手册中提到的P6和奔腾处理器感到有点困惑,为什么没有提到我的酷睿i5处理器。也许是其他什么原因导致CPU刷新其指令预取队列?任何见解都将非常有帮助!

6uxekuva

6uxekuva1#

我认为,你应该检查CPU的**MACHINE_CLEARS.SMC**性能计数器(MACHINE_CLEARS事件的一部分)(它在桑迪Bridge 1中可用,在你的Air Powerbook中使用;您也可以在您的Xeon处理器上找到,也就是Nehalem 2-搜索“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

机器清除

指标描述
某些事件要求清除整个流水线,并从上一条失效的指令之后立即重新启动。此度量衡量三个此类事件:内存排序违规、自修改代码以及某些加载到非法地址范围的情况。
可能的问题
执行时间的很大一部分用于处理机器清除。请检查MACHINE_CLEARS事件以确定具体原因。
SMC:http://software.intel.com/sites/products/documentation/doclib/stdxe/2013/amplifierxe/win/win_reference/snb/events/machine_clears.html
MACHINE_CLEARS事件代码:0xC 3 SMC掩码:0x04
检测到自修改代码(SMC)。
检测到的自修改代码机器清除数。
英特尔还提到了smc http://software.intel.com/en-us/forums/topic/345561(链接自Intel Performance Bottleneck Analyzer's taxonomy
当检测到自修改代码时激发此事件。这通常可由进行二进制编辑的人员使用,以强制代码采用特定路径(如黑客)。此事件统计程序写入代码段的次数。自修改代码会在所有英特尔64和IA-32处理器中导致严重的惩罚。修改的缓存线会写回到二级和LLC缓存。此外,则需要重新加载指令,因此导致性能损失。
我想,你会看到一些这样的事件。如果是的话,然后CPU能够检测到自修改代码的行为,并引发“机器清除”-管道完全重新启动。第一阶段是获取,它们将向二级缓存请求新的操作码。我对每次执行代码时SMC事件的准确计数非常感兴趣-这将为我们提供一些延迟估计值。(SMC以某些单位计算,其中1个单位假定为1.5个CPU周期-英特尔优化手册的B.6.2.6)
我们可以看到英特尔说“从最后一条退休指令之后重新启动",所以我认为最后一条退休指令将是mov;而且你的nops已经在管道中了。但是SMC将在mov退休时被提高,它将杀死管道中的所有东西,包括nops。
SMC导致的管道重启并不便宜,Agner在Optimizing_assembly.pdf中有一些度量-“17.10自修改代码(所有处理器)”(我认为任何Core 2/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“11.6自修改代码”.这个版本仍然没有说任何关于较新的CPU,但提到当你使用不同的虚拟地址修改时,行为可能在微体系结构之间不兼容(它可能在你的Nehalem/桑迪Bridge上工作,而可能在.. Skymont上不工作)

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

实际上,线性地址检查不会在IA-32处理器之间造成兼容性问题。包含自修改代码的应用程序使用相同的线性地址来修改和获取指令。
可能使用与用于获取指令的线性地址不同的线性地址来修改指令的系统软件(例如调试器)将在执行经修改的指令之前执行串行化操作(例如CPUID指令),所述串行化操作将自动地使指令高速缓冲存储器与预取队列再同步。(有关使用自修改代码的更多信息,请参见第8.1.3节“处理自修改代码和交叉修改代码”。
对于Intel 486处理器,该高速缓存中的指令将同时修改高速缓存和内存中的指令,但如果在写入之前预取了指令,则执行的指令可能是旧版本的指令。要防止执行旧指令,请在修改指令的任何写入之后立即编写跳转指令,以清除指令预取单元

真实的更新,已在Google上搜索**“SMC Detection”**(带引号),其中提供了现代酷睿2/酷睿iX如何检测SMC的一些详细信息,以及SMC检测器中挂有Xenons和Pentiums的许多勘误表:

1.用于跟踪流水线中的运行中指令的系统和方法

  1. DOI 10.1535/itj.1203.03(谷歌搜索,www.example.com上有免费版本citeseerx.ist.psu.edu)-在Penryn中添加了“包含过滤器”,以降低错误SMC检测的数量;现有夹杂物检测机制”如图9所示
  2. http://www.google.com/patents/US6405307-SMC检测逻辑的早期专利
    根据专利US 6237088(图5,概述),存在“行地址缓冲器”(具有许多线性地址,每个提取的指令一个地址,或者换句话说,充满具有高速缓存行精度的提取的IP的缓冲器)。每个存储,或者每个存储的更精确的“存储地址”相位将被馈送到并行比较器以检查,是否将存储与任何当前执行的指令相交。
    这两项专利都没有明确说明,在SMC逻辑中使用物理地址还是逻辑地址...桑迪bridge中的L1 i是VIPT(虚拟索引、物理标记、索引的虚拟地址和标记中的物理地址),根据http://nick-black.com/dankwiki/index.php/Sandy_Bridge,因此当L1缓存返回数据时,我们有物理地址。我认为英特尔可能在SMC检测逻辑中使用物理地址。
    更有甚者,http://www.google.com/patents/US6594734@1999(2003年出版,请记住CPU设计周期大约是3-5年)在“总结”部分中说SMC现在在TLB中并使用物理地址(或者换句话说-请不要试图欺骗SMC检测器):
  • 使用翻译后备缓冲器 * 来检测自修改代码...[其]具有存储在其中的物理页地址,可使用存储到存储器中的 * 物理存储器地址在所述物理页地址上执行 * 窥探 *...为了提供比地址页更精细的粒度,精细HIT位包含于高速缓冲存储器中的每一条目中,所述条目将高速缓冲存储器中的信息与存储器内的页的部分相关联。

(页面的一部分,在专利US 6594734中称为象限,听起来像1 K子页面,不是吗?)
然后他们说
因此,* 由将指令存储到存储器中触发的探听 * 可通过将存储在指令高速缓冲存储器内的所有指令的物理地址与存储在相关联的一个或多个存储器页内的所有指令的地址进行比较来执行SMC检测。如果存在地址匹配,则指示存储器位置已被修改。在地址匹配的情况下,指示SMC条件,指令高速缓冲存储器和指令管线由引退单元冲洗,且从存储器提取新指令以存储到指令高速缓冲存储器中。

因为用于SMC检测的探听是物理的,并且ITLB通常接受线性地址作为输入以转换成物理地址,所以ITLB另外形成为物理地址上的内容可寻址存储器,并且包括附加的输入比较端口(称为探听端口或反向转换端口)。
--因此,为了检测SMC,它们强制存储通过监听将物理地址转发回指令缓冲区(类似的监听将从其他内核/CPU或从DMA写入到我们的高速缓存...),如果监听的物理地址与高速缓存线冲突,则存储在指令缓冲器中,我们将通过从iTLB传送到引退单元的SMC信号重新启动流水线。可以想象,从dTLB到iTLB再到退休单元的这种监听循环将浪费多少CPU时钟(它不能引退下一个“nop”指令,虽然它比mov执行得早,而且没有副作用)。但是WAT?ITLB有物理地址输入和第二个CAM(又大又热)只是为了支持和防御疯狂和欺骗性的自修改代码。
PS:如果我们使用巨大的页面(4 M或1G)呢?L1 TLB有巨大的页面条目,并且可能有很多错误的SMC检测到1/4的4 MB页面...
PPS:存在一个变体,即具有不同线性地址的SMC的错误处理仅出现在早期P6/Ppro/P2......

gcxthw6b

gcxthw6b2#

我曾经被告知并阅读过英特尔的手册,可以将指令写入内存,但指令预取队列已经(可能)取出了过时的指令,并将(可能)执行这些旧指令。我没有成功观察到这种行为。
是的,你会的。
所有或几乎所有的现代英特尔处理器都比手册要求严格:
它们根据物理地址而不仅仅是线性地址来窥探管道。
允许处理器实现比手册更严格。
他们可能会选择这样做,因为他们遇到了不遵守手册中规则的代码,他们不想破坏这些代码。
或者......因为遵守架构规范的最简单方法(在SMC的情况下,正式的规范是“直到下一个序列化指令”,但在实践中,对于遗留代码,规范是“直到下一个执行的分支超过???字节”)可能会更严格。

smdncfj3

smdncfj33#

Sandybridge家族(至少是Skylake)仍然有同样的行为,显然是在窥探物理地址。

不过你的测试有点过于复杂了。我看不出远跳转的意义,如果你把SMC函数组装(必要时链接)成一个平面二进制,你只需要打开+ mmap两次。创建a1a2函数指针,然后main可以在Map后使用return a1(a2)
如果有人想在自己的计算机上试用,这里有一个简单的测试工具:(open/assert/mmap代码块是从问题中复制的,感谢您提供起点。)

缺点是,每次都必须重新构建SMC平面二进制文件,因为用MAP_SHAREDMap它实际上会修改它. IDK如何获得同一物理页的两个Map,而不会修改底层文件;写入MAP_PRIVATE会将其转移到不同的物理页面。2因此,既然我意识到了这一点,那么将机器码写入文件并将其Map是有意义的。3但我的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源代码:

(See How to generate plain binaries like nasm -f bin with the GNU GAS assembler?,用于as + ldnasm -f替代方案)

;;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

在运行Linux 4.20.3-arch 1 -1-ARCH的i7- 6700 k上,我们 * 没有 * 观察到陈旧代码获取。用0覆盖立即数1mov确实在运行之前修改了该指令。

peter@volta:~/src/experiments$ gcc -Og -g smc-stale.c
peter@volta:~/src/experiments$ nasm smc-func.asm && ./a.out; echo $?
0
# remember to rebuild smc-func every time, because MAP_SHARED modifies it
l2osamch

l2osamch4#

它的目标是一个更老的CPU(英特尔8088),但在8088 MPH演示结束时的4通道音乐播放器不仅执行陈旧的指令,而且取决于它们是否陈旧!https://www.reenigne.org/blog/8088-pc-speaker-mod-player-how-its-done/

相关问题