本章节所有代码托管在miniOS_32
章节任务介绍
任务简介
上一节,我们构建了内核的第一个基本功能——打印字符及字符串
本节我们将介绍有关中断的知识以及如何打开中断,最终我们将使用代码打开时钟中断进行测试
本章的主要任务有:
- 打开中断机制,并使用时钟中断进行测试
前置知识
中断分类
按照事件来源进行划分,中断可分为外部中断和内部中断,来自CPU外部的事件叫外部中断,而来自CPU内部的事件叫内部中断
其中,外部中断按照是否可宕机划分,又可分为可屏蔽中断(意思是说,该中断事件不是太严重,还不能导致机器宕机,因此CPU可以暂时屏蔽掉不处理)和不可屏蔽中断(该类中断事件非常严重,会导致机器宕机,因此CPU应当立即处理,不可屏蔽)。
另外,内部中断也可以按照中断是否正常划分为软中断和异常,其中异常又可为故障(Fault)、陷阱(Trap)、终止(Abort)三种
如下所示
- 外部中断
- 内部中断
- 软中断
- 异常
- 故障(Fault)
- 陷阱(Trap)
- 终止(Abort)
外部中断
外部中断是指来自CPU外部的中断事件,外部中断的中断源必须是某个硬件,因此外部中断也叫硬件中断,例如来自网卡、打印机、硬盘等事件的中断等
如图所示,CPU为所有的硬件中断提供了两条信号线,所有来自外设的中断信号都可以通过这两条信号线链接到CPU
- INTR:用于接收来自硬件设备如硬盘、网卡、打印机发出的可屏蔽中断。
- 这类中断事件不会影响系统运行,因此CPU可以通过eflags寄存器的IF位将这些外设发出的中断屏蔽掉,甚至不处理
- 中断处理程序对于这类事件的处理可分为两部分进行处理
- 上半部处理:在关中断情况下进行处理,只完成中断应答或者硬件复位等重要性工作
- 下半部处理:在开中断的情况下处理,开始真正处理中断发生的事件,由于在开中断情况下处理,因此如果有新的中断发生,则该旧中断的下半部就会被换下CPU,转而去执行新中断的上半部,至于旧中断的下半部何时处理则取决于调度算法
- 可屏蔽中断的数量有限,每一种中断源都可以有一个中断向量号
- NMI:用于接受外部硬件发出的不可屏蔽中断。通过NMI信号线发给CPU的中断信号,都表示系统发生了致命的错误
- 这类中断事件的发生说明一定会影响系统的运行,如电源掉电、内存读写错误、总线奇偶校验错误等,必须要处理,CPU不可对齐屏蔽,不能坐视不理
- 所有的不可屏蔽中断只有一个中断向量号,为2号中断,这是因为不可屏蔽中断发生的时候基本任务软件已经无法解决,没必要按照事件再分配多余的中断向量号了

内部中断
内部中断又可分为软中断和异常
- 软中断:由软件主动发出的中断的信号,并不属于客观上的某种错误
- 以下我们在程序中写的可以发出的软中断指令
- int 8位立即数,即系统调用
- int3,调试断点指令
- into,中断溢出指令
- bound,检查数组索引越界指令
- 异常:异常是指指令执行期间CPU内部产生的错误
- 上述软中断列举的几个中断指令,除了第一个系统调用指令,其余三个也都可成为异常,因此他们都有一定风险在指令执行期间产生错误。此外,如DIV除法指令的分母为0,将引发0号异常;处理器无法识别某个机器码时也会发起6号异常
- 异常按照轻重程度也分为三种
- Fault,即故障,这类错误是可修复的,如缺页异常
- Trap,即陷阱,如int3断点调试指令,当int3发出这类异常并处理结束后可继续返回向下执行
- Abort,即终止,这是最严重的异常类型,一旦出现错误将无法修复,程序也将无法继续执行,因此操作系统为了自保,只能将此程序从进程表中去掉
中断描述符表
中断向量号与门描述符

