從零實現一個操作系統-day11

我的博客startcraft

IDT

昨天弄完了GDT,今天來弄IDT,IDT就是中斷描述符表。和GDT類似。
中斷就是一個電信號,它可以打斷cpu當前的操作,讓cpu執行你指定的中斷處理函數,等執行完,cpu會回去繼續執行它之前的操作,就像你移動鼠標,就是產生了一箇中斷,讓cpu先幫你移動鼠標。
既然打斷cpu讓他執行你指定的中斷處理函數,那麼中斷就是有編號的,讓cpu知道應該執行哪一個中斷處理函數,intel的處理器支持256箇中斷,也就是有0-255箇中斷編號
和GDT一樣,有中斷描述符,大小爲8字節,也是放在內存中,由IDTR寄存器保持IDT的位置和大小

include/idt.h(部分)

#ifndef INCLUDE_IDT_H_
#define INCLUDE_IDT_H_

#include "types.h"

// 初始化中斷描述符表
void init_idt();

// 中斷描述符
typedef
struct idt_entry_t {
    uint16_t base_lo;        // 中斷處理函數地址 15~0 位
    uint16_t sel;            // 目標代碼段描述符選擇子
    uint8_t  always0;        // 置 0 段
    uint8_t  flags;          // 一些標誌,文檔有解釋
    uint16_t base_hi;        // 中斷處理函數地址 31~16 位
}__attribute__((packed)) idt_entry_t;

// IDTR
typedef
struct idt_ptr_t {
    uint16_t limit;     // 限長
    uint32_t base;      // 基址
} __attribute__((packed)) idt_ptr_t;

上面定義了中斷描述符的結構體,和IDTR

中斷產生的過程

1.cpu在執行完一條語句之後會去看有沒有中斷產生,如果有則等待對應的時鐘信號到來之後去讀取中斷請求,通過中斷向量去IDT中獲取該中斷的中斷描述符。
2.中斷描述符有該中斷處理函數的段選擇子,然後用段選擇子去GDT中獲得該函數的段基址和屬性信息,然後是一些判斷
3.判斷通過之後cpu會保存當前的現場,就是將一些寄存器的值壓入棧中,然後去中斷描述符裏指定的中斷處理函數的地址執行該中斷函數
4.執行完成後,cpu將棧中的值拿回來也就是恢復現場,繼續之前的任務
由於cpu自動壓入棧中的數據是不夠的,我們要手動壓入一些寄存器的值和一些信息,將這些信息組成一個結構體:

include/idt.h

// 寄存器類型
typedef
struct pt_regs_t {
    uint32_t ds;        // 用於保存用戶的數據段描述符
    uint32_t edi;       // 從 edi 到 eax 由 pusha 指令壓入
    uint32_t esi; 
    uint32_t ebp;
    uint32_t esp;
    uint32_t ebx;
    uint32_t edx;
    uint32_t ecx;
    uint32_t eax;
    uint32_t int_no;    // 中斷號
    uint32_t err_code;      // 錯誤代碼(有中斷錯誤代碼的中斷會由CPU壓入)
    uint32_t eip;       // 以下由處理器自動壓入
    uint32_t cs;        
    uint32_t eflags;
    uint32_t useresp;
    uint32_t ss;
} pt_regs;

// 定義中斷處理函數指針
typedef void (*interrupt_handler_t)(pt_regs *);

// 註冊一箇中斷處理函數
void register_interrupt_handler(uint8_t n, interrupt_handler_t h);

// 調用中斷處理函數
void isr_handler(pt_regs *regs);

// 聲明中斷處理函數 0-19 屬於 CPU 的異常中斷
// ISR:中斷服務程序(interrupt service routine)
void isr0();        // 0 #DE 除 0 異常 
void isr1();        // 1 #DB 調試異常 
void isr2();        // 2 NMI 
void isr3();        // 3 BP 斷點異常 
void isr4();        // 4 #OF 溢出 
void isr5();        // 5 #BR 對數組的引用超出邊界 
void isr6();        // 6 #UD 無效或未定義的操作碼 
void isr7();        // 7 #NM 設備不可用(無數學協處理器) 
void isr8();        // 8 #DF 雙重故障(有錯誤代碼) 
void isr9();        // 9 協處理器跨段操作 
void isr10();       // 10 #TS 無效TSS(有錯誤代碼) 
void isr11();       // 11 #NP 段不存在(有錯誤代碼) 
void isr12();       // 12 #SS 棧錯誤(有錯誤代碼) 
void isr13();       // 13 #GP 常規保護(有錯誤代碼) 
void isr14();       // 14 #PF 頁故障(有錯誤代碼) 
void isr15();       // 15 CPU 保留 
void isr16();       // 16 #MF 浮點處理單元錯誤 
void isr17();       // 17 #AC 對齊檢查 
void isr18();       // 18 #MC 機器檢查 
void isr19();       // 19 #XM SIMD(單指令多數據)浮點異常

