我们的目标

本节中,将会介绍怎样处理中断和异常。在Skelix中加入异常处理给了我们当异常发生时执行指定代码的能力。

下载代码


到底什么是中断和异常?

比如说,当你在家里吃晚餐,突然电话响了,因为可能是紧急事件,所以你去拿起电话,这就是中断。异常是这样的,它更像你在吃东西的时候,突然发现食物中有蟑螂,只剩一半了,而且还在蹬腿,呕~~~。在接完电话或者吐完后,我们继续回到正常生活中去。希望在这个异常发生后,你还能有胃口。

简而言之,中断和异常都停止的处理器的处理流程并强迫它去做另外的一些事,做完后处理器继续原来的处理。中断和异常的不同在于中断可以被调用,例如硬件或者详细一点像键盘输入、系统时钟之类的,但是异常只会在执行指令时被事先定义好的条件触发,例如除零错,但是这里有异常的异常:),例如INT 3可以用户任务调用用于调试程序。

处理器把每一种中断或异常都关联到了一个独有的数值上去,这个数值实际上是中断向量表的索引。中断向量表在实模式下载地址0处,它们已经被我们的内核覆盖了,反正我们在保护模式下不能调用它们。

根据Intel手册,分别有两个原因可以造成中断也有两个原因可以触发异常,

中断

IRQ脚中断向量中断
IRQ008系统时钟
IRQ109键盘
IRQ20A桥连到PIC2
IRQ30BCOM2
IRQ40CCOM1
IRQ50DLPT2
IRQ60E软驱
IRQ70FLPT1
IRQ870CMOS实时时钟
IRQ971
IRQ1072
IRQ1173
IRQ1274PS/2鼠标
IRQ1375数学协处理器
IRQ1476硬盘IDE0
IRQ1577硬盘IDE1

IRQ是连接PIC(一会儿解释)到硬件的物理针脚,在AT机上有16个针脚。

异常

中断向量异常
00除零错
01调试异常
02不可屏蔽中断(NMI)
03断电(INT 3指令)
04上溢出(INTO指令)
05边界检查(BOUND指令)
06无效操作码
07无协处理器
08双重错误
09协处理器段超限
0A无效的TSS
0B段不存在
0C栈异常
0D一般保护错,当年Windows 9X下臭名卓著的蓝屏
0E页错误
0FIntel保留
10协处理器错
11-19Intel保留
1A-FF未用

有趣的是,我们注意到中断和异常间是有冲突的,IRQ0-IRQ7的中断向量和异常08-10的向量是重叠的,我们必须处理这个问题。

可屏蔽中断的向量索引是由两块8259A可编程中断控制器(PIC)决定的,它们级联到一起来处理硬件中断,PIC1处理IRQ0-IRQ7,PIC2处理IRQ8-IRQ15,当中断发生时,它们获得信号并通知处理器,接着处理器停止正常的执行流程,并通过向量索引来找到ISR(中断服务程序),接着调用ISR。我们可以通过8259A来重新映射新的向量索引值。

为了重映射这些IRQ,我们必须对8259A编程(原来pic_install中好几条注释放错了位置,沈峰指出了这些错误),这个不容易,你最好自己上网搜一下,但是重映射的代码基本大同小异,

04/init.c

static void 

pic_install(void) {

        outb(0x11, 0x20);

        outb(0x11, 0xa0);

        /*Remaps IRQ0-IRQ7 to 0x20-0x27 in interrupt vector table*/

        outb(0x20, 0x21); 

        /*Remaps IRQ8-IRQ15 to 0x28-0x2F in interrupt vector table*/

        outb(0x28, 0xa1);

        /*PIC2 is connected to PIC1 via IRQ2*/

        outb(0x04, 0x21);

        outb(0x02, 0xa1);

        /*Enables 8086/88 mode*/

        outb(0x01, 0x21);

        outb(0x01, 0xa1);

        /*Disables all interrupts from IRQ0-IRQ7*/

        outb(0xff, 0x21);

        /*Disables all interrupts from IRQ8-IRQ15*/

        outb(0xff, 0xa1);

}