上表即是计算机支持的所有中断和异常
中断机制本质上是来了一个中断信号后,调用相应的中断处理程序进行处理,所以CPU为了统一中断管理,就为每个中断信号分配一个整数,用此整数作为中断的D,这个整数就是所谓的中断向量,然后用此D作为中断描述符表中的索引,然后找到对应的表项,进而即可从中找到对应的中断处理程序。
其中
- 异常和不可屏蔽的中断向量号由CPU自动提供
- 可屏蔽中断的中断向量号由中断代理(接下来要介绍的8259A)提供
- 软中断的中断向量号有软件提供
由于一个中断向量号对应一个中断处理程序,所以需要有一张表对这个对应过程进行管理
在实模式下,用于存储中断处理程序入口的表叫中断向量表(Interrupt Vector Table,IVT)
我们接下来要介绍的是在保护模式下使用的中断描述符表(Interrupt Descriptor Table,IDT)
中断描述符表是保护模式下用于存储中断处理程序入口的表,当CPU 接收一个中断时,需要用中断向量在此表中检索对应的描述符,在该描述符中找到中断处理程序的起始地址,然后执行中断处理程序。
IDT中除了有中断描述符,还有任务门描述、陷阱门描述符、调用门描述符,他们统称为门描述符,IDT中只有门描述符
所有的描述符都是8个字节,段描述符描述的是一段内存(如内存的基址、大小和属性),而门描述符描述的是一段代码(中断处理程序)
以下是中断门描述符的格式,我们将来要通过构造它来填充IDT

以下是其他三种门描述符的格式



其中,在现代linux系统中,任务门和调用门用的很少,而我们在本节中也主要使用中断门描述符
中断描述符表寄存器
GDT
需要有一个寄存器(IDTR
)用于存储GDT的地址,IDT
也需要有一个寄存器来存储IDT的地址,即IDTR
,以下是IDTR的结构格式

其中0 ~ 15位存储IDT的表界限(表大小-1),16 ~ 47位存储IDT表基址
加载IDT的指令方式是
中断处理过程
完整的中断过程分为CPU外和CPU内两部分。
- CPU外:外部设备的中断由中断代理芯片接收,处理后将该中断的中断向量号发送到CPU。
- CPU 内:CPU执行该中断向量号对应的中断处理程序。
发生在CPU外的中断过程由接下来介绍的中段代理8259A完成
以下是发生在CPU内部的中断处理过程

如图所示
- 由于IDT中全都是门描述符,所以图中某门描述符”表示中断门、陷阱门或任务门
- 中断发生后,eflags中的NT位和TF位会被置0
- 如果中断对应的门描述符是中断门,标志寄存器 eflags中的
IF
位被自动置0,避免中断嵌套,即中断处理过程中又来了个新的中断,这是为防止在处理某个中断的过程中又来了一个相同的中断。这表示默认情况下,处理器会在无人打扰的方式下执行中断门描述符中的中断处理例程
- 若中断发生时对应的描述符是任务门或陷阱门的话,CPU是不会将
IF
位清0的。因为陷阱门主要用于调试,它允许 CPU响应更高级别的中断,所以允许中断嵌套。而对任务门来说,这是执行一个新任务
- **从中断返回的指令是iret,它从栈中弹出数据到寄存器
cs
、eip
、eflags
**等,根据特权级是否改变,判断是否要恢复旧栈。也就是说是否将栈中位于SS_old和ESP_old 位置的值弹出到寄存ss和 esp。当中断处理程序执行完成返回后,通过iret指令从栈中恢复eflags 的内容
可编程中断控制器8259A
8259A芯片介绍
为了让CPU获得每个外部设备的中断信号,最好的方式是在CPU中为每一个外设准备一个引脚接收中断,但这是不可能的,计算机中挂了很多外部设备,而且外设数量是没有上限的,无论CPU 中准备多少引脚都不够用。
上一节中我们说到,可屏蔽中断是通过INTR信号线进入CPU的,一般可独立运行的外部设备,如打印机、声卡等,其发出的中断都是可屏蔽中断,都共享这一根INTR信号线通知CPU。
但是,任务是串行在 CPU 上执行的,CPU每次只能执行一个任务,如果同时有多个外设发出中断,而 CPU只能先处理一个,它无法指定先响应哪个。同时,为了不使这些中断丢失,也需要为它们单独维护一个中断队列
这就是中断代理的作用,由它负责对所有可屏蔽中断仲裁,决定哪个中断优先被 CPU受理,同时向CPU提供中断向量号等功能

