本文前置内容:中断详解结构体对齐
本节对应分支:interrupt

概述

本节我们为操作系统加入中断,初始化中断描述符表,并为 0~0x2f 中断添加对应的中断处理程序。当前的思路是,在 interrupt.s 中定义实际中断例程的入口函数( 通过入口函数转移到实际中断例程 ),并利用汇编宏技术得到所有入口函数的地址,形成入口函数的地址数组 interrupt_entry_table ;然后在 idt.c 中引入该数组,进而我们能够很方便地向中断描述符中填写入口函数的地址。现在读者可能不明白这个思路的具体含义,别急,下面做具体阐述。

代码解析

interrupt.s

[bits 32]
%define ERROR_CODE nop     ; 若在相关的异常中cpu已经自动压入了错误码,为保持栈中格式统一,这里不做操作.
%define ZERO push 0        ; 若在相关的异常中cpu没有压入错误码,为了统一栈中格式,就手工压入一个0

extern interrupt_handler_table ;声明中断处理函数的指针数组

%macro VECTOR 2
INTERRUPT_ENTRY_%1:        ;中断处理entry
    %2
    push ds
    push es
    push fs
    push gs
    pushad

    push dword %1
    call [interrupt_handler_table + %1*4]  ;进入实际中断函数
    add esp, 4             ;外平栈

    popad
    pop gs
    pop fs
    pop es
    pop ds

    mov al,0x20            ;中断结束命令EOI
    out 0xa0,al            ;向从片发送
    out 0x20,al            ;向主片发送

    add esp,4			   ;跨过error_code,以保持堆栈平衡
    iret				   ;从中断返回,32位下等同指令iretd
%endmacro

;;;;;;;;;以下代码利用宏来定义函数;;;;;;;;;;;;;;;
VECTOR 0x00, ZERO        ;divide by zero
VECTOR 0x01, ZERO        ;debug
VECTOR 0x02, ZERO        ;non maskable interrupt
VECTOR 0x03, ZERO        ;breakpoint
VECTOR 0x04, ZERO        ;overflow
VECTOR 0x05, ZERO        ;bound range exceeded
VECTOR 0x06, ZERO        ;invalid opcode
VECTOR 0x07, ZERO        ;device not avilable
VECTOR 0x08, ERROR_CODE  ;double fault
VECTOR 0x09, ZERO        ;coprocessor segment overrun
VECTOR 0x0a, ERROR_CODE  ;invalid TSS
VECTOR 0x0b, ERROR_CODE  ;segment not present
VECTOR 0x0c, ZERO        ;stack segment fault
VECTOR 0x0d, ERROR_CODE  ;general protection fault
VECTOR 0x0e, ERROR_CODE  ;page fault
VECTOR 0x0f, ZERO        ;reserved
VECTOR 0x10, ZERO        ;x87 floating point exception
VECTOR 0x11, ERROR_CODE  ;alignment check
VECTOR 0x12, ZERO        ;machine check
VECTOR 0x13, ZERO        ;SIMD Floating - Point Exception
VECTOR 0x14, ZERO        ;Virtualization Exception
VECTOR 0x15, ZERO        ;Control Protection Exception
VECTOR 0x16, ZERO        ;reserved
VECTOR 0x17, ZERO        ;reserved
VECTOR 0x18, ERROR_CODE  ;reserved
VECTOR 0x19, ZERO        ;reserved
VECTOR 0x1a, ERROR_CODE  ;reserved
VECTOR 0x1b, ERROR_CODE  ;reserved
VECTOR 0x1c, ZERO        ;reserved
VECTOR 0x1d, ERROR_CODE  ;reserved
VECTOR 0x1e, ERROR_CODE  ;reserved
VECTOR 0x1f, ZERO        ;reserved
VECTOR 0x20, ZERO        ;clock 时钟中断
VECTOR 0x21, ZERO        ;键盘中断
VECTOR 0x22, ZERO        ;级联用的
VECTOR 0x23, ZERO        ;串口2对应的入口
VECTOR 0x24, ZERO        ;串口1对应的入口
VECTOR 0x25, ZERO        ;并口2对应的入口
VECTOR 0x26, ZERO        ;软盘对应的入口
VECTOR 0x27, ZERO        ;并口1对应的入口
VECTOR 0x28, ZERO        ;rtc实时时钟
VECTOR 0x29, ZERO        ;重定向
VECTOR 0x2a, ZERO        ;保留
VECTOR 0x2b, ZERO        ;保留
VECTOR 0x2c, ZERO        ;ps/2鼠标
VECTOR 0x2d, ZERO        ;fpu浮点单元异常
VECTOR 0x2e, ZERO        ;硬盘
VECTOR 0x2f, ZERO        ;保留