顺便提一下,好消息是,我们现在可以使用C编程了。上面代码里的outb是一个宏,工作起来像这样outb(byte, port),还有其它要用到的一些宏。

04/include/asm.h

#define cli() __asm__ ("cli\n\t")

#define sti() __asm__ ("sti\n\t")

 

#define halt() __asm__ ("cli;hlt\n\t");

#define idle() __asm__ ("jmp .\n\t");

 

#define inb(port) (__extension__({      \

unsigned char __res;    \

__asm__ ("inb   %%dx,   %%al\n\t"       \

                                         :"=a"(__res)   \

                                         :"dx"(port));  \

__res;  \

}))

 

#define outb(value, port) __asm__ (     \

"outb   %%al,   %%dx\n\t"::"al"(value), "dx"(port))

 

#define insl(port, buf, nr) \

__asm__ ("cld;rep;insl\n\t"     \

::"d"(port), "D"(buf), "c"(nr))

 

#define outsl(buf, nr, port) \

__asm__ ("cld;rep;outsl\n\t"    \

::"d"(port), "S" (buf), "c" (nr))

现在我们知道怎样重映射中断,并且让处理器知道中断向量表中正确的索引。但是,这里有另一个问题,实模式下的中断表被我们的内核覆盖了,这该怎么办?坏消息是,我们必须从头写……


IDT和ISR

处理器对怎样处理中断和异常一视同仁,当任何一个发生时,处理器终止当前任务的执行并切换到特定的程序段中去以处理中断或异常,这些程序段叫做中断服务程序(ISR)。中断服务程序一旦执行完毕,处理器会回到原来的任务执行。

然而,怎样让处理器找到这些程序段呢?处理器管理了一个表叫做中断描述符表(IDT),它包含了一些描述了怎样访问那些ISR的描述符。就像GDT那样,IDT的物理地址保存在IDTR

在IDT中的每一个中断和异常都有一个独特的数值,就是被我们重新映射的向量。IDT包含了一组64字节长的描述符,最多有256个描述符在表中。LIDT指令告诉处理器通过寄存器IDTR去找IDT,就想LGDT和GDT的关系那样。

看一下IDT项的格式,

IDT entry

我们熟悉大部分的域,我会在后面详细解释它们。实际上IDT中有好几类的描述符,Skelix使用中断门。

现在处理器知道了当中断或异常发生时到哪里去找处理程序,那么ISR应该是什么样的呢?因为中断或异常终止了正常的执行,并且最后还要回到正常流程中去,也就意味着ISR必须保存正常执行环境,它必须保存所有可能被影响到的寄存器并最终把它们的值重置为进入ISR之前的值。

如果ISR代码和当前任务有相同的权限级,那么ISR使用当前堆栈,否则任务切换代码会保存当前SSESPEFLAGSCSEIP,并从TSS(后面的教程中讨论它)中载入新的CSEIP和堆栈,并把保存的寄存器值压入新堆栈中。堆栈切换完成后(如果有的话),处理器把EFLAGSCSEIP按这个顺序压入栈中,一些异常还提供了关于错误信息的错误码,这个错误码也必须入栈保存。之后,保存在相应IDT描述符中的CSEIP被载入。因为我们使用中断门,EFLAGS中的IF标志位将被清除。从ISR返回就要把这个顺序反过来,大体来说……

ISR stack frame

嗯……还没睡着吧?我猜你大概是糊涂了,所以还是让代码说明一切吧。在上节最后,我们让03/load.s输出“Hello World!”。本节中,我们用一个对C代码的调用取代。

        .text

        .globl    pm_mode

        .include "kernel.inc"

        .org 0

pm_mode:

        movl    $DATA_SEL,%eax

        movw    %ax,    %ds

        movw    %ax,    %es

        movw    %ax,    %fs

        movw    %ax,    %gs

        movw    %ax,    %ss

        movl    $STACK_BOT,%esp

 

        cld

        movl    $0x10200,%esi

        movl    $0x200,    %edi

        movl    $KERNEL_SECT<<7,%ecx

        rep

        movsl

 

        call    init