如图所示,有关8259A芯片需要做以下几点说明
- Intel 处理器共支持 256 个中断,但8259A 只可以管理8个中断,所以为了多支持一些中断设备,需要将多个8259A 组合,也就是级联,有了级联这种组合后,每一个 8259A就被称为1片。
- 若采用级联方式,即多片8259A芯片串连在一起,最多可级联9个,也就是最多支持 64个中断(片8259A通过级联可支持7n+1个中断源)。
- 级联时只能有一片 8259A为主片 master,其余的均为从片 slave。
- 来自从片的中断只能传递给主片,再由主片向上传递给 CPU,也就是说只有主片才会向CPU发送INT中断信号。
8259A芯片编程
在8259A 内部有两组寄存器
-
一组是初始化命令寄存器组,用来保存初始化命令字(Imitialization Command Words,ICW),ICW共4个,ICW1~ICW4。
-
另一组寄存器是操作命令寄存器组,用来保存操作命令字(Operation Command Word,OCW),OCW共3个,OCW1~OCW3。
所以,我们对8259A的编程,也分为初始化和操作两部分
- 初始化部分操作,用来确定是否需要级联,设置起始中断向量号,设置中断结束模式。其编程就是往 8259A 的端口发送一系列 ICW。
- 由于从一开始就要决定 8259A的工作状态,所以要一次性写入很多设置,某些设置之间是具有关联、依赖性的,也许后面的某个设置会依赖前面某个ICW 写入的设置所以这部分要求严格的顺序,必须依次写入ICW1、ICW2、ICW3、ICW4。
- 操作部分是用OCW来操作控制8259A,前面所说的中断屏蔽和中断结束,就是通过往8259A端口发送 OCW 实现的。
- OCW的发送顺序不固定,3个之中先发送哪个都可以。
编写中断处理程序
启用中断
代码目录结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
.
├── bin
├── boot //计算机启动代码文件
│ ├── include
│ │ └── boot.inc
│ ├── loader.S
│ └── mbr.S
├── kernel
│ ├── global.h //各种描述符、段选择子等字段的宏定义文件
│ ├── init.c
│ ├── init.h
│ ├── interrupt.c //构造IDT、IDTR、打开时钟中断文件
│ ├── interrupt.h
│ ├── kernel.S //定义中断处理程序文件
│ └── main.c
├── lib
│ ├── kernel
│ │ ├── io.h //将对8259A的汇编控制代码封装为易用的c代码
│ │ ├── print.h
│ │ └── print.S
│ └── stdint.h
├── Makefile //编译脚本,Makefile形式的编译脚本
├── start.sh //编译脚本,有更清晰的编译命令和编译顺序,方便读者理解
└── test //内联汇编测试文件
├── base_asm.c
├── inlineASM.c
├── mem.c
├── reg.c
└── reg_constraint.c
|
代码核心逻辑
- 定义中断处理程序
- 通过中断处理程序构造中断描述符
- 把中断描述符填充进IDT构造IDT
- 在中断代理中暂时只打开时钟中断,也就是说只有时钟中断才能通过中断代理告知CPU,其余的中断信号都被拦截
定义中断处理程序
/kernel/kernel.S
中断处理程序通过宏进行循环构造
汇编中宏语法格式为
1
2
3
4
|
;宏定义的语法格式为:
; %macro 宏名 参数个数 ;参数顺序由占位符表示,如%1表示第一个参数,%2表示第二个参数
; 宏定义
; %endmacro
|
以下是中断处理程序的宏
这段中断处理程序的代码逻辑很简单,当其被调用时
- 首先打印字符串
intr_str
- 然后给8259A芯片发生信号结束中断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
%macro VECTOR 2 ;定义VECTOR宏,该宏用于定义中断处理程序
;参数1表示中断向量号,参数2表示错误码
section .text ;中断处理程序的代码段
intr%1entry: ;这是一个标号,用于表示中断程序的入口地址,即中断程序的函数名
%2 ;如果一个中断有错误码,则什么都不做,否则压入0作填充
;这段代码是为了模拟中断发生,如果我们中断调用后这段字符串打印出来了,说明我们的中断逻辑是正确的
push intr_str ;压入put_str的调用参数
call put_str
add esp,4 ;回收put_str函数压入的字符串参数的栈帧空间
;由于intr_str是一个指针类型,而该系统为32bit,因此该参数占用4个字节空间
;上述中断发生模拟结束后要结束中断
mov al,0x20 ;中断结束命令EOI
out 0xa0,al ;向主片发送OCW2,其中EOI位为1,告知结束中断,详见书p317
out 0x20,al ;向从片发送OCW2,其中EOI位为1,告知结束中断
add esp,4 ;抛弃有错误码的中断(如果有)
iret ;中断返回
section .data
dd intr%1entry ;存储各个中断入口程序的地址,形成intr_entry_table数组,定义的地址是4字节,32位
%endmacro
|
这段宏共有两个参数,这两个参数都是占位符
- 参数一表示中断号
- 这个中断号是拿来填充中断处理程序的函数名
- 比如这段宏调用
VECTOR 0x00,ZERO
,第一个参数是0x00
,于是它代表的中断处理程序的函数名就是intr0x00entry
- 参数二是一个填充字段。
- 中断处理程序在被调用前CPU会压入ss、eip、eflags寄存器等值进行保存,但是有点中断处理程序还会额外压入错误码信息,而有的不压入,于是为了保持栈结构的统一,定义了这个参数,这个参数是一个操作
- 其作用是,如果某个中断处理程序压入了错误码,则该参数代表的操作为空操作,什么也不做
- 否则就压入4字节的0以作填充