// 20-31 Intel 保留
void isr20();
void isr21();
void isr22();
void isr23();
void isr24();
void isr25();
void isr26();
void isr27();
void isr28();
void isr29();
void isr30();
void isr31();

// 32~255 用戶自定義異常
void isr255();

#endif  // INCLUDE_IDT_H_

上面定義了一個結構體,裏面是要保護的寄存器的值和中斷編號什麼的信息,然後定義了一個函數指針類型interrupt_handler_t,這個函數指針類型定義的函數指針指向的是返回值爲void,參數爲pt_regs *的函數,也就是我們自己寫的中斷處理函數的原型。
再下面定義了一系列的中斷服務程序,發現這些函數的類型與函數指針定義的類型不一樣,因爲這些不是中斷處理函數,而是要在他們當中調用中斷處理函數,它們還要做寄存器的操作。
在執行每一箇中斷處理函數之前都要進行保護現場,執行完成之後會恢復現場,我們可以把這些相同的操作提取出來,既然涉及到寄存器的修改就上彙編了

idt/idt_s.s

; 定義兩個構造中斷處理函數的宏(有的中斷有錯誤代碼,有的沒有)
; 用於沒有錯誤代碼的中斷
%macro ISR_NOERRCODE 1
[GLOBAL isr%1]
isr%1:
    cli                         ; 首先關閉中斷
    push 0                      ; push 無效的中斷錯誤代碼(起到佔位作用,便於所有isr函數統一清棧)
    push %1                     ; push 中斷號
    jmp isr_common_stub
%endmacro

; 用於有錯誤代碼的中斷
%macro ISR_ERRCODE 1
[GLOBAL isr%1]
isr%1:
    cli                         ; 關閉中斷
    push %1                     ; push 中斷號
    jmp isr_common_stub
%endmacro

; 定義中斷處理函數
ISR_NOERRCODE  0    ; 0 #DE 除 0 異常
ISR_NOERRCODE  1    ; 1 #DB 調試異常
ISR_NOERRCODE  2    ; 2 NMI
ISR_NOERRCODE  3    ; 3 BP 斷點異常 
ISR_NOERRCODE  4    ; 4 #OF 溢出 
ISR_NOERRCODE  5    ; 5 #BR 對數組的引用超出邊界 
ISR_NOERRCODE  6    ; 6 #UD 無效或未定義的操作碼 
ISR_NOERRCODE  7    ; 7 #NM 設備不可用(無數學協處理器) 
ISR_ERRCODE    8    ; 8 #DF 雙重故障(有錯誤代碼) 
ISR_NOERRCODE  9    ; 9 協處理器跨段操作
ISR_ERRCODE   10    ; 10 #TS 無效TSS(有錯誤代碼) 
ISR_ERRCODE   11    ; 11 #NP 段不存在(有錯誤代碼) 
ISR_ERRCODE   12    ; 12 #SS 棧錯誤(有錯誤代碼) 
ISR_ERRCODE   13    ; 13 #GP 常規保護(有錯誤代碼) 
ISR_ERRCODE   14    ; 14 #PF 頁故障(有錯誤代碼) 
ISR_NOERRCODE 15    ; 15 CPU 保留 
ISR_NOERRCODE 16    ; 16 #MF 浮點處理單元錯誤 
ISR_ERRCODE   17    ; 17 #AC 對齊檢查 
ISR_NOERRCODE 18    ; 18 #MC 機器檢查 
ISR_NOERRCODE 19    ; 19 #XM SIMD(單指令多數據)浮點異常

; 20~31 Intel 保留
ISR_NOERRCODE 20
ISR_NOERRCODE 21
ISR_NOERRCODE 22
ISR_NOERRCODE 23
ISR_NOERRCODE 24
ISR_NOERRCODE 25
ISR_NOERRCODE 26
ISR_NOERRCODE 27
ISR_NOERRCODE 28
ISR_NOERRCODE 29
ISR_NOERRCODE 30
ISR_NOERRCODE 31
; 32~255 用戶自定義
ISR_NOERRCODE 255