init初始化了一些硬件和系统表,此时它看起来像这样,

04/init.c

unsigned long long *idt = ((unsigned long long *)IDT_ADDR);

unsigned long long *gdt = ((unsigned long long *)GDT_ADDR);

方便起见,我们用一个long long来表示描述符,为了禁掉GCC的警告信息,必须使用在Makefile中使用编译选项--Wno-long-long

static void 

isr_entry(int index, unsigned long long offset) {

        unsigned long long idt_entry = 0x00008e0000000000ULL |

                        ((unsigned long long)CODE_SEL<<16);

        idt_entry |= (offset<<32) & 0xffff000000000000ULL;

isr_entry填充指定的IDT项,第一个参数是IDT中的索引,第二个参数是ISR地址。

现做一个模板,IDT项看起来像这样,

IDT format

IDT模板的值是0x00008e0000080000,它表示开始于地址0,使用选择子0x8(内核代码选择子),存在于内存并且DPL为0。

        idt_entry |= (offset) & 0xffff;

        idt[index] = idt_entry;

填充IDT。

}

 

static void 

idt_install(void) {

idt_install装入了256个ISR。

        unsigned int i = 0;

        struct DESCR {

                unsigned short length;

                unsigned long address;

        } __attribute__((packed)) idt_descr = {256*8-1, IDT_ADDR};

建立IDTR所用的结构,和GDTR的格式相同。我们需要GCC扩展__attribute__((packed))以阻止GCC填充结构。否则GCC会为了效率原因把这个结构扩展为64字节长,像2字节length,2字节填充,4字节address这样。

        for (i=0; i<VALID_ISR; ++i)

                isr_entry(i, (unsigned int)(isr[(i<<1)+1]));

        for (++i; i<256; ++i)

                isr_entry(i, (unsigned int)default_isr);

isr数组存储了所有的ISR,一会儿解释它。深吸一口气,你将看到一个很长的栈帧。VALID_ISR04/include/isr.h中定义,它指出了IDT中有多少个项是可用的,本节中用的值是32,它是上面介绍的所有中断和异常的总和。

剩余的IDT项被填充为默认ISR,就是在屏幕上改变一个字符来告诉我们有一个为处理的中断发生了。

        __asm__ __volatile__("lidt  %0\n\t"::"m"(idt_descr));

LIDT将IDT装入IDTR

}

 

static void 

pic_install(void) {

        outb(0x11, 0x20);

        outb(0x11, 0xa0);

        outb(0x20, 0x21);

        outb(0x28, 0xa1);

        outb(0x04, 0x21);

        outb(0x02, 0xa1);

        outb(0x01, 0x21);

        outb(0x01, 0xa1);

        outb(0xff, 0x21);

        outb(0xff, 0xa1);

}

 

void 

init(void) {

        int a = 3, b = 0;

 

        idt_install();

        pic_install();

        a /= b;

}

init的最后,我们做点坏事,让3/0好产生一个除零异常。

我们知道一旦有硬件发送信号到PIC,PIC会通知处理器中断当前执行的代码,然后处理器会找到指定的ISR去做我们想让它做的事。执行完ISR后,它会回到原来的执行流程中去。所以ISR才是我们关心的东西。

04/isr.s

                .text

                .include "kernel.inc"

                .globl  default_isr, isr 

 

                .macro  isrnoerror              nr

                isr\nr:

                pushl   $0

                pushl   $\nr

                jmp             isr_comm

                .endm

isrnoerror处理没有错误码的异常,它实际上就是一个外壳,仅仅把当做错误码的值0和ISR索引压入栈。

                .macro  isrerror                nr

                isr\nr:

                pushl   $\nr

                jmp             isr_comm

                .endm

用这两个宏,不论有无错误码,所有的异常都可以被一视同仁。