以下是通过调用上述宏构造的33个中断处理程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
; 调用之前写好的VECTOR宏来批量生成中断处理函数,
; 传入参数是中断号码与上面中断宏的%2步骤,这个步骤是什么都不做,还是压入0,详情看p303
VECTOR 0x00,ZERO
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO
VECTOR 0x08,ERROR_CODE
VECTOR 0x09,ZERO
VECTOR 0x0a,ERROR_CODE
VECTOR 0x0b,ERROR_CODE
VECTOR 0x0c,ZERO
VECTOR 0x0d,ERROR_CODE
VECTOR 0x0e,ERROR_CODE
VECTOR 0x0f,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ERROR_CODE
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ERROR_CODE
VECTOR 0x19,ZERO
VECTOR 0x1a,ERROR_CODE
VECTOR 0x1b,ERROR_CODE
VECTOR 0x1c,ZERO
VECTOR 0x1d,ERROR_CODE
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO
VECTOR 0x20,ZERO
|
之所以定义33个中断处理程序,是因为中断向量0 ~ 19为处理器内部固定的异常类型,20 ~ 31是Imntel保留的,可用的最低中断向量号是32
构造IDT
/kernel/interrupt.c
首先按照中断描述符的格式定义中断描述符的结构体
1
2
3
4
5
6
7
8
9
|
// 定义中断描述符结构体
struct gate_desc
{
uint16_t func_offset_low_word; // 中断处理程序在目标代码段的偏移量(低字)
uint16_t selector; // 选择子字段
uint8_t dcount; // 此项为双字计数字段,是门描述符中的第4字节。这个字段无用
uint8_t attribute; // 属性字段(包含P位、DPL位、S位、TYPE位)
uint16_t func_offset_high_word; // 中断处理程序在目标代码段的偏移量(高字)
};
|

