《ORANGE’S:一个操作系统的实现》读书笔记(十二)内核雏形(六)

        虽然我们的内核目前还没有做任何实质性的工作,但我们的代码组织结构却已经有了一个雏形,而且我们还编写了好用的Makefile,即使增加更多的代码,我们也可以容易地将它们组织起来。

添加中断处理

        那么现在我们要做什么呢?作为一个操作系统,进程毫无疑问是最基本也是最重要的东西,于是我们的下一个重大目标应该是实现一个进程。再进一步,我们应该逐渐拥有多个进程。如果从进程本身的角度来看,它只不过是一段执行中的代码,这样看起来它根我们已经实现的代码没有本质的区别。可是,如果从操作系统角度来看,进程必须是可控的,所以这就涉及进程和操作系统之间的转换。因为CPU只有一个,同一时间要么是客户进程在运行,要么是操作系统在运行。如果要实现进程,就需要一种控制权转换机制,这种机制便是中断。

        说起中断,我们有过使用。比如我们通过 int 15h 得到了计算机内存信息,我们在实模式下用 int 15h 得到内存信息,然后在保护模式下把它们显示出来。并不是我们故意把问题搞复杂,而是在保护模式下,中断机制发生了很大变化,原来的中断向量表已经被IDT所代替,实模式下能用的BIOS中断在保护模式下已经不能使用了。IDT叫做中断描述符表(Interrupt Descriptor Table)。IDT中断的描述符可以是下面三种之一:

  • 中断门描述符
  • 陷阱门描述符
  • 任务门描述符

        IDT的作用是将每一个中断向量和一个描述符对应起来。从这个意义上说,IDT也是一种向量表,虽然它形式上跟实模式下的向量表非常不同。

        我们来看一下中断向量到中断处理程序的对应过程,对应过程如下图所示。

        上文说到IDT可以有中断门、陷阱门和任务门。但任务门在有些操作系统中根本就没有用到,比如Linux,这里,我们也不做太多关注。中断门和陷阱门的结构如下图所示。 

中断和异常机制

        我们在说到中断时通常将它与异常相提并论,实际上,它们都是程序执行过程中的强制性转移,转移到相应的处理程序。中断通常在程序执行时因为硬件而随机发生,它们通常用来处理处理器外部的事件,比如外围设备的请求。软件通过执行 int n 指令也可以产生中断。异常则通常在处理器执行指令过程中检测到错误时发生,比如遇到零除的情况。处理器检测的错误条件有很多,比如保护违例、页错误等。

        不管中断还是异常,通俗来讲,都是软件或者硬件发生了某种情形而通知处理器的行为。于是,由此引出两个问题:一是处理器可以对何种类型的通知做出反应;二是当接到某种通知时做出何种处理。

        其实,再细想一下的话,就发现这里又引出了一个问题。假设处理器可以处理A、B、C三种中断(异常),分别进行a、b、c三种处理,我们得有一种方法把A、B、C和a、b、c对应起来。实际上,解决这个问题的方法就是中断向量。每一种中断(异常)都会对应一个中断向量号,而这个向量号通过IDT就与相应的中断处理程序对应起来了。

        那么,处理器可以处理哪些中断或异常呢?下表给出了处理器可以处理的中断和异常列表,而且给出了它们对应的向量号以及其它一些描述。

向量号

助记符

描述

类型

出错码

0

#DE

除法错

Fault

DIV 和 IDIV 指令

1

#DB

调试异常

Fault/Trap

任何代码和数据的访问

2

——

非屏蔽中断

Interrupt

非屏蔽外部中断

3

#BP

调试断点

Trap

指令 INT 3

4

#OF

溢出

Trap

指令 INTO

5

#BR

越界

Fault

指令 BOUND

6

#UD

无效(未定义的)操作码

Fault

指令 UD2 或者无效指令

7

#NM

设备不可用(无数学协处理器)

Fault

浮点或 WAIT/FWAIT 指令

8

#DF

双重错误

Abort

有(0)

所有能产生异常或NMI或INTR的指令

9

协处理器段越界(保留)

Fault

