动态指令修补

on the fly instruction patching

提问人:Bob5421 提问时间:11/11/2023 更新时间:11/11/2023 访问量:26

问:

我为 Linux 创建了一个小型调试器。 我试图创建一种机制来加密二进制指令并在执行前解密它们(我已经设置了硬件断点或分步运行)。 它在 100 次中有 99 次有效。

我认为问题是由于 L1 缓存造成的。 当我尝试解密指令时,此指令已在 CPU L1 缓存中。 我试过 ARM64 和 x86_64。我得到了同样的结果。

我的问题是像 gdb 或 lldb 这样的调试器如何在没有 L1 缓存副作用的情况下修补断点指令?

谢谢

调试低级

评论

1赞 jasonharper 11/11/2023
我的理解是,这在 x86 上不是问题:在写入和执行同一内存区域的异常情况下,数据写入会自动使同一地址的缓存指令失效。其他体系结构可能要求您在修改将要执行的内存后执行显式缓存刷新。
0赞 Peter Cordes 11/11/2023
使用自修改代码在 x86 上观察过时的指令获取 - 在当前的微架构上是不可能的。这在 x86 上不是问题,L1i 缓存是连贯的,即使是已经获取的指令地址也会被商店窥探。无论您有什么错误,它都不是由 x86-64 上的错误引起的。在 ARM 和 AArch64 上,您必须使 I 缓存失效,并强制将数据缓存写回到统一点,以便 I-fetch 将看到最近的存储。(使用适当的屏障说明,以确保这些提取在回写之后才会发生。
0赞 Peter Cordes 11/11/2023
尽管这只是当代码修改发生在同一个线程(实际上是同一个核心)中时。但是修改另一个进程的内存将涉及一个让它返回用户空间,我很确定这是一个序列化指令。如果你在同一内核上的中断处理程序中进行修改(在内核模块中?),这将不是问题。ptraceiret

答:

1赞 MadMan 11/11/2023 #1

x86/x86_64 是一个不寻常的野兽,因为如果你写入指令内存,它将使缓存的那一行失效,所以你不需要做任何事情。这是为了向后兼容 1980 年代的芯片,当时没有任何缓存。这意味着对于该处理器,L1 缓存肯定不是问题,除非您使用不同的线性地址修改指令,如下所述:

https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.pdf

11.6 自修改代码 写入当前缓存在处理器中的代码段中的内存位置会导致关联的缓存行(或行)失效。

例外情况是,如果你正在对地址映射做一些奇怪的事情(来自文档的同一部分):

系统软件,例如调试器,可能会修改 指令使用与用于获取的线性地址不同的线性地址 该指令将执行序列化操作,例如 CPUID 指令,在修改后的指令执行之前,这将 自动重新同步指令缓存和预取队列。

因此,在x86_64,您可以尝试 CPUID 指令。

除了这种情况,您的问题根本不是缓存 - 我会去其他地方寻找,例如寻找竞争条件(您还没有说非工作情况是什么样子的)。

对于其他现代处理器(包括 ARM),您需要自行使指令缓存的相关部分失效。

https://developer.arm.com/documentation/ddi0344/k/Babhejba

评论

0赞 Peter Cordes 11/11/2023
使用自修改代码在 x86 上观察过时指令获取中所示,当前的微架构(自 P6 以来的 Intel)使得即使通过不同的虚拟地址为同一物理页面写入时,也无法观察到过时指令获取。在纸面上,如果没有像 或 这样的序列化指令,就无法保证这一点,但实际上,您只需要它来交叉修改代码。cpuidserialize
0赞 Peter Cordes 11/11/2023
例如,在另一个软件线程(在另一个内核上)编写一些指令后,看到标志的正常获取/释放同步是不够的。您确实需要一个序列化指令来确保前端在我们的数据加载看到标志之前没有获取过时的数据;LoadLoad 排序仅适用于数据加载。(修改另一个进程的内存应该涉及在返回到用户空间时进行序列化。除非它只是一个?但我认为 Linux 特别避免使用过,至少如果 regs 已被修改。code_readyptraceiretsysretsysretptrace