在汇编指令执行期间中断该指令

6

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

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

例如,

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

这一行指令能否被中断?如果不能,那么它就是原子的吗?

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

如果没有“锁定”关键字,例如在8080汇编中,如何提供原子性?

例如,如果想要操作64位和,但没有办法使用“一行指令”,并且在对和进行操作时发生了中断。如何在汇编级别上防止这种情况发生?

对于我来说,这个概念正在逐渐变得清晰。


3
芯片设计师确保它是原子性的,这是必须的。中断处理程序绝不能破坏处理器状态,以至于多指令操作出现问题。在8080上做起来并不难,只需保存和恢复寄存器即可。中断逻辑本身已经保留了IP寄存器,RET将其恢复。几乎每个中断处理程序都以PUSH PSW开始,以保留标志和累加器寄存器。 - Hans Passant
1
我怀疑这不是针对8080完成的。然而,理论上讲,已经运行的指令可能会被中断。我一直在为不同的FPGA RISC处理器工作。在其中一个设计中,指令甚至可以以一种导致写入寄存器具有不一致值的方式被中断。在该设计中,返回地址将是被中断的指令的地址,因此在这种情况下将重复完整的指令。因此,至少存在允许中断指令的设计。 - Martin Rosenau
2个回答

6
所有“正常”的ISA,包括8080和x86,在同一核心上保证指令在中断方面是原子性的。要么指令完全执行并且它的所有结构效果都可见(在中断处理程序中),要么没有任何效果。任何与此规则不符的情况通常会被仔细记录。
例如,Intel的x86手册第3卷(大约1000页PDF) 特别指出以下内容:
“6.6 程序或任务重启 为了允许在处理异常或中断后重新启动程序或任务,在指令边界上保证所有异常(中止除外)都会报告异常。 所有中断都保证在指令边界上被执行。Intel的第1卷手册 中的一段旧文字谈到单核系统使用cmpxchg而不加lock前缀以原子方式读取修改写入(与其他软件而非硬件DMA访问相关)。
CMPXCHG指令通常用于测试和修改信号量。它会检查信号量是否空闲。如果信号量是空闲的,则标记为已分配;否则,获取当前所有者的ID。这一切都是在一个不可中断的操作中完成的[因为它是单个指令]。在单处理器系统中,CMPXCHG指令消除了在执行多个指令以测试和修改信号量之前切换到保护级别0(禁用中断)的需要。
对于多处理器系统,可以将CMPXCHG与LOCK前缀组合使用以原子方式执行比较和交换操作。(有关原子操作的更多信息,请参见Intel® 64和IA-32体系结构软件开发人员手册第3A卷第8章“多处理器管理”中的“锁定原子操作”部分。)
(有关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设置为“起始值-已复制字节数”,如果此时RCX非零,则会将RIP指向该指令,因此在中断后恢复时,rep movsb将再次运行并完成其余的复制。
其他的x86示例包括SIMD gather loads (AVX2/AVX512)和scatter stores (AVX512)。例如,vpgatherdd ymm0,[rdi + ymm1 * 4],ymm2可以根据设置的ymm2元素进行多达8个32位加载,并将结果合并到ymm0中。
在正常情况下(没有中断、页面故障或其他同步异常发生),您将在目标寄存器中获取数据,掩码寄存器最终为零。因此,掩码寄存器为CPU提供了一个存储进度的位置。
Gather和scatter都很慢,可能需要触发多个页面故障,因此对于同步异常,即使在处理页面故障取消所有其他页面的情况下,也可以保证前进。但更相关的是,这意味着避免重新执行TLB misses如果一个中间元素页面故障,并且不丢弃异步中断到达的工作。
一些长期运行的指令(如刷新所有核心上的所有数据缓存的wbinvd)在架构上不可中断,甚至架构上也无法中止(以丢弃部分工作并处理中断)。它是特权级别的,因此用户空间无法执行它作为拒绝服务攻击来导致高中断延迟。
记录有趣行为的相关示例是当x86的popad从堆栈顶部(段限制)弹出。这是一个异常(不是外部中断),在vol.3手册的6.5节“异常分类”中有记录,即故障/陷阱/中止(有关详细信息,请参见PDF文件)。
注意: 通常报告为故障的一种异常子集是不可重新启动的。这些异常导致一些处理器状态的丢失。例如,在执行一个跨越堆栈段末端的POPAD指令时会报告故障。在这种情况下,异常处理程序看到指令指针(CS:EIP)已恢复,就好像未执行POPAD指令一样。然而,内部处理器状态(通用寄存器)将被修改。这类情况被认为是编程错误。引发此类异常的应用程序应该由操作系统终止。
请注意,这仅在popad本身引起异常的情况下才会发生,而不是其他任何原因。外部中断无法像对rep movsbvpgatherdd那样分割popad
(我猜为了popad故障的目的,它实际上是迭代工作的,每次弹出1个寄存器并逻辑地修改RSP / ESP / SP以及目标寄存器。而不是在开始之前检查要加载的整个区域的段限制,因为那将需要额外的加法。)

乱序执行的CPU在中断时会回滚到退役状态。

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

请参见当中断发生时,流水线中的指令会发生什么?

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

有趣的事实是:x86中断并不完全序列化,至少在文件上没有保证。(在x86术语中,像cpuid和iret这样的指令被定义为序列化;排空乱序后端和存储缓冲区以及任何可能重要的内容。这是一个非常强大的屏障,许多其他东西则不是,例如mfence。)
在实践中(因为CPU在实践中不会重命名特权级),当中断处理程序运行时,乱序后端中就不会有任何旧的用户空间指令/微操作仍在飞行。
异步(外部)中断也可能会清空存储缓冲区,这取决于我们如何解释Intel's SDM vol.3 11.10中的措辞:“...在以下情况下,存储缓冲区的内容总是被清空到内存中:”...“当异常或中断被生成”。显然,这适用于异常(其中CPU核心本身生成中断),并且在服务中断之前也可能意味着清空存储缓冲区。
(来自已退役存储指令的存储数据不是推测性的;它肯定会发生,并且CPU已经放弃了需要能够回滚到该存储指令之前的状态。因此,一个充满散乱的缓存未命中存储的大型存储缓冲区可能会影响中断延迟。要么是等待它在任何中断处理程序指令可以运行之前完全清空,要么至少在ISR中的任何in/outlocked指令之前,如果存储缓冲区没有被清空的话,这些指令就无法执行。)
相关:Sandpile(https://www.sandpile.org/x86/coherent.htm)有一个表格列出了正在串行化的事物。中断和异常不在其中。但是,这并不意味着它们不会排空存储缓冲区。可以通过实验来测试这一点:在用户空间中进行存储并在ISR中加载(另一个共享变量),观察另一个核心是否发现存储-加载重排序。
这一部分内容实际上不属于这个答案,应该移动到其他地方。这里是因为在当线程在不同的CPU核心上调度时,预期的内存语义(如写入后读取)会发生什么?的评论讨论中,被引用作为错误的断言来源,即中断不会排空存储缓冲区,我在误解“不串行化”之后写下了这个结论。

1
Sandpile没有将硬件中断列为串行化,可能是因为它们不是指令。我认为该列表是串行化指令的列表,而不是串行化事件。但是,“doc?”字段对于中断和异常说“no”,我不确定这是什么意思。 - Hadi Brais
2
英特尔手册V2提到,“INT”指令基本上具有与“LFENCE”相同的序列化属性。但是,AMD手册没有这样说(据我所知)。此外,英特尔和AMD手册都提到,“异常和中断”会排空存储缓冲区和WC缓冲区。这表明,在此上下文中,“中断”一词指的是硬件中断,“异常”一词指的是程序错误异常和机器检查异常(请参见第3卷第6.4节)。在我看来,“异常和中断”是完全序列化的。 - Hadi Brais
我现在不想读整篇2008年的论文,你能指出它确切地在哪里说x86上的中断是串行化的吗?希望“中断”和“串行化”这些术语在论文中有明确定义,这样我们就不必猜测了。而且希望他们提供英特尔参考(作者不来自英特尔)。他们使用的是Simics模拟器,这是一种学术模拟器,这意味着他们的结果不一定反映实际处理器的工作方式。 - Hadi Brais
2
@HadiBrais:那篇论文是一个误导,他们只谈到序列化OoO执行,而不是内存。我看了3.2节,他们谈到CPU不重命名CS,因此syscall是序列化的。间接地,中断也是(至少在用户空间中),尽管他们甚至没有提到。我将从这个答案中删除该部分;第二次查看后,它与主题关系太远。(顺便说一下,我更新了链接,提供了更好格式的版本。ftp://ftp.cs.wisc.edu/sohi/papers/2008/hpca2008-serial.pdf。) - Peter Cordes

4
我不确定8080处理器被设计用于多CPU系统中的共享RAM,但这并不一定意味着这样的系统不存在或不可能存在。8086的锁定前缀用于这样的系统,以确保在执行内存读取、值修改和内存写入(RMW)序列时,只有一个CPU可以独占访问内存。锁定前缀不是为了防止指令被中断处理程序抢占。
你可以肯定的是,单个指令不会在执行过程中被打断。它们要么运行到完成,要么它们的副作用被恢复,并在以后重新启动。这是大多数CPU常见的实现方式。如果没有它,存在中断时编写良好的代码会很困难。
事实上,你不能使用单个8080指令执行64位加法,因此,该操作可以被ISR抢占。
如果你根本不想被抢占,则可以使用中断禁用和使能指令(DI和EI)来保护你的64位加法。
如果你想让ISR抢占64位加法,但又不想打扰64位加法所使用的寄存器,那么ISR必须通过例如PUSH和POP指令来保存和恢复这些寄存器。
查找8080手册以获取详细的中断处理描述(例如这里)。

1
在8086上,lock (还有和内存的xchg) 存在是为了保证与系统中其他非CPU设备(如 DMA 读取)的原子性。我认为这也适用于内存映射的I/O,在此情况下,CPU在进行读写操作时保持 #LOCK 信号被激活可能是很重要的。最早的SMP x86系统应该是386。(而类似于现代内存模型的更早系统是486;我想我曾经读过386不具备一些当前的保证。) - Peter Cordes
1
@PeterCordes 对于其他访问内存的设备,你可能是正确的。我只关注于CPU。 - Alexey Frunze
1
这是现代x86大多数情况下使用的方式,但你字面上说的是“8086锁定前缀”,而不是“x86锁定前缀”。这种用例在8086中不存在。(而且有趣的是,在SMP系统出现之前它就存在了。) - Peter Cordes
1
@PeterCordes 噢,是的,x86比8086更合适。 - Alexey Frunze

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接