如何将结构显式加载到 L1d 缓存中? CR0.CD = 1 在隔离核心 with/without 超线程上使用 INVD 的奇怪结果

How to explicitly load a structure into L1d cache? Weird results with INVD with CR0.CD = 1 on isolated core with/without hyperthreading

我的目标是将静态结构加载到 L1D 缓存中。之后使用这些结构成员执行一些操作并完成操作 运行 invd 以丢弃所有修改的缓存行。所以基本上我想在缓存内创建一个安全的环境,这样,在缓存内执行操作时,数据不会泄漏到 RAM 中。

为此,我有一个内核模块。我在结构的成员上放置了一些固定值。然后我禁用抢占,禁用所有其他 CPU 的缓存(当前 CPU 除外),禁用中断,然后使用 __builtin_prefetch() 将我的静态结构加载到缓存中。之后,我用新值覆盖之前放置的固定值。之后,我执行 invd(清除修改后的缓存行),然后为所有其他 CPU 启用缓存,启用中断并启用抢占。我的理由是,当我在原子模式下执行此操作时,INVD 将删除所有更改。从原子模式返回后,我应该会看到我之前放置的原始固定值。然而,这并没有发生。退出原子模式后,我可以看到用于覆盖先前放置的固定值的值。这是我的模块代码,

奇怪的是,重启电脑后,我的输出发生了变化,我只是不明白为什么。现在,我根本看不到任何变化。我发布了完整的代码,包括@Peter Cordes 建议的一些修复,

#include <linux/module.h>    
#include <linux/kernel.h>    
#include <linux/init.h>      
#include <linux/moduleparam.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Author");
MODULE_DESCRIPTION("test INVD");

static struct CACHE_ENV{
    unsigned char in[128];
    unsigned char out[128];
}cacheEnv __attribute__((aligned(64)));

#define cacheEnvSize (sizeof(cacheEnv)/64)
//#define change "Hello"
unsigned char change[]="hello";


void disCache(void *p){
    __asm__ __volatile__ (
        "wbinvd\n"
        "mov %%cr0, %%rax\n\t"
        "or $(1<<30), %%eax\n\t"
        "mov %%rax, %%cr0\n\t"
        "wbinvd\n"
        ::
        :"%rax"
    );

    printk(KERN_INFO "cpuid %d --> cache disable\n", smp_processor_id());

}


void enaCache(void *p){
    __asm__ __volatile__ (
        "mov %%cr0, %%rax\n\t"
        "and $~(1<<30), %%eax\n\t"
        "mov %%rax, %%cr0\n\t"
        ::
        :"%rax"
    );

    printk(KERN_INFO "cpuid %d --> cache enable\n", smp_processor_id());

}

int changeFixedValue (struct CACHE_ENV *env){
    int ret=1;
    //memcpy(env->in, change, sizeof (change));
    //memcpy(env->out, change,sizeof (change));

    strcpy(env->in,change);
    strcpy(env->out,change);
    return ret;
}

void fillCache(unsigned char *p, int num){
    int i;
    //unsigned char *buf = p;
    volatile unsigned char *buf=p;

    for(i=0;i<num;++i){
    
/*
        asm volatile(
        "movq [=12=],(%0)\n"
        :
        :"r"(buf)
        :
        );
*/
        //__builtin_prefetch(buf,1,1);
        //__builtin_prefetch(buf,0,3);
        *buf += 0;
        buf += 64;   
     }
    printk(KERN_INFO "Inside fillCache, num is %d\n", num);
}

static int __init device_init(void){
    unsigned long flags;
    int result;

    struct CACHE_ENV env;

    //setup Fixed values
    char word[] ="0xabcd";
    memcpy(env.in, word, sizeof(word) );
    memcpy(env.out, word, sizeof (word));
    printk(KERN_INFO "env.in fixed is %s\n", env.in);
    printk(KERN_INFO "env.out fixed is %s\n", env.out);

    printk(KERN_INFO "Current CPU %s\n", smp_processor_id());

    // start atomic
    preempt_disable();
    smp_call_function(disCache,NULL,1);
    local_irq_save(flags);

    asm("lfence; mfence" ::: "memory");
    fillCache(&env, cacheEnvSize);
    
    result=changeFixedValue(&env);

    //asm volatile("invd\n":::);
    asm volatile("invd\n":::"memory");

    // exit atomic
    smp_call_function(enaCache,NULL,1);
    local_irq_restore(flags);
    preempt_enable();

    printk(KERN_INFO "After: env.in is %s\n", env.in);
    printk(KERN_INFO "After: env.out is %s\n", env.out);

    return 0;
}

static void __exit device_cleanup(void){
    printk(KERN_ALERT "Removing invd_driver.\n");
}

