【问题标题】:Interrupting an assembly instruction while it is operating在运行时中断汇编指令
【发布时间】:2019-08-06 09:45:13
【问题描述】:

当一个中断到达 CPU 时,如果它被确认,它会在跳转到处理程序之前保存当前地址位置来处理。否则将被忽略。

我想知道汇编指令调用是否被中断。

例如,

mvi a, 03h ; put 3 value into acc. in 8080 assembly

单行指令可以中断吗?或者如果不是,它是原子的??

是否总能保证“一行汇编指令”总是原子的??

如果没有“lock”关键字,即在8080汇编中,那么如何提供原子性?

例如,如果要操作 64 位 sum 怎么办,但是“单行指令”没有办法做到这一点,并且在 sum 操作时出现中断。如何在装配级别防止它??

对我来说,这个概念正在开始酝酿。

【问题讨论】:

  • 芯片设计师确保它是原子的,它必须是。中断处理程序绝不能破坏处理器状态,从而导致多指令操作行为异常。通过简单地保存和恢复寄存器,在 8080 上并不难做到。中断逻辑本身已经保存了 IP 寄存器,RET 将其恢复。几乎每个中断处理程序都以 PUSH PSW 开始,以保​​留标志和累加器寄存器。
  • 我怀疑这是否适用于 8080。但是,理论上,已经运行的指令可能会被中断中断。我一直在研究不同的 RISC 处理器(用于 FPGA)。在一种设计中,指令甚至可以被中断,如果发生这种情况,被写入的寄存器具有不一致的值。在该设计中,返回地址将是已中断指令的地址,因此在这种情况下将重复完整的指令。所以至少存在允许中断指令的设计。

标签: assembly interrupt cpu-architecture atomic interrupted-exception


【解决方案1】:

是的,包括 8080 和 x86 在内的所有“正常”ISA 都保证指令对于同一内核上的中断是原子的。 一条指令已完全执行并且其所有架构效果都是可见的(在中断处理程序),或者它们都不是。任何与此规则的偏差通常都会仔细记录。


例如,Intel's x86 manual vol.3 (~1000 page PDF) 确实特别强调了这一点:

6.6 程序或任务重启
为了允许在处理异常或中断后重新启动程序或任务,所有异常 (中止除外)保证报告指令边界上的异常。 所有中断都保证是 采取指令边界。

Intel's vol.1 manual 中的一段旧段落谈到使用 cmpxchg 的单核系统 没有 lock 前缀以原子方式读取-修改-写入(相对于其他软件,而不是硬件 DMA访问)。

CMPXCHG 指令通常用于测试和修改信号量。它检查是否有信号量 免费。如果信号量空闲,则标记为已分配;否则获取当前所有者的 ID。 这一切都完成了 在一个不间断的操作中 [因为它是一条指令]。在单处理器系统中,CMPXCHG 指令无需 在执行多条指令以测试和修改信号量之前切换到保护级别 0(以禁用中断)。

对于多处理器系统,CMPXCHG 可以与 LOCK 前缀组合来执行比较和 原子交换操作。 (参见第 8 章“多处理器管理”中的“锁定原子操作”, 英特尔® 64 和 IA-32 架构软件开发人员手册第 3A 卷,了解有关原子的更多信息 操作。)

