我喜欢这个例子,所以我在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
的重复更改(除非我以某种方式明确地使缓存无效)。幸运的是,我的cpu似乎比那更聪明。
我猜cpu比较RAM(假设c
甚至驻留在RAM中),只要指令指针发出大跳跃(就像调用上面的mmapped内存一样),就会使指令缓存失效,并使缓存何时不匹配(全部?),但我希望得到更准确的信息。特别是,我想知道这种行为是否可以被认为是可预测的(除非硬件和操作系统存在任何差异),并依赖于它?
(我可能应该参考英特尔手册,但那个东西长达数千页,我往往会迷失它......)
答案 0 :(得分:24)
您所做的通常称为自我修改代码。英特尔的平台(也可能是AMD的平台)为您提供维护 i / d缓存一致性的工作,正如手册指出的那样(Manual 3A, System Programming)
11.6自修改代码
写入当前缓存在代码段中的内存位置 处理器导致关联的高速缓存行(或多个行)无效。
但是只要相同的线性地址用于修改和提取,这个断言就是有效的,而调试器和二进制加载器不是这种情况,因为它们不是t在相同的地址空间中运行:
包含自修改代码的应用程序使用相同的代码 用于修改和获取指令的线性地址。系统软件,如 调试器,可能使用不同的线性地址修改指令 与用于获取指令的操作相比,将执行序列化操作,例如a CPUID指令,在执行修改指令之前,将自动执行 重新同步指令缓存和预取队列。
例如,许多其他体系结构(如PowerPC)始终要求序列化操作,必须明确地执行(E500 Core Manual):
3.3.1.2.1自修改代码
当处理器修改任何可包含指令的内存位置时,软件必须 确保指令缓存与数据存储器和修改一致 使指令获取机制可见。即使缓存是这样,也必须这样做 禁用或页面被标记为缓存禁止。
有趣的是,即使禁用高速缓存,PowerPC也需要发出上下文同步指令;我怀疑它强制执行更深层次的数据处理单元,如加载/存储缓冲区。
您建议的代码在没有 snooping 或高级缓存一致性工具的架构上是不可靠的,因此可能会失败。
希望得到这个帮助。
答案 1 :(得分:6)
这很简单;写入指令高速缓存中的一个高速缓存行中的地址使其从指令高速缓存中失效。不涉及“同步”。
答案 2 :(得分:4)
CPU自动处理缓存失效,您无需手动执行任何操作。软件无法合理地预测任何时间点CPU高速缓存中将会或不会出现什么,因此需要由硬件来处理。当CPU看到您修改了数据时,它会相应地更新其各种缓存。
答案 3 :(得分:4)
顺便说一下,许多x86处理器(我工作过)不仅窥探指令缓存,还窥探管道,指令窗口 - 当前正在运行的指令。因此,自修改代码将在下一条指令生效。但是,我们鼓励您使用像CPUID这样的序列化指令来确保执行新编写的代码。
答案 4 :(得分:2)
我刚刚在我的一个搜索中找到了这个页面,想要分享我对这个Linux内核领域的知识!
您的代码按预期执行,这里没有任何意外。 mmap()系统调用和处理器高速缓存一致性协议为您提供了这个技巧。标志“PROT_READ | PROT_WRITE | PROT_EXEC”要求mmamp()正确设置该物理页面的L1 Cache的iTLB,dTLB和L2缓存的TLB。这种低级别体系结构特定内核代码根据处理器体系结构(x86,AMD,ARM,SPARC等等)的不同而不同。这里的任何内核错误都会搞乱你的程序!
这仅用于解释目的。 假设您的系统没有做太多,并且“a [0] = 0b01000000;”之间没有进程切换。并开始“printf(”\ n“):”...... 另外,假设您的处理器中有1K的L1 iCache,1K dCache,核心中有一些L2缓存,。 (现在这几天是几MB的数量级)