module_init(device_init);
module_exit(device_cleanup);

我得到以下输出:

[ 3306.345292] env.in fixed is 0xabcd
[ 3306.345321] env.out fixed is 0xabcd
[ 3306.345322] Current CPU (null)
[ 3306.346390] cpuid 1 --> cache disable
[ 3306.346611] cpuid 3 --> cache disable
[ 3306.346844] cpuid 2 --> cache disable
[ 3306.347065] cpuid 0 --> cache disable
[ 3306.347313] cpuid 4 --> cache disable
[ 3306.347522] cpuid 5 --> cache disable
[ 3306.347755] cpuid 6 --> cache disable
[ 3306.351235] Inside fillCache, num is 4
[ 3306.352250] cpuid 3 --> cache enable
[ 3306.352997] cpuid 5 --> cache enable
[ 3306.353197] cpuid 4 --> cache enable
[ 3306.353220] cpuid 6 --> cache enable
[ 3306.353221] cpuid 2 --> cache enable
[ 3306.353221] cpuid 1 --> cache enable
[ 3306.353541] cpuid 0 --> cache enable
[ 3306.353608] After: env.in is hello
[ 3306.353609] After: env.out is hello

我的Makefile

obj-m += invdMod.o
CFLAGS_invdMod.o := -o0
invdMod-objs := disable_cache.o  

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
    rm -f *.o

有没有想过我做错了什么?正如我之前所说,我希望我的输出保持不变。

我能想到的一个原因是 __builtin_prefetch() 没有将结构放入缓存。另一种将内容放入缓存的方法是在 MTRRPAT 的帮助下设置一个 write-back 区域。但是,我对如何实现这一目标一无所知。我发现 12.6. Creating MTRRs from a C programme using ioctl()’s 展示了如何创建 MTRR 区域,但我不知道如何将我的结构地址与该区域绑定。

我的CPU是:Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz

内核版本:Linux xxx 4.4.0-200-generic #232-Ubuntu SMP Wed Jan 13 10:18:39 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

