如何将结构显式加载到L1d缓存中?在带/不带超线程的隔离内核上使用CR0.CD=1的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
将删除所有更改。从原子模式返回后,我应该会看到之前设置的原始固定值。然而,这并没有发生。退出原子模式后,我可以看到用于覆盖先前放置的固定值的值。以下是我的模块代码
奇怪的是,重新启动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
"
"mov %%cr0, %%rax
"
"or $(1<<30), %%eax
"
"mov %%rax, %%cr0
"
"wbinvd
"
::
:"%rax"
);
printk(KERN_INFO "cpuid %d --> cache disable
", smp_processor_id());
}
void enaCache(void *p){
__asm__ __volatile__ (
"mov %%cr0, %%rax
"
"and $~(1<<30), %%eax
"
"mov %%rax, %%cr0
"
::
:"%rax"
);
printk(KERN_INFO "cpuid %d --> cache enable
", 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)
"
:
:"r"(buf)
:
);
*/
//__builtin_prefetch(buf,1,1);
//__builtin_prefetch(buf,0,3);
*buf += 0;
buf += 64;
}
printk(KERN_INFO "Inside fillCache, num is %d
", 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
", env.in);
printk(KERN_INFO "env.out fixed is %s
", env.out);
printk(KERN_INFO "Current CPU %s
", 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
":::);
asm volatile("invd
":::"memory");
// exit atomic
smp_call_function(enaCache,NULL,1);
local_irq_restore(flags);
preempt_enable();
printk(KERN_INFO "After: env.in is %s
", env.in);
printk(KERN_INFO "After: env.out is %s
", env.out);
return 0;
}
static void __exit device_cleanup(void){
printk(KERN_ALERT "Removing invd_driver.
");
}
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()
没有将结构放入缓存。另一种将内容放入缓存的方法是在MTRR
&;PAT
的帮助下设置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
推荐答案
在填充缓存底部调用printk
看起来非常不安全。您将比invd
多运行几个存储,因此printk
对内核数据结构(如日志缓冲区)所做的任何修改都可能被写回DRAM,或者如果它们在缓存中仍然是脏的,则可能会失效。如果某些存储(但不是所有存储)能够访问DRAM(因为缓存容量有限),则可能会使内核数据结构处于不一致的状态。
我猜您当前禁用超线程的测试显示,一切都比您希望的运行得更好,包括丢弃printk
完成的存储,以及丢弃changeFixedValue
完成的存储。这就解释了为什么代码完成后用户空间没有留下可供读取的日志消息。
要测试这一点,理想情况下,您会希望clflush
像clflush
那样做,但没有简单的方法可以做到这一点。也许wbinvd
然后changeFixedValue
然后invd
。(您没有在此核心上进入无填充模式,因此fillCache
不是您的存储/邀请想法工作所必需的,请参见下面的内容。)
启用超线程:
CR0.CD是按物理核心的,因此让您的HT同级核心禁用缓存也意味着隔离核心的CD=1。因此,启用了超线程后,即使在隔离的核心上也处于无填充模式。禁用超线程后,隔离核心仍正常。
编译时和运行时重新排序
asm volatile("invd
":::);
如果没有"memory"
拦截器,则会告诉编译器它被允许重新排序WRT。内存操作。显然,这不是您的问题所在,但这是您应该修复的错误。
asm("mfence; lfence" ::: "memory");
放在fillCache
之前可能也是一个好主意,以确保任何缓存未命中加载和存储不会继续进行,并可能在代码运行时分配新的缓存线。或者甚至可能是像asm("xor %eax,%eax; cpuid" ::: "eax", "ebx", "ecx", "edx", "memory");
这样的完全序列化指令,但我不知道CPUID阻止了哪个mfigure;lfigure不会。
标题问题:触摸内存将其放入缓存
PREFETCHT0(进入L1d缓存)为__builtin_prefetch(p,0,3);
。This answer显示参数如何映射到指令;根据编译器选项,您使用prefetchw
(写入意图)或我认为prefetcht1
(二级缓存)。
但实际上,由于您需要此选项以确保正确性,因此您不应使用硬件在忙时可能会掉线的可选提示。mfence; lfence
这将使硬件不太可能实际处于忙状态,但仍不是一个坏主意。
volatile
Read LikeREAD_ONCE
让GCC发出LOAD指令。或者使用volatile char *buf
WITH*buf |= 0;
或其他方法来代替预取,以确保线路是独占的,而不必让GCC发出prefetchw
。也许值得运行几次填充缓存,以便更好地确保每一行都处于您想要的状态。但是,由于您的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个字节。(我认为在默认情况下,全局结构将按16个ABI规则对齐。)并且您只打印每个数组的前几个字节。
更简单的方法:Memset(0),以避免将数据泄漏回DRAM
和BTW,在您完成后使用0
通过Memset覆盖您的缓冲区,也可以防止您的数据被写回DRAM,其可靠性与INVD差不多,但速度更快。(可以通过ASM使用手册rep stosb
,以确保它不会作为死存储进行优化)。
在这里,无填充模式可能还有助于阻止缓存未命中逐出现有行。AFAIK,这基本上锁定了缓存,因此不会发生新的分配,因此不会进行驱逐。(但您可能无法读取或写入其他正常内存,尽管您可以将结果留在寄存器中。)无填充模式(对于当前内核)将确保在重新启用分配之前使用Memset清除缓冲区是绝对安全的;在此期间不会有缓存未命中的风险,从而导致驱逐。但是,如果您的填充缓存实际工作正常,并且在您执行工作之前使所有行进入MESI修改状态,则加载和存储将命中L1d缓存,而不会有驱逐任何缓冲区行的风险。
如果您担心DRAM内容(而不是总线信号),则clflushopt在Memset之后选择每一行将减少漏洞窗口。(或者,如果0
对您不起作用,则可以从原始文件的干净副本中复制Memcpy,但希望您可以在私有副本中工作,并保持原始文件不变。您当前的方法总是有可能出现错误的回写,所以我不想依赖它来确保大缓冲区始终保持不变。)
不要将NT存储区用于手动Memset或Memcpy:这可能会在NT存储区之前清除&QORT";脏数据。一种选择是使用普通存储的Memset(0)或rep stosb
,然后使用NT存储再次循环。或者在每条生产线上做8倍Movq的普通门店,然后再做8倍Movnti,这样在继续之前,你可以在同一条生产线上背靠背地做这两件事。
为什么要填充缓存?
如果您没有使用无填充模式,那么在写入行之前是否缓存行应该都无关紧要。您只需要在invd
运行时您的写入在缓存中是脏的,这应该是正确的,即使它们是以这种方式从缓存中缺失的存储中获取的。
您已经在fillCache
和changeFixedValue
之间没有任何类似mfigure的障碍,这很好,但这意味着当您损坏缓存时,启动缓存时任何缓存未命中仍在进行中。
INVD本身is serializing,因此在丢弃缓存内容之前,它应该等待存储离开存储缓冲区。(所以,在你的工作之后,在INVD之前,把mfence;lfence
放在一起,应该没有什么不同。)换句话说,INVD应该丢弃仍在存储缓冲区中的可缓存存储以及脏缓存行,除非提交其中一些存储恰好会驱逐任何内容。
这篇关于如何将结构显式加载到L1d缓存中?在带/不带超线程的隔离内核上使用CR0.CD=1的INVD出现奇怪的结果的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!