;;;;;;;;;中断函数地址表;;;;;;;;;;
global interrupt_entry_table
interrupt_entry_table:
    dd INTERRUPT_ENTRY_0x00
    dd INTERRUPT_ENTRY_0x01
    dd INTERRUPT_ENTRY_0x02
    dd INTERRUPT_ENTRY_0x03
    dd INTERRUPT_ENTRY_0x04
    dd INTERRUPT_ENTRY_0x05
    dd INTERRUPT_ENTRY_0x06
    dd INTERRUPT_ENTRY_0x07
    dd INTERRUPT_ENTRY_0x08
    dd INTERRUPT_ENTRY_0x09
    dd INTERRUPT_ENTRY_0x0a
    dd INTERRUPT_ENTRY_0x0b
    dd INTERRUPT_ENTRY_0x0c
    dd INTERRUPT_ENTRY_0x0d
    dd INTERRUPT_ENTRY_0x0e
    dd INTERRUPT_ENTRY_0x0f
    dd INTERRUPT_ENTRY_0x10
    dd INTERRUPT_ENTRY_0x11
    dd INTERRUPT_ENTRY_0x12
    dd INTERRUPT_ENTRY_0x13
    dd INTERRUPT_ENTRY_0x14
    dd INTERRUPT_ENTRY_0x15
    dd INTERRUPT_ENTRY_0x16
    dd INTERRUPT_ENTRY_0x17
    dd INTERRUPT_ENTRY_0x18
    dd INTERRUPT_ENTRY_0x19
    dd INTERRUPT_ENTRY_0x1a
    dd INTERRUPT_ENTRY_0x1b
    dd INTERRUPT_ENTRY_0x1c
    dd INTERRUPT_ENTRY_0x1d
    dd INTERRUPT_ENTRY_0x1e
    dd INTERRUPT_ENTRY_0x1f
    dd INTERRUPT_ENTRY_0x20
    dd INTERRUPT_ENTRY_0x21
    dd INTERRUPT_ENTRY_0x22
    dd INTERRUPT_ENTRY_0x23
    dd INTERRUPT_ENTRY_0x24
    dd INTERRUPT_ENTRY_0x25
    dd INTERRUPT_ENTRY_0x26
    dd INTERRUPT_ENTRY_0x27
    dd INTERRUPT_ENTRY_0x28
    dd INTERRUPT_ENTRY_0x29
    dd INTERRUPT_ENTRY_0x2a
    dd INTERRUPT_ENTRY_0x2b
    dd INTERRUPT_ENTRY_0x2c
    dd INTERRUPT_ENTRY_0x2d
    dd INTERRUPT_ENTRY_0x2e
    dd INTERRUPT_ENTRY_0x2f

读者可能又会泄气,怎么又用汇编?能用 C 尽量用 C 不行嘛?哈哈,您的心情我表示理解。使用汇编来编写此文件,有以下几点原因:

  1. 用汇编处理错误码更加方便。
  2. 汇编能够直接发出 EOI 信号。
  3. 使用汇编的宏技术,所有宏函数直接展开,非常方便。

当然,你也可以使用 C 语言来书写,笔者认为用 C 语言书写此部分应该可以使内核体积更小,毕竟这里的几十个宏函数未来都会被展开,体积就稍微大些。读者可以写两个 C 函数,分别应对有错误码和无错误码的情况。