isr:            .long   divide_error, isr0x00, debug_exception, isr0x01

                .long   breakpoint, isr0x02, nmi, isr0x03

                .long   overflow, isr0x04, bounds_check, isr0x05

                .long   invalid_opcode, isr0x06, cop_not_avalid, isr0x07

                .long   double_fault, isr0x08, overrun, isr0x09

                .long   invalid_tss, isr0x0a, seg_not_present, isr0x0b

                .long   stack_exception, isr0x0c, general_protection, isr0x0d

                .long   page_fault, isr0x0e, reversed, isr0x0f

                .long   coprocessor_error, isr0x10, reversed, isr0x11

                .long   reversed, isr0x12, reversed, isr0x13

                .long   reversed, isr0x14, reversed, isr0x15

                .long   reversed, isr0x16, reversed, isr0x17

                .long   reversed, isr0x18, reversed, isr0x19

                .long   reversed, isr0x1a, reversed, isr0x1b

                .long   reversed, isr0x1c, reversed, isr0x1d

                .long   reversed, isr0x1e, reversed, isr0x1f

这是在04/init.c中使用的isr数组,它的元素成对定义,例如,divide_errorisr0x00divide_error是干活的代码段的实际入口,isr0x20是用上面两个宏其中之一产生的代码段,用来维持统一的栈帧。

/*

                +-----------+

                |  old  ss  |   76

                +-----------+

                |  old esp  |   72

                +-----------+

                |  eflags   |   68

                +-----------+

                |    cs     |   64

                +-----------+

                |   eip     |   60

                +-----------+

                |  0/err    |   56

                +-----------+

                |  isr_nr   | tmp = esp

                +-----------+

                |   eax     |   48

                +-----------+

                |   ecx     |   44

                +-----------+

                |   edx     |   40

                +-----------+

                |   ebx     |   36

                +-----------+

                |   tmp     |   32

                +-----------+

                |   ebp     |   28

                +-----------+

                |   esi     |   24

                +-----------+

                |   edi     |   20

                +-----------+

                |    ds     |   16

                +-----------+

                |    es     |   12

                +-----------+

                |    fs     |   8

                +-----------+

                |    gs     |   4

                +-----------+

                |    ss     |   0

                +-----------+

*/

很漂亮的栈帧吧:),所有的异常和中断都使用这个栈帧。

isr_comm:

把所有信息压入栈后,两个宏isrnoerrorisrerror转跳到这里开始一些共有的处理代码。

                pushal

                pushl   %ds

                pushl   %es

                pushl   %fs

                pushl   %gs

                pushl   %ss

压栈、压栈、压栈……

                movw    $DATA_SEL,%ax

                movw    %ax,    %ds

                movw    %ax,    %es

                movw    %ax,    %fs

                movw    %ax,    %gs

所有的数据段都载入权限0的选择子。

                movl    52(%esp),%ecx

                call    *isr(, %ecx, 8)

调用ISR干活,52是ISR在栈帧中的索引。

                addl    $4,             %esp    # for %ss

ISR结束后,栈回滚。最后一条语句跳过了栈中的SS,因为我们不能像其它寄存器那样把它POP出来。

                popl    %gs

                popl    %fs

                popl    %es

                popl    %ds

                popal

                addl    $8,             %esp    # for isr_nr and err_code

跳过栈中的错误码和ISR索引,把栈恢复成ISR调用之前的状态。

                iret

 

                isrNoError              0x00

                isrNoError              0x01

                isrNoError              0x02

                isrNoError              0x03

                isrNoError              0x04

                isrNoError              0x05

                isrNoError              0x06

                isrNoError              0x07

                isrError                0x08

                isrNoError              0x09

                isrError                0x0a

                isrError                0x0b

                isrError                0x0c

                isrError                0x0d

                isrError                0x0e

                isrNoError              0x0f

                isrError                0x10

                isrNoError              0x11

                isrNoError              0x12

                isrNoError              0x13

                isrNoError              0x14

                isrNoError              0x15

                isrNoError              0x16

                isrNoError              0x17

                isrNoError              0x18

                isrNoError              0x19

                isrNoError              0x1a

                isrNoError              0x1b

                isrNoError              0x1c

                isrNoError              0x1d

                isrNoError              0x1e

                isrNoError              0x1f

