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

时间:2012-06-12 01:10:37

标签: c assembly instructions cpu-cache self-modifying

我喜欢这个例子,所以我在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内存一样),就会使指令缓存失效,并使缓存何时不匹配(全部?),但我希望得到更准确的信息。特别是,我想知道这种行为是否可以被认为是可预测的(除非硬件和操作系统存在任何差异),并依赖于它?

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

5 个答案:

答案 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的数量级)

  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&amp; L2中,并且它不会进入物理内存。
  4. 让我们来函数调用“a()”。处理器再次执行从L2高速缓存到L1 iCache的指令获取,依此类推。
  5. 由于正确实现了低级别mmap()系统调用和缓存一致性协议,此用户模式程序的结果在任何处理器下的任何Linux上都是相同的!
  6. 如果您在任何嵌入式处理器环境下编写此代码而没有mmap()系统调用的操作系统帮助,您将发现您期望的问题。这是因为您没有使用H / W机制(TLB)或软件机制(内存屏障指令)。