Mon*_*nad 3 assembly self-modifying memory-barriers cpu-cache arm64
据我了解,编写并随后执行 JIT 或自修改代码的一般、更抽象的过程如下所示。
从这篇关于 x86 上自修改代码的文章中我可以看出,手动缓存管理显然是没有必要的。我认为 aclflushopt是必要的,但 x86 1显然会在从具有新指令的位置加载时自动处理缓存失效,这样指令提取就永远不会过时。我的问题不是关于 x86,但我想将其包括在内以进行比较。
AArch64 中的情况稍微复杂一些,因为它区分了可共享域以及缓存操作的“可见性”程度。仅从 ARMv8/ARMv9 的官方文档中,我首先得出了这个猜测。
dsb ishst确保在继续之前已全部写入,isb sy确保从内存中提取后续指令。但DMB/DSB/ISB 的文档说“ISB 后面的指令是从缓存或内存中获取的”。这给我的印象是缓存控制操作确实是必要的。我的新猜测是这样的。
dsb ishst确保在继续之前已全部写入,ic ivau新代码占用的所有缓存行。但我又忍不住觉得这也不太对劲。过了一会儿,我在文档中发现了一些我错过的东西,并且在一篇论文中发现了几乎相同的东西。他们都给出了一个看起来像这样的例子。
dc cvau, Xn ; Clean cache to PoU, so the newly written code will be visible
dsb ish ; Wait for cleaning to finish
ic ivau, Xn ; Invalidate cache to PoU, so the newly written code will be fetched
dsb ish ; Wait for invalidation to finish
isb sy ; Make sure new instructions are fetched from cache or memory
Run Code Online (Sandbox Code Playgroud)
对于一大块代码,这可能是一个清理循环,,dsb ish一个无效循环,,dsb ish然后是一个isb sy。如果这是不正确的,请纠正我。无论如何,这个例子是有道理的,我想我唯一错过的是dsb ish单独不同步I-cache和D-cache,并且新数据必须手动清理和失效。我对这篇文章的实际问题如下。
ic ivac,所以我猜测 PoU 就足够了,而我的 PoU 概念是有缺陷的。dsb ishst足够了,还是dsb ish强制的?dsb ish指令用于等待dc cvau和ic ivau指令完成。这意味着dsb ish[st]单独(即没有dc/ ic)不能确保数据同步,直到在内部可共享域中可见。我推测在这种情况下dc/ic是必要的,因为数据需要从 D-cache 移动到 I-cache,而dc/ic对于常规数据同步来说不是必需的。这种理解正确吗?0仅限于所有应该看到它的核心都会看到它。
1至少,所有相当现代的都应该如此。
(免责声明:这个答案是基于阅读规范和一些测试,而不是基于以前的经验。)
首先,架构参考手册(Gb 版)的 B2.2.5 中有针对这种具体情况(一个核心编写代码供另一个核心执行)的解释和示例代码。与您展示的示例的唯一区别是,在缓存失效完成后,isb需要在执行新代码的线程(我猜是您的“消费者”)中执行最终操作。
我发现尝试从架构参考中以更具体的术语理解“内部可共享域”、“统一点”等抽象结构很有帮助。
让我们考虑一个具有多个核心的系统。它们的 L1d 缓存是一致的,但它们的 L1i 缓存不需要与 L1d 统一,也不需要彼此一致。不过L2缓存是统一的。
系统没有任何方式让L1d和L1i直接相互对话;它们之间的唯一路径是通过 L2。因此,一旦我们将新代码写入 L1d,我们必须将其写回到 L2 ( dc cvau),然后使 L1i ( ic ivau) 无效,以便它从 L2 中的新代码重新填充。
在此设置中,PoU 是 L2 缓存,这正是我们想要清理/失效的地方。
D4-2646 页对这些术语有一些解释。尤其:
内部可共享共享域的 PoU 是确保该内部可共享共享域中的所有 PE 的指令和数据缓存以及转换表遍历能够看到内存位置的相同副本的点。
在这里,内部共享域将包含可以运行我们程序的线程的所有核心;事实上,它应该包含与我们运行相同内核的所有内核(第 B2-166 页)。而且因为我们正在使用的内存dc cvau可能标有内部共享属性或更好的属性,正如任何合理的操作系统应该为我们做的那样,它会清理域的 PoU,而不仅仅是我们核心 (PE) 的 PoU。这正是我们想要的:所有内核的所有指令缓存都将看到的缓存级别。
一致性点更靠下;这是系统上所有东西都能看到的级别 ,包括 DMA 硬件等。这很可能是主内存,位于所有缓存下方。我们不需要降低到那个水平;它只会减慢一切速度,没有任何好处。
希望这对您的问题 1 有帮助。
请注意,缓存清理和无效指令实际上是在“后台”运行,因此您可以执行一长串指令(就像对所有受影响的缓存行进行循环),而无需等待它们逐一完成。 dsb ish在最后使用一次以等待它们全部完成。
关于您的问题 #2 和 #3 的一些评论dsb。其主要目的是作为屏障;它确保我们的核心内(存储缓冲区等)中所有待处理的数据访问都被刷新到 L1d 缓存,以便所有其他核心都可以看到它们。这是一般线程间内存排序所需的屏障。(或者对于大多数目的,较弱的dmb就足够了;它强制排序,但实际上并不等待所有内容都被刷新。)但它不会对缓存本身执行任何其他操作,也不会说明超出范围的数据应该发生什么L1d。因此,就其本身而言,它的强度不足以满足我们这里的需要。
据我所知,“等待缓存维护完成”效果是dsb ish. 这似乎与指令的主要目的正交,我不确定他们为什么不提供单独的wcm指令。但无论如何,只有它
dsb ish有这个额外的功能;dsb ishst才不是。D4-2658:“在所有情况下,本节中的文本提到 DMB 或 DSB 时,这意味着所需访问类型为
加载和存储的DMB 或 DSB ”。
我在 Cortex A-72 上对此进行了一些测试。省略dc cvau或ic ivau通常会导致执行过时的代码,即使已dsb ish完成。另一方面,在dc cvau ; ic ivau没有任何 的情况下dsb ish,我没有观察到任何失败;但这可能是运气或者是这个实现的一个怪癖。
对于#4,我们一直在讨论的序列 ( dc cvau ; dsb ish ; ci ivau ; dsb ish ; isb) 适用于您将在编写代码的同一核心上运行代码的情况。但实际上哪个线程执行该序列并不重要dc cvau ; dsb ish ; ci ivau ; dsb ish
,因为缓存维护指令会导致所有核心按照指示进行清理/无效;不只是这个。见表D4-6。(但是,如果与dc cvau写入器位于不同的线程中,也许写入器必须dsb ish事先完成 a,以便写入的数据确实位于 L1d 中,而不是仍在写入器的存储缓冲区中?对此不确定。)
真正重要的部分是isb. 完成后ci ivau,L1i 缓存中的过时代码将被清除,并且任何内核的进一步指令提取都将看到新代码。然而,运行器核心
之前可能已经从 L1i 获取了旧代码,并且仍然在内部保存它(解码并在管道、uop 缓存、推测执行等中)。 isb刷新这些 CPU 内部机制,确保所有要执行的进一步指令在 L1i 缓存失效后实际上已从 L1i 缓存中获取。
因此,需要在将运行新编写的代码的isb线程中执行
。而且,您需要确保在所有缓存维护完全完成后才完成;也许可以让编写器线程通过条件变量等通知它。
我也测试过这个。如果所有缓存维护指令(加上一个)isb都由编写器完成,但运行器没有完成isb,那么它可以再次执行过时的代码。我只能在测试中重现这一点,其中编写器在运行程序同时执行的循环中修补指令,这可能确保运行程序已经获取了它。这是合法的,前提是旧指令和新指令分别是分支和 nop(参见 B2.2.5),这就是我所做的。(但不能保证它适用于任意新旧指令。)
我尝试了一些其他测试来尝试对其进行安排,以便该指令在修补之前不会实际执行,但它是应该预测采用的分支的目标,希望这会使其预取;但在这种情况下我无法执行过时的版本。
我不太确定的一件事是这一点。典型的现代操作系统很可能具有 W^X,其中没有虚拟页面可以同时可写和可执行。如果在编写代码后,您调用等效的方法
mprotect使页面可执行,那么操作系统很可能会为您处理所有缓存维护和同步(但我想自己也这样做也没什么坏处) )。
但另一种方法是使用别名:将可写的内存映射到一个虚拟地址,并在另一个虚拟地址上执行。写入者在前一个地址写入,运行者跳转到后者。在这种情况下,我认为您只需提供dc cvau可写地址和ic ivau可执行地址,但我找不到对此的确认。但我测试了它,无论将哪个别名传递给哪个缓存维护指令,它都有效,而如果完全省略任一指令,它就会失败。所以看起来缓存的维护是通过底层的物理地址来完成的。