目录
一、前景回顾
二、实现中断框架
三、代码实现
四、中断的压栈和出栈过程分析
五、运行测试
一、前景回顾
前面我们已经讲解了中断的基本知识,接下来要开始进行代码的实操。代码主要有两块,其中一块是关于可编程中断控制器8259A的代码,另一块主要是整个中断的代码。
二、实现中断框架
IDT:中断描述符表。
gate_desc:中断描述符。
intr_entry_table:中断处理入口函数表。该数组存储了所有中断处理入口函数的地址,其核心是通过call [idt_table + %1*4] 的方式在idt_table表中调用中断处理函数。
idt_table:中断处理函数表,该数组存储的才是真正的中断处理函数地址。
general_intr_handler:通用中断处理函数。
register_handler:中断注册函数,外设(例如定时器timer)通过调用该函数来注册自定义中断处理函数(如intr_timer_handler)。
接下来我们照着上面的流程图来详细地讲解整个中断框架的组成以及中断相关代码的实现。
首先我们看到IDT,也就是中断描述符表,中断描述符表是有多个中断描述符组成的,如下图所示:
所以我们通过结构体来构造这么一个描述符:
1 /*中断门描述符结构体*/ 2 struct gate_desc { 3 uint16_t func_offet_low_word; 4 uint16_t selector; 5 uint8_t dcount; //此项为双字计数字段,是门描述符中的第4字节 6 uint8_t attribute; 7 uint16_t func_offet_high_word; 8 };
中断向量号通过在IDT中索引得到对应的中断描述符,解析后便可以得到中断处理函数的所在地址,随后CPU便跳转执行该函数。按理说跳转执行中断函数就好了,怎么又多了一个intr_entry_table表出来?其实我们需要知道,CPU跳转执行中时,因为我们还会返回,所以需要进行上下文保护,也就是保护当前的寄存器环境,只有保护好了环境资源才可以跳转过去执行函数。又因为使用汇编来编写中断函数比较繁琐,可读性差,所以我们在C语言环境下进行中断函数编写。总的来说,当CPU拿到中断向量号后,在IDT中索引得到中断处理函数地址(intr%1entry)(这个其实并不是真正的中断处理函数地址,严格意义上来说应该是进入中断处理函数的函数,在这个函数中进行上下文环境保护,随后才跳转执行真正的中断处理函数),来看intr%1entry的构成:
intr%1entry: %2 push ds push es push fs push gs pushad ;8259A相关设置 mov al, 0x20 out 0xa0, al out 0x20, al push %1 ;将中断号压栈 call [idt_table + %1*4] ;调用中断处理函数 jmp intr_exit ;退出中断
如上所示,进入该函数后,这个%2是什么呢?它是一个宏定义,本质上就是起了一个占位作用。因为CPU在进入到中断后会向栈中压入部分寄存器环境,这是CPU自动完成的,不需要我们手动编写,保存的寄存器名称及顺序是:
1、如果发生特权级转移,此时要把低特权级的栈段选择子ss及栈指针esp保存到栈中。
2、压入标志寄存器eflags。
3、压入返回地址cs和eip。
4、如果此中断没有相应的错误码,至此,CPU把寄存器压栈的工作就完成了,如果有错误码,CPU在压入eip之后还会压入错误码。如下图所示:
所以为了保证栈顶指针的一致,在有错误码压入时,%2其实就是一个nop指令,撒也不做。如果没有错误码压入,那么%2就是push 0的指令,这样就能保证栈顶指针的一致性(详细代码请看后续代码实现)。这里我就说这么多,详情请查看原书《操作系统真象还原》p320~323页。
随后通过push和pushad指令将当前环境下的8个通用寄存器和4个段寄存器给保存起来,随后是8259A相关设置,这里就多说了。然后是将中断号压栈。最后通过call指令调用中断处理函数(这次真是中断处理函数了,如假包换),中断处理函数我们是使用C语言来编写的,因为这样便于阅读和修改。我们将所有的中断处理函数存放在idt_table中,为方便查询,并且后面注册中断处理函数时,只需要往idt_table中插入我们编写的中断处理函数就好了。可以看到idt_table中默认的中断处理函数都是general_intr_handler,我们通过register_handler函数给定时器注册了名为intr_timer_handler的中断处理函数。最后中断处理函数执行完毕后通过jmp intr_exit函数就完成环境恢复,且回到中断发生时的地址处继续执行代码。
三、代码实现
因为涉及到对于可编程中断控制器8259A的端口的读写,所以我们在project/lib/kernel目录下新建一个名为io.h的文件,在该文件中定义对端口的读写函数。
1 /************** 机器模式 ***************
2 b -- 输出寄存器QImode名称,即寄存器中的最低8位:[a-d]l。
3 w -- 输出寄存器HImode名称,即寄存器中2个字节的部分,如[a-d]x。
4
5 HImode
6 “Half-Integer”模式,表示一个两字节的整数。
7 QImode
8 “Quarter-Integer”模式,表示一个一字节的整数。
9 *******************************************/
10
11 #ifndef __LIB_IO_H
12 #define __LIB_IO_H
13 #include "stdint.h"
14
15 /* 向端口port写入一个字节*/
16 static inline void outb(uint16_t port, uint8_t data) {
17 /*********************************************************
18 a表示用寄存器al或ax或eax,对端口指定N表示0~255, d表示用dx存储端口号,
19 %b0表示对应al,%w1表示对应dx */
20 asm volatile ( "outb %b0, %w1" : : "a" (data), "Nd" (port));
21 /******************************************************/
22 }
23
24 /* 将addr处起始的word_cnt个字写入端口port */
25 static inline void outsw(uint16_t port, const void* addr, uint32_t word_cnt) {
26 /*********************************************************
27 +表示此限制即做输入又做输出.
28 outsw是把ds:esi处的16位的内容写入port端口, 我们在设置段描述符时,
29 已经将ds,es,ss段的选择子都设置为相同的值了,此时不用担心数据错乱。*/
30 asm volatile ("cld; rep outsw" : "+S" (addr), "+c" (word_cnt) : "d" (port));
31 /******************************************************/
32 }
33
34 /* 将从端口port读入的一个字节返回 */
35 static inline uint8_t inb(uint16_t port) {
36 uint8_t data;
37 asm volatile ("inb %w1, %b0" : "=a" (data) : "Nd" (port));
38 return data;
39 }
40
41 /* 将从端口port读入的word_cnt个字写入addr */
42 static inline void insw(uint16_t port, void* addr, uint32_t word_cnt) {
43 /******************************************************
44 insw是将从端口port处读入的16位内容写入es:edi指向的内存,
45 我们在设置段描述符时, 已经将ds,es,ss段的选择子都设置为相同的值了,
46 此时不用担心数据错乱。*/
47 asm volatile ("cld; rep insw" : "+D" (addr), "+c" (word_cnt) : "d" (port) : "memory");
48 /******************************************************/
49 }
50
51 #endif
io.h
在project/kernel目录下新建名为interrupt.c、interrupt.h、kernel.S、global.h文件。
1 #include "interrupt.h"
2 #include "stdint.h"
3 #include "global.h"
4 #include "io.h"
5 #include "print.h"
6
7 #define IDT_DESC_CNT 0x81 //目前支持的中断数
8
9 #define PIC_M_CTRL 0x20 //主片的控制端口是0x20
10 #define PIC_M_DATA 0x21 //主片的数据端口是0x21
11 #define PIC_S_CTRL 0xa0 //从片的控制端口是0xa0
12 #define PIC_S_DATA 0xa1 //从片的数据端口是0xa1
13
14
15 /*中断门描述符结构体*/
16 struct gate_desc {
17 uint16_t func_offet_low_word;
18 uint16_t selector;
19 uint8_t dcount; //此项为双字计数字段,是门描述符中的第4字节
20 uint8_t attribute;
21 uint16_t func_offet_high_word;
22 };
23
24 /*定义IDT表*/
25 static struct gate_desc idt[IDT_DESC_CNT];
26 extern intr_handler intr_entry_table[IDT_DESC_CNT];
27
28 char *intr_name[IDT_DESC_CNT]; //用于保存异常的名字
29 intr_handler idt_table[IDT_DESC_CNT]; //定义中断处理程序数组
30
31 /*通用的中断处理函数,一般用在异常出现时的处理*/
32 static void general_intr_handler(uint8_t vec_nr)
33 {
34 if (vec_nr == 0x27 || vec_nr == 0x2f) {
35 return ;
36 }
37
38 /*将光标置为0,从屏幕左上角清出一片打印异常信息的区域,方便阅读*/
39 set_cursor(0);
40 int cursor_pos = 0;
41 while (cursor_pos < 320) {
42 put_char(' ');
43 cursor_pos++;
44 }
45
46 set_cursor(0);
47 put_str("!!!!!!!!!!!exception message begin!!!!!!!!!");
48 set_cursor(88);
49 put_str(intr_name[vec_nr]);
50
51 //如果为pagefault,将缺失的地址打印出来并且悬停
52 if (vec_nr == 14) {
53 int page_fault_vaddr = 0;
54 asm volatile ("movl %%cr2, %0": "=r" (page_fault_vaddr)); //cr2存放造成pagefault的虚拟地址
55 put_str("\npage fault addr is: ");put_int(page_fault_vaddr);
56 }
57 put_str("!!!!!!!!!!!exception message end!!!!!!!!!!");
58 //能进入中断处理程序就表示已经处于关中断的情况下,不会出现进程调度的情况,因此下面的死循环可以一直执行
59 while (1);
60 }
61
62 /*完成一般中断处理函数注册及异常名称注册*/
63 static void exception_init(void)
64 {
65 int i;
66 for (i = 0; i < IDT_DESC_CNT; i++) {
67 idt_table[i] = general_intr_handler;
68 intr_name[i] = "unknow";
69 }
70 intr_name[0] = "#DE Divide Error";
71 intr_name[1] = "#DB Debug Exception";
72 intr_name[2] = "NMI Interrupt";
73 intr_name[3] = "#BP Breakpoint Exception";
74 intr_name[4] = "#OF Overflow Exception";
75 intr_name[5] = "#BR BOUND Range Exceeded Exception";
76 intr_name[6] = "#UD Invalid Opcode Exception";
77 intr_name[7] = "#NM Device Not Available Exception";
78 intr_name[8] = "#DF Double Fault Exception";
79 intr_name[9] = "Coprocessor Segment Overrun";
80 intr_name[10] = "#TS Invalid TSS Exception";
81 intr_name[11] = "#NP Segment Not Present";
82 intr_name[12] = "#SS Stack Fault Exception";
83 intr_name[13] = "#GP General Protection Exception";
84 intr_name[14] = "#PF Page-Fault Exception";
85 // intr_name[15] 第15项是intel保留项,未使用
86 intr_name[16] = "#MF x87 FPU Floating-Point Error";
87 intr_name[17] = "#AC Alignment Check Exception";
88 intr_name[18] = "#MC Machine-Check Exception";
89 intr_name[19] = "#XF SIMD Floating-Point Exception";
90 }
91
92 /* 初始化可编程中断控制器8259A */
93 static void pic_init(void) {
94 /* 初始化主片 */
95 outb(PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
96 outb(PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
97 outb(PIC_M_DATA, 0x04); // ICW3: IR2接从片.
98 outb(PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI
99
100 /* 初始化从片 */
101 outb(PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
102 outb(PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
103 outb(PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚
104 outb(PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI
105
106
107 /*打开时钟中断*/
108 outb(PIC_M_DATA, 0xfe);
109 outb(PIC_S_DATA, 0xff);
110
111 put_str("pic_init done\n");
112 }
113
114
115 /*创建中断门描述符*/
116 static void make_idt_desc(struct gate_desc *p_gdesc, uint8_t attr, intr_handler function)
117 {
118 p_gdesc->func_offet_low_word = (uint32_t)function & 0x0000FFFF;
119 p_gdesc->selector = SELECTOR_K_CODE;
120 p_gdesc->dcount = 0;
121 p_gdesc->attribute = attr;
122 p_gdesc->func_offet_high_word = ((uint32_t)function & 0xFFFF0000) >> 16;
123 }
124
125 /*初始化中断描述符表*/
126 static void idt_desc_init(void)
127 {
128 int i = 0;
129 for (i = 0; i <IDT_DESC_CNT; i++) {
130 make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
131 }
132
133 put_str("ide_desc_init done\n");
134 }
135
136 /*完成中断有关的所有初始化工作*/
137 void idt_init(void)
138 {
139 put_str("idt_init start\n");
140 idt_desc_init();
141 exception_init();
142 pic_init();
143
144 /*加载idt*/
145 uint64_t idt_operand = (sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16);
146 asm volatile("lidt %0" : : "m"(idt_operand));
147 put_str("idt_init done\n");
148 }
149
150
151 /*在中断处理程序数组第vector_no个元素中注册安装中断处理程序*/
152 void register_handler(uint8_t vector_no, intr_handler function)
153 {
154 idt_table[vector_no] = function;
155 }
interrupt.c
1 #ifndef __KERNEL_INTERRUPT_H
2 #define __KERNEL_INTERRUPT_H
3 #include "stdint.h"
4
5 typedef void* intr_handler;
6 void register_handler(uint8_t vector_no, intr_handler function);
7 void idt_init(void);
8 #endif
interrupt.h
1 [bits 32]
2 %define ERROR_CODE nop
3 %define ZERO push 0
4
5 extern put_str ;声明外部函数
6 extern idt_table ;声明外部中断函数数组
7
8 section .data
9 global intr_entry_table
10 intr_entry_table:
11
12 %macro VECTOR 2
13 section .text
14 intr%1entry:
15 %2
16 push ds
17 push es
18 push fs
19 push gs
20 pushad
21
22 ;8259A相关设置
23 mov al, 0x20
24 out 0xa0, al
25 out 0x20, al
26
27 push %1 ;将中断号压栈
28 call [idt_table + %1*4] ;调用中断处理函数
29 jmp intr_exit ;退出中断
30
31 section .data
32 dd intr%1entry
33 %endmacro
34
35 section .text
36 global intr_exit
37 intr_exit:
38 add esp, 4
39 popad
40 pop gs
41 pop fs
42 pop es
43 pop ds
44 add esp, 4
45 iretd
46
47 VECTOR 0x00, ZERO
48 VECTOR 0x01, ZERO
49 VECTOR 0x02, ZERO
50 VECTOR 0x03, ZERO
51 VECTOR 0x04, ZERO
52 VECTOR 0x05, ZERO
53 VECTOR 0x06, ZERO
54 VECTOR 0x07, ZERO
55 VECTOR 0x08, ERROR_CODE
56 VECTOR 0x09, ZERO
57 VECTOR 0x0A, ERROR_CODE
58 VECTOR 0x0B, ERROR_CODE
59 VECTOR 0x0C, ERROR_CODE
60 VECTOR 0x0D, ERROR_CODE
61 VECTOR 0x0E, ERROR_CODE
62 VECTOR 0x0F, ZERO
63 VECTOR 0x10, ZERO
64 VECTOR 0x11, ERROR_CODE
65 VECTOR 0x12, ZERO
66 VECTOR 0x13, ZERO
67 VECTOR 0x14, ZERO
68 VECTOR 0x15, ZERO
69 VECTOR 0x16, ZERO
70 VECTOR 0x17, ZERO
71 VECTOR 0x18, ZERO
72 VECTOR 0x19, ZERO
73 VECTOR 0x1A, ZERO
74 VECTOR 0x1B, ZERO
75 VECTOR 0x1C, ZERO
76 VECTOR 0x1D, ZERO
77 VECTOR 0x1E, ERROR_CODE
78 VECTOR 0x1F, ZERO
79 VECTOR 0x20, ZERO ;时钟中断对应的入口
80 VECTOR 0x21, ZERO ;键盘中断
81 VECTOR 0x22, ZERO ;级联
82 VECTOR 0x23, ZERO ;串口2
83 VECTOR 0x24, ZERO ;串口1
84 VECTOR 0x25, ZERO ;并口2
85 VECTOR 0x26, ZERO ;软盘
86 VECTOR 0x27, ZERO ;并口1
87 VECTOR 0x28, ZERO ;实时时钟
88 VECTOR 0x29, ZERO ;重定向
89 VECTOR 0x2A, ZERO ;保留
90 VECTOR 0x2B, ZERO ;保留
91 VECTOR 0x2C, ZERO ;ps/2 鼠标
92 VECTOR 0x2D, ZERO ;fpu 浮点单元异常
93 VECTOR 0x2E, ZERO ;硬盘
94 VECTOR 0x2F, ZERO ;保留
95
96
97
98
99
kernel.S
1 #ifndef __KERNEL_GLOBAL_H 2 #define __KERNEL_GLOBAL_H 3 #include "stdint.h" 4 5 /************ GDT描述符属性**************/ 6 #define RPL0 0 7 #define RPL1 1 8 #define RPL2 2 9 #define RPL3 3 10 11 #define TI_GDT 0 12 #define TI_LDT 1 13 14 #define DESC_G_4K 1 15 #define DESC_D_32 1 16 #define DESC_L 0 17 #define DESC_AVL 0 18 #define DESC_P 1 19 #define DESC_DPL_0 0 20 #define DESC_DPL_1 1 21 #define DESC_DPL_2 2 22 #define DESC_DPL_3 3 23 24 #define DESC_S_CODE 1 25 #define DESC_S_DATA DESC_S_CODE 26 #define DESC_S_SYS 0 27 #define DESC_TYPE_CODE 8 28 29 #define DESC_TYPE_DATA 2 30 #define DESC_TYPE_TSS 9 31 32 #define SELECTOR_K_CODE ((1 << 3) + (TI_GDT << 2) + RPL0) 33 #define SELECTOR_K_DATA ((2 << 3) + (TI_GDT << 2) + RPL0) 34 #define SELECTOR_K_STACK SELECTOR_K_DATA 35 #define SELECTOR_K_GS ((3 << 3) + (TI_GDT << 2) + RPL0) 36 37 /*用户进程的段描述符*/ 38 #define SELECTOR_U_CODE ((5 << 3) + (TI_GDT << 2) + RPL3) 39 #define SELECTOR_U_DATA ((6 << 3) + (TI_GDT << 2) + RPL3) 40 #define SELECTOR_U_STACK SELECTOR_U_DATA 41 42 43 #define GDT_ATTR_HIGH ((DESC_G_4K << 7) + (DESC_D_32 << 6) + (DESC_L << 5) + (DESC_AVL << 4)) 44 #define GDT_CODE_ATTR_LOW_DPL3 ((DESC_P << 7) + (DESC_DPL_3 << 5) + (DESC_S_CODE << 4) + DESC_TYPE_CODE) 45 #define GDT_DATA_ATTR_LOW_DPL3 ((DESC_P << 7) + (DESC_DPL_3 << 5) + (DESC_S_DATA << 4) + DESC_TYPE_DATA) 46 47 48 49 50 51 /******************** IDT描述符属性**********************/ 52 #define IDT_DESC_P 1 //1表示在内存中 53 #define IDT_DESC_DPL0 0 54 #define IDT_DESC_DPL3 3 55 #define IDT_DESC_32_TYPE 0xE 56 #define IDT_DESC_16_TYPE 0x6 57 58 #define IDT_DESC_ATTR_DPL0 \ 59 ((IDT_DESC_P << 7) + (IDT_DESC_DPL0 << 5) + IDT_DESC_32_TYPE) 60 #define IDT_DESC_ATTR_DPL3 \ 61 ((IDT_DESC_P << 7) + (IDT_DESC_DPL3 << 5) + IDT_DESC_32_TYPE) 62 63 64 65 /******************** TSS描述符属性**********************/ 66 #define TSS_DESC_D 0 67 #define TSS_ATTR_HIGH ((DESC_G_4K << 7) + (TSS_DESC_D << 6) + (DESC_L << 5) + (DESC_AVL << 4) + 0x0) 68 #define TSS_ATTR_LOW ((DESC_P << 7) + (DESC_DPL_0 << 5) + (DESC_S_SYS << 4) + DESC_TYPE_TSS) 69 70 #define SELECTOR_TSS ((4 << 3) + (TI_GDT << 2) + RPL0) 71 72 73 /*定义GDT中描述符的结构*/ 74 struct gdt_desc { 75 uint16_t limit_low_word; 76 uint16_t base_low_word; 77 uint8_t base_mid_byte; 78 uint8_t attr_low_byte; 79 uint8_t limit_high_attr_high; 80 uint8_t base_high_byte; 81 }; 82 83 84 85 #define EFLAGS_MBS (1 << 1) //此项必须设置 86 #define EFLAGS_IF_1 (1 << 9) //开中断 87 #define EFLAGS_IF_0 0 //关中断 88 #define EFLAGS_IOPL_3 (3 << 12) 89 90 #define EFLAGS_IOPL_0 (0 << 12) 91 92 #define NULL ((void *)0) 93 #define DIV_ROUND_UP(X, STEP) ((X + STEP - 1) / (STEP)) 94 95 #define PG_SIZE 4096 96 97 #endifglobal.h
此外还需要在project/kernel目录下新建timer.c和timer.h文件,因为我们本回测试会用上定时器中断,所以关于定时器8253的初始化以及中断函数注册就在这两个文件中实现了,定时器的介绍就请参阅原书《操作系统真象还原》P346~356页。
1 #include "timer.h"
2 #include "io.h"
3 #include "print.h"
4 #include "interrupt.h"
5
6 #define IRQ0_FREQUENCY 100
7 #define INPUT_FREQUENCY 1193180
8 #define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY
9 #define COUNTER0_PORT 0x40
10 #define COUNTER0_NO 0
11 #define COUNTER_MODE 2
12 #define READ_WRITE_LATCH 3
13 #define PIT_COUNTROL_PORT 0x43
14
15
16 static void intr_timer_handler(uint8_t vec_nr) {
17 put_str("int vector: 0x");
18 put_int(vec_nr);
19 put_char('\n');
20 }
21
22 static void frequency_set(uint8_t counter_port ,uint8_t counter_no,uint8_t rwl,uint8_t counter_mode,uint16_t counter_value)
23 {
24 outb(PIT_COUNTROL_PORT, (uint8_t) (counter_no << 6 | rwl << 4 | counter_mode << 1));
25 outb(counter_port, (uint8_t)counter_value);
26 outb(counter_port, (uint8_t)counter_value >> 8);
27 }
28
29 void timer_init(void)
30 {
31 put_str("timer_init start!\n");
32 frequency_set(COUNTER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);
33 register_handler(0x20, intr_timer_handler);
34 put_str("timer_init done!\n");
35 }
timer.c
1 #ifndef __KERNEL_TIMER_H
2 #define __KERNEL_TIMER_H
3 #include "stdint.h"
4
5 void timer_init(void);
6 static void intr_timer_handler(uint8_t vec_nr);
7
8 #endif
timer.h
最后,我们在project/kernel目录下新建init.c和init.h文件,init.c中有一个init.all函数,该函数用来包含所有初始化函数,比如到目前为止的中断初始化以及定时器初始化,后面还会有更多的初始化函数加入进来。
1 #include "init.h"
2 #include "print.h"
3 #include "interrupt.h"
4 #include "timer.h"
5
6 void init_all(void)
7 {
8 put_str("init_all\n");
9 idt_init(); //初始化中断
10 timer_init(); //初始化定时器
11 }
init.c
1 #ifndef __KERNEL_INIT_H
2 #define __KERNEL_INIT_H
3 void init_all(void);
4 #endif
init.h
四、中断的压栈和出栈过程分析
下面我们通过一个例子来分析一下整个中断的压栈和出栈过程,从而助于我们更好地了解整个中断过程。
这天CPU正在埋头苦干。突然,外设定时器计数溢出了,我们的中断控制器8259A接收到这一消息,马上火急火燎地跑来通知CPU(给予CPU当前发生中断的中断号:0x20),CPU知道这一消息,脑子突然一转,就这一瞬间的功夫,就做了两件事:1、判断是否有特权级转移:当前被中断的进程,也就是CPU正在做的事,处于0特权级,而中断处理程序也是0特权级,所以没有发生特权级转移。(如果发生了特权级转移,此时需要把低特权级的栈段选择子SS和栈指针esp保存在栈中)2、在当前栈中压入标志寄存器eflags、压入返回地址cs和eip(先压入cs,后压入eip,这个地址就是CPU正在执行的任务的地址,等CPU执行完中断后需要继续回来执行任务,所以需要把地址记下来,免得CPU找不到回来的路),如果对应的中断有相应的错误码,还会将错误码压入栈中,否则就结束(关于中断错误码,忘记的同学可以回看上一回)。
随后,根据中断号,CPU在IDT表索引找到了对应的中断描述符,由该描述符寻找到进入中断处理函数的函数地址,继而跳转过去,函数内容如下:
1 intr%1entry: 2 %2 3 push ds 4 push es 5 push fs 6 push gs 7 pushad 8 9 ;8259A相关设置 10 mov al, 0x20 11 out 0xa0, al 12 out 0x20, al 13 14 push %1 ;将中断号压栈 15 call [idt_table + %1*4] ;调用中断处理函数 16 jmp intr_exit ;退出中断
在该函数中,首先,如果前面已经压入了错误码,那么%2的作用就是执行nop,延时,对栈和寄存器都没有影响。如果没有压入错误码,就执行push 0的指令,这样做的目的只是为了兼容两种不同的压栈方式。随后便是将4个段寄存器ds、es、fs、gs和8个32位的通用寄存器压栈,保护现场。最后通过push %1将中断号压入栈,通过调用call[idt_table + %1*4]进入真正的中断处理函数,这里又会压入当前函数的返回地址cs和eip。所以此时的栈的情况如下左图所示。
中断处理函数就是我们定义的intr_timer_handler函数,我们在该函数中打印一次中断号0x20,随后通过栈内的地址返回,此时来到了jmp intr_exit命令,我们来看看intr_exit指的是什么,以及此时栈的情况,如上右图。
1 static void intr_timer_handler(uint8_t vec_nr) { 2 put_str("int vector: 0x"); 3 put_int(vec_nr); 4 put_char('\n'); 5 }
1 intr_exit: 2 add esp, 4 3 popad 4 pop gs 5 pop fs 6 pop es 7 pop ds 8 add esp, 4 9 iretd
首先通过add esp,4将栈顶指针加4,也就是跳过0x20这个地方,0x20是我们前面手动压入的中断号。随后通过popad将8个32位通用寄存器弹出,再通过pop指令将gs、fs、es和弹出,add esp,4跳过栈中0的位置,这个0也是我们前面手动压入的。此时栈的情况如下:
这里还剩下iretd指令没有执行了,这个指令的作用就是将eip、cs和eflags弹出,前提是当前栈顶为eip,也就是我们必须保证栈顶是eip,才能使该指令执行正确。那么此时我们栈顶的确是eip。该指令执行完毕后,通过前面压入的cs和eip,也就是函数返回地址,CPU又回去继续执行前面被中断所打断的任务了。讲到这里我们就将整个中断压栈和出栈的过程讲解完毕了。
啰嗦了这么多,总算结束了。这里还没有涉及到有特权级转换的情况,其实区别不会太大,等到了进程的章节再来补充。
五、运行测试
更新一下我们的main.c文件:
1 #include "print.h"
2 #include "init.h"
3 #include "timer.h"
4 #include "interrupt.h"
5
6 void main(void)
7 {
8 put_str("I am Kernel\n");
9 init_all();
10 asm volatile("sti");
11 while (1);
12 }
main.c
接下来通过这一长串的操作步骤将程序编译链接下载到磁盘中去:
gcc -m32 -I project/kernel/ -I project/lib/kernel/ -c -fno-builtin project/kernel/timer.c -o project/kernel/timer.o gcc -m32 -I project/kernel/ -I project/lib/kernel/ -c -fno-builtin project/kernel/main.c -o project/kernel/main.o gcc -m32 -I project/kernel/ -I project/lib/kernel/ -c -fno-builtin -o project/kernel/init.o project/kernel/init.c gcc -m32 -I ./project/kernel/ -I ./project/lib/kernel/ -c -fno-builtin -o ./project/kernel/interrupt.o ./project/kernel/interrupt.c nasm -f elf -o ./project/lib/kernel/print.o ./project/lib/kernel/print.S nasm -f elf -o ./project/kernel/kernel.o ./project/kernel/kernel.S ld -m elf_i386 -Ttext 0xc0001500 -e main -o build/kernel.bin project/kernel/main.o project/kernel/init.o project/kernel/interrupt.o project/lib/kernel/print.o project/kernel/kernel.o project/kernel/timer.o dd if=./build/kernel.bin of=./hd60M.img bs=512 count=200 seek=9 conv=notrunc
启动bochs,可以看到int vector:0x20的出现,这里是因为我们初始化定时器后,让定时器在每次定时溢出后触发中断(定时器的中断号为0x20),在定时器的中断处理函数中,我们打印定时器的中断号。这样也就证明了我们的程序没有问题。
好了,本回到此结束了,可以看到代码一多起来后,光是编译下载都挺麻烦的,所以接下来我们需要改进一下。预知后事如何,请看下回分解。