Wil*_*ill 24 c assembly instructions self-modifying cpu-cache
我喜欢这个例子,所以我在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;
}
Run Code Online (Sandbox Code Playgroud)
...显然有效:
>>> 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
Run Code Online (Sandbox Code Playgroud)
但老实说,我没想到它会起作用.我希望c[2] = 0
在第一次调用时缓存包含的指令c
,之后所有连续调用都c
将忽略对所做的重复更改c
(除非我以某种方式明确地使缓存无效).幸运的是,我的cpu似乎比那更聪明.
我猜想c
只要指令指针发出大跳跃(就像调用上面的mmapped内存一样),cpu就会将RAM(假设甚至驻留在RAM中)与指令缓存进行比较,并在缓存不匹配时使缓存无效(所有这些?),但我希望得到更准确的信息.特别是,我想知道这种行为是否可以被认为是可预测的(除了硬件和操作系统的任何差异),并依赖于?
(我可能应该参考英特尔手册,但那个东西长达数千页,我往往会迷失它......)
Ben*_*oit 26
你所做的通常被称为自修改代码.英特尔的平台(也可能是AMD的平台)可以帮助您维护i/d缓存一致性,正如手册所指出的那样(手册3A,系统编程)
11.6自修改代码
对当前在处理器中高速缓存的代码段中的存储器位置的写入导致相关联的高速缓存行(或多个行)无效.
但只要相同的线性地址用于修改和获取,这个断言就是有效的,而调试器和二进制加载器不是这种情况,因为它们不在相同的地址空间中运行:
包含自修改代码的应用程序使用相同的线性地址来修改和获取指令.使用与用于获取指令的线性地址不同的线性地址修改指令的系统软件(例如调试器)将在执行修改的指令之前执行序列化操作,例如CPUID指令,这将自动重新同步指令缓存和预取队列.
例如,许多其他体系结构(如PowerPC)总是要求序列化操作,必须明确地进行序列化操作(E500核心手册):
3.3.1.2.1自修改代码
当处理器修改任何可以包含指令的存储器位置时,软件必须确保指令高速缓存与数据存储器一致,并且修改对于指令获取机制是可见的.即使禁用缓存或页面标记为缓存禁止,也必须执行此操作.
有趣的是,即使禁用高速缓存,PowerPC也需要发出上下文同步指令; 我怀疑它强制执行更深层次的数据处理单元,如加载/存储缓冲区.
您提出的代码在没有窥探或高级缓存一致性设施的架构上是不可靠的,因此可能会失败.
希望这有帮助.
顺便说一句,许多 x86 处理器(我工作过的)不仅监听指令缓存,还监听管道、指令窗口——当前正在运行的指令。因此,自修改代码将在下一条指令下生效。但是,我们鼓励您使用 CPUID 之类的序列化指令来确保新编写的代码将被执行。