浮点指令(386之后的IA32处理器不再产生此种异常)

10

#TS

无效 TSS

Fault

任务切换或访问 TSS 时

11

#NP

段不存在

Fault

加载段寄存器或访问系统段时

12

#SS

堆栈段错误

Fault

堆栈操作或加载 SS 时

13

#GP

常规保护错误

Fault

内存或其它保护检验

14

#PF

页错误

Fault

内存访问

15

——

Intel 保留,未使用

16

#MF

x87FPU 浮点错(数学错)

Fault

x87FPU 浮点指令或 WAIT/FWAIT 指令

17

#AC

对齐校验

Fault

有(0)

内存中的数据访问(486开始支持)

18

#MC

Machine Check

Abort

错误码(如果有的话)和源依赖于具体模式(奔腾CPU开始支持)

19

#XF

SIMD 浮点异常

Fault

SSE 和 SSE2 浮点指令(奔腾III开始支持)

20~31

——

Intel 保留,未使用

32~255

——

用户定义中断

Interrupt

外部中断或 int n 指令

        上面表格中“类型”一栏可能会有疑惑,实际上,Fault、Trap、Abort是异常的三种类型,它们的具体解释如下:

  • Fault 是一种可被更正的异常,而且一旦被更正,程序可以不失连续地继续执行。当一个 fault 发生时,处理器会把产生的 fault 的指令之前的状态保存起来。异常处理程序的返回地址将会是产生 fault 的指令,而不是其后的那条指令。
  • Trap 是一种在发生 trap 的指令执行之后立即被报告的异常,它也允许程序或任务不失连续性地继续执行。异常处理程序的返回地址将会是产生 trap 的指令之后的那条指令。
  • Abort 是一种不总是报告精确异常发生位置的异常,它不允许程序或任务继续执行,而是用来报告严重错误的。

        只要你明白了它们分别的含义,可以称呼它们为错误、陷阱和终止。 

外部中断

        中断产生的原因有两种,一种是外部中断,也就是由硬件产生的中断,另一种是由指令 int n 产生的中断。外部中断需要建立硬件中断和向量号之间的对应关系。外部中断分为不可屏蔽中断(NMI)和可屏蔽中断两种,分别由CPU的两根引脚NMI和INTR来接收,如下图所示。

        NMI不可屏蔽,因为它与IF是否被设置无关。NMI中断对应的中断向量号为2,这在上表中已经有所说明。

        可屏蔽中断与CPU的关系是通过对可编程中断控制器8259A建立起来的。如果你是第一次听说8259A,那么你可以认为它是中断机制中所有外围设备的一个代理,这个代理不但可以根据优先级在同时发生中断的设备中选择应该处理的请求,而且可以通过对其寄存器的设置来屏蔽或打开相应的中断。

        由上图可以知道,与CPU相连的不是一片,而是两片级联的8259A,每个8259A有8根中断信息号线,于是两片级联总共可以挂接15个不同的外部设备。那么,这些设备发出的中断请求如何与中断向量对应起来呢?就是通过对8259A的设置完成的。在BIOS初始化它的时候,IRQ0~IRQ7被设置为对应向量号 08h~0Fh,而通过上表我们知道,在保护模式下像向量号 08h~0Fh 已经被占用了,所以我们不得不重新设置主从8259A。

        8259A是可编程中断控制器,对它的设置并不复杂,是通过向相应的端口写入特定的ICW(Initialization Command Word)来实现的。主8259A对应的端口地址是20h和21h,从8259A对应的端口地址是A0h和A1h。ICW共有4个,每一个都是具有特定格式的字节。为了先对初始化8259A的过程有个概括的了解,我们先来看一下初始化过程:

  1. 往端口 20h(主片)或 A0h(从片)写入ICW1。
  2. 往端口 21h(主片)或 A1h(从片)写入ICW2。
  3. 往端口 21h(主片)或 A1h(从片)写入ICW3。
  4. 往端口 21h(主片)或 A1h(从片)写入ICW4。

        这4步的顺序是不能颠倒的

        我们现在来看一下4个如下图所示的ICW的格式。

        我们看到,在写入ICW2时设置与中断向量号的对应。

        我在知乎上看到一篇《中断控制器》的文章,内容很详细,其中还有关于OCW1、OCW2的说明,地址是:中断控制器 - 知乎

