x86指令缓存是如何同步的?

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 上的过时指令获取](/sf/ask/1217689021/) - 当前的 x86 CPU 实际上比手动保证更强。即使来自同一物理页的不同虚拟映射,您也无法让它们在存储后执行过时的指令。是的,x86 很少有具有一致的 I-cache(和管道);这一切都是为了保持与现有代码的向后兼容性,这些代码适用于早期的非流水线 x86 CPU(如 8086 和 386)。与其他从一开始就采用流水线的 ISA 不同。 (2认同)

R..*_*R.. 6

这很简单; 写入指令高速缓存中的一个高速缓存行中的地址使其从指令高速缓存中失效.不涉及"同步".


Kra*_*lew 5

顺便说一句,许多 x86 处理器(我工作过的)不仅监听指令缓存,还监听管道、指令窗口——当前正在运行的指令。因此,自修改代码将在下一条指令下生效。但是,我们鼓励您使用 CPUID 之类的序列化指令来确保新编写的代码将被执行。