[GLOBAL isr_common_stub]
[EXTERN isr_handler]
; 中斷服務程序
isr_common_stub:
    pusha                    ; Pushes edi, esi, ebp, esp, ebx, edx, ecx, eax
    mov ax, ds
    push eax                ; 保存數據段描述符
    
    mov ax, 0x10            ; 加載內核數據段描述符表
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    
    push esp        ; 此時的 esp 寄存器的值等價於 pt_regs 結構體的指針
    call isr_handler        ; 在 C 語言代碼裏
    add esp, 4      ; 清除壓入的參數
    
    pop ebx                 ; 恢復原來的數據段描述符
    mov ds, bx
    mov es, bx
    mov fs, bx
    mov gs, bx
    mov ss, bx
    
    popa                     ; Pops edi, esi, ebp, esp, ebx, edx, ecx, eax
    add esp, 8               ; 清理棧裏的 error code 和 ISR
    iret
.end:

首先上面定義了兩個宏用來生成中斷服務程序,也就是之前頭文件聲明的那些void isr1(),這兩個宏指定的參數就一個,然後下面調用這個宏生成中斷服務程序,一共調用了33次,生成了33個不同的中斷服務程序.(調用宏就是把調用的第一個參數替換宏中%1的位置)
宏最後的jump就是跳轉到每一箇中斷服務程序公共的部分,也就是保護現場,調用中斷處理函數,恢復現場這些操作,其中call isr_handler調用的是c寫的函數
idt/idt.c

// 調用中斷處理函數
void isr_handler(pt_regs *regs)
{
    if (interrupt_handlers[regs->int_no]) {
          interrupt_handlers[regs->int_no](regs);
    } else {
        printk_color(rc_black, rc_blue, "Unhandled interrupt: %d\n", regs->int_no);
    }
}

這個函數的參數也是pt_regs *regs,x86下彙編調用c的函數參數傳遞是通過堆棧進行的,在上面的彙編代碼中將需要保護的寄存器的數據和錯誤代碼,中斷號等數據已經壓入棧中了,這時候的棧指針esp就相當於pt_regs *regs,所以將esp壓入棧中傳遞給c的函數
在該函數中interrupt_handlers是一個數組,數組元素是中斷處理函數指針,所有這個函數做的事就是判斷當前產生的這號中斷有沒有註冊中斷處理函數,若有就調用它,沒有就輸出提示信息,那麼相應的註冊中斷處理函數的函數就是
idt/idt.c

// 註冊一箇中斷處理函數
void register_interrupt_handler(uint8_t n, interrupt_handler_t h)
{
    interrupt_handlers[n] = h;
}

就是將函數指針放入對應的中斷號位置
最後是中斷描述符表的創建和加載

idt/idt.c

#include "common.h"
#include "string.h"
#include "debug.h"
#include "idt.h"

// 中斷描述符表
idt_entry_t idt_entries[256];

// IDTR
idt_ptr_t idt_ptr;

// 中斷處理函數的指針數組
interrupt_handler_t interrupt_handlers[256];

// 設置中斷描述符
static void idt_set_gate(uint8_t num, uint32_t base, uint16_t sel, uint8_t flags);

// 聲明加載 IDTR 的函數
extern void idt_flush(uint32_t);

