4.5.6 异常处理

4.5.6 异常处理

正如第 8 章中将讨论的,处理器中很多事情都会导致异常控制流,此时,程序执行的正常流程被破坏掉。异常可以由程序执行从内部产生,也可以由某个外部信号从外部产生。我们的指令集体系结构包括三种不同的内部产生的异常:1)halt 指令,2)有非法指令和功能码组合的指令,3)取指或数据读写试图访问一个非法地址。一个更完整的处理器设计应该也能处理外部异常,例如当处理器收到一个网络接口收到新包的信号,或是一个用户点击鼠标按钮的信号。正确处理异常是任何微处理器设计中很有挑战性的一方面。异常可能出现在不可预测的时间,需要明确地中断通过处理器流水线的指令流。我们对这三种内部异常的处理只是让你对正确发现和处理异常的真实复杂性略有了解。

我们把导致异常的指令称为异常指令(excepting instruction)。在使用非法指令地址的情况中,没有实际的异常指令,但是想象在非法地址处有一种“虚拟指令”会有所帮助。在简化的 ISA 模型中,我们希望当处理器遇到异常时,会停止,设置适当的状态码,如图 4-5 所示。看上去应该是到异常指令之前的所有指令都已经完成,而其后的指令都不应该对程序员可见的状态产生任何影响。在一个更完整的设计中,处理器会继续调用异常处理程序(exception handler),这是操作系统的一部分,但是实现异常处理的这部分超出了本书讲述的范围。

在一个流水线化的系统中,异常处理包括一些细节问题。首先,可能同时有多条指令会引起异常。例如,在一个流水线操作的周期内,取指阶段中有 halt 指令,而数据内存会报告访存阶段中的指令数据地址越界。我们必须确定处理器应该向操作系统报告哪个异常。基本原则是:由流水线中最深的指令引起的异常,优先级最高。在上面那个例子中,应该报告访存阶段中指令的地址越界。就机器语言程序来说,访存阶段中的指令本来应该在取指阶段中的指令开始之前就结束的,所以,只应该向操作系统报告这个异常。

第二个细节问题是,当首先取出一条指令,开始执行时,导致了一个异常,而后来由于分支预测错误,取消了该指令。下面就是一个程序示例的目标代码:

0x000: 6300                  |    xorq %rax,%rax
0x002: 741600000000000000    |    jne target        # Not taken
0x00b: 30f00100000000000000  |    irmovq $1,%rax    # Fall through
0x015: 00                    |    halt
0x016:                       | target:
0x016: ff                    |    .byte 0xFF        # Invalid instruction code

在这个程序中,流水线会预测选择分支,因此它会取出并以一个值为 0xFF 的字节作为指令(由汇编代码中 .byte 伪指令产生的)。译码阶段会因此发现一个非法指令异常。稍后,流水线会发现不应该选择分支,因此根本就不应该取出位于地址 0x016 的指令。流水线控制逻辑会取消该指令,但是我们想要避免出现异常。

第三个细节问题的产生是因为流水线化的处理器会在不同的阶段更新系统状态的不同部分。有可能会出现这样的情况,一条指令导致了一个异常,它后面的指令在异常指令完成之前改变了部分状态。比如说,考虑下面的代码序列,其中假设不允许用户程序访问 64 位范围的高端地址:

1  irmovq $1,%rax
2  xorq %rsp,%rsp     # Set stack pointer to 0 and CC to 100
3  pushq %rax         # Attempt to write to 0xfffffffffffffff8
4  addq %rax,%rax     # (Should not be executed) Would set CC to 000

pushq 指令导致一个地址异常,因为减小栈指针会导致它绕回到 0xfffffffffffffff8。访存阶段中会发现这个异常。在同一周期中,addq 指令处于执行阶段,而它会将条件码设置成新的值。这就会违反异常指令之后的所有指令都不能影响系统状态的要求。

一般地,通过在流水线结构中加入异常处理逻辑,我们既能够从各个异常中做出正确的选择,也能够避免出现由于分支预测错误取出的指令造成的异常。这就是为什么我们会在每个流水线寄存器中包括一个状态码 stat(图 4-41 和图 4-52)。如果一条指令在其处理中于某个阶段产生了一个异常,这个状态字段就被设置成指示异常的种类。异常状态和该指令的其他信息一起沿着流水线传播,直到它到达写回阶段。在此,流水线控制逻辑发现出现了异常,并停止执行。

为了避免异常指令之后的指令更新任何程序员可见的状态,当处于访存或写回阶段中的指令导致异常时,流水线控制逻辑必须禁止更新条件码寄存器或是数据内存。在上面的示例程序中,控制逻辑会发现访存阶段中的 pushq 导致了异常,因此应该禁止 addq 指令更新条件码寄存器。

让我们来看看这种处理异常的方法是怎样解决刚才提到的那些细节问题的。当流水线中有一个或多个阶段出现异常时,信息只是简单地存放在流水线寄存器的状态字段中。异常事件不会对流水线中的指令流有任何影响,除了会禁止流水线中后面的指令更新程序员可见的状态(条件码寄存器和内存),直到异常指令到达最后的流水线阶段。因为指令到达写回阶段的顺序与它们在非流水线化的处理器中执行的顺序相同,所以我们可以保证第一条遇到异常的指令会第一个到达写回阶段,此时程序执行会停止,流水线寄存器 W 中的状态码会被记录为程序状态。如果取出了某条指令,过后又取消了,那么所有关于这条指令的异常状态信息也都会被取消。所有导致异常的指令后面的指令都不能改变程序员可见的状态。携带指令的异常状态以及所有其他信息通过流水线的简单原则是处理异常的简单而可靠的机制。