设置8259A和建立IDT

        好了,我们现在可以来设置8259A和建立IDT了。首先,我们先来写一个函数用于设置8259A。新建代码文件 i8259.c,放在 kernel 目录下,代码 kene/i8359.c 设置8259A函数如下所示。

PUBLIC void init_8259A()
{
    /* Master 8259, ICW1. */
    out_byte(INT_M_CTL, 0x11);

    /* Slave  8259, ICW1. */
    out_byte(INT_S_CTL, 0x11);

    /* Master 8259, ICW2. 设置 '主8259' 的中断入口地址为 0x20. */
    out_byte(INT_M_CTLMASK, INT_VECTOR_IRQ0);

    /* Slave  8259, ICW2. 设置 '从8259' 的中断入口地址为 0x28 */
    out_byte(INT_S_CTLMASK, INT_VECTOR_IRQ8);

    /* Master 8259, ICW3. IR2 对应 '从8259'. */
    out_byte(INT_M_CTLMASK, 0x4);

    /* Slave  8259, ICW3. 对应 '主8259' 的 IR2. */
    out_byte(INT_S_CTLMASK, 0x2);

    /* Master 8259, ICW4. */
    out_byte(INT_M_CTLMASK, 0x1);

    /* Slave  8259, ICW4. */
    out_byte(INT_S_CTLMASK, 0x1);

    /* Master 8259, OCW1.  */
    out_byte(INT_M_CTLMASK, 0xFF);

    /* Slave  8259, OCW1.  */
    out_byte(INT_S_CTLMASK, 0xFF);
}

        我们把初始化8259A的函数命名为init_8259A,将相应的端口定义成宏。宏定义代码如下所示。

        代码 include/const.h,8259A的端口。

/* 8259A interrupt controller ports. */
#define INT_M_CTL     0x20 /* I/O port for interrupt controller       <Master> */
#define INT_M_CTLMASK 0x21 /* setting bits in this port disables ints <Master> */
#define INT_S_CTL     0xA0 /* I/O port for second interrupt controller<Slave>  */
#define INT_S_CTLMASK 0xA1 /* setting bits in this port disables ints <Slave>  */

        代码 include/protect.h,中断向量。 

#define INT_VECTOR_IRQ0         0x20
#define INT_VECTOR_IRQ8         0X28

        函数init_8259A中只用到了一个函数,就是用来写端口的out_byte,它的函数体位于kliba.asm中,代码如下所示。其中,不但有out_byte,用于对端口进行写操作,也添加了in_byte,用来对端口进行读操作。由于端口操作可能需要时间,所以两个函数都加了点空操作以便有微小的延迟。 

; void out_byte(u16 port, u8 value);
out_byte:
    mov edx, [esp + 4]      ; port
    mov al, [esp + 8]   ; value
    out dx, al
    nop                     ; 一点延迟
    nop
    ret

; u8 in_byte(u16 port);
in_byte:
    mov edx, [esp + 4]      ; port
    xor eax, eax
    in al, dx
    nop
    nop                     ; 一点延迟
    ret

        这两个函数的原型放在了 include/proto.h 中,这是一个新建的头文件,用来存放函数声明,代码如下所示。可以看到,我们把start.c中函数disp_str的声明也放到了该文件中。 

PUBLIC void out_byte(u16 port, u8 value);
PUBLIC u8   in_byte(u16 port);
PUBLIC void disp_str(char * info);

        在挪动disp_str的函数声明时,你一定也想到了还有memcpy的函数声明,同样,我们把它也放进一个头文件中,不过和disp_str放进的不是同一个头文件,而是新建一个头文件,将其放入,头文件名称是string.h,放在include目录下,路径为:include/string.h。

        需要注意的是,由于函数声明放到了头文件中,不要忘记在相应的 .c 文件中包含这些头文件。

        这些地方都修改完成后,最后一件事情就是修改Makefile。不但要添加新的目标kernel/i8259.o,而且由于头文件的变化,kernel/start.o的依赖关系也要进行改变。