为异常生成所有的isr0x??

default_isr:

                incb    0xb8000

                movb    $2,             0xb8001

                movb    $0x20,  %al

                outb    %al,    $0x20

                outb    %al,    $0xa0

告诉PIC1和PIC2我们的ISR调用已经完成,它们可以接受新的中断。

                iret

这是为未用的IDT项准备的默认ISR,我们在04/init.c中用过它,它仅仅是在屏幕上用先有的颜色告诉我们什么刚刚发生,并告诉PIC我们的ISR调用已经完成。这两个outb很重要,否则不会有新的中断进来。


啊!除零错!

在这里,所有的异常处理程序仅仅是打印寄存器信息,我展示一下是怎么做的。

04/exceptions.c

#include <kprintf.h>

#include <asm.h>

#include <scr.h>

 

void

divide_error(void) {

        __asm__ ("pushl %%eax;call      info"::"a"(KPL_PANIC));

        halt();

}

 

void

debug_exception(void) {

        __asm__ ("pushl %%eax;call      info"::"a"(KPL_PANIC));

        halt();

}

 

void

breakpoint(void) {

        __asm__ ("pushl %%eax;call      info"::"a"(KPL_PANIC));

        halt();

}

 

void

nmi(void) {

        __asm__ ("pushl %%eax;call      info"::"a"(KPL_PANIC));

        halt();

}

 

void

overflow(void) {

        __asm__ ("pushl %%eax;call      info"::"a"(KPL_PANIC));

        halt();

}

 

void

bounds_check(void) {

        __asm__ ("pushl %%eax;call      info"::"a"(KPL_PANIC));

        halt();

}

 

void

invalid_opcode(void) {

        __asm__ ("pushl %%eax;call      info"::"a"(KPL_PANIC));

        halt();

}

 

void

cop_not_avalid(void) {

        __asm__ ("pushl %%eax;call      info"::"a"(KPL_PANIC));

        halt();

}

 

void

double_fault(void) {

        __asm__ ("pushl %%eax;call      info"::"a"(KPL_PANIC));

        halt();

}

 

void

overrun(void) {

        __asm__ ("pushl %%eax;call      info"::"a"(KPL_PANIC));

        halt();

}

 

void

invalid_tss(void) {

        __asm__ ("pushl %%eax;call      info"::"a"(KPL_PANIC));

        halt();

}

 

void

seg_not_present(void) {

        __asm__ ("pushl %%eax;call      info"::"a"(KPL_PANIC));

        halt();

}

 

void

stack_exception(void) {

        __asm__ ("pushl %%eax;call      info"::"a"(KPL_PANIC));

        halt();

}

 

void

general_protection(void) {

        __asm__ ("pushl %%eax;call      info"::"a"(KPL_PANIC));

        halt();

}

 

void

page_fault(void) {

        __asm__ ("pushl %%eax;call      info"::"a"(KPL_PANIC));

        halt();

}

 

void

reversed(void) {

        __asm__ ("pushl %%eax;call      info"::"a"(KPL_PANIC));

        halt();

}

 

void

coprocessor_error(void) {

        __asm__ ("pushl %%eax;call      info"::"a"(KPL_PANIC));

        halt();

}

所有的ISR通过调用info来在屏幕上显示信息。

void

