在我们这个系列的第一篇文章中,我们就已经使用过了 BIOS 10H 中断,用来在屏幕上打印一行字符: 计算机是如何启动的?如何制作自己的操作系统
除了 10H 中断,我们还使用过 BIOS 21H 中断,用来让实地址模式的程序退出,以及使用 15H 中断获取物理内存的信息: 保护模式进阶 — 再回实模式 实战分页机制实现 — 通过实际内存大小动态调整页表个数
在计算机体系中,硬件中断是触发 CPU 与其他硬件设备进行通信的重要方式。 但你有没有发现,这些中断都是在实地址模式下使用的,一旦进入保护模式,我们就再没有使用过中断功能,那么,在保护模式中,我们是否还可以像实地址模式中一样,通过 int 指令触发 BIOS 中断呢?答案是不可以,因为我们在进入保护模式前,通过 cli 指令关闭了硬件中断。 但是,在系统运行中,常常会出现需要暂停当前流程,响应突发事件的中断场景,那么,有什么办法让我们能够在软件的层面实现硬件中断的类似效果呢?答案当然是可以的,本文就来详细介绍。
有时,我们运行了错误的指令,或指令执行时发生了错误,例如去计算除 0 的情况,或者前面提到的程序调用过程中错误特权级的切换等,这类异常情况就是系统中的“异常”。
处理器预设了一系列异常,他们分为三类:
向量号 | 助记符 | 类型 | 描述 | 来源 |
---|---|---|---|---|
#DE | 错误 | 除零错误 | DVI和IDIV指令 | |
1 | #DB | 错误/陷阱 | 调试异常,用于软件调试 | 任何代码或数据引用 |
2 | | 中断 | NMI中断 | 不可屏蔽的外部中断 |
3 | #BP | 陷阱 | 断点 | INT 3指令 |
4 | #OF | 陷阱 | 溢出 | INTO指令 |
5 | #BR | 错误 | 数组越界 | BOUND指令 |
6 | #UD | 错误 | 无效指令(没有定义的指令) | UD2指令(奔腾Pro CPU引入此指令)或任何保留的指令 |
7 | #NM | 错误 | 数学协处理器不存在或不可用 | 浮点或WAIT/FWAIT指令 |
8 | #DF | 终止 | 双重错误(Double Fault) | 任何可能产生异常的指令、不可屏蔽中断或可屏蔽中断 |
9 | #MF | 错误 | 向协处理器传送操作数时检测到页错误(Page Fault)或段不存在,486及以后集成了协处理器,本错误就保留不用了 | 浮点指令 |
10 | #TS | 错误 | 无效TSS | 任务切换或访问TSS |
11 | #NP | 错误 | 段不存在 | 加载段寄存器或访问系统段 |
12 | #SS | 错误 | 栈段错误 | 栈操作或加载SS寄存器 |
13 | #GP | 错误 | 通用/一般保护异常,如果一个操作违反了保护模式下的规定,而且该情况不属于其他异常,CPU就是认为是该异常 | 任何内存引用或保护性检查 |
14 | #PF | 错误 | 页错误 | 任何内存引用 |
15 | | 保留 | | |
16 | #MF | 错误 | 浮点错误 | 浮点或WAIT/FWAIT指令 |
17 | #AC | 错误 | 对齐检查 | 对内存中数据的引用(486CPU引入) |
18 | #MC | 终止 | 机器检查(Machine Check) | 错误代码和来源与型号有关(奔腾CPU引入) |
19 | #XF | 错误 | SIMD浮点异常 | SIMD浮点指令(奔腾III CPU引入) |
20~31 | 保留 | | ||
32~255 | 用户自定义中断 | 中断 | 可屏蔽中断 | 来自INTR的外部中断或INT n指令 |
正常的程序运行,除了发生异常外,即便是发生跳转,也都是程序主动的行为,但有时,处理器外部的硬件事件,比如外围设备的请求突然到来等都是随机发生的,我们可以预先设定事件发生时执行的程序,但不能预知事件何时到来,这样的场景就是“中断”,也就是上表中标记为“Interrupt”的类型,另一个触发中断的方式是通过 int 指令手动触发,这就是中断产生的两大原因:
不可屏蔽中断和可屏蔽中断分别是通过 CPU 的 NMI 引脚和 INTR 引脚触发的,顾名思义,可屏蔽中断对硬件中断实现了是否屏蔽的标识,这意味着更加灵活的中断控制,因此也是所有中断最为常用的类型。 为了控制中断的屏蔽,以及在众多中断中控制中断触发的优先级等功能,CPU 在 INTR 引脚上级联了两个 8259A 芯片,8259A 芯片就是“可编程中断控制器”。 如下图所示,这两个级联的 8259A 芯片,每一个都有 8 根中断信号线,从而可以挂接 15 个不同的外部设备,在实地址模式下,IRQ0 ~ IRQ7 被设置为了中断向量号 08h ~ 0Fh 的中断。
8259A 芯片有两种工作状态:
加电之初,8259A 处于编程状态,此时 CPU 可以通过 out 指令,向分别挂载在 20h、21h 端口和 A0h、A1h 端口的主 8259A 芯片和从 8259A 芯片写入特定的初始化指令来实现芯片的设置,这个特定的初始化指令被称为 ICW(Initialization Command Word),ICW 命令共有 4 个,分别是 ICW1、ICW2、ICW3、ICW4,他们必须从 1 到 4 依次进行初始化。 ICW 具体取值如下:
可以看到,由于 80x86 体系约定使用主片的 IR2 引脚级联从片,我们就可以确定全部的 ICW 字段取值了,其中最为重要的是 ICW2 的中断向量号标识,他表示 IQR0 对应的中断向量号,此后,IQR1 ~ IQR7 会分别对应 IQR0 的中断向量号 + 1 ~ IQR0 的向量中断号 + 7。
下面的代码展示了如何初始化 8259A,在实地址模式或是保护模式下执行都可以,但只能执行一次,且必须按照顺序执行:
完成了上述初始化操作,8259A 就从编程状态进入了操作状态,此时我们可以通过操作控制字 OCW(Operation Control Word)来实现操作控制,虽然和 ICW 一样,OCW 也不只有一个,而是有 OCW1、OCW2、OCW3 三个,但实际上我们只需要使用 OCW1 和 OCW2。 他们分别具有下面两个功能:
当我们需要屏蔽或打开外部中断时,只需要设置好 OCW1,然后通过 out 021h, OCW1
或者 out 0A1h, OCW1
就可以实现主 8259A 或是从 8259A 某个或某几个中断的屏蔽或打开。
这个操作也同样被 8259A 芯片认为是一种中断,而此前我们通过 ICW4 设置了非自动 EOI 模式,所以需要在上述操作完成后通过 OCW2 发送 EOI 信号,OCW2 的 EOI 信号可以选择通过 020h 端口或 0A0h 端口发送给主或从 8259A 中的一个。
下图展示了 OCW1 和 OCW2 的字段含义:
mov al, 0FEh ; 主 OCW1,仅开启定时器中断
out 021h, al ; 主 8259A,OCW1
mov al, 0FFh ; 从 OCW1,关闭所有中断
out 0A1h, al ; 从 8259A,OCW1
mov al, 20h ; OCW2,发送 EOI 信号
out 020h, al ; 主 8259A,OCW2
本文我们详细介绍了保护模式下的中断和异常与实地址模式下的不同之处,以及如何通过程序操作硬件 — 可编程中断控制器初始化、屏蔽或打开中断的响应,这些是理解硬件系统、操作系统的基础知识,也是硬件的部分。 本文涉及的实际开发内容比较少,你是否已经迫不及待的想要立即尝试一下如何通过程序让我们能够在保护模式下触发和响应中断呢?敬请期待下一篇文章中的实战吧。
《Orange’s 一个操作系统的实现》。 《linux 内核完全注释》。