// 初始化中斷描述符表
void init_idt()
{   
    bzero((uint8_t *)&interrupt_handlers, sizeof(interrupt_handler_t) * 256);
    
    idt_ptr.limit = sizeof(idt_entry_t) * 256 - 1;
    idt_ptr.base  = (uint32_t)&idt_entries;
    
    bzero((uint8_t *)&idt_entries, sizeof(idt_entry_t) * 256);

    // 0-32:  用於 CPU 的中斷處理
    idt_set_gate( 0, (uint32_t)isr0,  0x08, 0x8E);
    idt_set_gate( 1, (uint32_t)isr1,  0x08, 0x8E);
    idt_set_gate( 2, (uint32_t)isr2,  0x08, 0x8E);
    idt_set_gate( 3, (uint32_t)isr3,  0x08, 0x8E);
    idt_set_gate( 4, (uint32_t)isr4,  0x08, 0x8E);
    idt_set_gate( 5, (uint32_t)isr5,  0x08, 0x8E);
    idt_set_gate( 6, (uint32_t)isr6,  0x08, 0x8E);
    idt_set_gate( 7, (uint32_t)isr7,  0x08, 0x8E);
    idt_set_gate( 8, (uint32_t)isr8,  0x08, 0x8E);
    idt_set_gate( 9, (uint32_t)isr9,  0x08, 0x8E);
    idt_set_gate(10, (uint32_t)isr10, 0x08, 0x8E);
    idt_set_gate(11, (uint32_t)isr11, 0x08, 0x8E);
    idt_set_gate(12, (uint32_t)isr12, 0x08, 0x8E);
    idt_set_gate(13, (uint32_t)isr13, 0x08, 0x8E);
    idt_set_gate(14, (uint32_t)isr14, 0x08, 0x8E);
    idt_set_gate(15, (uint32_t)isr15, 0x08, 0x8E);
    idt_set_gate(16, (uint32_t)isr16, 0x08, 0x8E);
    idt_set_gate(17, (uint32_t)isr17, 0x08, 0x8E);
    idt_set_gate(18, (uint32_t)isr18, 0x08, 0x8E);
    idt_set_gate(19, (uint32_t)isr19, 0x08, 0x8E);
    idt_set_gate(20, (uint32_t)isr20, 0x08, 0x8E);
    idt_set_gate(21, (uint32_t)isr21, 0x08, 0x8E);
    idt_set_gate(22, (uint32_t)isr22, 0x08, 0x8E);
    idt_set_gate(23, (uint32_t)isr23, 0x08, 0x8E);
    idt_set_gate(24, (uint32_t)isr24, 0x08, 0x8E);
    idt_set_gate(25, (uint32_t)isr25, 0x08, 0x8E);
    idt_set_gate(26, (uint32_t)isr26, 0x08, 0x8E);
    idt_set_gate(27, (uint32_t)isr27, 0x08, 0x8E);
    idt_set_gate(28, (uint32_t)isr28, 0x08, 0x8E);
    idt_set_gate(29, (uint32_t)isr29, 0x08, 0x8E);
    idt_set_gate(30, (uint32_t)isr30, 0x08, 0x8E);
    idt_set_gate(31, (uint32_t)isr31, 0x08, 0x8E);

    // 255 將來用於實現系統調用
    idt_set_gate(255, (uint32_t)isr255, 0x08, 0x8E);

    // 更新設置中斷描述符表
    idt_flush((uint32_t)&idt_ptr);
}

// 設置中斷描述符
static void idt_set_gate(uint8_t num, uint32_t base, uint16_t sel, uint8_t flags)
{
    idt_entries[num].base_lo = base & 0xFFFF;
    idt_entries[num].base_hi = (base >> 16) & 0xFFFF;

    idt_entries[num].sel     = sel;
    idt_entries[num].always0 = 0;

    // 先留下 0x60 這個魔數,以後實現用戶態時候
    // 這個與運算可以設置中斷門的特權級別爲 3
    idt_entries[num].flags = flags;  // | 0x60
}

// 調用中斷處理函數
void isr_handler(pt_regs *regs)
{
    if (interrupt_handlers[regs->int_no]) {
          interrupt_handlers[regs->int_no](regs);
    } else {
        printk_color(rc_black, rc_blue, "Unhandled interrupt: %d\n", regs->int_no);
    }
}

// 註冊一箇中斷處理函數
void register_interrupt_handler(uint8_t n, interrupt_handler_t h)
{
    interrupt_handlers[n] = h;
}

和GDT類似,這裏先將保存中斷處理函數指針的數組初始化爲0,然後初始化IDTR,和GDT不同的是,GDT的數組長度是我們指定的,而IDT的數組長度是256,因爲一共有256箇中斷描述符,再然後將之前定義的33箇中斷服務程序寫入IDT,最後更新IDTR寄存器。
idt/idt_s.s

[GLOBAL idt_flush]
idt_flush:
    mov eax, [esp+4]  ; 參數存入 eax 寄存器
    lidt [eax]        ; 加載到 IDTR
    ret
.end:

測試一下,修改init/entry.c

#include "console.h"
#include "debug.h"
#include "gdt.h"
#include "idt.h"
int kern_entry()
{
    init_debug();
    init_gdt();
    init_idt();
    console_clear();

    printk_color(rc_black, rc_green, "Hello, OS kernel!\n");

    asm volatile ("int $0x3");
    asm volatile ("int $0x4");
    return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章