每天十五分钟,熟读一个技术点,水滴石穿,一切只为渴望更优秀的你!
————零声学院
大多数读者可能对 16 位实地址模式下的中断机制有所了解,例如中断向量、外部 I/O
中断以及异常,这些内容在 32 位的保护模式下依然有效。两种模式之间最本质的差别就是在
保护模式引入的中断描述符表。
1 中断向量
Intel x86 系列微机共支持 256 种向量中断,为使处理器较容易地识别每种中断源,将
它们从 0~256 编号,即赋予一个中断类型码 n,Intel 把这个 8 位的无符号整数叫做一个向
量,因此,也叫中断向量。所有 256 种中断可分为两大类:异常和中断。异常又分为故障(Fault)
和陷阱(Trap),它们的共同特点是既不使用中断控制器,又不能被屏蔽。中断又分为外部可
屏蔽中断(INTR)和外部非屏蔽中断(NMI),所有 I/O 设备产生的中断请求(IRQ)均引起屏
蔽中断,而紧急的事件(如硬件故障)引起的故障产生非屏蔽中断。
非屏蔽中断的向量和异常的向量是固定的,而屏蔽中断的向量可以通过对中断控制器的
编程来改变。Linux 对 256 个向量的分配如下。
• 从 0~31 的向量对应于异常和非屏蔽中断。
• 从 32~47 的向量(即由 I/O 设备引起的中断)分配给屏蔽中断。
• 剩余的从 48~255 的向量用来标识软中断。Linux 只用了其中的一个(即 128 或 0x80向量)用
来实现系统调用。当用户态下的进程执行一条 int 0x80 汇编指令时,CPU 就切换到
内核态,并开始执行 system_call( )内核函数。
2 外设可屏蔽中断
Intel x86 通过两片中断控制器 8259A 来响应 15 个外中断源,每个 8259A 可管理 8 个中
断源。第 1 级(称主片)的第 2 个中断请求输入端,与第 2 级 8259A(称从片)的中断输出
端 INT 相连,如图 3.1 所示。我们把与中断控制器相连的每条线叫做中断线,要使用中断线,
就得进行中断线的申请,就是 IRQ(Interrupt ReQuirement ),我们也常把申请一条中断线
称为申请一个 IRQ 或者是申请一个中断号。IRQ 线是从 0 开始顺序编号的,因此,第一条 IRQ
线通常表示成 IRQ0。IRQn 的缺省向量是 n+32;如前所述,IRQ 和向量之间的映射可以通过中
断控制器端口来修改。
并不是每个设备都可以向中断线上发中断信号,只有对某一条确定的中断线拥有了控制
权,才可以向这条中断线上发送信号。由于计算机的外部设备越来越多,所以 15 条中断线已
经不够用了。中断线是非常宝贵的资源,只有当设备需要中断的时候才申请占用一个 IRQ,
或者是在申请 IRQ 时采用共享中断的方式,这样可以让更多的设备使用中断。
中断控制器 8259A 执行如下操作。
(1)监视中断线,检查产生的中断请求(IRQ)信号。
(2)如果在中断线上产生了一个中断请求信号。
a. 把接受到的 IRQ 信号转换成一个对应的向量。
b. 把这个向量存放在中断控制器的一个 I/O 端口,从而允许 CPU 通过数据总线读此
向量。
c. 把产生的信号发送到 CPU 的 INTR 引脚——即发出一个中断。
d. 等待,直到 CPU 确认这个中断信号,然后把它写进可编程中断控制器(PIC)的
一个 I/O 端口;此时,清 INTR 线。
(3)返回到第一步。
对于外部 I/O 请求的屏蔽可分为两种情况,一种是从 CPU 的角度,也就是清除 eflag 的
中断标志位(IF),当 IF=0 时,禁止任何外部 I/O 的中断请求,即关中断;一种是从中断控
制器的角度,因为中断控制器中有一个 8 位的中断屏蔽寄存器(IMR),每位对应 8259A 中的
一条中断线,如果要禁用某条中断线,则把 IRM 相应的位置 1,要启用,则置 0。
3 异常及非屏蔽中断
异常就是 CPU 内部出现的中断,也就是说,在 CPU 执行特定指令时出现的非法情况。非
屏蔽中断就是计算机内部硬件出错时引起的异常情况。从图 3.1 可以看出,二者与外部 I/O
接口没有任何关系。Intel 把非屏蔽中断作为异常的一种来处理,因此,后面所提到的异常
也包括了非屏蔽中断。在 CPU 执行一个异常处理程序时,就不再为其他异常或可屏蔽中断请
求服务,也就是说,当某个异常被响应后,CPU 清除 eflag 的中 IF 位,禁止任何可屏蔽中断。
但如果又有异常产生,则由 CPU 锁存(CPU 具有缓冲异常的能力),待这个异常处理完后,才
响应被锁存的异常。我们这里讨论的异常中断向量在 0~31 之间,不包括系统调用(中断向
量为 0x80)。
Intel x86 处理器发布了大约 20 种异常(具体数字与处理器模式有关)。Linux 内核必
须为每种异常提供一个专门的异常处理程序。这里特别说明的是,在某些异常处理程序开始
执行之前,CPU 控制单元会产生一个硬件错误码,内核先把这个错误码压入内核栈中。
在表 3.1 中给出了 Pentium 模型中异常的向量、名字、类型及简单描述。更多的信息可
以在 Intel 的技术文挡中找到。
18~31 由 Intel 保留,为将来的扩充用。
另外,如表 3.2 所示,每个异常都由专门的异常处理程序来处理(参见本章后面“异常
处理“部分),它们通常把一个 UNIX 信号发送到引起异常的进程。
4 中断描述符表
在实地址模式中,CPU 把内存中从 0 开始的 1K 字节作为一个中断向量表。表中的每个表
项占 4 个字节,由两个字节的段地址和两个字节的偏移量组成,这样构成的地址便是相应中
断处理程序的入口地址。但是,在实模式下,由 4 字节的表项构成的中断向量表显然满足不
了要求。这是因为,c除了两个字节的段描述符,偏移量必用 4 字节来表示;d要有反映模
式切换的信息。因此,在实模式下,中断向量表中的表项由 8 个字节组成,如图 3.2 所示,
中断向量表也改叫做中断描述符表 IDT(Interrupt Descriptor Table)。其中的每个表项叫
做一个门描述符(Gate Descriptor),“门”的含义是当中断发生时必须先通过这些门,然后
才能进入相应的处理程序。
其中类型占 3 位,表示门描述符的类型,这些描述符如下。
1.任务门(Task gate)
其类型码为 101,门中包含了一个进程的 TSS 段选择符,但偏移量部分没有使用,因为 TSS
本身是作为一个段来对待的,因此,任务门不包含某一个入口函数的地址。TSS 是 Intel 所提供
的任务切换机制,但是 Linux 并没有采用任务门来进行任务切换(参见第五章的任务切换)。
2.中断门(Interrupt gate)
其类型码为 110,中断门包含了一个中断或异常处理程序所在段的选择符和段内偏移量。
当控制权通过中断门进入中断处理程序时,处理器清 IF 标志,即关中断,以避免嵌套中断的
发生。中断门中的 DPL(Descriptor Privilege Level)为 0,因此,用户态的进程不能访问
Intel 的中断门。所有的中断处理程序都由中断门激活,并全部限制在内核态。
3.陷阱门(Trap gate)
其类型码为 111,与中断门类似,其唯一的区别是,控制权通过陷阱门进入处理程序时
维持 IF 标志位不变,也就是说,不关中断。
4.系统门(System gate)
这是 Linux 内核特别设置的,用来让用户态的进程访问 Intel 的陷阱门,因此,门描述
符的 DPL 为 3。通过系统门来激活 4 个 Linux 异常处理程序,它们的向量是 3、4、5 及 128,
也就是说,在用户态下,可以使用 int3、into、bound 及 int0x80 四条汇编指令。
最后,在保护模式下,中断描述符表在内存的位置不再限于从地址 0 开始的地方,而是
可以放在内存的任何地方。为此,CPU 中增设了一个中断描述符表寄存器 IDTR,用来存放中
断描述符表在内存的起始地址。中断描述符表寄存器 IDTR 是一个 48 位的寄存器,其低 16
位保存中断描述符表的大小,高 32 位保存 IDT 的基址,如图 3.3 所示。
5 相关汇编指令
为了有助于读者对中断实现过程的理解,下面介绍几条相关的汇编指令。
1.调用过程指令 CALL
指令格式:CALL 过程名
说明:i386 在取出 CALL 指令之后及执行 CALL 指令之前,使指令指针寄存器 EIP 指向紧
接 CALL 指令的下一条指令。CALL 指令先将 EIP 值压入栈内,再进行控制转移。当遇到 RET
指令时,栈内信息可使控制权直接回到 CALL 的下一条指令
2.调用中断过程指令 INT
指令格式:INT 中断向量
说明:EFLAG、CS 及 EIP 寄存器被压入栈内。控制权被转移到由中断向量指定的中断处
理程序。在中断处理程序结束时,IRET 指令又把控制权送回到刚才执行被中断的地方。
3.调用溢出处理程序的指令 INTO
指令格式:INTO
说明:在溢出标志为 1 时,INTO 调用中断向量为 4 的异常处理程序。EFLAG、CS 及 EIP
寄存器被压入栈内。控制权被转移到由中断向量 4 指定的异常处理程序。在中断处理程序结
束时,IRET 指令又把控制权送回到刚才执行被中断的地方。
4.中断返回指令 IRET
指令格式:IRET
说明:IRET 与中断调用过程相反:它将 EIP、CS 及 EFLAGS 寄存器内容从栈中弹出,并
将控制权返回到发生中断的地方。IRET 用在中断处理程序的结束处。
5.加载中断描述符表的指令 LIDT
格式:LIDT 48 位的伪描述符
说明:LIDT 将指令中给定的 48 位伪描述符装入中断描述符寄存器 IDTR。伪描述符和中
断描述符表寄存器的结构相同,都是由两部分组成:在低字(低 16 位)中装的是界限,在高
双字(高 32 位)中装的是基址。这条指令只能出现在操作系统的代码中。
中断或异常处理程序执行的最后一条指令是返回指令 IRET。这条指令将使 CPU 进行如下
操作后,把控制权转交给被中断的进程。
• 从中断处理程序的内核栈中恢复相应寄存器的值。如果一个硬件错码被压入堆栈,则
先弹出这个值,然后,依次将 EIP、CS 及 EFLSG 从栈中弹出。
• 检查中断或异常处理程序的 CPL 是否等于 CS 中的最低两位,如果是,这就意味着被
中断的进程与中断处理程序都处于内核态,也就是没有更换堆栈,因此,IRET 终止执行,返
回到被中断的进程。否则,转入下一步。
• 从栈中装载 SS 和 ESP 寄存器,返回到用户态堆栈。
• 检查 DS、ES、FS 和 GS 四个段寄存器的内容,看它们包含的选择符是否是一个段选择
符,并且其 DPL 是否小于 CPL。如果是,就清除其内容。这么做的原因是为了禁止用户态的
程序(CPL=3)利用内核曾用过的段寄存器(DPL=0)。如果不这么做,怀有恶意的用户就可能
利用这些寄存器来访问内核的地址空间。