OBJS            = kernel/kernel.o kernel/start.o kernel/i8259.o kernel/global.o kernel/protect.o lib/klib.o lib/kliba.o lib/string.o
......
kernel/start.o : kernel/start.c include/type.h include/const.h include/protect.h include/proto.h include/string.h
    $(CC) $(CFLAGS) -o $@ $<
kernel/i8259.o : kernel/i8259.c include/type.h include/const.h include/proto.h include/protect.h
    $(CC) $(CFLAGS) -o $@ $<

        确定文件依赖关系的时候,你可能觉得有点麻烦,尤其是头文件越来越多的时候。不过不要紧,GCC提供了一个参数“-M”,可以自动生成指定文件的依赖关系。下面是“gcc -M”的典型用法: 

        我们直接把输出复制到Makefile中就可以了。其实,这时我们已经可以make一下了。虽然目前还没有完成任何实质性的工作,但是make一下,测试一下自己的工作有没有错误还是有必要的。

        现在我们开始初始化IDT,说到IDT,我们前面有初始化过GDT,那么这里可以用初始化GDT的方法来初始化IDT。首先修改start.c,代码如下所示。

#include "global.h"
......
    /* idt_ptr[6] 共 6 个字节:0~15:Limit   16~47:Base。用作 sidt/lidt 的参数。 */
    u16* p_idt_limit = (u16*)(&idt_ptr[0]);
    u32* p_idt_base = (u32*)(&idt_ptr[2]);
    *p_idt_limit = IDT_SIZE * sizeof(GATE) - 1;
    *p_idt_base = (u32)&idt;

        代码跟先前初始化GDT的部分基本上是一样的,只是所有的GDT变成了IDT。这里我们将原先位于start.c开头的gdt[]和gdt_ptr[]的声明移动到了头文件 include/global.h 中了,这是一个新建的头文件。该头文件除了包含gdt[]和gdt_ptr[],还有新增加的变量idt[]和idt_ptr[]。之所以把全局变量声明都放在了其中是为了代码的美观和可读性。代码 include/global.h 如下所示。

/* EXTERN is defined as extern except in global.c */
#ifdef GLOBAL_VARIABLES_HERE
#undef EXTERN
#define EXTERN
#endif

EXTERN int          disp_pos;
EXTERN u8           gdt_ptr[6];     /* 0~15:Limit   16~47:Base */
EXTERN DESCRIPTOR   gdt[GDT_SIZE];
EXTERN u8           idt_ptr[6];     /* 0~15:Limit   16~47:Base */
EXTERN GATE         idt[IDT_SIZE];

        EXTERN定义在const.h中,通常情况下它被定义成extern。但是在global.h中,如果宏GLOBAL_VARIABLES_HERE被定义的话,EXTERN将会被定义成空值。这样做的意图联系global.c你就全明白了。你会发现,通过宏GLOBAL_VARIABLES_HERE的使用,在让所有变量只出现一次的同时,预编译结束后,global.c和其它.c文件中的结果不同。在global.c中,变量前面没有extern关键字,而在其它文件中,变量前会有extern关键字。

        代码 kernel/global.c,全局变量。

#define GLOBAL_VARIABLES_HERE

#include "type.h"
#include "const.h"
#include "protect.h"
#include "proto.h"
#include "global.h"

        代码 include/const.h,EXTERN。 

/* EXTERN is defined as extern except global.c */
#define EXTERN extern
......
#define IDT_SIZE    256

        可以看到,IDT_SIZE的定义也在const.h中。

        另外,GATE的定义在protect.h中。

        代码 include/protect.h,GATE。