接下来剖析代码:

  • 第 5 行,interrupt_handler_table 是位于 idt.c 的指针数组,其中的指针指向实际的中断处理函数。

  • 第 7 行,%macro VECTOR 2 ,这是汇编宏技术。前面我们使用过 equ 宏定义,它只能定义单行宏;对于多行宏,就需要使用 %macro 实现,其声明方式如下:

    %macro 宏名 参数个数
    ........
    代码体
    ........
    %endmacro
    

    如果在代码体中想引用某个参数,则必须用 %数字 的方式来引用,参见第 8 行与第 9 行。我们将宏名定义为 VECTOR,并引入了两个参数。怎么压入参数呢?看第 35~82 行,直接在宏名后接上两个参数即可,参数直接用逗号隔开。注意,宏定义属于预处理指令(伪指令),这些宏会在编译期展开,也就是说,编译后,interrupt.s 中会有 0x30 个第 8~31 行这样的代码段

  • 第 8 行为中断入口标号,代表入口函数的地址,下面定义函数指针的数组时会使用这些标号。

  • 第 9 行,该行有两种情况,一种是 ZERO 宏对应的 push 0 ,另一种是 ERROR_CODE 宏对应的 nop 指令,具体是哪种情况取决于利用宏定义函数时压入的什么参数,参见 35~82 行。注意,对于有错误码的中断,CPU 会自动压入错误码;对于没有错误码的中断,CPU 则不进行动作(nop);然而,对于前者,CPU 在函数返回时主动弹出错误码,必须由我们手动弹出错误码,这点尤其重要! 为了方便操作,有错误码的中断我们不做处理,无错误码的我们就压入 0,这样就统一了各中断函数的弹栈行为,无需特殊处理。关于错误码,参见中断详解

  • 第 10~14 行,保存当前寄存器环境。由于在 17 行,我们调用了 C 语言编写的实际的中断处理函数,这必将破坏当前的寄存器环境,因此需要保存段寄存器和通用寄存器。其他寄存器会由 CPU 自动保存,关于这部分还请参见中断详解

  • 第 16 行,压入中断号,这是实际中断处理函数的参数。在我们的系统中,大多数异常我们不做处理,但发生异常时我们需要知道抛出了哪个异常,因此需要通过中断号来定位错误源。

  • 第 17 行,interrupt_handler_table 是 idt.c 中的数组,该数组中装载的是实际中断处理函数的地址。因为是指针数组,指针大小为 4 字节,因此需要用序号乘 4 才能找到函数的地址。

  • 第 18 行进行平栈,关于平栈请参见函数调用约定

  • 第 26~28 行,发送 EOI 信号,通知 8259A 芯片中断处理结束。这部分内容参考《操作系统-真相还原》。

  • 第 30 行,主动跨过错误码,原因已在前面阐述。

  • 第 86~134 行,定义中断入口数组,即函数指针数组。该数组 interrupt_entry_table[] 会在 dit.c 文件中被引用。

global.h

//文件说明:global.h
#ifndef OSLEARNING_GLOBAL_H
#define OSLEARNING_GLOBAL_H

#define	 RPL0  0
#define	 RPL1  1
#define	 RPL2  2
#define	 RPL3  3

#define TI_GDT 0
#define TI_LDT 1

#define SELECTOR_K_CODE	   ((1 << 3) + (TI_GDT << 2) + RPL0)
#define SELECTOR_K_DATA	   ((2 << 3) + (TI_GDT << 2) + RPL0)
#define SELECTOR_K_STACK   SELECTOR_K_DATA
#define SELECTOR_K_VIDEO   ((3 << 3) + (TI_GDT << 2) + RPL0)

//================IDT描述符P================
#define	 IDT_DESC_P_ON	 1
#define	 IDT_DESC_P_OFF	 0
//================IDT描述符DPL==============
#define	 IDT_DESC_DPL0   0       //为什么只有0和3?
#define	 IDT_DESC_DPL3   3
//==========中断门的s位与type位=================
#define	 IDT_DESC_GATE   0xE     // S=0(系统段),TYPE=1110(32位中断门)
//============================================
struct gate_desc
{
    short offset_L;       // 段内偏移 0 ~ 15 位
    short selector;       // 代码段选择子
    char reserved ;       // 保留不用
    char s_type :5;       // 系统段:任务门/中断门/陷阱门/调用门
    char DPL    :2;       // 使用 int 指令访问的最低权限
    char present:1;       // 是否有效
    short offset_H;       // 段内偏移 16 ~ 31 位
} __attribute__((packed));//声明不要进行对齐
//=================GDT/IDT指针=================
struct xdt_ptr
{
    unsigned short   limit;
    unsigned int     base;
}__attribute__((packed));
//======加载GDT/LDT指针的函数,直接内联===========
static inline void load_xdt(struct xdt_ptr* p, unsigned short limit, unsigned int base)
{
    p->base=base;
    p->limit=limit;
}
#endif //OSLEARNING_GLOBAL_H
  • 结构体 gate_desc 是中断描述符结构。该结构体有两点需要注意:
    1)使用了位域,即为 s_typeDPLpresent 字段按位分配而非按字节分配。可别以为声明了 char 就是分配了一个字节。
    2)结构体声明的末尾 __attribute__((packed)) 是在**指示编译器不要进行结构体对齐,这点很重要** 。详细参考结构体对齐
  • xdt_ptr 是 IDTR/GDTR 的结构体。当前我们只使用 IDTR,后续还会使用 GDTR 。