(有关 lock 前缀的更多信息,以及它是如何实现与非锁定 add [mem], 1 的,请参阅 Can num++ be atomic for 'int num'?

正如英特尔在第一段中指出的那样,实现多指令原子性的一种方法是禁用中断,然后在完成后重新启用。这比使用互斥锁更好保护一个更大的整数,尤其是当您谈论主程序和中断处理程序之间共享的数据时。如果在主程序持有锁时发生中断,它不能等待锁被释放;那永远不会发生。

在简单的有序流水线,尤其是微控制器上,禁用中断通常非常便宜。 (有时您需要保存之前的中断状态,而不是无条件启用中断。例如,可能在中断已禁用的情况下调用的函数。)

无论如何,禁用中断是您可以在 8080 上以原子方式使用 64 位整数执行某些操作的方法。


根据为该指令记录的规则,一些长时间运行的指令可中断的。

例如x86 的 rep-string 指令,如 rep movsb(任意大小的单指令 memcpy)在架构上等同于重复基本指令 (movsb) RCX 次,每次递减 RCX 并递增或递减指针输入(RSI和 RDI)。复制期间到达的中断可以设置 RCX starting_value - byte_copied 并且(如果 RCX 非零)让 RIP 指向该指令,因此在中断后恢复 rep movsb 将再次运行并完成其余的复制。

其他 x86 示例包括 SIMD 收集负载 (AVX2/AVX512) 和分散存储 (AVX512)。例如。 vpgatherdd ymm0, [rdi + ymm1*4], ymm2 最多执行 8 个 32 位加载,根据 ymm2 的元素设置。结果被合并到ymm0中。

在正常情况下(在收集期间没有中断、没有页面错误或其他同步异常),您在目标寄存器中获取数据,并且掩码寄存器最终归零。因此,掩码寄存器为 CPU 提供了存储进度的地方。

收集和分散很慢,并且可能需要触发多个页面错误,因此对于同步异常,即使在处理页面错误取消映射所有其他页面的病态条件下,这也可以保证向前进展。但更相关的是,这意味着避免在中间元素页面错误时重做 TLB 未命中,并且在异步中断到达时不丢弃工作。


其他一些长时间运行的指令(例如 wbinvd 刷新所有内核中的所有数据缓存)在架构上不可中断,甚至在架构上可中止(丢弃部分工作并去处理中断)。它具有特权,因此用户空间无法将其作为拒绝服务攻击执行,从而导致高中断延迟。


记录有趣行为的相关示例是当 x86 popad 离开堆栈顶部(段限制)时。这是一个异常(不是外部中断),在第 3 卷手册中的第 6.5 节异常分类(即故障/陷阱/中止,有关详细信息,请参阅 PDF)中进行了记录。

注意
通常报告为故障的一个异常子集不可重新启动。此类异常会导致损失 一些处理器状态。例如,执行一个POPAD instruction,其中堆栈帧 越过堆栈段的末尾会导致报告错误。在这种情况下, 异常处理程序看到指令指针 (CS:EIP) 已恢复,就好像 POPAD 指令尚未执行。 但是,内部处理器状态(通用 寄存器)将被修改。这种情况被认为是编程错误。一个应用程序 导致此类异常的应由操作系统终止。

请注意,这仅在popad 本身导致异常,不会出于任何其他原因。外部中断不能像 rep movsbvpgatherdd 那样拆分 popad

(我猜出于popad 故障的目的,它有效地迭代工作,一次弹出 1 个寄存器并在逻辑上修改 RSP/ESP/SP 以及目标寄存器。而不是检查整个区域,它将在开始之前加载段限制,因为我猜这需要额外的添加。)


无序 CPU 在中断时回滚到退出状态。

像现代 x86 这样具有乱序执行和将复杂指令拆分为多个微指令的 CPU 仍然可以确保这种情况。当中断到达时,CPU 必须在两条正在运行的指令之间选择一个点作为中断在架构上发生的位置。它必须丢弃已经在解码或开始执行任何后续指令时完成的任何工作。假设中断返回,它们将被重新获取并重新开始执行。

When an interrupt occurs, what happens to instructions in the pipeline?

正如 Andy Glew 所说,当前的 CPU 不会重命名特权级别,因此逻辑上发生的事情(中断/异常处理程序在早期指令完成后执行)与 实际 发生的事情相匹配。

有趣的事实是:x86 中断没有完全序列化,至少在纸面上不能保证。 (在 x86 术语中,像 cpuidiret 这样的指令被定义为序列化;耗尽 OoO 后端和存储缓冲区,以及其他任何可能重要的东西。这是一个非常强大的障碍和许多其他事情 不是,例如mfence。)

实际上(因为 CPU 实际上不会重命名特权级别),当中断处理程序运行时,乱序后端中不会有任何旧的用户空间指令/微指令仍在运行.

异步(外部)中断也可能耗尽存储缓冲区,这取决于我们如何解释Intel's SDM vol.3 11.10 的措辞:*the 在以下情况下,存储缓冲区的内容总是被排空到内存中:“...”当异常或中断产生”。显然,这适用于异常(CPU 内核本身产生中断) ),也可能意味着在服务中断之前。

(来自 retired 存储指令的存储数据不是推测性的;它肯定会发生,并且 CPU 已经放弃了它需要能够回滚到该存储指令之前的状态。所以一个充满分散缓存未命中存储的大型存储缓冲区可能会损害中断延迟。 在任何中断处理程序指令完全可以运行之前等待它耗尽,或者至少在 ISR 中的任何 in/outlocked 指令可能发生之前,如果事实证明存储缓冲区是'没有耗尽。)

相关:Sandpile (https://www.sandpile.org/x86/coherent.htm) 有一个正在序列化的事物表。中断和异常不是。但同样,这并不意味着它们不会耗尽存储缓冲区。这可以通过实验进行测试:在用户空间中的存储和 ISR 中的加载(不同共享变量的)之间寻找 StoreLoad 重新排序,正如另一个内核所观察到的那样。

这部分的一部分并不真正属于这个答案,应该移到其他地方。它在这里是因为在 What happens to expected memory semantics (such as read after write) when a thread is scheduled on a different CPU core? 上的 cmets 中的讨论将此作为可能错误声称的来源中断不会耗尽存储缓冲区,这是我在误解“不序列化”后写的。

【讨论】:

  • Sandpile 没有将硬件中断列为序列化,可能是因为它们不是指令。我认为该列表是序列化指令的列表,而不是序列化事件。但是“医生?”字段对中断和异常说“不”,我不确定这意味着什么。
  • Intel手册V2提到INT指令基本上与LFENCE具有相同的序列化属性。 AMD 手册并没有说明这一点(AFAICT)。此外,英特尔和 AMD 手册都提到“异常和中断”会耗尽存储缓冲区和 WC 缓冲区。这表明本文中的术语“中断”是指硬件中断,而术语“异常”是指程序错误异常和机器检查异常(参见第 3 卷第 6.4 节)。在我看来,“异常和中断”正在完全序列化。
  • 我现在不想阅读整个 2008 年的论文,你能指出它到底在哪里说 x86 上的中断正在序列化吗?希望论文中对术语“中断”和“序列化”进行了明确定义,因此我们不必猜测。也希望他们提供英特尔参考(作者不是来自英特尔)。他们使用了 Simics 模拟器,这是一个学术模拟器,这意味着他们的结果不一定显示真正的处理器是如何工作的。
  • @HadiBrais:那篇论文是红鲱鱼;他们只是在谈论序列化 OoO exec,而不是内存。我正在查看第 3.2 节,他们在其中谈论 CPU 不重命名 CS,因此 syscall 正在序列化。并且暗示,中断也是如此(至少在从用户空间获取时),尽管他们甚至没有提到这一点。我将从这个答案中删除该部分;再看一遍后,它的关系太遥远了。 (顺便说一句,我将链接更新为格式更好的版本。ftp.cs.wisc.edu/sohi/papers/2008/hpca2008-serial.pdf.
【解决方案2】:

我不确定 8080 是否设计用于具有共享 RAM 的多 CPU 系统,但这并不一定意味着此类系统不可能或不存在。 8086 锁定前缀用于此类系统,以确保在执行一系列内存读取、值修改、内存写入 (RMW) 时,只有一个 CPU 可以独占访问内存。锁定前缀不是用来保护一条或几条指令不被中断处理程序抢占的。

您可以确保个别指令不会在飞行途中被打断。要么让它们运行直到完成,要么恢复它们的任何副作用并在稍后重新启动它们。这是大多数 CPU 上的常见实现。没有它,就很难在存在中断的情况下编写行为良好的代码。

确实,您无法使用单个 8080 指令执行 64 位加法,因此该操作可以被 ISR 抢占。

如果您根本不想要这种抢占,您可以使用中断禁用和启用指令(DI 和 EI)保护您的 64 位添加。

如果您想让 ISR 抢占 64 位但不干扰 64 位添加使用的寄存器,则 ISR 必须保存和恢复这些寄存器,例如使用 PUSH 和 POP 指令。

查找 8080 手册了解中断处理的详细说明(例如 here)。

【讨论】:

  • 在 8086 上,lock(和带有内存的 xchg)存在于系统中其他非 CPU 设备的原子性,例如DMA 读取。我认为,对于内存映射 I/O 的使用,CPU 在读取 + 写入时保持#LOCK 信号断言可能很重要。我认为最早的 SMP x86 系统是 386。 (而早期的类似现代内存模型的东西是 486;我想我读过 386 并没有一些当前的保证。)
  • @PeterCordes 你可能是对的 w.r.t.其他内存访问设备。我只关注 CPU。
  • 嗯,这就是它在现代 x86 上的主要用途,但您实际上说的是“8086 锁定前缀”,而不是“x86 锁定前缀”。该用例在 8086 中不存在。(有趣的是,它在 SMP 系统之前就存在。)
  • @PeterCordes 啊,是的,x86 比 8086 更适合。
猜你喜欢
  • 1970-01-01
  • 2023-03-20
  • 1970-01-01
  • 1970-01-01
  • 2016-01-23
  • 1970-01-01
  • 2012-02-27
  • 2011-11-22
  • 2016-04-21
相关资源
最近更新 更多