/* 门描述符 */
typedef struct s_gate
{
    u16 offset_low;     /* Offset Low */
    u16 selector;       /* Selector */
    u8 dcount;          /* 该字段只在调用门描述符中有效。如果在利用
                           调用门调用子程序时引起特权级的转换和堆栈
                           的改变,需要将外层堆栈中的参数复制到内层
                           堆栈。该双字计数字段就是用于说明这种情况
                           发生时,要复制的双字参数的数量。 */
    u8 attr;            /* P(1)  DPL(2)  DT(1)  TYPE(4) */
    u16 offset_high;    /* Offset High */
}GATE;

        好了,start.c修改完之后,我们在kernel.asm中添加两句,导入idt_ptr这个符号并加载IDT。现在,加载IDT的代码已经写完了。不过,现在IDT内还没有任何内容呢,我们现在需要向IDT内添加内容。

        我们在上面的表格中给出了处理器可以处理的中断和异常列表,现在,我们把这些中断和异常的处理程序进行添加。虽然它们总数有十几个,但是我们却可以用相似的方法来处理它们。

        中断或异常发生时eflags、cs、eip被压栈,如果有错误码的话,错误码也被压栈。所以我们对异常处理的总体思想是,如果有错误码,则直接把向量号压栈,然后执行一个函数exception_handler;如果没有错误码,则先在栈中压入一个0xFFFFFFFF,再把向量号压栈并随后执行exception_handler。

        函数exception_handler的原型如下所示:

void exception_handler(int vec_no, int err_code, int eip, int cs, int eflags);

        由于C调用约定是调用者恢复堆栈,所以不用担心exception_handler会破坏堆栈中的eip、cs以及eflags。

        代码 kernel/kernel.asm,中断和异常。

extern idt_ptr
...
global _start   ; 导出 _start

global divide_error
global single_step_exception
global nmi
global breakpoint_exception
global overflow
global bounds_check
global inval_opcode
global copr_not_available
global double_fault
global copr_seg_overrun
global inval_tes
global segment_not_present
global stack_exception
global general_protection
global page_fault
global copr_error
...
    lidt   [idt_ptr]
...
divide_error:
    push    0xFFFFFFFF  ; no err code
    push    0           ; vector_no = 0
    jmp     exception
single_step_exception:
    push    0xFFFFFFFF  ; no err code
    push    1           ; vector_no = 1
    jmp     exception
nmi:
    push    0xFFFFFFFF  ; no err code
    push    2           ; vector_no = 2
    jmp     exception
breakpoint_exception:
    push    0xFFFFFFFF  ; no err code
    push    3           ; vector_no = 3
    jmp     exception
overflow:
    push    0xFFFFFFFF  ; no err code
    push    4           ; vector_no = 4
    jmp     exception
bounds_check:
    push    0xFFFFFFFF  ; no err code
    push    5           ; vector_no = 5
    jmp     exception
inval_opcode:
    push    0xFFFFFFFF  ; no err code
    push    6           ; vector_no = 6
    jmp     exception
copr_not_available:
    push    0xFFFFFFFF  ; no err code
    push    7           ; vector_no = 7
    jmp     exception
double_fault:
    push    8           ; vector_no = 8
    jmp     exception
copr_seg_overrun:
    push    0xFFFFFFFF  ; no err code
    push    9           ; vector_no = 9
    jmp     exception
inval_tes:
    push    10          ; vector_no = A
    jmp     exception
segment_not_present:
    push    11          ; vector_no = B
    jmp     exception
stack_exception:
    push    12          ; vector_no = C
    jmp     exception
general_protection:
    push    13          ; vector_no = D
    jmp     exception
page_fault:
    push    14          ; vector_no = E
    jmp     exception
copr_error:
    push    0xFFFFFFFF  ; no err code
    push    16          ; vector_no = 10h
    jmp     exception

exception:
    call    exception_handler
    add     esp, 4*2    ; 让栈顶指向 EIP,堆栈中从顶向下依次是:EIP、CS、EFLAGS
    hlt

        我们看到,在上面代码的最后,栈顶被调整为指向eip,堆栈中从顶向下依次是:eip、cs、eflags。虽然目前我们到这里就让程序停住了,但这样做有利于提醒我们以后修改时注意,用iretd返回前的样子应该是这样的。

        接下来我们来看一下函数exception_handler的实现,首先把屏幕的前5行通过打印空格的方式清空,然后把堆栈中的参数打印出来。

        代码 kernel/protect.c,异常处理函数。

