28

我被告知并阅读了英特尔的手册,可以将指令写入内存,但指令预取队列已经获取了过时的指令并将执行那些旧指令。我一直未能成功观察到这种行为。我的方法如下。

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

对当前缓存在处理器中的代码段中的内存位置的写入会导致相关的缓存行(或多个行)无效。此检查基于指令的物理地址。此外,P6 系列和 Pentium 处理器检查对代码段的写入是否会修改已预取执行的指令。如果写入影响预取指令,则预取队列无效。后一种检查基于指令的线性地址。

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

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 和 Pentium 处理器以及没有提到我的 Core i5 处理器感到有些困惑。也许是其他原因导致 CPU 刷新其指令预取队列?任何见解都会非常有帮助!

4

3 回答 3

29

我认为,您应该检查 CPU 的MACHINE_CLEARS.SMC性能计数器(MACHINE_CLEARS事件的一部分)(它在您的 Air powerbook 中使用的 Sandy Bridge 1中可用;在您的 Xeon 上也可用,即 Nehalem 2 - 搜索“smc” )。您可以使用oprofileperfIntelVtune来查找其值:

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 64 和 IA-32 处理器中造成严重损失。修改后的高速缓存行被写回 L2 和 LLC 高速缓存。此外,需要重新加载指令,从而导致性能下降。

我想,你会看到一些这样的事件。如果是,则 CPU 能够检测到自修改代码的行为并引发“机器清除” - 管道的完全重启。第一阶段是 Fetch,他们会向 L2 缓存请求新的操作码。我对每次执行代码时 SMC 事件的确切计数非常感兴趣——这将给我们一些关于延迟的估计。(SMC 以某些单位计算,其中 1 个单位被假定为 1.5 个 cpu 周期 - B.6.2。 6 intel优化手册)

我们可以看到英特尔说“在最后一条退役指令之后重新启动。”,所以我认为最后一条退役指令将是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 - 我将尝试查找实际的英特尔文档......现在实际上无法回答您的真实问题。

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

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

和下一节

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

因此,可能存在一些控制可写和可执行子页面交叉点的示意图。

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

我对提到 P6 和 Pentium 处理器的手册有点困惑

如果您已经获取、解码并执行了一些陈旧版本的英特尔说明手册,这是可能的。您可以重置管道并检查此版本:订单号:325462-047US,2013 年 6 月 “11.6 自修改代码”。这个版本仍然没有说明较新的 CPU,但提到当您使用不同的虚拟地址进行修改时,微体系结构之间的行为可能不兼容(它可能在您的 Nehalem/Sandy Bridge 上工作,可能在.. Skymont 上不工作)

11.6 自修改代码 对当前缓存在处理器中的代码段中的内存位置的写入会导致相关的缓存行(或多个行)无效。此检查基于指令的物理地址。此外,P6 系列和 Pentium 处理器检查对代码段的写入是否会修改已预取执行的指令。如果写入影响预取指令,则预取队列无效。后一种检查基于指令的线性地址。对于 Pentium 4 和 Intel Xeon 处理器,在目标指令已解码并驻留在跟踪缓存中的代码段中写入或窥探指令会使整个跟踪缓存无效。

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

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

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

真正的更新,用谷歌搜索“SMC 检测”(带引号),并且有一些细节是现代 Core2/Core iX 如何检测 SMC 以及许多带有 Xeons 和 Pentiums 的勘误表挂在 SMC 检测器中:

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

  2. DOI 10.1535/itj.1203.03(谷歌,在 citeseerx.ist.psu.edu 有免费版本) - Penryn 中添加了“包含过滤器”以减少错误 SMC 检测的数量;“现有夹杂物检测机制”如图 9 所示

  3. http://www.google.com/patents/US6405307 - SMC 检测逻辑的旧专利

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

两个专利都没有明确说,在SMC逻辑中会使用物理地址还是逻辑地址... Sandy bridge中的L1i是VIPT(Virtually indexed, physical tagged , virtual address for index and physical address in tag.)根据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 检测器):

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

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

然后他们说

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

因为 SMC 检测的监听是物理的,并且 ITLB 通常接受一个线性地址作为输入以转换为物理地址,所以 ITLB 额外形成为物理地址上的内容可寻址存储器,并包括一个额外的输入比较端口(称为作为监听端口或反向翻译端口)

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

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

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

于 2013-06-30T23:17:26.147 回答
11

我被告知并阅读了英特尔的手册,可以将指令写入内存,但指令预取队列[可能]已经获取了过时的指令,并将[可能]执行那些旧指令。我一直未能成功观察到这种行为。

是的,你会的。

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

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

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

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

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

于 2013-08-22T18:55:39.280 回答
2

Sandybridge-family (at least Skylake) still has the same behaviour, apparently snooping on physical address.

Your test is somewhat overcomplicated, though. I don't see the point of the far jump, and if you assemble (and link if necessary) the SMC function into a flat binary you can just open + mmap it twice. Make a1 and a2 function pointers, then main can return a1(a2) after mapping.

Here's a simple test harness, in case anyone wants to try on their own machine: (The open/assert/mmap block was copied from the question, thanks for the starting point.)

(Downside, you have to rebuild the SMC flat binary every time, because mapping it with MAP_SHARED actually modifies it. IDK how to get two mappings of the same physical page that won't modify the underlying file; writing to a MAP_PRIVATE would COW it to a different physical page. So writing the machine code to a file and them mapping it makes sense now that I realize this. But my asm is still a lot simpler.)

// 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 source for the test function:

(See How to generate plain binaries like nasm -f bin with the GNU GAS assembler? for an as+ld alternative to nasm -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

On an i7-6700k running Linux 4.20.3-arch1-1-ARCH, we do not observe stale code fetch. The mov that overwrote the immediate 1 with a 0 did modify that instruction before it ran.

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
于 2019-02-08T06:32:28.583 回答