【自制操作系统09】中断的代码实现
由于中断这块的知识和代码都占较大篇幅,因此分成两章来讲,上一讲 【自制操作系统08】中断 讲述了中断的理论知识,本讲开始上代码
一、到目前为止的程序流程图
为了让大家清楚目前的程序进度,画了到目前为止的程序流程图,如下。
右半部分的时序图,就是我们今天要做做的事情,其实一句话就是:初始化中断描述符表,其中中断例程非常简单,只是简单地将中断向量号输出在屏幕上
二、先上代码
主要代码
1 #include "print.h" 2 #include "init.h" 3 void main(void){ 4 put_str("I am kernel\n"); 5 init_all(); 6 asm volatile("sti"); 7 while(1); 8 }
1 #include "init.h" 2 #include "print.h" 3 #include "interrupt.h" 4 5 // 负责初始化所有模块 6 void init_all() { 7 put_str("init_all\n"); 8 idt_init(); 9 }
1 #include "interrupt.h" 2 #include "stdint.h" 3 #include "global.h" 4 #include "io.h" 5 6 7 #define PIC_M_CTRL 0x20 //主片控制端口 8 #define PIC_M_DATA 0x21 //主片数据端口 9 #define PIC_S_CTRL 0xa0 //从片控制端口 10 #define PIC_S_DATA 0xa1 //从片数据端口 11 12 #define IDT_DESC_CNT 0x21 //目前总共支持的中断数 13 14 // 中断门描述符结构体 15 struct gate_desc{ 16 uint16_t func_offset_low_word; 17 uint16_t selector; 18 uint8_t dcount; 19 uint8_t attribute; 20 uint16_t func_offset_high_word; 21 }; 22 23 // 静态函数声明,非必须 24 static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function); 25 // 中断门描述符表的数组 26 static struct gate_desc idt[IDT_DESC_CNT]; 27 // 用于保存异常名 28 char* intr_name[IDT_DESC_CNT]; 29 // 定义中断处理程序数组,在kernel.asm中定义的intrXXentry。只是中断处理程序的入口,最终调用idt_table中的处理程序 30 intr_handler idt_table[IDT_DESC_CNT]; 31 // 声明引用定义在kernel.asm中的中断处理函数入口数组 32 extern intr_handler intr_entry_table[IDT_DESC_CNT]; 33 // 初始化可编程中断控制器 8259A 34 static void pic_init(void) { 35 36 /*初始化主片 */ 37 outb (PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4 38 outb (PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20, 也就是IR[0-7] 为 0x20 ~ 0x27 39 outb (PIC_M_DATA, 0x04); // ICW3: IR2 接从片 40 outb (PIC_M_DATA, 0x01); // ICW4: 8086 模式, 正常EOI 41 42 /*初始化从片 */ 43 outb (PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4 44 outb (PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28, 也就是IR[8-15]为0x28 ~ 0x2F 45 outb (PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2 引脚 46 outb (PIC_S_DATA, 0x01); // ICW4: 8086 模式, 正常EOI 47 48 /*打开主片上IR0,也就是目前只接受时钟产生的中断 */ 49 outb (PIC_M_DATA, 0xfe); 50 outb (PIC_S_DATA, 0xff); 51 52 put_str(" pic_init done\n"); 53 } 54 55 //创建中断门描述符 56 static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) { 57 p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF; 58 p_gdesc->selector = SELECTOR_K_CODE; 59 p_gdesc->dcount = 0; 60 p_gdesc->attribute = attr; 61 p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16; 62 } 63 64 // 初始化中断描述符表 65 static void idt_desc_init(void) { 66 int i; 67 for(i = 0; i < IDT_DESC_CNT; i++) { 68 make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]); 69 } 70 put_str(" idt_desc_init done\n"); 71 } 72 73 // 通用的中断处理函数,一般用在异常出现时的处理 74 static void general_intr_handler(uint8_t vec_nr) { 75 if(vec_nr == 0x27 || vec_nr == 0x2f) { 76 return; 77 } 78 put_str("int vector:0x"); 79 put_int(vec_nr); 80 put_char('\n'); 81 } 82 83 // 完成一般中断处理函数注册及异常名称注册 84 static void exception_init(void) { 85 int i; 86 for(i = 0; i < IDT_DESC_CNT; i++) { 87 // 默认为这个,以后会由 register_handler 来注册具体处理函数 88 idt_table[i] = general_intr_handler; 89 intr_name[i] = "unknown"; 90 } 91 intr_name[0] = "#DE Divide Error"; 92 intr_name[1] = "#DB Debug Exception"; 93 intr_name[2] = "NMI Interrupt"; 94 intr_name[3] = "#BP Breakpoint Exception"; 95 intr_name[4] = "#OF Overflow Exception"; 96 intr_name[5] = "#BR BOUND Range Exceeded Exception"; 97 intr_name[6] = "#UD Invalid Opcode Exception"; 98 intr_name[7] = "#NM Device Not Available Exception"; 99 intr_name[8] = "#DF Double Fault Exception"; 100 intr_name[9] = "Coprocessor Segment Overrun"; 101 intr_name[10] = "#TS Invalid TSS Exception"; 102 intr_name[11] = "#NP Segment Not Present"; 103 intr_name[12] = "#SS Stack Fault Exception"; 104 intr_name[13] = "#GP General Protection Exception"; 105 intr_name[14] = "#PF Page-Fault Exception"; 106 // intr_name[15] 第 15 项是 intel 保留项,未使用 107 intr_name[16] = "#MF x87 FPU Floating-Point Error"; 108 intr_name[17] = "#AC Alignment Check Exception"; 109 intr_name[18] = "#MC Machine-Check Exception"; 110 intr_name[19] = "#XF SIMD Floating-Point Exception"; 111 } 112 113 114 // 完成有关中断到所有初始化工作 115 void idt_init() { 116 put_str("idt_init start\n"); 117 idt_desc_init(); // 初始化中断描述符表 118 exception_init(); // 初始化通用中断处理函数 119 pic_init(); // 初始化8259A 120 121 // 加载idt 122 uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)((uint32_t)idt << 16))); 123 asm volatile("lidt %0" : : "m" (idt_operand)); 124 put_str("idt_init done\n"); 125 }
1 [bits 32] 2 %define ERROR_CODE nop 3 %define ZERO push 0 4 5 extern idt_table 6 7 section .data 8 global intr_entry_table 9 intr_entry_table: 10 11 %macro VECTOR 2 12 section .text 13 intr%1entry: 14 %2 15 push ds 16 push es 17 push fs 18 push gs 19 pushad 20 21 ;如果是从片上进入到中断,除了往从片上发送EOI外,还要往主片上发送EOI 22 mov al,0x20 23 out 0xa0,al 24 out 0x20,al 25 26 push %1 27 call [idt_table + %1*4] 28 jmp intr_exit 29 30 section .data 31 dd intr%1entry 32 %endmacro 33 34 section .text 35 global intr_exit 36 intr_exit: 37 add esp,4 38 popad 39 pop gs 40 pop fs 41 pop es 42 pop ds 43 add esp,4 44 iretd 45 46 VECTOR 0X00,ZERO 47 VECTOR 0X01,ZERO 48 VECTOR 0X02,ZERO 49 VECTOR 0X03,ZERO 50 VECTOR 0X04,ZERO 51 VECTOR 0X05,ZERO 52 VECTOR 0X06,ZERO 53 VECTOR 0X07,ZERO 54 VECTOR 0X08,ZERO 55 VECTOR 0X09,ZERO 56 VECTOR 0X0a,ZERO 57 VECTOR 0X0b,ZERO 58 VECTOR 0X0c,ZERO 59 VECTOR 0X0d,ZERO 60 VECTOR 0X0e,ZERO 61 VECTOR 0X0f,ZERO 62 VECTOR 0X10,ZERO 63 VECTOR 0X11,ZERO 64 VECTOR 0X12,ZERO 65 VECTOR 0X13,ZERO 66 VECTOR 0X14,ZERO 67 VECTOR 0X15,ZERO 68 VECTOR 0X16,ZERO 69 VECTOR 0X17,ZERO 70 VECTOR 0X18,ZERO 71 VECTOR 0X19,ZERO 72 VECTOR 0X1a,ZERO 73 VECTOR 0X1b,ZERO 74 VECTOR 0X1c,ZERO 75 VECTOR 0X1d,ZERO 76 VECTOR 0X1e,ERROR_CODE 77 VECTOR 0X1f,ZERO 78 VECTOR 0X20,ZERO
Makefile
头文件
1 #ifndef __KERNEL_GLOBAL_H 2 #define __KERNEL_GLOBAL_H 3 #include "stdint.h" 4 5 #define RPL0 0 6 #define RPL1 1 7 #define RPL2 2 8 #define RPL3 3 9 10 #define TI_GDT 0 11 #define TI_LDT 1 12 13 #define SELECTOR_K_CODE ((1 << 3) + (TI_GDT << 2) + RPL0) 14 #define SELECTOR_K_DATA ((2 << 3) + (TI_GDT << 2) + RPL0) 15 #define SELECTOR_K_STACK SELECTOR_K_DATA 16 #define SELECTOR_K_GS ((3 << 3) + (TI_GDT << 2) + RPL0) 17 18 /*-------------- IDT 描述符属性 ------------*/ 19 #define IDT_DESC_P 1 20 #define IDT_DESC_DPL0 0 21 #define IDT_DESC_DPL3 3 22 #define IDT_DESC_32_TYPE 0xE // 32 位的门 23 #define IDT_DESC_16_TYPE 0x6 // 16 位的门,不会用到,定义它只为和32 位门区分 24 #define IDT_DESC_ATTR_DPL0 ((IDT_DESC_P << 7) + (IDT_DESC_DPL0 << 5) + IDT_DESC_32_TYPE) 25 #define IDT_DESC_ATTR_DPL3 ((IDT_DESC_P << 7) + (IDT_DESC_DPL3 << 5) + IDT_DESC_32_TYPE) 26 27 #endif
1 #ifndef __KERNEL_INIT_H 2 #define __KERNEL_INIT_H 3 void init_all(void); 4 #endif
1 #ifndef __KERNEL_INTERRUPT_H 2 #define __KERNEL_INTERRUPT_H 3 #include "stdint.h" 4 typedef void* intr_handler; 5 void idt_init(void); 6 7 /* 定义中断的两种状态: 8 * INTR_OFF值为0,表示关中断, 9 * INTR_ON值为1,表示开中断 */ 10 enum intr_status { // 中断状态 11 INTR_OFF, // 中断关闭 12 INTR_ON // 中断打开 13 }; 14 15 enum intr_status intr_get_status(void); 16 enum intr_status intr_set_status (enum intr_status); 17 enum intr_status intr_enable (void); 18 enum intr_status intr_disable (void); 19 void register_handler(uint8_t vector_no, intr_handler function); 20 #endif
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 对端口指定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
三、代码解读
这段代码也是看的我理解了好久好久,但其实做的事情真的非常简单,请看下图
代码有几个关键的结构,在此列出来
- gate_desc:中断描述符的结构
- idt:中断描述符表数组(里面存储的是一个个 64 位的中断描述符,结构就是上述的 gate_desc)
- intr_entry_table:中断例程入口地址数组(里面存储的是一个个地址,指向一段程序,该程序主要工作就是跳转到 intr_table 表,这里面存的是真正的中断处理程序,如下)
- intr_table:中断例程地址数组(里面存储的是一个个地址,指向真正的中断处理程序)
- general_intr_handler:通用中断处理程序的方法
- register_handler:个性化的中断处理程序的方法(用来代替上面的 general_intr_handler 方法,本章暂时没有)
整个代码即使完成了这样几件事:
- 在内存某位置存放一个 中断描述符表数组 idt
- 该 idt 中的每一个中断描述符,让其中断例程地址字段,指向 中断例程入口数组 intr_entry_table 的每一个元素
- intr_entry_table 的每一个元素对应的是一段入口程序,该程序核心代码是 call [intr_table + i * 4],其目的是指向 中断例程地址数组 intr_table 的每一个元素
- intr_table 的每一个元素就是真正的中断程序的代码地址,本讲中的处理代码 general_intr_handler 仅仅是简单地打印一个字符串,日后用详细的函数 register_handler 替换
- 最后初始化 pic,用 lidt 指令加载中断描述符表 idt,就开启了中断
四、运行
我们看到,时钟中断(向量号为 0x20)进来,持续输出我们中断处理程序中要输出的内容 “int vector:中断向量号”
写在最后:开源项目和课程规划
如果你对自制一个操作系统感兴趣,不妨跟随这个系列课程看下去,甚至加入我们(下方有公众号和小助手微信),一起来开发。
参考书籍
《操作系统真相还原》这本书真的赞!强烈推荐
项目开源
当你看到该文章时,代码可能已经比文章中的又多写了一些部分了。你可以通过提交记录历史来查看历史的代码,我会慢慢梳理提交历史以及项目说明文档,争取给每一课都准备一个可执行的代码。当然文章中的代码也是全的,采用复制粘贴的方式也是完全可以的。
如果你有兴趣加入这个自制操作系统的大军,也可以在留言区留下您的联系方式,或者在 gitee 私信我您的联系方式。
课程规划
本课程打算出系列课程,我写到哪觉得可以写成一篇文章了就写出来分享给大家,最终会完成一个功能全面的操作系统,我觉得这是最好的学习操作系统的方式了。所以中间遇到的各种坎也会写进去,如果你能持续跟进,跟着我一块写,必然会有很好的收货。即使没有,交个朋友也是好的哈哈。
目前的系列包括
- 【自制操作系统01】硬核讲解计算机的启动过程
- 【自制操作系统02】环境准备与启动区实现
- 【自制操作系统03】读取硬盘中的数据
- 【自制操作系统04】从实模式到保护模式
- 【自制操作系统05】开启内存分页机制
- 【自制操作系统06】终于开始用 C 语言了,第一行内核代码!
- 【自制操作系统07】深入浅出特权级
- 【自制操作系统08】中断
公众号 - 低并发编程