PUBLIC void exception_handler(int vec_no, int err_code, int eip, int cs, int eflags)
{
    int i;
    int text_color = 0x74;  /* 灰底红字 */

    char* err_msg[] = {
        "#DE Divide Error",
        "#DB RESERVED",
        "#--  NMI Interrupt",
        "#BP Breakpoint",
        "#OF Overflow",
        "#BP BOUND Range Exceeded",
        "#UD Invalid Opcode (Undefined Opcode)",
        "#NM Device Not Available (No Math Coprocessor)",
        "#DF Double Fault",
        "   Coprocessor Segment Overrun (reserved)",
        "#TS Invalid TSS",
        "#NP Segment Not Present",
        "#SS Stack-Segment Fault",
        "#GP General Protection",
        "#PF Page Fault",
        "--  (Intel reserved. Do not use.)",
        "#MF x87 FPU Floating-Point Error (Math Fault)",
        "#AC Alignment Check",
        "#MC Machine Check",
        "#XF SIMD Floating-Point Exception"
    };

    /* 通过打印空格的方式清空屏幕的前五行,并把 disp_pos 清零 */
    disp_pos = 0;
    for (i = 0; i < 80*5; i++) {
        disp_str(" ");
    }
    disp_pos = 0;

    disp_color_str("Exception! --> ", text_color);
    disp_color_str(err_msg[vec_no], text_color);
    disp_color_str("\n\n", text_color);
    disp_color_str("DFLAGS:", text_color);
    disp_int(eflags);
    disp_color_str("CS:", text_color);
    disp_int(cs);
    disp_color_str("EIP:", text_color);
    disp_int(eip);

    if (err_code != 0xFFFFFFFF) {
        disp_color_str("Error code:", text_color);
        disp_int(err_code);
    }
}

        在这里,我们新建了一个文件protect.c用来放置exception_handler。需要注意的是,每新建一个源文件,我们都要在Makefile中做出相应的改变。

        为了突出显示,exception_handler中打印字符串不再使用disp_str而使用disp_color_str(),它和disp_str()基本上是一样的,区别在于增加了一个设置颜色的参数,代码如下所示。

        代码 lib/kliba.asm,函数disp_color_str。

disp_color_str:
    push ebp
    mov ebp, esp

    mov esi, [ebp + 8]  ; pszInfo
    mov edi, [disp_pos]
    mov ah, [esp + 12]  ; color
.1:
    lodsb
    test al, al
    jz .2
    cmp al, 0Ah ; 换行
    jnz .3
    push eax
    push ebx
    mov eax, edi
    mov bl, 160
    div bl
    and eax, 0FFh
    inc eax
    mov bl, 160
    mul bl
    mov edi, eax
    pop ebx
    pop eax
    jmp .1
.3:
    mov [gs:edi], ax
    add edi, 2
    jmp .1
.2:
    mov [disp_pos], edi

    pop ebp
    ret

        另外,为了显示整数,我们新编写了函数disp_int(),它被定义在新建的klib.c中,代码如下所示。

        代码 lib/klib.c,函数disp_int。

/* itoa */
/* 数字前面的 0 不被展示出来,比如 0000B800 被显示成 B800 */
PUBLIC char * itoa(char * str, int num)
{
    char* p = str;
    char ch;
    int i;
    int flag = 0;

    *p++ = '0';
    *p++ = 'x';
    if (num == 0) {
        *p++ = '0';
    } else {
        for (i = 28; i >= 0; i -= 4) {
            ch = (num >> i) & 0xF;
            if (flag || (ch > 0)) {
                flag = 1;
                ch += '0';
                if (ch > '9') {
                    ch += 7;
                }
                *p++ = ch;
            }
        }
    }

    *p = 0;

    return str;
}

/* 显示数字 */
PUBLIC void disp_int(int input)
{
    char output[16];
    itoa(output, input);
    disp_str(output);
}

        disp_int很简单,用itoa()将整数转换成字符串显示出来。itoa()也定义在klib.c中,不过它和C语言库中函数itoa()比起来要简单的多,目的只是把一个32位的数值用十六进制的方式显示出来,既不支持其它进制转换,也不考虑有符号数等情况。

        现在我们已经有了异常处理函数,该是设置IDT的时候了。我们把设置IDT的代码放进函数init_prot()中,它也位于protect.c中。protect.c通篇几乎只调用一个函数,就是init_idt_desc(),它用来初始化一个门描述符。其中用到的函数指针类型是这样定义的,位于代码 include/type.h:

