深入浅出MIPS 四 MIPS的异常与中断

MIPS的异常和中断,同其他体系结构,例如Intel的IA32架构下的中断/调用门/陷阱机制类似,其目的主要有三:
一,提供一个合法地从用户态到内核态的切换通道,使得程序能够访问如CP0、KSeg等平时被保护的资源;
二,处理一些非法的操作,如TLB Miss/Address Error等;
三,处理外部和内部的中断。与IA32架构区别的是,所有的中断均来自0号Exception。

《See MIPS Run》中,将MIPS的异常机制称为“精确异常”。用通俗的语言解释之,由于异常是执行指令时同步发生,因此,在造成异常的指令之前执行的指令,无疑均是有效的。然而,由于MIPS的高度流水体系结构,在引发异常的指令执行时,后面一条指令已经完成了读取和译码的预备工作,万事俱备,只待ALU部件空闲即执行之。当异常产生时,这些预备工作便被废弃。CPU从异常中返回时,再重新做读取和译码的工作。
因此,我们就可以保证,在异常发生时,异常指令之后所有的指令均不会被执行。这样,就不需要在MIPS的异常处理例程(Exception Handler)中为延迟槽(Delay Slot)指令而烦恼了。

MIPS的异常类型,在前文中已经有所提及。为了方便读者阅读,先再次给出一个概览,然后再详解一些常见的异常。
MIPS异常类型总共有:
0: Interrupt,中断;
1: TLB Modified,试图修改TLB中映射为只读的内存地址;
2: TLB Miss Load,试图读取一个没有在TLB中映射到物理地址的虚拟地址;
3: TLB Miss Store,试图向一个没有在TLB中映射到物理地址的虚拟地址存入数据;
4: Address Error Load,试图从一个非对齐的地址读取信息;
5: Address Error Store,试图向一个非对齐的地址写入信息;
6: Instruction Bus Error,一般是指令Cache出错;
7: Data Bus Error,一般是数据Cache出错;
8: Syscall,由syscall指令产生。操作系统下,通用的由用户态进入内核态的方法。可以类比IA32的“调用门”理解;
9: Break Point,由break指令产生。最常见的bp指令,是由编译器产生的,在除法运算时插入一个break point指令,以达到在除0时抛出错误信息的目的。因此,如果在定位问题时发现了一个Break Point异常,且它的异常分代码为07,应当考虑是出现了除0的情形;
10: RI,保留指令。在CPU执行到一条没有定义的指令时,进入此异常;
11: Co-processor Unavilible,协处理器不可用。这个异常是由于试图对不存在的协处理器进行操作引起的。特别的,在没有浮点协处理器的处理器上执行这条命令,会导致这个异常。随之,操作系统会调用模拟浮点的lib库,来实现软件的浮点运算;
12: Overflow,算术溢出。只有带符号的运算会引起这个异常;
13: Trap,这个异常来源于trap指令。和syscall指令类似地,trap指令也会引起一个异常,但trap指令可以附带一些条件,这样可以用于调试程序用。
14: VCEI,指令高速缓存中的虚地址一致性错误。(没明白怎么回事,还有待高手补充)
15: Float Point Exception,浮点异常;
16: Co-processor 2 Exception,协处理器2的异常;
17~22,留作将来的扩展;
23: Watch,内存断点异常。当设定了WatchLo/WatchHi两个寄存器时起作用。当load/store的虚拟地址和WatchLo/WatchHi中匹配时,会引发这样一个异常;/* 这个地方经典著作《See MIPS Run》犯了一个错误,将虚拟地址写成了物理地址 */
24~30,留作将来的扩展;

其中,Exception 0-5, 8-11, 13, 23这几个异常类型较为常见。
Exception 0:Interrupt,外部中断。它是唯一一个异步发生的异常。之所以说中断是异步发生的,是因为相对于其他异常来说,从时序上看,中断的发生是不可预料的,无法确定中断的发生是在流水线的哪一个阶段。MIPS的五级流水线设计如下:
IF, RD, ALU, MEM, WB。MIPS处理器的中断控制部分有这样的设计:在中断发生时,如果该指令已经完成了MEM阶段的操作,则保证该指令执行完毕。反之,则丢弃流水线对这条指令的工作。除NMI外,所有的内部或外部硬件中断(Hardware Interrupt)均共用这一个异常向量(Exception Vector)。前面提到的CP0中的Counter/Compare这一对计数寄存器,当Counter计数值和Compare门限值相等时,即触发一个硬件中断。

