有人告诉我,并从英特尔的手册,有可能写入到存储器上的指令已阅读,但是指令预取队列中已经取出了陈旧的指令并且将执行那些老的指令。 我一直在观察这种行为是不成功的。 我的方法如下。
从部分11.6英特尔软件开发手册指出
到存储器位置的写入在一个代码段,其目前在处理器缓存导致关联的高速缓存行(或线)被无效。 此检查是基于指令的物理地址。 此外,P6系列与奔腾处理器检查代码段的写入是否会修改已预取执行的指令。 如果写入会影响预取指令,预取队列无效。 这后一种检查基于指令的线性地址。
因此,它看起来就像如果我希望执行stale指示,我需要有两个不同的线性地址指向同一个物理页。 所以,我的内存映射文件,以两个不同的地址。
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。 否则,如果陈旧指令被执行(两条NOP指令),VAL == 0。
我已经在1.7GHz的Intel酷睿i5(2011年的MacBook Air)和英特尔(R)至强(R)CPU X3460 @ 2.80GHz的运行此。 每一次,但是,我看到一个指示CPU VAL == 1总是注意到新的指令。
与我想观察行为人的经验? 是我的推理是否正确? 我有点困惑的手动提P6和奔腾处理器,以及什么缺乏提我的酷睿i5处理器。 也许别的东西是怎么回事引起CPU刷新其指令预取队列? 任何有识之士将是非常有益的!
我想,你应该检查MACHINE_CLEARS.SMC
性能计数器(部分MACHINE_CLEARS
的CPU(这是Sandy Bridge的可用的情况下) 1 ,这是在您的空气的PowerBook使用;也可在你的至强,这是Nehalem的2 -搜索 “SMC”)。 您可以使用oprofile
, perf
或英特尔的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面膜:0×04
检测自修改代码(SMC)。
将清除检测自修改码机的数量。
英特尔还表示,关于SMC http://software.intel.com/en-us/forums/topic/345561 (从链接英特尔性能瓶颈分析仪的分类
当检测自修改代码此事件。 这通常可以使用的人谁做二进制编辑,迫使它采取一定的路径(例如黑客)。 此事件统计,一个程序写入到代码段的次数。 自修改代码导致所有英特尔64和IA-32处理器的严厉的惩罚。 经修改高速缓存行写回到L2和LLC缓存。 此外,该指令将需要重新加载,因此造成的性能损失。
我想,你会看到一些这样的活动。 如果是这样,那么CPU能够检测的自我修改代码的行为,并提出了“机器清除” - 管道的全面重启。 第一阶段取。他们会问L2缓存为新的操作码。 我在按你的代码的执行SMC事件的确切数量非常感兴趣 - 这将会给我们介绍一下延迟一些估计。(SMC在一些单位,其中1个单位被假定为1.5的CPU周期数 - B.6.2。英特尔优化手册第6)
我们可以看到,英特尔称,“从过去的退休指令后立即重新启动。”,所以我觉得最后退休的指令将是mov
; 和你的NOP已经在酝酿中。 但是,SMC将在MOV退休升起,它会在管道杀死一切,包括空指令。
这SMC引起管道重启并不便宜,昂纳已在一些测量Optimizing_assembly.pdf - “17.10自修改代码(所有的处理器)”(我认为任何的Core2 / CoreiX就像是PM这里):
用于修改后立即执行的代码的惩罚为P1大约19个时钟周期,31 PMMX,和150-300为的PPro,P2,P3,PM。 后自修改代码P4将清除整个跟踪缓存。 80486个早期的处理器所需要的改性和修改后的代码之间的一个跳跃以冲洗的代码高速缓存中。 ...
自修改代码不被认为是良好的编程习惯。 它应该被用来只有当速度增益为充实和修改后的代码被执行这么多次的优势远远大于惩罚使用自修改代码。
在这里推荐不同的线性地址的使用失败SMC探测器: https://stackoverflow.com/a/10994728/196561 -我会尽力找到实际的Intel文档...不能真正回答你的真正的问题了。
可能会有一些线索在这里: 优化手册,248966-026,2012年4月 “3.6.9混合代码和数据”:
在代码段将可写数据可能无法从自修改代码来区分。 在代码段可写数据可能遭受同样的性能损失为自修改代码。
和下一节
软件应避免在正在执行相同的1 KB的子页面写入代码页或正被写入的,同样的2 KB的子页面获取代码。 此外,共享直接或推测含有页面执行的代码与另一处理器作为数据页可以触发,导致机器和跟踪缓存的整个管道被清除的SMC条件。 这是由于自修改代码的条件。
因此,有可能是一些图表控制可写和可执行子页面的交叉点。
你可以尝试从其他线程(交叉修改代码)做修改 - 但需要非常小心线程同步和管道冲洗(您可能想在写线程延误的一些暴力破解;刚同步后CPUID期望)。 但是你应该知道,他们已经解决了这个问题使用“ 核武器 ” -检查US6857064专利。
我有点困惑的手动提P6和奔腾处理器
这是可能的,如果你有读取,解码和执行英特尔的使用说明书的一些陈旧版本。 您可以重置管道,并检查该版本: 订单编号:325462-047US,2013年6月 “11.6自修改代码”。 这个版本还没有说,有关新CPU的东西,但提到当您使用不同的虚拟地址修改,该行为可能是微体系结构之间的不兼容(它可能在你的Nehalem / Sandy Bridge的工作,可能无法正常工作的.. Skymont)
11.6自修改代码甲写入当前在该处理器缓存导致关联的高速缓存行(或线)将被无效的代码段的存储器位置。 此检查是基于指令的物理地址。 此外,P6系列与奔腾处理器检查代码段的写入是否会修改已预取执行的指令。 如果写入会影响预取指令,预取队列无效。 这后一种检查基于指令的线性地址。 为奔腾4和Intel Xeon处理器,写入或代码段中的跟踪高速缓存的指令,其中目标指令已解码和驻地的窥探,无效整个跟踪缓存。 后者的行为意味着当在Pentium 4和Intel Xeon处理器上运行,自我修改代码,程序可能会导致性能严重下降。
在实践中,对线性地址检查不应造成IA-32处理器之间的兼容性的问题。 包括自修改代码的应用程序使用相同的线性地址,用于修改和取指令。
系统软件,诸如调试,这有可能会使用不同的线性地址比用于取指令修改的指令时,将执行串行化操作,例如一个CPUID指令,执行修改后的指令之前,这将自动重新同步指令缓存和预取队列。 (参见8.1.3节“处理自我和交叉修改代码,”关于使用自修改代码的更多信息。)
对于英特尔486处理器,在缓存中的指令写入将修改它的高速缓存和内存这两个,但如果指令被写入前预取,旧版本的指令可以被执行的一个。 为了防止老指令被执行时,通过修饰的指令的任何写之后立即编码跳转指令刷新指令预取单元
REAL更新 ,用Google搜索“SMC检测”(带引号),并有一些细节酷睿/酷睿xi如何现代化检测SMC与至强和奔腾挂在SMC探测器也有很多勘误表:
http://www.google.com/patents/US6237088用于跟踪流水线中指令@ 2001的系统和方法
DOI 10.1535 / itj.1203.03(谷歌它,就在citeseerx.ist.psu.edu免费版) - 的“夹杂FILTER” Penryn的加入来降低假SMC检测的次数; 该“现有列入检测机构”被描绘图9上
http://www.google.com/patents/US6405307 -对SMC检测逻辑的年龄较大的专利
根据专利US6237088(FIG5,摘要)有“行地址缓冲器”(与许多线性地址每取出指令地址 - 或在其他字缓冲器满载与高速缓存行提取的精度的IP地址)。 每家商店的每一家商店,或者更准确的“存储地址”阶段将送入平行比较检查,会相交存储任何的当前正在执行的指令或没有。
这两个专利没有明确说,他们会在SMC逻辑使用物理或逻辑地址... L1I在Sandy Bridge的是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探测器):
使用翻译旁视缓冲器中检测自修改代码 。[其]具有存储在其中的该窥探可以使用商店的物理存储器地址到存储器来执行过度的物理页地址。 ...要提供更好的粒度比地址的页,FINE HIT比特包括与高速缓存在高速缓存中的页面的部分存储器内的关联信息的每个条目。
(页面的一部分,被称为在专利US6594734象限,听起来像1K子页面,是吗?)
然后他们说:
因此窥探,通过存储指令触发到内存中 ,可以通过比较存储的指令缓存与存储相关的页面或存储器页中所有指令的地址范围内的所有指令的物理地址进行SMC检测。 如果地址匹配,则表明一个内存位置已被修改。 在地址匹配的情况下,指示SMC条件,指令高速缓冲存储器和指令流水线由引退单元冲洗和新指令被从存储器获取的用于存储到指令高速缓冲存储器。
因为SMC检测窥探是物理和ITLB通常接受作为输入翻译成一个物理地址的线性地址,ITLB另外形成为对物理地址的内容寻址存储器,并且包括附加输入比较端口(称为作为一个探听端口或逆转换端口)
- 所以,检测SMC,他们迫使商店通过窥探转发物理地址返回指令缓存(类似窥探将从其他核心/ CPU或从DMA传送写入缓存我们....),如果窥探的物理层。 与高速缓存行,存储在指令缓冲区地址冲突,我们将通过从ITLB到退役单元传递SMC信号重启管道。 能想象有多少个CPU时钟会从DTLB这种窥探循环通过ITLB和退役单元(它不能退下一个“NOP”指令,虽然它早于MOV执行且没有副作用)被浪费。 但是WAT? ITLB具有物理地址输入和第二CAM(大和热)只是为了支持和抵御疯狂作弊自修改代码。
PS:如果我们将用大内存页的工作是什么(4M,也可以是1G)? 该L1TLB有巨大的网页条目,并有可能成为很多假SMC的检测1/4的4 MB页...
PPS:有一个变型中,SMC的错误处理与不同的线性地址只在早期P6 / PPRO / P2是本...
有人告诉我,并从英特尔的手册,有可能写入到存储器上的指令已阅读,但是指令预取队列中有[可能有]已经取出陈旧的指令,将[可]执行那些老的指令。 我一直在观察这种行为是不成功的。
是的,你会。
所有或几乎所有的现代英特尔处理器比手动更严格:
他们窥探基于物理地址的管道,而不只是线性的。
处理器允许实现比手册严格。
他们可以选择是,因为他们遇到了代码,不遵守规则手册,他们不希望打破。
还是......因为最简单的方法,坚持建筑规范(在SMC的情况下使用是正式“直到下一次串行化指令”,但在实践中,对于遗留代码,是“直到下一次采取分支超过???字节远“)可能会更严格。
SandyBridge的家庭(至少SKYLAKE微架构),仍然具有相同的行为,对物理地址显然窥探。
您的测试是有点过于复杂,虽然 。 我没有看到远处跳跃的点,如果你(如果有必要和链接)组装SMC功能成扁平的二元您可以打开+ MMAP两次。 使a1
和a2
函数指针,则主要可以return a1(a2)
映射后。
这里有一个简单的测试工具,如果有人想试试自己的机器上 :(开/断言/ 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源供试功能:
(参见如何生成纯二进制想与GNU GAS汇编NASM -f仓?一个as
+ ld
替代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
在运行Linux 4.20.3-arch1-1拱的i7-6700k,我们没有发现陈旧的代码读取。 该mov
即改写眼前1
与0
做了修改指令,它跑了。
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