typedef void (*int_handler) ();

        所有的异常处理程序都必须与此声明完全一致。

        代码 kernel/protect.c,函数init_idt_desc。

/* 初始化 386 中断门 */
PRIVATE void init_idt_desc(unsigned char vector, u8 desc_type, int_handler handler, unsigned char privilege)
{
    GATE* p_gate = &idt[vector];
    u32 base = (u32)handler;
    p_gate->offset_low = base & 0xFFFF;
    p_gate->selector = SELECTOR_KERNEL_CS;
    p_gate->dcount = 0;
    p_gate->attr = desc_type | (privilege << 5);
    p_gate->offset_high = (base >> 16) & 0xFFFF;
}

        在init_port()中,所有描述符都被初始化为中断门。函数中用到了若干宏,其中以INT_VECTOR_开头的宏表示中断向量,DA_386IGate表示中断门,定义在protect.h中,PRIVILEGE_KRNL和PRIVILEGE_USER定义在const.h中。另外,调用init_8259A()的语句也放在了这个函数中。

        代码 kernel/protect.c,函数init_prot。

/* 中断处理函数 */
void divide_error();
void single_step_exception();
void nmi();
void breakpoint_exception();
void overflow();
void bounds_check();
void inval_opcode();
void copr_not_available();
void double_fault();
void copr_seg_overrun();
void inval_tes();
void segment_not_present();
void stack_exception();
void general_protection();
void page_fault();
void copr_error();

PUBLIC void init_prot()
{
    init_8259A();

    // 全部初始化成中断门(没有陷阱门)
    init_idt_desc(INT_VECTOR_DIVIDE, DA_386IGate, divide_error, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_DEBUG, DA_386IGate, single_step_exception, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_NMI, DA_386IGate, nmi, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_BREAKPOINT, DA_386IGate, breakpoint_exception, PRIVILEGE_USER);
    init_idt_desc(INT_VECTOR_OVERFLOW, DA_386IGate, overflow, PRIVILEGE_USER);
    init_idt_desc(INT_VECTOR_BOUNDS, DA_386IGate, bounds_check, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_INVAL_OP, DA_386IGate, inval_opcode, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_COPROC_NOT, DA_386IGate, copr_not_available, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_DOUBLE_FAULT, DA_386IGate, double_fault, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_COPROC_SEG, DA_386IGate, copr_seg_overrun, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_INVAL_TSS, DA_386IGate, inval_tes, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_SEG_NOT, DA_386IGate, segment_not_present, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_STACK_FAULT, DA_386IGate, stack_exception, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_PROTECTION, DA_386IGate, general_protection, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_PAGE_FAULT, DA_386IGate, page_fault, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_COPROC_ERR, DA_386IGate, copr_error, PRIVILEGE_KRNL);
}

        至此设置IDT的代码总算添加的差不多了,我们现在来调用init_prot()。

        代码 kernel/start.c,函数init_prot。

    init_prot();
    
    disp_str("-----\"cstart\" ends-----\n");

        对Makefile进行相应的修改之后,我们就可以make一下了,通过运行,你会发现什么效果也没有。那是因为我们添加了异常处理程序,但是没有异常发生。所以我们就制造一个异常来试试看。Intel为我们准备了一个指令叫做ud2,能够产生一个 #UD 异常,我们就在kernel.asm中添加一条ud2指令,代码如下所示。 

