从零实现一个操作系统-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;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值