idt.c

#include "../include/interrupt.h"
#include "../include/global.h"
#include "../include/print.h"
#include "../include/system.h"

static struct xdt_ptr idt_ptr ;
static char*  interrupt_name[IDT_DESC_CNT];
static struct gate_desc idt[IDT_DESC_CNT];                 //idt-中断描述符表
extern intr_handler interrupt_entry_table[IDT_DESC_CNT];   //引用interrupt.s中的中断处理函数入口数组,注意,这是一个指针数组
       intr_handler interrupt_handler_table[IDT_DESC_CNT]; //实际中断处理例程的地址
void make_idt_desc(struct gate_desc* p_desc, unsigned char DPL, intr_handler function) {
    p_desc->offset_L = (unsigned int)function & 0x0000FFFF;       //低16位赋值给offset_L,高位丢弃
    p_desc->offset_H = ((unsigned int)function>>16) & 0x0000FFFF; //低16位赋值给offset_L,高位丢弃
    p_desc->selector = SELECTOR_K_CODE;
    p_desc->reserved = 0;
    p_desc->s_type   = IDT_DESC_GATE;
    p_desc->DPL      = DPL;
    p_desc->present  = IDT_DESC_P_ON;
}

void idt_desc_init() {
    for (int i = 0; i < IDT_DESC_CNT; i++)
    {
        make_idt_desc(&idt[i],  IDT_DESC_DPL0, interrupt_entry_table[i]);
    }
    put_str("idt is done\n",BG_BLACK+FT_YELLOW);
}

/* 初始化可编程中断控制器8259A */
void pic_init() {

    /* 初始化主片 */
    outb (PIC_M_CTRL, 0x11);   // ICW1: 边沿触发,级联8259, 需要ICW4.
    outb (PIC_M_DATA, 0x20);   // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
    outb (PIC_M_DATA, 0x04);   // ICW3: IR2接从片.
    outb (PIC_M_DATA, 0x01);   // ICW4: 8086模式, 正常EOI

    /* 初始化从片 */
    outb (PIC_S_CTRL, 0x11);	// ICW1: 边沿触发,级联8259, 需要ICW4.
    outb (PIC_S_DATA, 0x28);	// ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
    outb (PIC_S_DATA, 0x02);	// ICW3: 设置从片连接到主片的IR2引脚
    outb (PIC_S_DATA, 0x01);	// ICW4: 8086模式, 正常EOI
    /* 打开主片上IR0,也就是目前只接受时钟产生的中断 */
    outb (PIC_M_DATA, 0xfe);
    outb (PIC_S_DATA, 0xff);
    put_str("pic_init done\n",BG_BLACK+FT_RED);
}

void idt_init() {
    put_str("idt_init start\n",BG_BLACK+FT_YELLOW);
    idt_desc_init();	         //初始化中断描述符表
    general_handler_regist();    //默认中断函数注册
    pic_init();		             //初始化8259A
    load_xdt(&idt_ptr,IDT_DESC_CNT*8-1,idt); //注意,limit=size-1,书中代码有误
    /* 加载idt */
    asm volatile("lidt idt_ptr");
    put_str("idt_init done\n",BG_BLACK+FT_YELLOW);
}

void general_intr_handler(unsigned char vec_num)
{
    if(vec_num==0x27 || vec_num==0x2f)
        return;
    put_str("\ninterrupt ",BG_BLACK+FT_RED);
    put_int(vec_num, BG_BLACK+FT_RED,HEX);
    put_str(" occur: ",BG_BLACK+FT_RED);
    put_str(interrupt_name[vec_num],BG_BLACK+FT_RED);
    put_int(time, BG_BLACK+FT_RED,DEC);
}

