加入中断-代码剖析
概述
本节我们为操作系统加入中断,初始化中断描述符表,并为 0~0x2f 中断添加对应的中断处理程序。当前的思路是,在 interrupt.s
中定义实际中断例程的入口函数( 通过入口函数转移到实际中断例程 ),并利用汇编宏技术得到所有入口函数的地址,形成入口函数的地址数组 interrupt_entry_table
;然后在 idt.c
中引入该数组,进而我们能够很方便地向中断描述符中填写入口函数的地址。现在读者可能不明白这个思路的具体含义,别急,下面做具体阐述。
代码解析
interrupt.s
1 | [bits 32] |
读者可能又会泄气,怎么又用汇编?能用 C 尽量用 C 不行嘛?哈哈,您的心情我表示理解。使用汇编来编写此文件,有以下几点原因:
- 用汇编处理错误码更加方便。
- 汇编能够直接发出 EOI 信号。
- 使用汇编的宏技术,所有宏函数直接展开,非常方便。
当然,你也可以使用 C 语言来书写,笔者认为用 C 语言书写此部分应该可以使内核体积更小,毕竟这里的几十个宏函数未来都会被展开,体积就稍微大些。读者可以写两个 C 函数,分别应对有错误码和无错误码的情况。
接下来剖析代码:
-
第 5 行,
interrupt_handler_table
是位于idt.c
的指针数组,其中的指针指向实际的中断处理函数。 -
第 7 行,
%macro VECTOR 2
,这是汇编宏技术。前面我们使用过equ
宏定义,它只能定义单行宏;对于多行宏,就需要使用%macro
实现,其声明方式如下:1
2
3
4
5%macro 宏名 参数个数
........
代码体
........
%endmacro如果在代码体中想引用某个参数,则必须用
%数字
的方式来引用,参见第 8 行与第 9 行。我们将宏名定义为 VECTOR,并引入了两个参数。怎么压入参数呢?看第 35~82 行,直接在宏名后接上两个参数即可,参数直接用逗号隔开。注意,宏定义属于预处理指令(伪指令),这些宏会在编译期展开,也就是说,编译后,interrupt.s
中会有 0x30 个第 8~31 行这样的代码段 。 -
第 8 行为中断入口标号,代表入口函数的地址,下面定义函数指针的数组时会使用这些标号。
-
第 9 行,该行有两种情况,一种是
ZERO
宏对应的push 0
,另一种是ERROR_CODE
宏对应的nop
指令,具体是哪种情况取决于利用宏定义函数时压入的什么参数,参见 35~82 行。注意,对于有错误码的中断,CPU 会自动压入错误码;对于没有错误码的中断,CPU 则不进行动作(nop);然而,对于前者,CPU 在函数返回时主动弹出错误码,必须由我们手动弹出错误码,这点尤其重要! 为了方便操作,有错误码的中断我们不做处理,无错误码的我们就压入 0,这样就统一了各中断函数的弹栈行为,无需特殊处理。关于错误码,参见中断详解。 -
第 10~14 行,保存当前寄存器环境。由于在 17 行,我们调用了 C 语言编写的实际的中断处理函数,这必将破坏当前的寄存器环境,因此需要保存段寄存器和通用寄存器。其他寄存器会由 CPU 自动保存,关于这部分还请参见中断详解。
-
第 16 行,压入中断号,这是实际中断处理函数的参数。在我们的系统中,大多数异常我们不做处理,但发生异常时我们需要知道抛出了哪个异常,因此需要通过中断号来定位错误源。
-
第 17 行,interrupt_handler_table 是 idt.c 中的数组,该数组中装载的是实际中断处理函数的地址。因为是指针数组,指针大小为 4 字节,因此需要用序号乘 4 才能找到函数的地址。
-
第 18 行进行平栈,关于平栈请参见函数调用约定。
-
第 26~28 行,发送 EOI 信号,通知 8259A 芯片中断处理结束。这部分内容参见:8259A编程 。
-
第 30 行,主动跨过错误码,原因已在前面阐述。
-
第 86~134 行,定义中断入口数组,即函数指针数组。该数组
interrupt_entry_table[]
会在dit.c
文件中被引用。
global.h
1 | //文件说明:global.h |
- 结构体
gate_desc
是中断描述符结构。该结构体有两点需要注意:
1)使用了位域,即为s_type
,DPL
,present
字段按位分配而非按字节分配。可别以为声明了 char 就是分配了一个字节。
2)结构体声明的末尾__attribute__((packed))
是在**指示编译器不要进行结构体对齐,这点很重要** 。详细参考结构体对齐 。 xdt_ptr
是 IDTR/GDTR 的结构体。当前我们只使用 IDTR,后续还会使用 GDTR 。
idt.c
1 |
|
- make_idt_desc() 函数用于构造单个中断门描述符。注意第三个参数,是中断入口函数的指针,
typedef void* intr_handler
,该声明在 interrupt.h 中。 - idt_desc_init() 函数用来构造整个 IDT 表。
- pic_init() 函数用来初始化 8259A 芯片,并将当前设置为只接收时钟中断 。其中还用到了 outb() 函数,该函数用汇编书写,在
port_io.s
文件中。端口号的宏在interrupt.h
中。 - general_intr_handler() 便是便是我们期待已久的实际中断程序,不过现在它很简陋。先统一将所有的中断处理程序都设置为该函数,未来我们会使用 register_handler() 来注册专门的中断程序。另外,0x27 和 0x2f 无需处理 。
- 最后,在 general_handler_regist() 中将 interrupt_handler_table 数组的每一个元素赋值为 general_intr_handler 函数的指针。再次强调,现在虽然每个中断都使用同一个函数,但后期对于某些中断我们会将其专门化,现在只是为中断提供基本的信息,以便产生中断时我们能明白发生了什么中断。
大家可能对各个函数之间的关系感到混乱,下面用一张图来帮助各位理清思绪:
本文结束。