操作系统:保护模式下的中断和异常
作者:互联网
博客的代码均节选自《Orange's一个操作系统的实现》
前置知识:
GDT的结构(最好是自己写过),实模式下的中断相关知识
正篇
在保护模式下,因为种种原因(比如实模式寻址方式的变化之类的),BIOS提供的中断服务是不可用的。但是中断还是非常重要的一个概念,以至于以后的任务切换,外设访问,异常处理都需要依赖于中断。此时变需要用户手动编写一些中断处理程序。
在正式开始编写中断机制前,先要了解一些基础知识。
异常
当我们程序把0当成除数,或者是代码段跳转的时候特权级(CPL,DPL,RPL)发生了问题,CPU便会抛出一个异常(exception)来告诉操作系统。
异常的分类有3种,分别是:
- Fault -> 可更正异常,一旦被更正,程序可以不失连续性地运行下去。当fault发生的时候,CPU会把fault之前的状态保存起来,异常处理程序的返回地址将会是产生fault的那一条指令而不是其之后的指令。
- Trap -> 当程序发生trap异常之后会被立即报告,和fault不同的是,处理程序返回的是发生trap的下一行指令(也就是说会跳过发生trap异常的指令)。
- Abort -> 是一种不总是报告精确位置的异常,发生的时候意味着CPU不允许程序继续执行下去。发生这个异常的时候意味着发生了严重的错误。
异常都是同步事件,并且都是无法屏蔽的,也就是说异常的接受与否和IF位是没有关系的。非常有意思的是,在程序中类似于int 10h的中断指令其实也是被当作异常来处理的。
下面是intel预设的异常列表:
向量号 | 助记符 | 类型 | 描述 | 来源 |
---|---|---|---|---|
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指令 |
中断
中断其实是一种程序本身无法预料的外部设备信号(程序内部发生的异常和int指令在这里就理解成程序本身是可以预测的)。
除了程序内部的中断调用(int),我们最关心的就是外部设备的中断了。外部设备的中断也被分为两大类:
- 可屏蔽中断
- 不可屏蔽中断
不可屏蔽中断时由 #NMI 引脚传输,它的屏蔽与否和IF位的设置没啥关系。所以这里我们主要还是来关注可屏蔽中断
可屏蔽中断
可屏蔽中断通过#INTR 引脚来传输,CPU在 #INTR 上级联了两片8259A芯片,也就是可编程中断控制器8259A。可屏蔽中断和CPU的互通时通过控制8259A来实现的,不深究具体硬件细节的情况下可以把它理解成一种外部中断的统筹,可以通过对其进行设置来控制中断的接受与屏蔽。下面就是8259A的大概样子:
整个8259A芯片一共有15个接口,也就是说一共可以挂在15个外部设备。在BIOS加电的时候芯片的IRQ0 ~ IRQ7被设置成向量08H ~ 0FH。但是在之前的表中我们可以发现,08H ~ 0FH已经被占用了,所以这里我们还需要对主从8259A芯片重新设置。
8259A芯片的初始化
我们通常时通过写4个ICW(Initialization Command Word)来实现初始化的。加点开始的时候,主8256A芯片的端口号是20H和21H,从8259A芯片的端口号是A0H和A1H,我们通过向这几个端口写入ICW1,ICW2,ICW3,ICW4来初始化。特别要注意的是,这几个ICW必须以1 ~ 4的顺序来写入端口 ,不能颠倒顺序。下面是ICW的组成:
下面是用ICW初始化芯片的代码:
Init8259A:
mov al, 011h
out 020h, al ; 主8259, ICW1.
call io_delay
out 0A0h, al ; 从8259, ICW1.
call io_delay
mov al, 020h ; IRQ0 对应中断向量 0x20
out 021h, al ; 主8259, ICW2.
call io_delay
mov al, 028h ; IRQ8 对应中断向量 0x28
out 0A1h, al ; 从8259, ICW2.
call io_delay
mov al, 004h ; IR2 对应从8259
out 021h, al ; 主8259, ICW3.
call io_delay
mov al, 002h ; 对应主8259的 IR2
out 0A1h, al ; 从8259, ICW3.
call io_delay
mov al, 001h
out 021h, al ; 主8259, ICW4.
call io_delay
out 0A1h, al ; 从8259, ICW4.
call io_delay
8259A芯片的设置
初始化完成后,我们就可以对芯片进行设置了。对芯片的设置是通过对端口写入OCW(Operation Command Word)来实现的。
OCW的组成非常简单,一共有8位,每一位代表了相应中断的开关与否(1是关闭,0是打开)。OCW实际上被写入了中断屏蔽寄存器IMR(Interrupt Mask Register)中,当设备发来中断信号的时候IMR会判断是否抛弃这个信号。
下面是一个OCW使用的例子,例子中我们屏蔽了除了时钟中断以外的所有中断。
mov al, 11111110b ; 仅仅开启定时器中断
out 021h, al ; 主8259, OCW1.
call io_delay
mov al, 11111111b ; 屏蔽从8259所有中断
out 0A1h, al ; 从8259, OCW1.
call io_delay
io_delay是一个延时函数,代码在这里:
io_delay:
nop
nop
nop
nop
ret
IDT
一个中断的正常发生包含了两个部分:
- 从设备要能够传递到CPU
- CPU要能找到中断号对应的代码
上面对8259芯片的设置解决了第一个问题,现在我们要来解决第二个问题。
中断描述符表IDT(Interrupt Descriptor Table),是一种用来存储中断向量对应的中断描述符的表。IDT存储的是中断向量和处理程序选择子+偏移的对应,也就是说如果我们在程序内部,从这个角度上来看其实IDT和实模式下的中断向量表是一样的概念。IDT描述符包含以下3种:
- 中断门
- 陷阱门
- 任务门
一个IDT描述符包含了中断处理程序的选择子,偏移,属性等信息。中断号是从0开始连续升序的,所以没必要在描述符中包含中断号。
中断门/陷阱门描述符结构如下图:
其实中断门和陷阱门还是有区别的。通过中断门进行中断调用的时候会对IF进行复位,所以会防止其他中断对当前中断的干扰,但是陷阱门并不会。
IDT的简单定义:
[SECTION .idt]
ALIGN 32
[BITS 32]
LABEL_IDT:
; 门 目标选择子, 偏移, DCount, 属性
%rep 255
Gate SelectorCode32, SpuriousHandler, 0, DA_386IGate
%endrep;通过宏来使得所有IDT描述都都一样,反正也只是测试用
IdtLen equ $ - LABEL_IDT
IdtPtr dw IdtLen - 1 ; 段界限
dd 0 ; 基地址
Gate宏定义(实际上就是让程序长得稍微好看了一点):
; usage: Gate Selector, Offset, DCount, Attr
; Selector: dw
; Offset: dd
; DCount: db
; Attr: db
%macro Gate 4
dw (%2 & 0FFFFh) ; 偏移 1 (2 字节)
dw %1 ; 选择子 (2 字节)
dw (%3 & 1Fh) | ((%4 << 8) & 0FF00h) ; 属性 (2 字节)
dw ((%2 >> 16) & 0FFFFh) ; 偏移 2 (2 字节)
%endmacro ; 共 8 字节
这里我们偷懒把所有中断号对应的中断处理程序都初始化成同一个。
IDT也是描述符表,所以安装过程其实和GDT类似
; 为加载 IDTR 作准备
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_IDT ; eax <- idt 基地址
mov dword [IdtPtr + 2], eax ; [IdtPtr + 2] <- idt 基地址
cli
lidt [IdtPtr]
一定要记得提前关中断
还要记得在进入32位代码段并初始化完成寄存器之后要调用8259A芯片的初始化函数
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_CODE32:
mov ax, SelectorData
mov ds, ax ; 数据段选择子
mov es, ax
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子
mov ax, SelectorStack
mov ss, ax ; 堆栈段选择子
mov esp, TopOfStack
call Init8259A
目前唯一的测试用中断处理程序:
_SpuriousHandler:
SpuriousHandler equ _SpuriousHandler - $$
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov al, '!'
mov [gs:((80 * 0 + 75) * 2)], ax ; 屏幕第 0 行, 第 75 列。
jmp $
iretd
由于初始化IDT 需要的是中断处理程序起始位置的偏移,所以才会有
SpuriousHandler equ _SpuriousHandler - $$
这句话(其实就是相对于32位代码段开头的偏移)
接着就可以随便发生一个中断来测试了。结果应该会在屏幕上显示一个“!”
如果打开了8259A芯片并启用了时钟中断,再在IDT中注册了对应的中断处理程序,我们就可以在32位下使用时钟中断了。
标签:保护模式,操作系统,中断,IDT,8259,al,mov,指令 来源: https://www.cnblogs.com/jrdxy/p/13931073.html