引言
- 本章节我们将会通过 2 个实验来深入理解保护模式下的中断
实验一
- 实验目标:自定义一个保护模式下的软中断(int 0x80),使得在使用 int 0x80 中断后在屏幕上打印字符串 “int 0x80”
- 实现思路
- 直接开始吧,让我们先找个以前实现过的基础代码:loader.asm
- 回顾代码的框架
boot.asm 跳转到 loader.asm,并打印 “Welcome to KOS.” | CODE16_START ; 实模式,打印 “Loader...” | CODE32_START ; 进入保护模式,打印 “Enter protection”
- 老规矩,先把最终实现代码给你们,再来一步一步讲解过程.最终代码:loader.asm
- 先准备好 2 个中断服务程序,注意,中断服务程序必须用 iret 返回
- DefaultHandler 函数用于默认中断处理函数,仅提供函数地址作用,可以不用实现函数内部功能;Int0x80Handler 函数就是我们本次实验的目标函数,内部实现打印 “int 0x80” 字符串
DefaultHandler: iret DefaultHandler_Offset equ DefaultHandler-CODE32_START Int0x80Handler: mov ebp, msg_int0x80_offset mov bl, 0x0F ; 打印属性,黑底白字 ; 坐标 (0, 3) mov dl, 0x00 mov dh, 0x03 call print_str_32 iret Int0x80Handler_Offset equ Int0x80Handler-CODE32_START
- 接下来构建一个 IDT 中断描述符表,可以借用 【调用门】代码中门描述符的定义。共 256 个中断描述符
; 中断描述符表定义 ; 选择子, 偏移地址, 参数个数,属性 IDT_BASE : Gate CODE32_SELECTOR, DefaultHandler_Offset, 0, DA_INTR_GATE Gate CODE32_SELECTOR, DefaultHandler_Offset, 0, DA_INTR_GATE Gate CODE32_SELECTOR, DefaultHandler_Offset, 0, DA_INTR_GATE ...(重复 256 次) IDT_LEN equ $ - IDT_BASE
- 我们看到中断描述符表中有大量重复的代码,我们可以使用 rep 指令处理重复操作,rep 使用格式如下:
; 重复 n 次 xxx %rep n xxx %endrep
- 改动一下
IDT_BASE: %rep 256 Gate CODE32_SELECTOR, DefaultHandler_Offset, 0, DA_INTR_GATE %endrep IDT_LEN equ $ - IDT_BASE
- int 0x80 中断在整个中断描述符表中排第 129 位,于是再次改动一下:
; IDT 中断描述符表定义 ; 选择子, 偏移地址, 参数个数, 属性 IDT_BASE: %rep 128 Gate CODE32_SELECTOR, DefaultHandler_Offset, 0, DA_INTR_GATE %endrep Gate CODE32_SELECTOR, Int0x80Handler_Offset, 0, DA_INTR_GATE %rep 127 Gate CODE32_SELECTOR, DefaultHandler_Offset, 0, DA_INTR_GATE %endrep IDT_LEN equ $ - IDT_BASE
- 中段描述符表 IDT 我们已经构建好了,参考全局描述符表加载(lgdt),我们需要告诉 CPU 哪片内存是 IDT,即 lidt 指令,其格式是 lidt [6 个字节的内存数据首地址]
- 我们只需要先定义这 6 个字节的数据
IDT_PTR : dw IDT_LEN - 1 dd IDT_BASE
- 然后再使用 lidt 就可以了,注意:要放在实模式下调用
lidt [IDT_PTR]
- 准备工作全部完成啦,下面就是在 32 位保护模式下调用 int 0x80 触发中断
- 运行一下,看看最终效果
实验二
- 实验目标:处理外部时钟中断(接主 8259A IRQ0 引脚),接收到时钟中断后,在屏幕上循环打印 '0'-'9'
- 先提供实验完整代码:loader.asm
- 实现思路跟实验一一样,都是套路
- 由于本实验依赖 8259A,所以先把 8259A驱动编写 中实现的驱动代码复制过来并初始化 8259A
call pic_init
- 准备中断服务函数,注意程序最后需要调用 write_m_EOI 手动结束中断,如果不手动结束中断,那么该中断只会触发一次
TimerHandler: cmp al, '9' ; 判断 al 是否为 '9' je .to_0 ; 如果 al=9,则跳转到 .to_0 处执行 inc al jmp .display ; 跳转到 .display 处执行 .to_0: mov al, '0' ; al = '0' .display: ; 使用现存方式打印,al 为要打印的字符,ah 为打印属性 mov ah, 0x0F ; 打印属性,黑底白字 mov [gs:(80*4+0)*2], ax ; (80 * 4 + 0)*2 ; 坐标 (0, 4) call write_m_EOI ; 手动结束中断 iret TimerHandler_Offset equ TimerHandler-CODE32_START
- 回想一下8259A驱动编写 中,pic_init 将主 8259A IRQ0 引脚(外部时钟)的中断向量号设置为了
• 0x20,于是我们在中段描述符表 IDT 中添加对应的中断描述符(第 33 个) ... Gate CODE32_SELECTOR, TimerHandler_Offset, 0, DA_INTR_GATE ...
- 编译运行一下,理论是应看到实验现象,但是并没有
- 思考一下,8259A 的 IMR 寄存器起到放行作用,是不是 IMR 寄存器默认不放行呢?
- 于是,我们把 IMR 寄存器的 bit0 (对应IRQ0,即外部时钟引脚)清零
EnableTimer: push ax call read_m_IMR and al, 0xFE call write_m_IMR pop ax ret
- 再次编译运行一下,发现还是不行,又是什么原因导致没有出现我们想要的现象呢?
- 我们又想到, 在讲 8259A 的时候我们就说过,不过 8259A 能屏蔽中断,CPU 也可以屏蔽中断,是不是 CPU 把外部中断屏蔽了呢?
- 实验一下不就知道了吗,sti 指令就可以开启 CPU 中断(cli:关中断)
- 果然,加上 sti 指令后,屏幕上果然循环打印出 '0'-'9'。这说明 CPU 默认是屏蔽外部中断的
- 再改动一下,不执行 EnableTimer 函数,即不手动放行 8259A IRQ 中断
; call EnableTimer
sti
- 发现依然能循环打印出 '0'-'9',这说明 8259A 的 IMR 寄存器是默认放行的
- 最后贴上实验截图,虽然无法看到 '0'-'9' 跳动过程,但至少能证明我们的代码是实实在在运行成功的
中断嵌套
- 提到中断,那么我们就得思考一个问题,那就是中断嵌套。什么是中断嵌套呢?
- 比如我们上面实验二所做的外部时钟中断,假设时钟中断引脚 IRQ0 每 5ms 触发一次,而对应的中断服务程序执行实时间是 20ms。这时候就会遇到一个问题,在第一次中断服务程序正在执行的过程中,又触发了一次中断,这种情况我们就称之为中断嵌套
- 从 pic_init 初始化主 8259A 函数中 ICW4 寄存器的设置中,我们知道,主 8259A 被设置成了特殊全嵌套模式,即中断服务程序执行过程中,仍然响应同级中断
- 对于外部中断,不光 8259A 的 IMR 寄存器提供了中断屏蔽机制, CPU 也有个总开关(eflags 的 IF 位),也可以屏蔽中断
- 就拿实验二的代码,让我们反汇编后断点调试,看一看中断执行前后 IF 位的情况吧。IF=1:可以响应外部中断;IF=0:屏蔽外部中断
- make 之后反汇编一下
ndisasm -o 0x900 loader.bin > loader.txt
- 查看 loader.txt 文件,找到开启中断 sti 指令地址:0x1214,TimerHandler 函数入口处(cmp al, '9')地址:0x125B,TimerHandler 函数最后返回指令 iret 地址:0x1273,使用 reg 指令,看一下各状态下 eflags 中 IF 位的变化(大写表示值为 1, 小写表示值为 0)
<bochs:1> b 0x1214 <bochs:2> b 0x125B <bochs:3> b 0x1273 <bochs:4> c ... (0) Breakpoint 1, 0x00001214 in ?? () Next at t=16768062 (0) [0x00001214] 0008:00000032 (unk. ctxt): sti ; fb <bochs:5> reg eax: 0x00000030 48 ecx: 0x00000009 9 edx: 0x00000300 768 ebx: 0x0000000f 15 esp: 0x00000fff 4095 ebp: 0x00000011 17 esi: 0x0000130f 4879 edi: 0x00000923 2339 eip: 0x00000032 eflags 0x00000046: id vip vif ac vm rf nt IOPL=0 of df if tf sf ZF af PF cf <bochs:6> s Next at t=16768063 (0) [0x00001215] 0008:00000033 (unk. ctxt): jmp .-2 (0x00001215) ; ebfe <bochs:7> reg eax: 0x00000030 48 ecx: 0x00000009 9 edx: 0x00000300 768 ebx: 0x0000000f 15 esp: 0x00000fff 4095 ebp: 0x00000011 17 esi: 0x0000130f 4879 edi: 0x00000923 2339 eip: 0x00000033 eflags 0x00000246: id vip vif ac vm rf nt IOPL=0 of df IF tf sf ZF af PF cf <bochs:8> c (0) Breakpoint 3, 0x00001273 in ?? () Next at t=16921763 (0) [0x00001273] 0008:00000091 (unk. ctxt): iretd ; cf <bochs:9> reg eax: 0x00000f31 3889 ecx: 0x00000009 9 edx: 0x00000300 768 ebx: 0x0000000f 15 esp: 0x00000ff3 4083 ebp: 0x00000011 17 esi: 0x0000130f 4879 edi: 0x00000923 2339 eip: 0x00000091 eflags 0x00000003: id vip vif ac vm rf nt IOPL=0 of df if tf sf zf af pf CF <bochs:10> s Next at t=16921764 (0) [0x00001215] 0008:00000033 (unk. ctxt): jmp .-2 (0x00001215) ; ebfe <bochs:11> reg eax: 0x00000f31 3889 ecx: 0x00000009 9 edx: 0x00000300 768 ebx: 0x0000000f 15 esp: 0x00000fff 4095 ebp: 0x00000011 17 esi: 0x0000130f 4879 edi: 0x00000923 2339 eip: 0x00000033 eflags 0x00000246: id vip vif ac vm rf nt IOPL=0 of df IF tf sf ZF af PF cf <bochs:12>
- 从调试信息上看
- sti 指令的本质就是将 eflags IF 位置 1
- 在中断服务程序 TimerHandler 执行期间, IF 位为 0,说明 CPU 在中断服务程序执行过程中本身是不响应其它中断的
- 如果你想实现在中断服务程序执行过程中依旧能响应其它中断,可以在中断服务程序的开始处手动加上 sti 指令