我们需要通过中断描述符构造IDT,当然也需要定义IDT,其实就是个gate_desc(中断描述符j结构体定义)类型的数组
1
2
|
// 定义中断门描述符结构体数组,形成中断描述符表idt,该数组中的元素是中断描述符
static struct gate_desc idt[IDT_DESC_CNT];
|
接下来首先按照中断描述符的格式填充中断描述符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/*
函数功能:构建中断描述符
函数实现:按照中断描述符结构体定义填充字段
参数:
中断门描述符地址
属性
中断处理函数
*/
static void make_idt_desc(struct gate_desc *p_gdesc, uint8_t attr, intr_handler function)
{
p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF;
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16;
}
|
然后调用上述函数,将中断描述符循环填充进IDT以构造IDT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/*
函数功能:构建中断描述符表idt
函数实现:循环调用make_idt_desc构建中断描述符,形成中断描述符表idt
参数:中断描述符表中的某个中断描述符地址,属性字段,中断处理函数地址
*/
static void idt_desc_init(void)
{
int i;
for (i = 0; i < IDT_DESC_CNT; i++)
{
make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
}
put_str(" idt_desc_init done\n");
}
|
其中intr_entry_table
保存的就是kernel.S
定义的33个中断处理程序的入口地址(函数地址)
打开时钟中断
最后一步的工作就是初始化8259A芯片,同时打开时钟中断,只让时钟中断的信号经过中断代理告知CPU
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/*初始化可编程中断控制器8259A*/
static void pic_init()
{
/* 初始化主片 */
outb(PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb(PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
outb(PIC_M_DATA, 0x04); // ICW3: IR2接从片.
outb(PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI
/* 初始化从片 */
outb(PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb(PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
outb(PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚
outb(PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI
/* 打开主片上IR0,也就是目前只接受时钟产生的中断 */
outb(PIC_M_DATA, 0xfe);
outb(PIC_S_DATA, 0xff);
put_str("pic_init done\n");
}
|
其中有关对8259A的操作和控制函数定义在
/lib/kernel/io.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
#ifndef __LIB_IO_H
#define __LIB_IO_H
#include "stdint.h"
// 一次送一字节的数据到指定端口,static指定只在本.h内有效,inline是让处理器将函数编译成内嵌的方式,就是在该函数调用处原封不动地展开
// 此函数有两个参数,一个端口号,一个要送往端口的数据
static inline void outb(uint16_t port, uint8_t data)
{
/*********************************************************
a表示用寄存器al或ax或eax,对端口指定N表示0~255, d表示用dx存储端口号,
%b0表示对应al,%w1表示对应dx */
asm volatile("outb %b0, %w1" : : "a"(data), "Nd"(port));
}
// 利用outsw(端口输出串,一次一字)指令,将ds:esi指向的addr处起始的word_cnt(存在ecx中)个字写入端口port,ecx与esi会自动变化
static inline void outsw(uint16_t port, const void *addr, uint32_t word_cnt)
{
/*********************************************************
+表示此限制即做输入又做输出.
outsw是把ds:esi处的16位的内容写入port端口, 我们在设置段描述符时,
已经将ds,es,ss段的选择子都设置为相同的值了,此时不用担心数据错乱。*/
asm volatile("cld; rep outsw" : "+S"(addr), "+c"(word_cnt) : "d"(port));
} // S表示寄存器esi/si
/* 将从端口port读入的一个字节返回 */
static inline uint8_t inb(uint16_t port)
{
uint8_t data;
asm volatile("inb %w1, %b0" : "=a"(data) : "Nd"(port));
return data;
}
/* 将从端口port读入的word_cnt个字写入addr */
static inline void insw(uint16_t port, void *addr, uint32_t word_cnt)
{
/******************************************************
insw是将从端口port处读入的16位内容写入es:edi指向的内存,
我们在设置段描述符时, 已经将ds,es,ss段的选择子都设置为相同的值了,
此时不用担心数据错乱。*/
asm volatile("cld; rep insw" : "+D"(addr), "+c"(word_cnt) : "d"(port) : "memory");
} // D表示寄存器edi/di //通知编译器,内存已经被改变了
#endif
|
填充IDTR,加载IDT
调用上述函数,完成所有的初始化工作后,按照IDTR的格式构造IDTR,以加载IDT
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/*完成有关中断的所有初始化工作*/
void idt_init()
{
put_str("idt_init start\n");
idt_desc_init(); // 构建中段描述符表
pic_init(); // 初始化中断控制器,只接受来自时钟中断的信号
/* 加载idt */
uint64_t idt_operand = (((uint64_t)(uint32_t)idt << 16) | (sizeof(idt) - 1)); // 定义要加载到IDTR寄存器中的值
asm volatile("lidt %0" : : "m"(idt_operand));
put_str("idt_init done\n");
}
|

中断测试
/kernel/main.c
1
2
3
4
5
6
7
8
9
|
#include "print.h"
#include "init.h"
void main(void)
{
put_str("I am kernel\n");
init_all(); // 初始化中断,同时只放行时钟中断
asm volatile("sti"); // 为演示中断处理,在此临时开中断,sti是开中断指令
while (1);
}
|
完整源码可参见miniOS_32
编译运行
编译脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
mkdir -p bin
#编译mbr
nasm -o $(pwd)/bin/mbr -I $(pwd)/boot/include/ $(pwd)/boot/mbr.S
dd if=$(pwd)/bin/mbr of=~/bochs/hd60M.img bs=512 count=1 conv=notrunc
#编译loader
nasm -o $(pwd)/bin/loader -I $(pwd)/boot/include/ $(pwd)/boot/loader.S
dd if=$(pwd)/bin/loader of=~/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc
#编译print函数
nasm -f elf32 -o $(pwd)/bin/print.o $(pwd)/lib/kernel/print.S
# 编译kernel
nasm -f elf32 -o $(pwd)/bin/kernel.o $(pwd)/kernel/kernel.S
#编译main函数
gcc-4.4 -o $(pwd)/bin/main.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/main.c
#编译interrupt
gcc-4.4 -o $(pwd)/bin/interrupt.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/interrupt.c
#编译init
gcc-4.4 -o $(pwd)/bin/init.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/init.c
#将main函数与print函数进行链接
ld -m elf_i386 -Ttext 0xc0001500 -e main -o $(pwd)/bin/kernel.bin $(pwd)/bin/main.o $(pwd)/bin/print.o $(pwd)/bin/init.o $(pwd)/bin/interrupt.o $(pwd)/bin/kernel.o
#将内核文件写入磁盘,loader程序会将其加载到内存运行
dd if=$(pwd)/bin/kernel.bin of=~/bochs/hd60M.img bs=512 count=200 conv=notrunc seek=9
#rm -rf bin/*
|
运行结果如下

在bochs中调试IDT结果如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
<bochs:2> info idt
Interrupt Descriptor Table (base=0xc0002c40, limit=263):
IDT[0x00]=32-Bit Interrupt Gate target=0x0008:0xc0001810, DPL=0
IDT[0x01]=32-Bit Interrupt Gate target=0x0008:0xc0001829, DPL=0
IDT[0x02]=32-Bit Interrupt Gate target=0x0008:0xc0001842, DPL=0
IDT[0x03]=32-Bit Interrupt Gate target=0x0008:0xc000185b, DPL=0
IDT[0x04]=32-Bit Interrupt Gate target=0x0008:0xc0001874, DPL=0
IDT[0x05]=32-Bit Interrupt Gate target=0x0008:0xc000188d, DPL=0
IDT[0x06]=32-Bit Interrupt Gate target=0x0008:0xc00018a6, DPL=0
IDT[0x07]=32-Bit Interrupt Gate target=0x0008:0xc00018bf, DPL=0
IDT[0x08]=32-Bit Interrupt Gate target=0x0008:0xc00018d8, DPL=0
IDT[0x09]=32-Bit Interrupt Gate target=0x0008:0xc00018f0, DPL=0
IDT[0x0a]=32-Bit Interrupt Gate target=0x0008:0xc0001909, DPL=0
IDT[0x0b]=32-Bit Interrupt Gate target=0x0008:0xc0001921, DPL=0
IDT[0x0c]=32-Bit Interrupt Gate target=0x0008:0xc0001939, DPL=0
IDT[0x0d]=32-Bit Interrupt Gate target=0x0008:0xc0001952, DPL=0
IDT[0x0e]=32-Bit Interrupt Gate target=0x0008:0xc000196a, DPL=0
IDT[0x0f]=32-Bit Interrupt Gate target=0x0008:0xc0001982, DPL=0
IDT[0x10]=32-Bit Interrupt Gate target=0x0008:0xc000199b, DPL=0
IDT[0x11]=32-Bit Interrupt Gate target=0x0008:0xc00019b4, DPL=0
IDT[0x12]=32-Bit Interrupt Gate target=0x0008:0xc00019cc, DPL=0
IDT[0x13]=32-Bit Interrupt Gate target=0x0008:0xc00019e5, DPL=0
IDT[0x14]=32-Bit Interrupt Gate target=0x0008:0xc00019fe, DPL=0
IDT[0x15]=32-Bit Interrupt Gate target=0x0008:0xc0001a17, DPL=0
IDT[0x16]=32-Bit Interrupt Gate target=0x0008:0xc0001a30, DPL=0
IDT[0x17]=32-Bit Interrupt Gate target=0x0008:0xc0001a49, DPL=0
IDT[0x18]=32-Bit Interrupt Gate target=0x0008:0xc0001a62, DPL=0
IDT[0x19]=32-Bit Interrupt Gate target=0x0008:0xc0001a7a, DPL=0
IDT[0x1a]=32-Bit Interrupt Gate target=0x0008:0xc0001a93, DPL=0
IDT[0x1b]=32-Bit Interrupt Gate target=0x0008:0xc0001aab, DPL=0
IDT[0x1c]=32-Bit Interrupt Gate target=0x0008:0xc0001ac3, DPL=0
IDT[0x1d]=32-Bit Interrupt Gate target=0x0008:0xc0001adc, DPL=0
IDT[0x1e]=32-Bit Interrupt Gate target=0x0008:0xc0001af4, DPL=0
IDT[0x1f]=32-Bit Interrupt Gate target=0x0008:0xc0001b0c, DPL=0
IDT[0x20]=32-Bit Interrupt Gate target=0x0008:0xc0001b25, DPL=0
|
改进中断
代码目录结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
.
├── bin
├── boot //计算机启动代码文件
│ ├── include
│ │ └── boot.inc
│ ├── loader.S
│ └── mbr.S
├── kernel
│ ├── global.h //各种描述符、段选择子等字段的宏定义文件
│ ├── init.c
│ ├── init.h
│ ├── interrupt.c //构造IDT、IDTR、打开时钟中断文件
│ ├── interrupt.h
│ ├── kernel.S //定义中断处理程序文件
│ └── main.c
├── lib
│ ├── kernel
│ │ ├── io.h //将对8259A的汇编控制代码封装为易用的c代码
│ │ ├── print.h
│ │ └── print.S
│ └── stdint.h
├── Makefile //编译脚本,Makefile形式的编译脚本
├── start.sh //编译脚本,有更清晰的编译命令和编译顺序,方便读者理解
|
改进代码
上述我们代码中,我们通过宏定义了33个中断处理程序,但是由一个问题,那就是我们的中断处理程序的具体实现是通过汇编写的,这很不利、于我们将来使用c语言自定义中断处理程序的具体实现,针对这个问题,我们主要做以下几点改进
修改/kernel/kernel.S对中断处理程序的定义逻辑
让中断在进入中断处理程序之后,具体的处理实现通过调用c函数来完成,而不是全部耦合进汇编代码里
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
extern idt_table
section .data ;定义数据段
global intr_entry_table ;定义中断处理程序数据,数组的元素值是中断处理程序,共33个
intr_entry_table:
;宏定义的语法格式为:
; %macro 宏名 参数个数
; 宏定义
; %endmacro
%macro VECTOR 2 ;定义VECTOR宏,该宏用于定义中断处理程序
section .text ;中断处理程序的代码段
intr%1entry: ;这是一个标号,用于表示中断程序的入口地址,即中断程序的函数名
%2 ;压入中断错误码(如果有)
push ds ; 以下是保存上下文环境
push es
push fs
push gs
pushad
; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI
mov al,0x20 ; 中断结束命令EOI
out 0xa0,al ;向主片发送OCW2,其中EOI位为1,告知结束中断,详见书p317
out 0x20,al ;向从片发送OCW2,其中EOI位为1,告知结束中断
push %1 ; 不管idt_table中的目标程序是否需要参数,都一律压入中断向量号,调试时很方便
call [idt_table + %1*4] ; 调用idt_table中的C版本中断处理函数
jmp intr_exit
section .data
dd intr%1entry ;存储各个中断入口程序的地址,形成intr_entry_table数组,定义的地址是4字节,32位
%endmacro
section .text
global intr_exit
intr_exit:
; 以下是恢复上下文环境
add esp, 4 ; 跳过中断号
popad
pop gs
pop fs
pop es
pop ds
add esp, 4 ;对于会压入错误码的中断会抛弃错误码(这个错误码是执行中断处理函数之前CPU自动压入的),对于不会压入错误码的中断,就会抛弃上面push的0
iretd ; 从中断返回,32位下iret等同指令iretd
|
在上述修正后的代码里,当所有初始化工作完成,开始执行中断处理的时候,我们通过调用c函数完成,这样就把具体的中断处理逻辑解放了出来,而不是全部耦合进一段代码里
可以看到,具体的调用其实是按照中断号(宏定义的第一个参数%1)调用IDT的对应表项,这个表项里对应的是具体中断实现函数
1
|
call [idt_table + %1*4] ; 调用idt_table中的C版本中断处理函数
|
具体的调用实现在/kernel/interrupt.c文件中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
#if 1
/*用c语言定义中断处理函数的具体实现,不再用汇编语言在kernel.S中定义中断处理函数的实现*/
// 参数:中断向量号
static void general_intr_handler(uint8_t vec_nr)
{
// 伪中断向量,无需处理
if (vec_nr == 0x27 || vec_nr == 0x2f)
{
return;
}
put_str("int vector:0x");
put_int(vec_nr);
put_char('\n');
}
#endif
/* 完成一般中断处理函数注册及异常名称注册 */
static void exception_init(void)
{ // 完成一般中断处理函数注册及异常名称注册
int i;
for (i = 0; i < IDT_DESC_CNT; i++)
{
/* idt_table数组中的函数是在进入中断后根据中断向量号调用的,
见kernel/kernel.S的call [idt_table + %1*4] */
idt_table[i] = general_intr_handler; // 默认为general_intr_handler
// 以后会由register_handler来注册具体处理函数。
intr_name[i] = "unknown"; // 先统一赋值为unknown
}
intr_name[0] = "#DE Divide Error";
intr_name[1] = "#DB Debug Exception";
intr_name[2] = "NMI Interrupt";
intr_name[3] = "#BP Breakpoint Exception";
intr_name[4] = "#OF Overflow Exception";
intr_name[5] = "#BR BOUND Range Exceeded Exception";
intr_name[6] = "#UD Invalid Opcode Exception";
intr_name[7] = "#NM Device Not Available Exception";
intr_name[8] = "#DF Double Fault Exception";
intr_name[9] = "Coprocessor Segment Overrun";
intr_name[10] = "#TS Invalid TSS Exception";
intr_name[11] = "#NP Segment Not Present";
intr_name[12] = "#SS Stack Fault Exception";
intr_name[13] = "#GP General Protection Exception";
intr_name[14] = "#PF Page-Fault Exception";
// intr_name[15] 第15项是intel保留项,未使用
intr_name[16] = "#MF x87 FPU Floating-Point Error";
intr_name[17] = "#AC Alignment Check Exception";
intr_name[18] = "#MC Machine-Check Exception";
intr_name[19] = "#XF SIMD Floating-Point Exception";
}
|
上述代码的逻辑是
我们把具体的中断处理实现通过general_intr_handler
完成,然后把general_intr_handler
放在IDT中,这样便可通过中断号调用对应的中断处理程序了

编译运行

预览: