gpt4 book ai didi

c - 如何将结构显式加载到 L1d 缓存中?

转载 作者:行者123 更新时间:2023-12-04 11:11:37 32 4
gpt4 key购买 nike

我的目标是将静态结构加载到 L1D 缓存中。之后使用这些结构成员执行一些操作,并在完成操作后运行 invd丢弃所有修改过的缓存行。所以基本上我想在缓存内创建一个安全的环境,这样在缓存内执行操作时,数据就不会泄漏到 RAM 中。
为此,我有一个内核模块。我在结构的成员上放置了一些固定值。然后我禁用抢占,禁用所有其他 CPU(当前 CPU 除外)的缓存,禁用中断,然后使用 __builtin_prefetch()将我的静态结构加载到缓存中。之后,我用新值覆盖之前放置的固定值。之后,我执行 invd (清除修改后的缓存行)然后启用缓存到所有其他 CPU,启用中断和启用抢占。我的理由是,当我在原子模式下这样做时,INVD将删除所有更改。从原子模式回来后,我应该看到我之前放置的原始固定值。然而,这并没有发生。退出原子模式后,我可以看到用于覆盖先前放置的固定值的值。这是我的模块代码,
奇怪的是,在重新启动 PC 后,我的输出发生了变化,我只是不明白为什么。现在,我根本没有看到任何变化。我正在发布完整的代码,包括@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 $0,(%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()没有将结构放入缓存中。另一种将内容放入缓存的方法是设置 write-back区域在 MTRR 的帮助下& PAT .但是,我对如何实现这一目标一无所知。我找到了 12.6. Creating MTRRs from a C programme using ioctl()’s显示如何创建 MTRR区域,但我不知道如何将结构的地址与该区域绑定(bind)。
我的 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/LinuxGCC 版本: 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

最佳答案

打电话printk看起来很不安全在fillCache 的底部。您将要经营更多的商店,然后是 invd , 所以任何修改 printk对内核数据结构(如日志缓冲区)的修改可能会被写回 DRAM,或者如果它们在缓存中仍然脏,则可能会失效。如果某些但不是所有存储都进入 DRAM(因为缓存容量有限),您可能会使内核数据结构处于不一致状态。
我猜您当前禁用 HT 的测试表明一切都比您希望的要好,包括丢弃由 printk 完成的商店 ,以及丢弃由 changeFixedValue 完成的商店.这可以解释为什么代码完成后没有留给用户空间读取的日志消息。
要对此进行测试,您最好是 clflush printk 所做的一切,但没有简单的方法可以做到这一点。也许 wbinvd然后 changeFixedValue然后 invd . (你没有在这个核心上进入无填充模式,所以 fillCache 不是你的商店/invd 想法工作所必需的,见下文。)

启用超线程:
CR0.CD 是每个物理核心,所以让你的 HT 兄弟核心禁用缓存也意味着隔离核心的 CD=1。 因此,启用 HT 后,即使在隔离核心上,您也处于无填充模式。
HT 关闭后,隔离内核仍然正常。

编译时和运行时重新排序asm volatile("invd\n":::);没有 "memory" clobber 告诉编译器它可以重新排序。内存操作。显然,这不是您的问题,但这是您应该修复的错误。
asm("mfence; lfence" ::: "memory"); 可能也是个好主意就在之前 fillCache , 以确保任何缓存未命中的加载和存储都不会仍在运行中,并且可能会在您的代码运行时分配新的缓存行。或者甚至可能是像 asm("xor %eax,%eax; cpuid" ::: "eax", "ebx", "ecx", "edx", "memory"); 这样的完全序列化指令,但我不知道 CPUID 阻止了哪个 mfence;围栏不会。

标题问题:触摸内存将其带入缓存
PREFETCHT0(进入 L1d 缓存)是 __builtin_prefetch(p,0,3); . This answer显示 args 如何映射到指令;您正在使用 prefetchw (写意向)或者我认为 prefetcht1 (L2 缓存)取决于编译器选项。
但实际上,由于您需要这样做以确保正确性,因此您不应该使用硬件在繁忙时可以丢弃的可选提示。 mfence; lfence会让硬件不太可能真的很忙,但仍然不是一个坏主意。
使用 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。 (在这里你可以而且应该简单地对齐你的缓冲区,但 The right way to use function _mm_clflush to flush a large struct 展示了如何循环遍历任意大小的可能未对齐结构的每个缓存行。)
这并不能解释您的问题,因为唯一可能被遗漏的部分是 env.out 的最后最多 48 个字节。 . (我认为默认 ABI 规则下全局结构将按 16 对齐。)而且您只打印每个数组的前几个字节。

更简单的方法:memset(0) 避免将数据泄漏回 DRAM
顺便说一句,用 0 覆盖你的缓冲区完成后通过 memset 还应该防止您的数据像 INVD 一样可靠地写回 DRAM,但速度更快。 (也许是通过 asm 的手册 rep stosb 以确保它不能像死商店一样优化)。
无填充模式在这里也可能有用,以阻止缓存未命中驱逐现有行。 AFAIK,这基本上锁定了缓存,因此不会发生新的分配,因此不会被驱逐。 (但您可能无法读取或写入其他正常内存,尽管您可以将结果留在寄存器中。)
无填充模式(对于当前核心)将使在重新启用分配之前使用 memset 清除缓冲区绝对安全;在导致驱逐期间没有缓存未命中的风险。尽管如果您的 fillCache 实际上工作正常并且在您开始工作之前让您的所有行都进入 MESI Modified 状态,您的负载和存储将在 L1d 缓存中命中,而不会存在驱逐任何缓冲行的风险。
如果您担心 DRAM 内容(而不是总线信号),那么 在memset之后的每一行clflushopt都会减少漏洞的窗口 . (或者如果 0 对您不起作用,则来自原始副本的 memcpy,但希望您可以只在私有(private)副本中工作并且保持原稿不变。使用当前方法始终可以进行杂散回写,因此我不想依靠它来确定总是保持一个大缓冲区未修改。)
不要将 NT 存储用于手动 memset 或 memcpy:这可能会在 NT 存储之前刷新“ secret ”脏数据。一种选择是使用普通商店或 rep stosb memset(0) ,然后用 NT 存储再次循环。或者也许每行做 8 次 movq 普通存储,然后是 8 次 movnti,所以你在继续之前对同一行背靠背做这两件事。

为什么要fillCache?
如果您不使用无填充模式,那么在写入行之前是否缓存这些行都无关紧要。当 invd 时,您只需要您的写入在缓存中变脏运行,即使它们是从缓存中丢失的商店中获得的,这也应该是正确的。
您在 fillCache 之间已经没有像 mfence 这样的任何障碍和 changeFixedValue ,这很好,但意味着当您弄脏缓存时,任何因启动缓存而导致的缓存未命中仍在进行中。
INVD 本身 is serializing ,所以在丢弃缓存内容之前,它应该等待存储离开存储缓冲区。 (所以将 mfence;lfence 放在你的工作之后,在 INVD 之前,应该没有任何区别。)换句话说,INVD 应该丢弃仍在存储缓冲区中的可缓存存储,以及脏缓存行,除非提交其中的一些商店碰巧驱逐任何东西。

关于c - 如何将结构显式加载到 L1d 缓存中?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66772632/

32 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com