Exception 1:TLB Modified,内存修改异常。如果一块内存在TLB映射时,其属性设定为Read Only,那么,在试图修改这块内存内容时,处理器就会进入这个异常。显然,这个异常是在Memory阶段发生的。但是,按“精确异常”的原则,在异常发生时,ALU阶段的操作均无效,也就是说,向内存地址中的写入操作,实际上是不会被真正执行的。这一判断原则,也适用于后面的内存读写相关的异常,包括TLB Miss/Address Error/Watch等。
Exception 2/3:TLB Miss Load/Write,如果试图访问没有在MMU的TLB中映射的内存地址,会触发这个异常。在支持虚拟内存的操作系统中,这会触发内存的页面倒换,系统的Exception Handler会将所需要的内存页从虚拟内存中调入物理内存,并更新相应的TLB表项。
Exception 4/5:Address Error Load/Write,如果试图访问一个非对齐的地址,例如lw/sw指令的地址非4字节对齐,或lh/sh的地址非2字节对齐,就会触发这个异常。一般地,操作系统在Exception Handler中对这个异常的处理,是分开两次读取/写入这个地址。虽然一般的操作系统内核都处理了这个异常,最后能够完成期待的操作,但是由于会引起用户态到内核态的切换,以及异常的退出,当这样非对齐操作较多时会严重影响程序的运行效率。因此,编译器在定义局部和全局变量时,都会自动考虑到对齐的情况,而程序员在设计数据结构时,则需要对对齐做特别的斟酌。
Exception 6/7:Instruction/Data Bus Error,一般地原因是Cache尚未初始化的时候访问了Cached的内存空间所致。因此,要注意在系统上电后,Cache初始化之前,只访问Uncached的地址空间,也就是0xA0000000-0xBFFFFFFF这一段。默认地,上电初始化的入口点0xBFC00000就位于这一段。(某些MIPS实现中可以通过外部硬线连接修改入口点地址,但为了不引发无法预料的问题,不要将入口点地址修改为Uncached段以外的地址)
Exception 8:Syscall,系统调用的正规入口,也就是在用户模式下进入内核态的正规方式。我们可以类比x86下Linux的系统调用0x80来理解它。它是由一条专用指令syscall触发的。
Exception 9:Break Point,绝对断点指令。和syscall指令类似,它也是由专用指令break触发的。它指示了系统的一些异常情况,编程人员可以在某些不应当出现的异常分支里面加入这个指令,便于及早发现问题和调试。我们可以用高级语言中的assert机制来类比理解它。最常见的Break异常的子类型为0x07,它是编译器在编译除法运算时自动加入的。如果除数为0则执行一条break 0x07指令。这样,当出现被0除的情况时,系统就会抛出一个异常,并执行Coredump,以便于程序员定位除0错误的根因。
Exception 10:RI,执行了没有定义的指令,系统就会发生这个异常。
Exception 11,Co-Processor Unaviliable,试图访问的协处理器不存在。比如,在没有实现CP2的处理器上执行对CP2的操作,就会触发这个异常。
Exception 12,Overflow,算术溢出。会引起这个异常的指令,仅限于加减法中的带符号运算,如add/addi这样的指令。因此,一般地,编译器总是将加减法运算编译为addiu这样的无符号指令。由于MIPS处理异常需要一定的开销,这样可以避免浪费。
Exception 13,Trap,条件断点指令。它由trap系列指令引发。与Break指令不同的是,只有满足断点指令中的条件,才会触发这个异常。我们可以类比x86下的int 3断点异常来理解它。
Exception 14,VCEI,(不明白!谁知道是干嘛使的?)
Exceotion 15,Float Point Exception,浮点协处理器1的异常。它由CP1自行定义,与CP1的具体实现相关。其实就是专门为CP1保留的异常入口。
Exception 16,协处理器2的异常,和前一个异常一样,是和CP2的具体实现相关的。
Exception 23,Watch异常。前面讲到Watch寄存器可以监控一段内存,当访问/修改这段内存时,就会触发这个异常。在异常处理例程中,通过异常栈可以反推出是什么地方对这段内存进行了读/写操作。这个异常是用来定位内存意外写坏问题的一柄利器。

异常发生的时候,系统会完成一个从用户态到内核态的切换。我们前面提到,对系统某些资源的访问(如CP0等协处理器,Kernel Segment内存),是必须在内核态进行的。因此,如果希望使系统从用户态进入内核态,那么,就必须产生一个异常。
MIPS的异常处理,通常来说,和其他体系结构的异常/中断/陷阱处理,没有太多的区别,总的来说分为三段:
1,保存现场寄存器组(Register File)。在堆栈中开辟一段区域,将32个通用寄存器和CP0的相关寄存器,如Status,BadVaddr,Cause等,保存在这段内存中。其中尤为重要的是EPC,EPC指向引发异常时的指令。
在这个步骤中,首先保存的应该是通用寄存器组,随后是epc/cause/status/badvaddr这几个epc0中的寄存器。从cp0到内存的数据传输必须通过通用寄存器。一般地,编程时的约定是使用k0和k1这两个寄存器暂存。如下例:(适用于32位MIPS模式)

sw zero, 0(sp)
sw at, 4(sp)
sw v0, 8(sp)
...
sw ra, 124(sp) /*先保存通用寄存器组*/
mfc k0, epc
nop /* mfc太慢,要在延迟槽中加一个nop */
sw k0, 128(sp)
mfc k0, cause
nop
sw k0, 132(sp)
...

2,异常处理部分。以Address Error异常为例,当异常发生时,根据保存的BadVaddr,调用两次非对齐加载/存储指令,对内存地址进行数据的读写操作。
3,返回。将保存在堆栈中的寄存器组内容恢复。
从异常状态返回的这个动作,是由硬件完成的。它必须同时完成三个操作:
1,将SR寄存器恢复;2,返回到EPC寄存器所指向的地址继续执行;3,恢复到用户态。如《See MIPS Run》提到的,如果这三个过程没有能够“原子地”执行完毕,那么将会导致一个安全漏洞,用户有可能在某种情况下僭越CPU内核态设定的壁垒,从而非法获得管理员权限。
在MIPS I和MIPS II处理器中,使用rfe这条指令,来进行“从异常中恢复”,也就是恢复SR寄存器,并且将系统从内核态恢复到用户态。但这条指令并没有将执行的指令地址返回到异常发生的指令处。这项工作应当由在此之前的一条JR指令来执行。这样,从异常中返回的相关汇编代码应当为:
mfc k0, epc
jr k0
rfe /* 在上一条jr指令的延迟槽中执行,这样可以保证原子性 */

在MIPS III及以后的处理器中,从异常中返回不再需要这样的繁文缛节,只需要一条eret指令便万事俱备了。

你可能感兴趣的:(嵌入式)