P7 中最重要的概念就是 异常,本文的全部内容都将围绕 异常 一词进行叙述
而系统桥等内容在本文中不会提及,请结合教程自行学习
什么是异常
在 MIPS 体系结构中,中断、自陷、系统调用以及其他打断程序正常执行流的事件统称为异常
See MIPS Run Linux
本文中的 异常 与 See MIPS Run Linux 中的定义一致。
需要注意到异常是一个中性词
为何要有异常
异常的存在,能让程序专注当前的逻辑,而将所有的异常情况交给异常处理程序处理。
这样,某些游离于正常逻辑之外的情况比如键盘触发,访存错误等就不需要程序使用大量的周期去检测,
而是统一包装成异常等待确认和处理。
什么时候发生一个异常
本次课设中给出了 CPU 需要捕捉的异常以及对应的情况,
主要分为两种:内部指令异常和外部中断异常,详情见下:
异常与中断码 | 助记符与名称 | 指令与指令类型 | 描述 |
---|---|---|---|
0 | Int (外部中断) |
所有指令 | 中断请求,来源于计时器与外部中断。 |
4 | AdEL (取指异常) |
所有指令 | PC 地址未字对齐。 |
PC 地址超过 0x3000 ~ 0x6ffc 。 |
|||
AdEL (取数异常) |
lw |
取数地址未与 4 字节对齐。 | |
lh |
取数地址未与 2 字节对齐。 | ||
lh , lb |
取 Timer 寄存器的值。 | ||
load 型指令 | 计算地址时加法溢出。 | ||
load 型指令 | 取数地址超出 DM、Timer0、Timer1、中断发生器的范围。 | ||
5 | AdES (存数异常) |
sw |
存数地址未 4 字节对齐。 |
sh |
存数地址未 2 字节对齐。 | ||
sh , sb |
存 Timer 寄存器的值。 | ||
store 型指令 | 计算地址加法溢出。 | ||
store 型指令 | 向计时器的 Count 寄存器存值。 | ||
store 型指令 | 存数地址超出 DM、Timer0、Timer1、中断发生器的范围。 | ||
8 | Syscall (系统调用) |
syscall |
系统调用。 |
10 | RI (未知指令) |
- | 未知的指令码。 |
12 | Ov (溢出异常) |
add , addi , sub |
算术溢出。 |
如何捕捉这些异常
对于内部指令异常,通常会有一个发生的关键时间点。
比方说 load
指令的运算溢出可在 E
级 ALU
处完成判断
那么,假设有多条指令,在不同的流水级发生异常该如何处理呢?
显然,由于指令之间的依赖关系和因果逻辑,我们应该优先响应异常指令中时间上最靠前的指令(而不是最先出现异常的指令)。
为了解决这个问题,可以令异常也一起流水,到某个特定的流水级进行捕捉,以此保证异常之间的时序关系
显然,捕捉异常的元件必须放置在可能出现异常的流水级之后。
经计算该元件只能放在 E
,M
或者 W
级。
我们称这个元件为 CP0
,后文中所有异常相关信息的储存都将放置在这个模块中
对于外部指令,产生异常时由 CP0
直接捕捉
如何打断当前工作流
一般来说,我们将 CP0
捕捉到异常的对应指令称之为 受害指令,对应的 PC
称之为 VPC
假设我们想要能够从异常返回并且不受破坏接着执行被打断的执行流,流水线中的每条指令要么执行完毕,要么就像我们根本没见过一样
See MIPS Run Linux
自然的,令受害指令之前的指令为需要执行完毕的指令,而受害指令及其之后的指令为应该还未执行的指令,就如同在受害指令之前断开了一般。
满足如上要求的 CPU 称之为支持 精确异常
在单周期 CPU 中,这样的结构很容易实现。
但是在多周期流水线 CPU 中,多条指令处在不同的阶段并行运行。
当指令触发异常时,受害指令本身和后面的指令可能已经结束了某些阶段的任务。
如何解决这一问题?
注意到指令流经流水线过时,可能留下影响的操作只有修改 PC
,写入流水线寄存器,写入乘除模块,写入 DM
和写入 GRF
。
由此方案如下:
- 由于后续需要跳转到异常处理程序,
PC
无需处理 - 流水线寄存器储存的是指令中间信息,直接清空
- 在我们的课设中,不考虑写入乘除模块的影响
- 只需要还原
GRF
和DM
的写入,分别在M
和W
级- 一个简单的方法是直接将
CP0
放在M
级及之前 - 当异常发生时,受害指令和之后的指令都未写入
DM
,GRF
,也就不必完成复杂的回退操作
- 一个简单的方法是直接将
而对于受害指令之前的指令,继续流水即可完成完成。
如何进入异常处理程序?
打断当前工作流后,直接让修改 PC 跳转到异常处理程序入口,课设要求为 0x4180
如何进行异常处理?
见 See MIPS Run Linux 对应章节
如何从异常处理程序返回?
按照定义,自异常处理程序返回后,应从受害指令开始重新执行,以接驳正常工作流。
那么在异常处理之前,CP0
需要保存受害指令的 PC
即 VPC
,从而保证程序的正确返回。
我们将储存程序返回位置的寄存器称之为 EPC
,其中 EPC
= VPC
若受害指令是一条延迟槽指令,仅仅重新执行延迟槽指令会导致对应跳转指令失效。
在该情况下,需要将对应跳转指令一并重新执行,也就是说 EPC
中保存的值应该为跳转指令的 PC
也就是 VPC
- 4
当然,异常处理程序也可能修改 EPC
的值以返回不同的位置,此处不表
最后异常处理程序将会使用命令 eret
跳转到 EPC
对应地址,即从异常处理程序返回
细节
由于阻塞的存在,受害指令可能有两种情况:
- 一条平平无奇的普通指令
- 由于阻塞产生的空泡指令
注意到在情况 2 真正的受害指令应该是产生空泡的对应指令,
所以在产生空泡时需要继承原指令的 PC
和延迟槽标记
注意异常发生时若 CP0
不接受则正常运行
注意 AdEL
和 RI
出现时指令视作 nop
(无乘除指令则不需要考虑)
注意 eret
和 mtc0
的冲突(ERET
和 CP0
同级则不需要考虑)
注意 Execode
中断和其他异常的优先级
总结
流程
- 产生内部异常
- 产生外部异常或内部异常流水到
CP0
处CP0
捕捉异常,给出全局异常信号CP0
保存异常相关信息
- 清空受害指令及之后的全部指令信息
- 跳转到异常处理程序入口
- 完成异常处理程序
eret
指令结束异常处理程序,跳转回到正常工作流
为了支持异常,我们需要依次完成如下操作:
- 让流水线内部能检测内/外部异常并进行异常流水,延迟槽标记流水
- 设计
CP0
模块CP0
模块应放置在E
级及之后,M
级及之前:E
级或M
级CP0
模块应保存异常处理的相关信息:异常类型ExcCode
,返回位置EPC
,延迟槽标记BD
等
- 完成
mfc0
,mtc0
指令支持 - 完成
systemcall
,eret
指令支持
一些想说的话
P7 整体而言包含的内容较多,需要援引学习的资料更是多上加多。
为了每个部分都有所提及,又要控制整体篇幅,很容易导致教程整体结构较散。
关于各类实现的细节比如说 CP0
相应外部中断异常的条件,CP0
中究竟需要保存什么信息等需要大家自行阅读 See MIPS Run Linux 等资料
愿大家武运昌隆,顺利过关