为了满足某些安全属性,我想确保一个重要数据在语句访问时已经在缓存中(因此不会有缓存未命中)。例如,对于此代码
...
a += 2;
...
我想确保在a
执行之前a += 2
位于缓存中。
我正在考虑使用x86的PREFETCHh
指令来实现这个目标:
...
__prefetch(&a); /* pseudocode */
a += 2;
...
但是,我已经读过在a += 2
之前插入预取指令可能为时已晚,以确保在a
执行时a += 2
位于缓存中。这个说法是真的吗?如果是,我可以通过在预取之后插入CPUID
指令来修复它,以确保执行了prefectch指令(因为英特尔手册说PREFETCHh
是针对CPUID
订购的) ?
答案 0 :(得分:5)
是的,您需要预先获取大约内存延迟的提前期,以使其达到最佳状态。 Ulrich Drepper的What Every Programmer Should Know About Memory谈到了很多关于预取的内容。
实现这一目标对于单一访问来说将是非常重要的。太快了,你的数据可能会在你关心的insn之前被驱逐出去。太晚了,它可能会减少一些访问时间。调整这将取决于编译器版本/选项,以及您正在运行的硬件。 (更高的每周期指令意味着您需要提前预取。更高的内存延迟也意味着您需要提前预取。)
由于您要对a
执行读取 - 修改 - 写入,因此应使用PREFETCHW
(如果可用)。其他预取指令只是预取用于读取,因此RMW的读取部分可能会命中,但我认为存储部分可能会被MOSI缓存一致性延迟,从而获得缓存行的写入所有权。
如果a
不是原子的,您也可以提前加载a
并在寄存器中使用该副本。在这种情况下,回到全球的商店很容易错过,最终可能会停止执行。
你可能很难用编译器做一些可靠的事情,而不是自己编写asm。任何其他想法也需要检查编译器输出,以确保编译器做了你希望的。
预取指令不一定预取任何内容。它们是“提示”,当未完成的负载数量接近最大值(即几乎没有负载缓冲区)时,可能会被忽略。
另一种选择是加载它(不仅仅是预取),然后用CPUID
序列化。 (抛出结果的负载就像预取一样)。在序列化指令之前,负载必须完成,并且序列化insn之后的指令在此之前不能开始解码。我认为预取可以在数据到达之前退出,这通常是一个优势,但在这种情况下,我们不关心一个操作是否以牺牲整体性能为代价。
来自英特尔的insn参考手册(请参阅x86标签wiki)CPUID
的条目:
序列化指令执行 保证以前完成对先前指令的标志,寄存器和存储器的任何修改 获取并执行下一条指令。
我认为像这样的序列相当不错(但在先发制人的多任务系统中仍然不能保证任何东西):
add [mem], 0 # can't retire until the store completes, requiring that our core owns the cache line for writing
CPUID # later insns can't start until the prev add retires
add [mem], 2 # a += 2 Can't miss in cache unless an interrupt or the other hyper-thread evicts the cache line before this insn can execute
这里我们使用add [mem], 0
作为写预取,否则就是接近无操作。 (这是非原子读取 - 修改 - 重写)。如果您执行PREFETCHW
/ PREFETCHW
/ CPUID
,我不确定add [mem], 2
是否真的能确保缓存行已准备就绪。 insn是wrt订购的。 CPUID,但手册并没有说预取效果是有序的。
如果a
为volatile
,那么(void)a;
将获得gcc或clang以发出加载insn。我假设大多数其他编译器(MSVC?)是相同的。您可以(void) *(volatile something*)&a
取消引用指向volatile
的指针并强制加载a
的地址。
要保证存储器访问将在缓存中命中,您需要以固定优先级运行固定到不接收中断的核心。根据操作系统的不同,定时器中断处理程序可能足够轻,以至于从缓存中驱逐数据的可能性足够低。
如果您的进程在执行预取insn和执行实际访问之间被取消预定,则数据可能已从至少L1缓存中逐出。
所以你不可能打败一个决心对你的代码进行定时攻击的攻击者,除非以实时优先级运行是否切合实际。攻击者可以运行许多内存密集型代码的线程......