void general_handler_regist()
{
    for(int i=0;i<IDT_DESC_CNT;i++)
    {
        interrupt_handler_table[i]= general_intr_handler;   //将一般函数的地址安装到中断函数表中
        interrupt_name[i]="unknown";
    }
    interrupt_name[0]    = "Divide Error\n";
    interrupt_name[1]    = "Debug Exception\n";
    interrupt_name[2]    = "NMI Interrupt\n";
    interrupt_name[3]    = "Breakpoint Exception\n";
    interrupt_name[4]    = "Overflow Exception\n";
    interrupt_name[5]    = "BOUND Range Exceeded Exception\n";
    interrupt_name[6]    = "Invalid Opcode Exception\n";
    interrupt_name[7]    = "Device Not Available Exception\n";
    interrupt_name[8]    = "Double Fault Exception\n";
    interrupt_name[9]    = "Coprocessor Segment Overrun\n";
    interrupt_name[0xa]  = "Invalid TSS Exception\n";
    interrupt_name[0xb]  = "Segment Not Present\n";
    interrupt_name[0xc]  = "Stack Fault Exception\n";
    interrupt_name[0xd]  = "General Protection Exception\n";
    interrupt_name[0xe]  = "Page-Fault Exception\n";
    //interrupt_name[15]  第15项是intel保留项,未使用
    interrupt_name[0x10] = "x87 FPU Floating-Point Error\n";
    interrupt_name[0x11] = "Alignment Check Exception\n";
    interrupt_name[0x12] = "Machine-Check Exception\n";
    interrupt_name[0x13] = "SIMD Floating-Point Exception\n";
    interrupt_name[0x14] = "Virtualization Exception\n";
    interrupt_name[0x15] = "Control Protection Exception\n";
    interrupt_name[0x16] = "reserved interrupt-unknown\n";
    interrupt_name[0x17] = "reserved interrupt-unknown\n";
    interrupt_name[0x18] = "reserved interrupt-unknown\n";
    interrupt_name[0x19] = "reserved interrupt-unknown\n";
    interrupt_name[0x1a] = "reserved interrupt-unknown\n";
    interrupt_name[0x1b] = "reserved interrupt-unknown\n";
    interrupt_name[0x1c] = "reserved interrupt-unknown\n";
    interrupt_name[0x1d] = "reserved interrupt-unknown\n";
    interrupt_name[0x1e] = "reserved interrupt-unknown\n";
    interrupt_name[0x1f] = "reserved interrupt-unknown\n";
    interrupt_name[0x20] = "Clock interrupt\n";
    interrupt_name[0x21] = "Keyboard interrupt\n";
    interrupt_name[0x22] = "Clock interrupt\n";
    interrupt_name[0x23] = "Cascade\n";
    interrupt_name[0x24] = "Unknown\n";
    interrupt_name[0x25] = "Unknown\n";
    interrupt_name[0x26] = "Unknown\n";
    interrupt_name[0x27] = "Unknown\n";
    interrupt_name[0x28] = "RTC\n";
    interrupt_name[0x29] = "Relocation\n";
    interrupt_name[0x2a] = "Reserved\n";
    interrupt_name[0x2b] = "Reserved\n";
    interrupt_name[0x2c] = "Unknown\n";
    interrupt_name[0x2d] = "FPU Exception\n";
    interrupt_name[0x2e] = "Disk interrupt\n";
    interrupt_name[0x2f] = "Reserved\n";
}
  • make_idt_desc() 函数用于构造单个中断门描述符。注意第三个参数,是中断入口函数的指针,typedef void* intr_handler ,该声明在 interrupt.h 中。
  • idt_desc_init() 函数用来构造整个 IDT 表。
  • pic_init() 函数用来初始化 8259A 芯片,并将当前设置为只接收时钟中断 。其中还用到了 outb() 函数,该函数用汇编书写,在 port_io.s 文件中。端口号的宏在 interrupt.h 中。
  • general_intr_handler() 便是便是我们期待已久的实际中断程序,不过现在它很简陋。先统一将所有的中断处理程序都设置为该函数,未来我们会使用 register_handler() 来注册专门的中断程序。另外,0x27 和 0x2f 无需处理
  • 最后,在 general_handler_regist() 中将 interrupt_handler_table 数组的每一个元素赋值为 general_intr_handler 函数的指针。再次强调,现在虽然每个中断都使用同一个函数,但后期对于某些中断我们会将其专门化,现在只是为中断提供基本的信息,以便产生中断时我们能明白发生了什么中断。

大家可能对各个函数之间的关系感到混乱,下面用一张图来帮助各位理清思绪:


运行截图

本文结束。

文章作者: 极简
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 后端技术分享
自制操作系统
喜欢就支持一下吧