学过8086/8088汇编的人肯定对于中断这个概念都不陌生。在80386中,这个概念在一定程度上发生了变化,并引入了“异常”这个新概念。本篇文章就是围绕在操作系统开发中涉及到中断和异常的讨论。
中断在系统中是由外部事件所引起的,如:一次I/O操作的结束。其产生与CPU当前所执行的指令没有关系。从是否能够被屏蔽来划分,可将其分为两类,即可屏蔽中断与不可屏蔽中断,其中前者由CPU的INTR引脚接收信号,后者由NMI引脚接收信号。
由于产生中断的中断源并不单一,因此在INTR接收中断的时候,同样还要接收一个8位的中断向量号,以判断是谁发出的中断请求。CPU对于某个中断向量号是由谁发出原则上并没有规定。但在实际系统中,为了避免产生冲突,部分中断向量号都有自己固定的中断源,这一工作是由可编程中断控制器(PIC)完成的。在 80386系统上,该PIC是8259A。该芯片功能十分强大,不仅能向CPU提供中断向量号,还可以自主处理中断请求的优先级。每个8259A芯片可以支持8个中断请求信号,并可进行级连。对于8259A的介绍,我们将在文章的后半部分进行。
对于是否屏蔽可屏蔽中断可以通过8259A有选择地控制,也可以通过CPU的CLI和STI指令实现。CLI和STI指令可以设置EFLAGS寄存器的IF位,如果该位被清除,则CPU会禁止外部中断传递信号给INTR引脚。但对于CPU内部异常和NMI该位不起作用。在执行这两条指令时,必须要保证当前CPL小于等于IOPL,否则会引起通用保护故障。当然虽然NMI是不可屏蔽中断,但通过将CMOS端口(0x70)中第7位置1这种手段也可以将NMI也屏蔽掉的,当然需要打开该中断将该位清零就可以了。
实现代码如下:
#define PORT_CMOS 0x70 void disable_NMI() { byte val; val = inportb(PORT_CMOS); outportb(PORT_CMOS, val | 0x80); } void enable_NMI() { byte val; val = inportb(PORT_CMOS); outportb(PORT_CMOS, val & 0x7F); } |
异常是在CPU执行指令期间遇到非法指令所产生的。因此异常与当前指令存在着关系,例如:除零,特权级不正确等等,都会触发异常。80386可以识别多种不同的异常,并以不同的中断向量号来标示它们。在异常发生时,CPU就根据原先设定好的中断向量号转到不同的中断处理程序(ISR)执行。
根据是由是否可恢复和恢复点位置不同将异常划分为三种。它们是故障(Fault),陷阱(Trap)和中止(Abort)。
80386 认为故障是可以排除的,因此在CPU遇到引起故障的指令的时候,会保存当前的CS和EIP值,并转去执行故障处理程序。在故障排除后,执行IRET指令回到刚在引发故障的位置,重新执行刚才触发故障的指令。例如,当程序企图装入一个不存在的段时将会引发一个故障,这时操作系统会将该段装入,并重新进行刚才的操作。
陷阱与故障的区别主要在于,在执行陷阱处理程序之前,系统会保存CS和EIP的值为引起陷阱的下一条要执行指令所在的位置。例如,软中断就是典型的陷阱。
中止是在系统发生严重错误时产生的。在引起中止后,当前执行的程序不能被恢复执行。并且系统在接受到中止后,中止处理程序要重新建立各种系统表。引起这类错误的主要原因是系统表的数据不一致或者非法等。
下表列出了80386在保护模式下的中断和异常
向量号 | 名称 | 异常类型 | 出错代码 | 相关指令 |
0 | 除法错 | 故障 | 无 | DIV,IDIV |
1 | 调试 | 故障/陷阱 | 无 | 调试状态下 |
3 | 断点 | 陷阱 | 无 | INT 3 |
4 | 溢出 | 陷阱 | 无 | INTO |
5 | 界限检查 | 故障 | 无 | BOUND |
6 | 无效操作码 | 故障 | 无 | 非法指令 |
7 | 无80X87 | 故障 | 无 | ESC, WAIT再无协处理器情况下 |
8 | 双重故障 | 中止 | 有 | 任何指令 |
9 | NPX | 中止 | 无 | ESC的操作数超过段尾 |
0AH | 无效TSS | 故障 | 有 | JMP、CALL、IRET或中断 |
0BH | 段不存在 | 故障 | 有 | 装载段寄存器的指令 |
0CH | 堆栈段异常 | 故障 | 有 | 任何使用SS寄存器的访问 |
0DH | 通用保护 | 故障 | 有 | 任何内存访问 |
0EH | 页异常 | 故障 | 有 | 任何内存访问 |
10H | 协处理器出错 | 故障 | 无 | ESC, WAIT |
11H—0FFH | 软中断 | 陷阱 | 无 | INT n |
这里一些异常在发生时会将错误码压入堆栈,这些错误码都是导致错误的选择子。对于错误代码为0的异常,其除了表示空选择子导致的错误外,当CPU不能确定时也会返回该数值。
这里需要指出的是,对于异常0DH,它的产生没有一个准确的原因,但通常来讲是基于以下几个原因:
门描述符
在系统中除了存储段描述符和系统段描述符外,还有一类门描述符。这种描述符是用来描述控制转移的入口点。任务内特权级的改变和任务间且换都是通过这种描述符实现的。其中,门描述符共分为4种分别是,调用门(CallGates),陷阱门(Trap Gates),中断门(Interrupt Gates)和任务门(TaskGates)。由于本章内容主要是涉及中断和异常,因此,在这里我们将对陷阱门和中断门进行详细介绍,对于另两种门会简要的做一介绍。
陷阱门和中断门
这两种门是用来描述中断和异常的入口的。其只能出现在IDT中(对于IDT后面将有详细描述),不能出现在GDT和LDT中。
这两种门的格式如下表:
中断门描述符 | ||||||||||||||||||||||||||||||||
偏移量 | 3 1 |
3 0 |
2 9 |
2 8 |
2 7 |
2 6 |
2 5 |
2 4 |
2 3 |
2 2 |
2 1 |
2 0 |
1 9 |
1 8 |
1 7 |
1 6 |
1 5 |
1 4 |
1 3 |
1 2 |
1 1 |
1 0 |
9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+4 | 偏移量(位31..16) |
P | DPL | 0 | D | 1 | 10 | 000 | 保留 | |||||||||||||||||||||||
+0 | CS寄存器选择子 |
偏移量(位15..0) |
陷阱门描述符 | ||||||||||||||||||||||||||||||||
偏移量 | 3 1 |
3 0 |
2 9 |
2 8 |
2 7 |
2 6 |
2 5 |
2 4 |
2 3 |
2 2 |
2 1 |
2 0 |
1 9 |
1 8 |
1 7 |
1 6 |
1 5 |
1 4 |
1 3 |
1 2 |
1 1 |
1 0 |
9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+4 | 偏移量(位31..16) |
P | DPL | 0 | D | 1 | 11 | 000 | 保留 | |||||||||||||||||||||||
+0 | CS寄存器选择子 |
偏移量(位15..0) |
注:DPL - 描述符特权级
P - 门有效标志
D - 门规模(1 = 32位; 0 - 16位)
这里的段选择子用来查找GDT和IDT,得到一个代码段描述符,并最终得到代码段的基地址,再加上图中的偏移量就能够得到中断处理程序的入口了。由于中断处理程序是在当前任务的上下文中运行的,因此可能会出现中断处理程序与被中断程序特权级不一致的问题,这时就会发生堆栈切换。对于由软中断所产生的中断和异常CPU要求,CPL必须小于等于门的DPL。
在整个中断处理程序中,CPU会将TF置成0,以禁止中断处理程序单步执行,并将NT置成 0,以在使用IRET指令返回时是回到同一个任务。对于中断门和陷阱门,其就在于对EFLAGS寄存器中IF标志的处理方法不同,当调用中断门时,IF被清除。而调用陷阱门时则不对IF进行处理。
在从中断处理程序返回的过程中,如果当初是通过陷阱门或中断门进入的,则从堆栈顶弹出EIP和 CS,以及EFLAGS。然后根据CS寄存器选择子的RPL字段确定返回后的特权级。值得注意的是,如果RPL为一个内层特权级,则将会产生通用保护故障。对于需要提供出错误码的中断处理程序,则必须先人为地从堆栈中弹出出错误码,在执行IRET指令返回。
进入中断和异常还可以通过任务门,即将中断处理程序作为一个任务进行处理,使用该方法即将中断处理程序当成一个任务来看待,对于这种方式的具体操作在以后的文章中会有讨论。而对于调用门由于其只能出现在GDT和LDT中,因此与我们这里讨论的中断和异常无关。
中断描述表(IDT)
前面我们提到了一个叫做IDT的表,这个表的作用实际上与在实模式下的IVT(中断向量表)相同。不过在具体内容上IDT要比IVT丰富的多,在IDT中装载的是我们前面介绍过的门描述符,而不仅仅向IVT那样仅包含一个中断处理程序的地址。
IDT 是由门描述符组成的一个数组,每个门描述符对应一个中断/异常向量。像全局描述符(GDT)一样,在系统中IDT也仅存在一个。其可以保存在内存中的任何位置,CPU通过访问IDTR寄存器获取IDT的位置。IDTR的长度为48位,其中包括保存IDT的32位线性地址和16位的大小。对于IDTR寄存器的操作包含两个指令,一个是LIDT,另一个SIDT。LIDT用来将指定的IDT所在线性地址和其长度装入IDTR寄存器。而SIDT则是将IDTR寄存器的内容读出。值得注意的是,LIDT仅能在CPL为0时执行,而SIDT则不受此限制,可以运行在任何特权级下。
当系统发生中断或异常时,CPU会以所产生的中断向量号为索引去查找IDT,通过找到的门描述符,转到中断处理程序处执行。
下面是设置中断描述表的代码
%define PARAM_1 ebp+8+4*0 _LIDT: push ebp mov ebp, esp mov eax, [PARAM_1] lidt [eax] pop ebp ret _SIDT: |
#define ACS_PRESENT 0x80 #define ACS_INT 0x0E #define ACS_INT_GATE (ACS_INT | ACS_PRESENT) #define ACS_DPL_0 0x00 #define ACS_DPL_1 0x20 #define ACS_DPL_2 0x40 #define ACS_DPL_3 0x60 //IDTR结构 //中断描述符 //设置中断描述表 //清空IDT // Int 0Dh - 通用保护故障 // Int 0Eh - 页面错误 // IRQ0...0Fh 设置0x20为起始中断向量号,从0x20到0x2F的初始化 // SYS_INT 操作系统中断调用 |
在系统发生中断或异常时,为了能够尽快处理紧急或重要的事务,系统将它们按类型赋予了不同的优先级。CPU在处理时总是优先处理优先级最高的中断或异常,而对于同一级别的中断或异常,则按照先进先出(FIFO)的原则处理。
当CPU 在处理中断或异常时,如果又产生了其他的中断或异常,这时CPU会检查产生的中断或异常的优先级是否比当前处理的要高,如果是的话,则CPU会保存当前中断处理的上下文,然后转去处理优先级最高的那个中断或者异常。对于那些未接收处理的异常,系统则将它们扔掉。而未接受处理的中断则将保持悬挂状态。系统之所以这样做,主要是基于以下原因,即硬件的异常永远为最高优先级,所以其永远不会被丢弃。因此丢弃的异常都是由软件所引起,而这些异常由于未被解决,所以在系统处理完当前中断或异常后,会重新执行这些引起异常的点,再次触发异常,并等待解决。
下表按照优先级由高到低的顺序列出了中断和异常类型:
80386响应 中断/异常 的优先级 |
中断/异常类型 |
调试故障 | |
其它故障 | |
陷阱指令INT n和INTO | |
调试陷阱 | |
NMI中断 | |
INTR中断 |
在前面我们已经看到,在CPU处理的各种中断中,有很大一部分是来自外部硬件设备的中断,这些中断通过可编程控制器(PIC)控制。在IBMPC兼容机上该控制器为Intel 8259A芯片。
单个8259A芯片最多可以连接8个中断源,但由于可以最多将9个该芯片级连,因此,其最多可以接受64个中断源。在IBMPC机上采用2个8259A芯片级连,最多支持15个中断源。这两个芯片一个叫做Master,另一个叫做Slave。之所以这么称呼是因为,由于CPU只具有INTR这一个中断线,所以Slave必须连级到Master上,占用MasterPIC的IRQ2,将IRQ9重定向到IRQ2上。
8259A芯片处理中断的过程,主要是通过芯片内3个内部寄存器进行的。这三个寄存器分别为IMR,IRR和ISR其中,IMR用作过滤被屏蔽的中断,IRR用来存放被悬挂的中断并等待进一步处理,ISR用来保存CPU正在处理的中断。
另外8259A芯片还有一个叫做优先级仲裁的单元。该单元的作用是在8259A同时接受到多个中断时,根据各个中断的优先级,挑选具有最高优先级的中断传递给CPU处理。
在大致介绍这几个单元后,下面我们来看一些8259A在处理中断时的具体过程。
首先,外部中断请求(IR0到IR7)传输到IMR,IMR根据此中断请求是否被屏蔽,以决定是将其丢弃,还是放入IRR中等待进一步处理。当8259A等待到一个中断时机时,优先级仲裁单元会从所有放入IRR中的中断请求中挑出一个优先级最高的中断,传递给CPU处理。值得注意的是中断优先级是随着中断请求号降低而提高的。在CPU的INTR引脚接收到8259A发送过来的信号后,CPU会暂停执行下一条指令,并向8259A发送一个INTA信号。在 8259A接收到该信号后,就会将ISR中代表该中断的位置1,并将IRR中相应的位清零。以表示该中断正在被CPU处理。接着CPU会向8259A再发送一个INTA信号,向其请求中断向量号。这时,8259A会根据先前设置好的起始向量号再加上中断请求号计算出中断向量号,并将其放入数据总线中。这时候,如果8259A的EOI通知被设定为自动模式,那么8259A就会自动将ISR中刚才置1的位清零。在CPU获得该中断向量号后,就会转去调用该中断服务程序。在处理完该中断后如果8259A的EOI通知被设定为人工模式,则还要向8259A发送一个EOI。通常来讲,这一工作往往是在中断服务程序中完成。在8259A接收到该EOI通知后,就会将ISR中刚才置1的位清零。
以上就是8259A处理一个中断的整个过程的简述。由于中断请求存在着优先级,因此,如果在一个中断处理期间,8259A又收到了新的中断请求,则首先跟当前处理的优先级进行比较,如果新到的中断请求的优先级高于当前处理的中断请求,则马上处理新到的中断请求,否则则将新到的中断请求放入IRR。
对于8259A的操作,是通过端口进行的。其中, Master的端口地址为0x20, 0x21, Slave的端口地址位0xA0,0xA1。8259A具有两种命令,一种是ICW,其作用是用来初始化8259A芯片。另一个是OCW,其作用是用来向 8259A发送命令。虽然在系统启动后BIOS会自动初始化8259A,但这并不是我们所需要的。因为在进入保护模式后,我们要设置IDT,因此我们必须根据所设置的IDT去初始化8259A.
对8259A的操作有两类命令,其中一类是ICW,另一类是OCW。ICW用来对8259A进行初始化,而OCW则用来在初始化后对8259A发布命令。有意思的是,8259A的两个端口对于这两类命令的发布是有固定安排的。对于0x20和0xA0端口,你可以向它们写入ICW1,OCW2,OCW3,读取IRR和ISR。对于0x21和0xA1端口,你可以向它们写入ICW2,ICW3,ICW4, 并能够读写IMR寄存器。
下面我们分别来讨论这几个命令
ICW1:该命令作为初始化序列的第一条命令,一旦向端口送入该命令,8259A就认为初始化序列开始。
位 | 功能 |
7:5 | MCS-80/85模式下的中断向量地址 |
4 | 必须设置为1 |
3 | 0:Edge Triggered Interrupts 1:Level Triggered Interrupts |
2 | 0:Call Address Interval of 8 1:Call Address Interval of 4 |
1 | 0:Cascaded PICs 1:Single PIC |
0 | 0:Don't need ICW4 1:Will be Sending ICW4 |
在设置时,对于80x86的CPU,其应设置为(00010001),也就是0x11。
ICW2:该命令用来指定所初始化的8259A中断请求的起始向量。其中ICW2的低3位必须为0,其这么做的原因在于当该8259A接收到一个中断请求时,低3位会自动填充为所接受到的向量号。因此这也就决定了我们设置的起始中断向量,必须为8的倍数。
ICW3:Master PIC和Slave PIC对于ICW3命令具有不同的格式
对于Master PIC,Slave PIC被接到了Master PIC的哪个IRQ上,则ICW3中相应的位就置1。在8259A中,由于SlavePIC是级连在Master PIC的IRQ2上的,因此ICW3的值应该为(00000100),也就是0x04。而对于SlavePIC其高5位必须设置为零,低3位为该PIC被级连到哪个Master PIC的IRQ号,在8259A中,其SlavePIC的值为(00000010),即0x02。
ICW4:
位 | 功能 |
7:5 | 保留,设置为0 |
4 | 0:Not Special Fully Nested Mode 1:Special Fully Nested Mode |
3:2 | 0x:Non-Buffered Mode 10:Buffered Mode - Slave 11:Buffered Mode - Master |
1 | 0:Normal EOI 1:Auto EOI |
0 | 0:MCS-80/85 1:8086/8088 Mode |
在80x86模式下,我们采用默认的Full Nested Mode,将ICW4设置为(000000001),即0x01。
而我们之所以我们要采用NormalEOI,其原因在于我们要允许中断请求的按优先级抢占。如果我们将EOI通知设定为自动模式,那么在CPU发出第二个 INTA信号后,8259A中相应的ISR就会自动清零,而此时该中断服务程序还没有被调用。如果在该中断服务程序被调用的过程中,8259A收到了优先级比当前正在处理的中断优先级低的中断请求,由于正在处理的中断在ISR中相应的位已经清零,因此这个新的中断请求就完全可以抢占正在处理的优先级比它高的中断服务程序。
下面是初始化8259A的代码:
void init_8259A(byte master_vector,byte slave_vector) { outportb (PORT_8259A_M, 0x11); /* 开始对8259A进行初始化*/ outportb (PORT_8259A_S, 0x11); outportb (PORT_8259A_M+1, master_vector); /* 起始中断向量号*/ outportb (PORT_8259A_S+1, slave_vector); outportb (PORT_8259A_M+1, 1<<2); /*设置对IRQ2的掩码 */ outportb (PORT_8259A_S+1, 2); /* 设置对IRQ2的级连*/ outportb (PORT_8259A_M+1, 1); /* 完成对8259A的初始化*/ outportb (PORT_8259A_S+1, 1); } |
在介绍完初始化这几个命令后,我们开始介绍如何通过OCW对8259A进行操作。
OCW1:该命令用来屏蔽所设定的中断请求。其操作方式是,向你要屏蔽的中断请求所在的8259A发送一个操作控制字。需要屏蔽哪个中断请求就将该字上相应的位置1即可。
实例代码如下:
#define PORT_INT_MASK_M 0x21 void mask_IRQ(byte IRQ) if(IRQ > 15) if(IRQ < 8) void unmask_IRQ(byte IRQ) if(IRQ > 15) if(IRQ < 8) |
OCW2:
位 | 功能 |
7:5 | |
4:3 | Must be set to 0 |
2:0 |
如果OCW2中的bit6被设置为0,那么该命令将对整个8259A有效。否则,将针对bit2:0这3位所代表的IRQ进行操作。由于我们前面已经将8259A设置为手动EOI模式,所以在这里我们要将bit7:5设置为(001)
OCW3:
位 | 功能 |
7 | Must be set to 0 |
6:5 | 10:Reset Special Mask 11:Set Special Mask |
4 | Must be set to 0 |
3 | Must be set to 1 |
2 | 0:No Poll Command 1:Poll Command |
1:0 | 10:Next Read Returns IMR 11:Next Read Returns ISR |