csinit:     ; 这个跳转指令强制使用刚刚初始化的结构
    ud2

        在make,运行,不出意外的话,你应该可以看到如下图所示的效果了,异常的助记符、名字以及eflags、cs、eip的值都被打印出来了。 

        这个是没有错误码的异常,我们再来产生一个有错误码的异常,把ud2这行指令修改成jmp 0x40:0。运行,你会看到错误码也显示出来了。 

        虽然只是初始化8259A和设置IDT这两项任务,却也费了我们这么多精力,零碎的东西还真不少。不过,现在我们已经有了异常处理机制,今后,即便出了错,我们也能方便地知道错误出在什么地方以及错误的类型。

        不过,8259A虽然已经设置完成,但是我们还没有真正开始使用它。通过上文的描述我们知道,两片级联的8259A可以挂接15个不同的外部设备,我们也应有15个中断处理程序。位简单起见,我们写两个带参数的宏,用它们作为中断处理程序。

        代码 kernel/kernel.asm,对应8259A的中断例程。

extern spurious_irq
...
global hwint00
global hwint01
global hwint02
global hwint03
global hwint04
global hwint05
global hwint06
global hwint07
global hwint08
global hwint09
global hwint10
global hwint11
global hwint12
global hwint13
global hwint14
global hwint15

        在这里,所有的中断都会触发一个函数spurious_irq(),这个函数的定义如下代码所示。

        代码 kernel/i8259.c,函数spurious_irq。

PUBLIC void spurious_irq(int irq)
{
    disp_str("spurious_irq: ");
    disp_int(irq);
    disp_str("\n");
}

        spurious_irq()函数什么也不做,仅仅是把IRQ号打印出来而已。下面我们就来设置IDT。

        代码 kernel/protect.c,设置IDT。

void hwint00();
void hwint01();
void hwint02();
void hwint03();
void hwint04();
void hwint05();
void hwint06();
void hwint07();
void hwint08();
void hwint09();
void hwint10();
void hwint11();
void hwint12();
void hwint13();
void hwint14();
void hwint15();

PUBLIC void init_prot()
{
...
    init_idt_desc(INT_VECTOR_IRQ0 + 0, DA_386IGate, hwint00, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_IRQ0 + 1, DA_386IGate, hwint01, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_IRQ0 + 2, DA_386IGate, hwint02, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_IRQ0 + 3, DA_386IGate, hwint03, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_IRQ0 + 4, DA_386IGate, hwint04, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_IRQ0 + 5, DA_386IGate, hwint05, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_IRQ0 + 6, DA_386IGate, hwint06, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_IRQ0 + 7, DA_386IGate, hwint07, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_IRQ8 + 0, DA_386IGate, hwint08, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_IRQ8 + 1, DA_386IGate, hwint09, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_IRQ8 + 2, DA_386IGate, hwint10, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_IRQ8 + 3, DA_386IGate, hwint11, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_IRQ8 + 4, DA_386IGate, hwint12, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_IRQ8 + 5, DA_386IGate, hwint13, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_IRQ8 + 6, DA_386IGate, hwint14, PRIVILEGE_KRNL);
    init_idt_desc(INT_VECTOR_IRQ8 + 7, DA_386IGate, hwint15, PRIVILEGE_KRNL);
}

        现在已经可以make并运行了,但是不会有什么效果,因为我们不但没有通过任何方式设置IF位,而且在init_8259A()中把所有中断都屏蔽掉了。那么我们先到i8259.c做代码的修改。

    /* Master 8259, OCW1. */
    out_byte(INT_M_CTLMASK, 0xFD);

    /* Slave 8259, OCW1. */
    out_byte(INT_S_CTLMASK, 0xFF);
}

        在这里,我们向主8259A相应端口写入了0xFD,由于0xFD对应的二进制是11111101,于是键盘中断被打开,而其它中断仍然处于屏蔽状态,最后,在kernel.asm中添加sti指令设置IF位。 

csinit:     ; 这个跳转指令强制使用刚刚初始化的结构
    sti
    hlt

        make,运行,开始没有什么特殊的现象,但当我们敲击键盘任意键的时候,字符串“spurious_irq:0x1”就出现了,这表明当前的IRQ号为1,正是对应的键盘中断。

一点说明

        在上面的代码中,有一些简单明了的符号声明、导入,以及对Makefile的更改等内容并没有记录在文章中,所以我把这些代码打成压缩包放在文章末尾,方便在阅读的过程中参考相应的代码。

公众号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值