我喜欢的例子,所以我写了一个有点自修改代码在C ...
#include <stdio.h>
#include <sys/mman.h> // linux
int main(void) {
unsigned char *c = mmap(NULL, 7, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|
MAP_ANONYMOUS, -1, 0); // get executable memory
c[0] = 0b11000111; // mov (x86_64), immediate mode, full-sized (32 bits)
c[1] = 0b11000000; // to register rax (000) which holds the return value
// according to linux x86_64 calling convention
c[6] = 0b11000011; // return
for (c[2] = 0; c[2] < 30; c[2]++) { // incr immediate data after every run
// rest of immediate data (c[3:6]) are already set to 0 by MAP_ANONYMOUS
printf("%d ", ((int (*)(void)) c)()); // cast c to func ptr, call ptr
}
putchar('\n');
return 0;
}
......这些工作,显然是:
>>> gcc -Wall -Wextra -std=c11 -D_GNU_SOURCE -o test test.c; ./test
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
不过说实话,我没想到它在所有的工作。 我预期包含指令c[2] = 0
时第一次调用要被缓存c
,在这之后,以所有连续的呼叫c
将忽略对所作的反复变化c
(除非我莫名其妙explicitedly无效的高速缓冲存储器)。 幸运的是,我的CPU似乎更聪明。
我想CPU比较RAM(假设c
与指令高速缓存甚至驻留在RAM)每当指令指针使得一个大的上下的跳跃(如与呼叫上述mmapped存储器),以及无效高速缓存时它不匹配(所有的呢?),但我希望得到的是更准确的信息。 我特别想知道,如果这种行为可以被认为是可预测的(不包括硬件和操作系统的任何差异),并依靠?
(我也许应该指的是英特尔手册,但是那个东西是长数千页,我往往迷失在它...)
你要做的就是通常称为自修改代码 。 英特尔的平台(也可能是AMD的太)做的工作对你维持I / d高速缓存一致性 ,因为手动点出来( 手册3A,系统编程 )
11.6自修改代码
到存储器位置的写入在一个代码段,其目前在处理器缓存导致关联的高速缓存行(或线)被无效。
但作为同一个线性地址用于修改和获取,这对于调试和二进制装载机 ,因为他们不会在同一个地址空间中运行的情况下,这种说法,只要有效:
包括自修改代码的应用程序使用相同的线性地址,用于修改和取指令。 系统软件,诸如调试,这有可能会使用不同的线性地址比用于取指令修改的指令时,将执行串行化操作,例如一个CPUID指令,执行修改后的指令之前,这将自动重新同步指令缓存和预取队列。
例如,序列化操作总是被许多其他结构(如PowerPC),其中必须明确地进行(请e500内核手册 ):
3.3.1.2.1自修改代码
当处理器修改可以包含指令的任何存储位置,软件必须确保指令高速缓存由与数据存储器相一致,而且修饰,以指令提取机制可见。 必须这样做,即使高速缓存被禁用,或者如果页面标记缓存禁止的。
有趣的是,要注意到,PowerPC的需要,即使高速缓存被禁用上下文同步指令的发布; 我怀疑它强制更深的数据处理单元的冲洗,如加载/存储缓冲器。
你提出的代码是不可靠的架构没有窥探或先进的高速缓存一致性设施,因此很可能会失败。
希望这有助于。
这是非常简单的; 在写这在的一个缓存线的指令缓存的地址从指令高速缓存无效的。 不“同步”是参与。
该CPU自动处理缓存失效,您不必手动做任何事情。 软件不能合理地预测什么会或不会在CPU缓存在任何时间点,所以它是由硬件来照顾这。 当CPU看到你修改的数据,它相应更新它的各种缓存。
顺便说一句,很多x86处理器(即我的工作)不仅窥探指令缓存,而且管道,指令窗口 - 当前在飞行中的说明。 因此,自修改代码将生效非常下一条指令。 但是,我们鼓励你使用像CPUID序列化指令,以确保新的代码会被执行。
我只是在我搜索的一个到达了该网页,并希望分享这方面Linux内核的我的知识!
您的代码按预期执行,在这里没有惊喜给我。 该mmap()的系统调用和处理器高速缓存一致性协议做这一招适合你。 这些标志“PROT_READ | PROT_WRITE | PROT_EXEC”询问mmamp()正确设置ITLB,L1缓存的DTLB和TLB这个物理页的L2缓存。 这种低层次的体系结构的核心代码执行此不同,具体取决于处理器的体系结构(x86,AMD,ARM,SPARC等)。 这里任何内核bug会弄乱你的计划!
这只是为了说明的目的。 假设你的系统没有做多少和有间之间没有进程切换“一个[0] = 0b01000000;” 并启动的“printf(” \ n“):” ......另外,假设你有L1 ICACHE,1K数据缓存的1K在您的处理器,并在一些核心的L2缓存。 (现在是一个天,这些都在几MB的顺序)
- mmap()的设置您的虚拟地址空间和iTLB1,dTLB1和TLB2s。
- “一个[0] = 0b01000000;” 实际上陷阱(H / W魔法)为内核代码和你的物理地址将设置和所有处理器的TLB将由内核加载。 然后,你将被返回到用户模式和处理器将实际负载16个字节(H / W魔术一个[0]到[3])到L1 D-超速缓存和L2高速缓存。 处理器将真正进入内存再次,只有当你是指一个[4]等(暂时忽略预测负荷!)。 你的时间完成“一个[7] = 0b11000011;”,你的处理器做了2脉冲串读出的上永恒总线的每个16个字节。 仍然没有实际写入到物理内存。 所有写入都是L1 D-超速缓存(H / W魔法,处理器知道)和L2高速缓存内发生所以对于和脏位被设置为高速缓存行。
- “[3] ++;” 将在汇编代码存储指令,但处理器将在L1数据缓存和L2,只有存储和它不会去的物理内存。
- 让我们来调用函数“一()”。 再次处理器执行取指令从二级高速缓存到L1 ICACHE等。
- 该用户模式程序的结果将是在任何Linux相同的任何处理器下,由于正确执行低电平MMAP()系统调用和高速缓存一致性协议的!
- 如果不用的mmap()系统调用的OS协助编写任何嵌入式处理器环境下这段代码,你会发现你期待的问题。 这是因为你没有使用任何H / W机制(TLB的)或软件机制(内存屏障指令)。