Sev*_*ves 0 x86 assembly caching prefetch
为了满足某些安全属性,我想确保一个重要数据在语句访问时已经在缓存中(因此不会有缓存未命中).例如,对于此代码
...
a += 2;
...
Run Code Online (Sandbox Code Playgroud)
我想确保a在a += 2执行之前就在缓存中.
我正在考虑使用PREFETCHhx86 的指令来实现这个目的:
...
__prefetch(&a); /* pseudocode */
a += 2;
...
Run Code Online (Sandbox Code Playgroud)
但是,我已经读过,之前插入预取指令a += 2可能为时已晚,无法确保a在a += 2执行时是否在缓存中.这个说法是真的吗?如果是,我可以通过CPUID在预取之后插入指令来修复它,以确保执行了prefectch指令(因为英特尔手册中说明了相关PREFETCHh的命令CPUID)?
是的,您需要预先获取大约内存延迟的提前期,以使其达到最佳状态.Ulrich Drepper的每个程序员应该知道的关于内存的事情谈论很多关于预取的内容.
实现这一目标对于单一访问来说将是非常重要的.太快了,你的数据可能会在你关心的insn之前被驱逐出去.太晚了,它可能会减少一些访问时间.调整这将取决于编译器版本/选项,以及您正在运行的硬件.(更高的每周期指令意味着您需要提前预取.更高的内存延迟也意味着您需要提前预取).
由于您要执行读取 - 修改 - 写入a,因此应使用(PREFETCHW如果可用).其他预取指令只是预取用于读取,因此RMW的读取部分可能会命中,但我认为存储部分可能会被MOSI缓存一致性延迟,从而获得缓存行的写入所有权.
如果a不是原子的,您也可以a提前加载并在寄存器中使用副本.在这种情况下,回到全球的商店很容易错过,最终可能会停止执行.
你可能很难用编译器做一些可靠的事情,而不是自己编写asm.任何其他想法也需要检查编译器输出,以确保编译器做了你希望的.
预取指令不一定预取任何东西.它们是"提示",当未完成的负载数量接近最大值(即几乎没有负载缓冲区)时,可能会被忽略.
另一种选择是加载它(不仅仅是预取)然后用a序列化CPUID.(抛出结果的负载就像预取一样).在序列化指令之前必须完成加载,并且序列化insn之后的指令在此之前不能开始解码.我认为预取可以在数据到达之前退出,这通常是一个优点,但在这种情况下我们不关心一个操作以牺牲整体性能为代价.
从英特尔的insn ref手册(请参阅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
Run Code Online (Sandbox Code Playgroud)
这里我们使用的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缓存中逐出.
所以你不可能击败一个决心对你的代码进行定时攻击的攻击者,除非以实时优先级运行是现实的.攻击者可以运行许多内存密集型代码的线程......