GCC 版本:gcc (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609

我用 -O0 参数编译了这个模块

更新 2:关闭超线程

我用 echo off > /sys/devices/system/cpu/smt/control 关闭了超线程。在那之后,运行宁我的模块似乎,changeFixedValue() & fillCache() 没有被调用。

输出:

[ 3971.480133] env.in fixed is 0xabcd
[ 3971.480134] env.out fixed is 0xabcd
[ 3971.480135] Current CPU 3
[ 3971.480739] cpuid 2 --> cache disable
[ 3971.480956] cpuid 1 --> cache disable
[ 3971.481175] cpuid 0 --> cache disable
[ 3971.482771] cpuid 2 --> cache enable
[ 3971.482774] cpuid 0 --> cache enable
[ 3971.483043] cpuid 1 --> cache enable
[ 3971.483065] After: env.in is 0xabcd
[ 3971.483066] After: env.out is 0xabcd

在fillCache底部调用printk看起来很不安全。您将要 运行 一些存储,然后是 invd,因此 printk 对内核数据结构(如日志缓冲区)所做的任何修改都可能被写回 DRAM 或者可能会如果它们在缓存中仍然脏,则无效。如果一些但不是所有存储都进入 DRAM(因为缓存容量有限),您可能会使内核数据结构处于不一致状态。

我猜你当前禁用 HT 的测试显示一切都比你希望的更好,包括丢弃 printk 完成的存储,以及丢弃changeFixedValue 完成的商店。这可以解释缺少留给 user-space 在您的代码完成后阅读的日志消息。

要对此进行测试,您最好 clflush printk 所做的一切,但没有简单的方法可以做到这一点。也许 wbinvd 然后 changeFixedValue 然后 invd。 (你没有在这个核心上进入无填充模式,所以 fillCache 不是你的商店/invd 想法工作所必需的,见下文。)


启用超线程:

CR0.CD 是每个物理核心,因此让您的 HT 兄弟核心禁用缓存也意味着隔离核心的 CD=1。 因此,在启用 HT 的情况下,您 即使在隔离核心上也处于无填充模式。

关闭超线程后,隔离核心依然正常。


编译时和运行时重新排序

asm volatile("invd\n":::); 没有 "memory" 破坏告诉编译器它允许重新排序 wrt。内存操作。显然这不是你的问题,但这是一个你应该修复的错误。

asm("mfence; lfence" ::: "memory"); 放在 fillCache 之前可能也是一个好主意,以确保任何缓存未命中的加载和存储不会仍在运行中,并且可能会在您的代码分配新的缓存行运行宁。或者甚至可能是像 asm("xor %eax,%eax; cpuid" ::: "eax", "ebx", "ecx", "edx", "memory"); 这样的完全序列化指令,但我不知道 CPUID 会阻止哪些 mfence; lfence 不会。


题目问题:touching memory to bring it into cache

PREFETCHT0(进入 L1d 缓存)是 __builtin_prefetch(p,0,3);This answer 显示 args 如何映射到指令;您正在使用 prefetchw(写入意图)或者我认为 prefetcht1(L2 缓存)取决于编译器选项。

但是真的因为你需要这个来确保正确性,所以你不应该使用可选的提示来提示 HW 在繁忙时会掉线。 mfence; lfence 会使它不太可能让 HW 实际上很忙,但仍然是个不错的主意。

使用 volatile 类似 READ_ONCE 的读法让 GCC 发出加载指令。或者使用 volatile char *buf*buf |= 0; 或其他东西来真正 RMW 而不是预取,以确保该行是独占的,而不必让 GCC 发出 prefetchw.

也许值得 运行宁 fillCache 几次,只是为了更加确保每一行都正确地处于您想要的状态。但是由于您的 env 小于 4k,每一行都将位于 L1d 缓存中的不同集合中,因此在分配另一行时不存在一行被丢弃的风险(除非是 L3 缓存的哈希函数中的别名?但即便如此, 伪 LRU 驱逐应该可靠地保持最近的行。)


按 128 对齐您的数据,一对对齐的缓存行

static struct CACHE_ENV { ... } cacheEnv; 不保证缓存行大小对齐;你缺少 C11 _Alignas(64) 或 GNU C __attribute__((aligned(64)))。所以它可能跨越 sizeof(T)/64 行以上。或者为了更好的衡量,为 L2 相邻线预取器对齐 128。 (在这里你可以而且应该简单地对齐你的缓冲区,但是 展示了如何遍历任意大小的可能未对齐结构的每个缓存行。)

这并不能解释您的问题,因为唯一可能遗漏的部分是 env.out 的最后最多 48 个字节。 (我认为全局结构在默认 ABI 规则下将按 16 位对齐。)而且您只打印每个数组的前几个字节。


更简单的方法:memset(0) 以避免将数据泄漏回 DRAM

顺便说一句,在完成后通过内存集用 0 覆盖您的缓冲区也应该防止您的数据像 INVD 一样可靠地写回 DRAM,但速度更快。 (也许通过 asm 手动 rep stosb 以确保它不能作为死存储进行优化)。

无填充模式在这里也可能有用,可以阻止缓存未命中逐出现有行。 AFAIK,这基本上锁定了缓存,因此不会发生新的分配,因此不会发生驱逐。 (但是您可能无法读取或写入 other 正常内存,尽管您可以在寄存器中留下结果。)

无填充模式(对于当前核心)在重新启用分配之前使用 memset 清除缓冲区绝对安全;在导致驱逐的过程中没有缓存未命中的风险。尽管如果你的 fillCache 实际上工作正常并且在你开始你的工作之前让你的所有行进入 MESI 修改状态,你的加载和存储将命中 L1d 缓存而不会有驱逐任何缓冲行的风险。

如果您担心 DRAM 内容(而不是总线信号),那么 memset 之后的每一行 clflushopt 将减少window 漏洞。 (如果 0 对你不起作用,或者从原始副本的干净副本中获取 memcpy,但希望你可以在私人副本中工作,并且不修改原件。你当前的流浪回写总是可能的方法,所以我不想依赖它来始终保持大缓冲区不变。)

不要将 NT 存储用于手动 memset 或 memcpy:这可能会在 NT 存储之前刷新“秘密”脏数据。一种选择是使用普通存储或 rep stosb 进行 memset(0),然后使用 NT 存储再次循环。或者也许每行做 8x movq 正常存储,然后 8x movnti,所以你在继续之前对同一行背对背地做这两件事。


为什么要填充缓存?

如果您不使用无填充模式,那么在您写入行之前是否缓存这些行甚至都不重要。当 invd 运行 时,您只需要在缓存中写入脏数据,即使它们是从缓存中丢失的存储中获取的,这也应该是正确的。

您在 fillCachechangeFixedValue 之间已经没有像 mfence 这样的障碍,这很好,但这意味着当您弄脏缓存时,启动缓存的任何缓存未命中仍在运行中。

INVD 本身 is serializing,因此它应该在丢弃缓存内容之前等待存储离开存储缓冲区。 (所以在你的工作之后,在 INVD 之前放置 mfence;lfence 应该没有任何区别。)换句话说,INVD 应该丢弃仍在存储缓冲区中的可缓存存储,以及脏缓存行,除非提交一些那些商店碰巧驱逐了任何东西。