1. 程式人生 > >【自制作業系統09】中斷的程式碼實現

【自制作業系統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 }
main.c
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 }
init.c
  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 }
interrupt.c
 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
kernel.asm 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
global.h
1 #ifndef __KERNEL_INIT_H
2 #define __KERNEL_INIT_H
3 void init_all(void);
4 #endif
init.h
 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
interrupt.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 對埠指定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

三、程式碼解讀

這段程式碼也是看的我理解了好久好久,但其實做的事情真的非常簡單,請看下圖

 程式碼有幾個關鍵的結構,在此列出來

  • gate_desc:中斷描述符的結構
  • idt:中斷描述符表陣列(裡面儲存的是一個個 64 位的中斷描述符,結構就是上述的 gate_desc)
  • intr_entry_table:中斷例程入口地址陣列(裡面儲存的是一個個地址,指向一段程式,該程式主要工作就是跳轉到 intr_table 表,這裡面存的是真正的中斷處理程式,如下)
  • intr_table:中斷例程地址陣列(裡面儲存的是一個個地址,指向真正的中斷處理程式)
  • general_intr_handler:通用中斷處理程式的方法
  • register_handler:個性化的中斷處理程式的方法(用來代替上面的 general_intr_handler 方法,本章暫時沒有)

 整個程式碼即使完成了這樣幾件事:

  1. 在記憶體某位置存放一個 中斷描述符表陣列 idt
  2. 該 idt 中的每一箇中斷描述符,讓其中斷例程地址欄位,指向 中斷例程入口陣列 intr_entry_table 的每一個元素
  3. intr_entry_table 的每一個元素對應的是一段入口程式,該程式核心程式碼是 call [intr_table + i * 4],其目的是指向 中斷例程地址陣列 intr_table 的每一個元素
  4. intr_table 的每一個元素就是真正的中斷程式的程式碼地址,本講中的處理程式碼 general_intr_handler 僅僅是簡單地列印一個字串,日後用詳細的函式 register_handler 替換
  5. 最後初始化 pic,用 lidt 指令載入中斷描述符表 idt,就開啟了中斷

四、執行

我們看到,時鐘中斷(向量號為 0x20)進來,持續輸出我們中斷處理程式中要輸出的內容 “int vector:中斷向量號”

 

寫在最後:開源專案和課程規劃

如果你對自制一個作業系統感興趣,不妨跟隨這個系列課程看下去,甚至加入我們,一起來開發。

參考書籍

《作業系統真相還原》這本書真的贊!強烈推薦

專案開源

專案開源地址:https://gitee.com/sunym1993/flashos

當你看到該文章時,程式碼可能已經比文章中的又多寫了一些部分了。你可以通過提交記錄歷史來檢視歷史的程式碼,我會慢慢梳理提交歷史以及專案說明文件,爭取給每一課都準備一個可執行的程式碼。當然文章中的程式碼也是全的,採用複製貼上的方式也是完全可以的。

如果你有興趣加入這個自制作業系統的大軍,也可以在留言區留下您的聯絡方式,或者在 gitee 私信我您的聯絡方式。

課程規劃

本課程打算出系列課程,我寫到哪覺得可以寫成一篇文章了就寫出來分享給大家,最終會完成一個功能全面的作業系統,我覺得這是最好的學習作業系統的方式了。所以中間遇到的各種坎也會寫進去,如果你能持續跟進,跟著我一塊寫,必然會有很好的收貨。即使沒有,交個朋友也是好的哈哈。

目前的系列包括

  • 【自制作業系統01】硬核講解計算機的啟動過程
  • 【自制作業系統02】環境準備與啟動區實現
  • 【自制作業系統03】讀取硬碟中的資料
  • 【自制作業系統04】從真實模式到保護模式
  • 【自制作業系統05】開啟記憶體分頁機制
  • 【自制作業系統06】終於開始用 C 語言了,第一行核心程式碼!
  • 【自制作業系統07】深入淺出特權級
  • 【自制作業系統08】中斷

&n