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

我喜欢这个例子,所以我在c中写了一些自修改代码…

#include  #include  // 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缓存包含c[2] = 0的指令,之后对c所有连续调用都将忽略对c的重复更改(除非我以某种方式明确地使缓存无效)。 幸运的是,我的cpu似乎比那更聪明。

我猜想只要指令指针发出大跳跃(就像调用上面的mmapped内存一样),cpu就会比较RAM(假设c甚至驻留在RAM中)和指令缓存,并且当缓存不匹配时使缓存无效(所有这些?),但我希望得到更准确的信息。 特别是,我想知道这种行为是否可以被认为是可预测的(除了硬件和操作系统的任何差异),并依赖于?

(我可能应该参考英特尔手册,但那个东西长达数千页,我往往会迷失它……)

你所做的通常被称为自修改代码 。 英特尔的平台(也可能是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()正确设置该物理页面的L1 Cache的iTLB,dTLB和L2缓存的TLB。 这种低级别体系结构特定内核代码根据处理器体系结构(x86,AMD,ARM,SPARC等)进行不同的处理。 这里的任何内核错误都会搞乱你的程序!

这仅用于解释目的。 假设您的系统没有做太多工作,并且“a [0] = 0b01000000;”之间没有进程切换。 并开始“printf(”\ n“):”…另外,假设您的处理器中有1K的L1 iCache,1K dCache,核心中有一些L2缓存,。 (现在这几天是几MB的顺序)

  1. mmap()设置您的虚拟地址空间和iTLB1,dTLB1和TLB2。
  2. “一个[0] = 0b01000000;” 将实际陷阱(H / W魔术)转换为内核代码,您的物理地址将被设置,所有处理器TLB将由内核加载。 然后,您将返回用户模式,您的处理器实际上将16字节(H / W magic a [0]至[3])加载到L1 dCache和L2 Cache中。 处理器将真正再次进入内存,只有当您引用[4]等等时(暂时忽略预测加载!)。 当你完成“a [7] = 0b11000011;”时,你的处理器在永久总线上完成了2个16字节的突发读取。 仍然没有实际写入物理内存。 所有WRITE都发生在L1 dCache(H / W magic,处理器知道)和L2缓存中,因此为Dirty位设置为Cache-line。
  3. “[3] ++;” 将在汇编代码中具有STORE指令,但处理器将仅存储在L1 dCache和L2中,并且它不会进入物理内存。
  4. 让我们来函数调用“a()”。 处理器再次执行从L2高速缓存到L1 iCache的指令获取,依此类推。
  5. 由于低级mmap()系统调用和缓存一致性协议的正确实现,此用户模式程序的结果在任何处理器下的任何Linux上都是相同的!
  6. 如果您在任何嵌入式处理器环境下编写此代码而没有mmap()系统调用的OS帮助,您将发现您期望的问题。 这是因为您没有使用H / W机制(TLB)或软件机制(内存屏障指令)。