info(enum KP_LEVEL kl, 

         unsigned int ret_ip, unsigned int ss, unsigned int gs, unsigned int fs, 

         unsigned int es, unsigned int ds, unsigned int edi, unsigned int esi, 

         unsigned int ebp, unsigned int esp, unsigned int ebx, unsigned int edx, 

         unsigned int ecx, unsigned int eax, unsigned int isr_nr, unsigned int err,

         unsigned int eip, unsigned int cs, unsigned int eflags,

         unsigned int old_esp, unsigned int old_ss) {

         static const char *exception_msg[] = {

                "DIVIDE ERROR",

                "DEBUG EXCEPTION",

                "BREAK POINT",

                "NMI",

                "OVERFLOW",

                "BOUNDS CHECK",

                "INVALID OPCODE",

                "COPROCESSOR NOT VALID",

                "DOUBLE FAULT",

                "OVERRUN",

                "INVALID TSS",

                "SEGMENTATION NOT PRESENT",

                "STACK EXCEPTION",

                "GENERAL PROTECTION",

                "PAGE FAULT",

                "REVERSED",

                "COPROCESSOR_ERROR",

        };

        unsigned int cr2, cr3;

        (void)ret_ip;

        __asm__ ("movl  %%cr2,  %%eax":"=a"(cr2));

        __asm__ ("movl %%cr3,   %%eax":"=a"(cr3));

        if (isr_nr < sizeof exception_msg)

                kprintf(kl, "EXCEPTION %d: %s\n=======================\n",

                                isr_nr, exception_msg[isr_nr]);

        else

                kprintf(kl, "INTERRUPT %d\n=======================\n", isr_nr);

        kprintf(kl, "cs:\t%x\teip:\t%x\teflags:\t%x\n", cs, eip, eflags);

        kprintf(kl, "ss:\t%x\tesp:\t%x\n", ss, esp);

        kprintf(kl, "old ss:\t%x\told esp:%x\n", old_ss, old_esp);

        kprintf(kl, "errcode:%x\tcr2:\t%x\tcr3:\t%x\n", err, cr2, cr3);

        kprintf(kl, "General Registers:\n=======================\n");

        kprintf(kl, "eax:\t%x\tebx:\t%x\n", eax, ebx);

        kprintf(kl, "ecx:\t%x\tedx:\t%x\n", ecx, edx);

        kprintf(kl, "esi:\t%x\tedi:\t%x\tebp:\t%x\n", esi, edi, ebp);

        kprintf(kl, "Segment Registers:\n=======================\n");

        kprintf(kl, "ds:\t%x\tes:\t%x\n", ds, es);

        kprintf(kl, "fs:\t%x\tgs:\t%x\n", fs, gs);

}

Makefile需要大改来使代码通过编译。

AS=as -Iinclude

LD=ld

CC=gcc

CPP=gcc -E -nostdinc -Iinclude

用GCC自动生成依赖关系。

CFLAGS=-Wall -pedantic -W -nostdlib -nostdinc -Wno-long-long -I include -fomit-frame-pointer

-Wall -pedantic -W是警告选项,-nostdlib告诉GCC不要用标准库,-nostdinc -Iinclude告诉GCC应该在目录include下寻找头文件而不搜索标准头文件目录,-Wno-long-long禁止关于long long不是C89标准的警告,-fomit-frame-pointer很重要,否则info不能正确的处理栈帧。

KERNEL_OBJS= load.o init.o isr.o libcc.o scr.o kprintf.o exceptions.o

 

#.c.s:

#    ${CC} ${CFLAGS} -S -o $*.s $<

 

.s.o:

    ${AS} -a $< -o $*.o >$*.map

 

all: final.img

 

final.img: bootsect kernel

    cat bootsect kernel > final.img

    @wc -c final.img

 

bootsect: bootsect.o

    ${LD} --oformat binary -N -e start -Ttext 0x7c00 -o bootsect $<

 

kernel: ${KERNEL_OBJS}

    ${LD} --oformat binary -N -e pm_mode -Ttext 0x0000 -o $@ ${KERNEL_OBJS}

    @wc -c kernel

 

clean:

    rm -f *.img kernel bootsect *.o

 

dep:

    sed '/\#\#\# Dependencies/q' < Makefile > tmp_make

    (for i in *.c;do ${CPP} -M $$i;done) >> tmp_make

    mv tmp_make Makefile

 

### Dependencies:

自动生成依赖关系,### Dependencies:很重要,别删。

执行make clean && make dep && makemake dep必须在make之前调用。

making process of tutorial04

如果我们做的都对的话,我们就可以看到错误信息了:)

divide by zero exception


你可以自由使用我的代码,如有疑问请联系我