【问题标题】:Prefetch instruction behavior预取指令行为
【发布时间】:2016-04-21 05:49:54
【问题描述】:

为了满足一些安全属性,我想确保一个重要数据在语句访问时已经在缓存中(这样就不会有缓存未命中)。例如,对于这段代码

...
a += 2;
...

我想确保aa += 2 执行之前就在缓存中。

我正在考虑使用 x86 的 PREFETCHh 指令来实现这一点:

...
__prefetch(&a);     /* pseudocode */
a += 2;
...

但是,我读到在 a += 2 之前插入预取指令可能为时已晚,无法确保在执行 a += 2a 在缓存中。这种说法是真的吗?如果是真的,我可以通过在预取后插入 CPUID 指令来确保 prefectch 指令已被执行来修复它(因为英特尔手册说 PREFETCHh 是相对于 CPUID 排序的)?

【问题讨论】:

  • 我不确定这实际上会完成什么。您所做的只是将 fetch_into_cache(a)、read_from_cache(a)、write_to_cache(a) 的顺序更改为 prefetch_into_cache(a)、read_from_cache(a)、write_to_cache(a)。除了现在可能在预取和读取之间存在巨大延迟。
  • 您希望完成什么确切?避免定时攻击?

标签: caching assembly x86 prefetch


【解决方案1】:

是的,您需要以大约内存延迟的前置时间进行预取,以使其达到最佳状态。 Ulrich Drepper 的 What Every Programmer Should Know About Memory 谈了很多关于预取的内容。

对于单次访问来说,实现这一点非常重要。太快了,您的数据可能会在您关心的 insn 之前被驱逐。太晚了,它可能会减少一些访问时间。调整这将取决于编译器版本/选项,以及您正在运行的硬件。 (更高的每周期指令意味着您需要更早地预取。更高的内存延迟也意味着您需要更早地预取)。

由于您想对a 进行读-修改-写操作,因此您应该使用PREFETCHW(如果有)。其他预取指令仅用于读取预取,因此 RMW 的读取部分可能会命中,但我认为存储部分可能会因 MOSI 缓存一致性获得缓存行的写入所有权而延迟。

如果a 不是原子的,您也可以提前加载a 并在寄存器中使用副本。在这种情况下,返回全局的存储很容易丢失,但最终可能会停止执行。

你可能很难用编译器可靠地做一些事情,而不是自己编写 asm。任何其他想法也需要检查编译器输出以确保编译器完成了您希望的操作。

预取指令不一定要预取任何东西。它们是“提示”,当未完成加载的数量接近最大值(即几乎没有加载缓冲区)时,它们可能会被忽略。


另一种选择是加载它(不仅仅是预取),然后使用CPUID 进行序列化。 (丢弃结果的负载就像预取)。加载必须在序列化指令之前完成,序列化 insn 之后的指令在此之前无法开始解码。我认为预取可以在数据到达之前退出,这通常是一个优势,但在这种情况下,我们不关心一个操作会以牺牲整体性能为代价。

来自英特尔的 insn 参考手册(参见 标签 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 / CPUID / add [mem], 2,我不确定PREFETCHW 是否真的会确保缓存行准备就绪。 insn 是按 wrt 排序的。 CPUID,但是手册上并没有说预取效果是有序的。


如果avolatile,那么(void)a; 将让gcc 或clang 发出一个load insn。我假设大多数其他编译器(MSVC?)都是相同的。您可以使用(void) *(volatile something*)&a 取消引用指向volatile 的指针并强制从a 的地址加载。


为了保证内存访问会在缓存中命中,您需要以固定到不接收中断的内核的实时优先级运行。根据操作系统的不同,定时器中断处理程序可能足够轻量级,以至于从缓存中逐出数据的机会足够低。

如果您的进程在执行预取 insn 和进行实际访问之间被取消调度,则数据可能至少已从 L1 缓存中被逐出。

因此,除非以实时优先级运行是现实的,否则您不太可能击败决心对您的代码进行定时攻击的攻击者。攻击者可以运行许多线程的内存密集型代码...

【讨论】:

  • 感谢您的精彩回答!不过我有几个问题。首先让我确认一下:加载是指常规加载指令,对吗?如果我加载它而不使用 CPUID 或其他东西进行序列化,是否会发生缓存未命中?
  • 其实x86没有加载指令...你的意思是mov吗?
  • @Seves。是的,只是一个普通的负载,比如mov eax, [mem]。或者add eax, [mem] 还包括一个加载,并将解码为一个添加微指令和一个加载微指令。如果您不序列化,那么当第二个 insn 运行并且也未命中时,第一个 insn 上的缓存未命中仍然可能很突出。请参阅我的编辑。顺便说一句,如果您想关心指令命名,那么 x86 确实 有一个加载 insn:lodsb[esi] 加载到 al,并递增 esi。 :P 但是,当你的意思是“加载”时,说“加载”比“带有内存源的mov”更容易。
  • 好答案。另请注意,不能保证预取指令实际预取任何内容(如果压力太大,它可能会被 CPU 简单地忽略)。你应该考虑做一个真正的负载。
  • 这篇文章提供了一种比在现有硬件上“以固定到不接收中断的内核的实时优先级运行”更少约束的方法:usenix.org/conference/usenixsecurity12/technical-sessions/…
猜你喜欢
  • 2011-03-08
  • 2010-09-10
  • 2014-05-06
  • 1970-01-01
  • 2018-08-06
  • 2017-11-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多