如何将结构显式加载到L1d缓存中?
我的目标是将静态结构加载到 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__ (
"wbinvdn"
"mov %%cr0, %%raxnt"
"or $(1<<30), %%eaxnt"
"mov %%rax, %%cr0nt"
"wbinvdn"
::
:"%rax"
);
printk(KERN_INFO "cpuid %d --> cache disablen", smp_processor_id());
}
void enaCache(void *p){
__asm__ __volatile__ (
"mov %%cr0, %%raxnt"
"and $~(1<<30), %%eaxnt"
"mov %%rax, %%cr0nt"
::
:"%rax"
);
printk(KERN_INFO "cpuid %d --> cache enablen", 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 %dn", 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 %sn", env.in);
printk(KERN_INFO "env.out fixed is %sn", env.out);
printk(KERN_INFO "Current CPU %sn", 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("invdn":::);
asm volatile("invdn":::"memory");
// exit atomic
smp_call_function(enaCache,NULL,1);
local_irq_restore(flags);
preempt_enable();
printk(KERN_INFO "After: env.in is %sn", env.in);
printk(KERN_INFO "After: env.out is %sn", 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。使用 ioctl() 从 C 程序创建 MTRR显示了如何创建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 (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。这可以解释为什么代码完成后没有留给用户空间读取的日志消息。
为了测试这一点,理想情况下您希望使用clflushprintk 所做的一切,但没有简单的方法可以做到这一点。或许,wbinvd然后changeFixedValue再invd。(您没有在此核心上进入无填充模式,因此fillCache您的商店/invd 想法不需要工作,请参见下文。)
启用超线程:
CR0.CD 是每个物理核心,所以让你的 HT 兄弟核心禁用缓存也意味着隔离核心的 CD=1。 因此,启用 HT 后,即使在隔离核心上,您也处于无填充模式。
HT 关闭后,隔离内核仍然正常。
编译时和运行时重新排序
asm volatile("invdn":::);没有"memory"clobber告诉编译器它可以重新排序wrt。内存操作。显然,这不是您的问题,但这是您应该修复的错误。
放在asm("mfence; lfence" ::: "memory");之前可能也是一个好主意fillCache,以确保任何缓存未命中的加载和存储都不会仍在运行中,并且可能会在您的代码运行时分配新的缓存行。或者甚至可能是像 那样的完全序列化指令asm("xor %eax,%eax; cpuid" ::: "eax", "ebx", "ecx", "edx", "memory");,但我不知道 CPUID 阻止了哪个 mfence;围栏不会。
标题问题:触摸内存将其带入缓存
PREFETCHT0(进入 L1d 缓存)是__builtin_prefetch(p,0,3);. 这个答案显示了 args 如何映射到指令;您正在使用prefetchw(write-intent) 或我认为prefetcht1(L2 cache) 取决于编译器选项。
但实际上,由于您需要这样做以确保正确性,因此您不应该使用硬件在繁忙时可以丢弃的可选提示。 mfence; lfence会让硬件不太可能真的很忙,但仍然不是一个坏主意。
使用volatileread likeREAD_ONCE让 GCC 发出加载指令。或者使用volatile char *bufwith*buf |= 0;或 something 来真正的 RMW 而不是 prefetch,以确保该行是独家拥有的,而不必让 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。(在这里,您可以而且应该简单地对齐缓冲区,但是使用函数 _mm_clflush 刷新大型结构的正确方法显示了如何循环遍历任意大小的可能未对齐结构的每个缓存行。)
这并不能解释您的问题,因为唯一可能被遗漏的部分是env.out. (我认为默认 ABI 规则下全局结构将按 16 对齐。)而且您只打印每个数组的前几个字节。
更简单的方法:memset(0) 避免将数据泄漏回 DRAM
顺便说一句,0完成后用via memset覆盖缓冲区也应该可以防止您的数据像 INVD 一样可靠地写回 DRAM,但速度更快。(也许是rep stosb通过 asm的手册,以确保它不能像死商店一样优化掉)。
无填充模式在这里也可能有用,以阻止缓存未命中驱逐现有行。AFAIK,这基本上锁定了缓存,因此不会发生新的分配,因此不会被驱逐。(但您可能无法读取或写入其他正常内存,尽管您可以将结果留在寄存器中。)
无填充模式(对于当前核心)将使在重新启用分配之前使用 memset 清除缓冲区绝对安全;在导致驱逐期间没有缓存未命中的风险。尽管如果您的 fillCache 实际上工作正常并且在您开始工作之前让您的所有行进入 MESI Modified 状态,您的负载和存储将在 L1d 缓存中命中,而不会冒驱逐任何缓冲行的风险。
如果您担心 DRAM 内容(而不是总线信号),那么在 memset之后的每一行 clflushopt 将减少漏洞窗口。(或者来自原始副本的 memcpy 如果0对您不起作用,但希望您可以在私人副本中工作并且保持原稿不变。使用您当前的方法总是可以进行杂散回写,所以我不会“不想依靠它来确保始终保持大缓冲区不变。)
不要将 NT 存储用于手动 memset 或 memcpy:这可能会在 NT 存储之前刷新“秘密”脏数据。一种选择是使用普通存储或 使用 memset(0) rep stosb,然后使用 NT 存储再次循环。或者也许每行做 8 次 movq 普通存储,然后是 8 次 movnti,所以你在继续之前对同一行背靠背做这两件事。
为什么要fillCache?
如果您不使用无填充模式,那么在写入行之前是否缓存这些行都无关紧要。您只需要在invd运行时将写入在缓存中弄脏,即使它们是从缓存中丢失的存储中获得的,也应该如此。
你已经没有像之间MFENCE任何障碍fillCache和changeFixedValue,这是好的,但手段,从吸缓存任何高速缓存未命中仍然在飞行时,你脏了。
INVD 本身是序列化的,所以它应该在丢弃缓存内容之前等待存储离开存储缓冲区。(所以mfence;lfence在你的工作之后,在 INVD 之前,应该没有任何区别。)换句话说,INVD 应该丢弃仍在存储缓冲区中的可缓存存储,以及脏缓存行,除非提交其中一些存储碰巧驱逐任何东西。
- CR0.CD is effectively per physical core. The effective CD value is the OR of the CD bits of each of the sibling logical cores. With HT enabled, setting CD to one disables cache fills in the entire physical core and so the secret cache lines are not filled. Although I don't think this approach guarantees that the secret data is not written to memory because there is no architectural guarantee that writebacks, replacements, or invalidations due to snoops don't occur. (Writebacks may occur automatically to reduce the probability of uncorrectable errors, perhaps even in no-fill mode.)