Norlit OS —— 自制操作系统 第5章 中断处理

来源:互联网 发布:mac 视频下载软件 编辑:程序博客网 时间:2024/05/11 19:38

5  中断处理

5.1         Interrupt

在正式开始之前,笔者想说的是笔者把之前的打印函数用汇编写了一遍放在lib.asm里,居然bin文件的大小小了2KB!看来GCC在优化还是没人厉害。

切入正题。我们的内核,还不能称之为内核,因为我们的内核还欠缺了一个重要因素:中断处理。

中断,是一种从打断CPU顺序执行的方式,有外部中断也有内部中断。可以用来做异常报告、设备通讯之用途。我们先讲讲内部中断。

Intel的处理器中,有一个表叫做中断向量表。在那之前,我们先看看从Intel手册中获得的中断异常一览表:

表格5.1.1中断和异常列表

我们来认识一下这些异常的种类:

Fault:错误,这种异常是可以更正的,一旦错误被更正以后,程序可以继续执行。更正后,程序将继续执行原先的指令。

Trap: 陷阱,这种异常也是可以更正的,一旦错误被更正以后,程序可以继续执行,但是会执行下一条指令。

Abort:中止,这种异常不可以更正的,一般是严重的错误。

了解了这些含义之后,我们就要问了,怎么捕捉这些异常呢?

 

5.2         中断向量表

为了处理中断,我们先来介绍一下中断向量表。

中断向量表和GDT一样,是很多8字节的描述符构成的表。不过,和GDT不一样的是,第一项不一定是空项。不过,中断描述符和GDT就差很多了。中断描述符实质上是一个门描述符。

Intel大叔说,中断描述符可以是任务门,中断门或者陷阱门。这些门又是什么东西呢?

我们先除去任务门不说。

中断门和陷阱门,其实就是一个指向代码的地址。我们都知道操作系统和应用程序的权限不同。CPU中也有特权级的概念。运行应用程序的时候,如果中断产生了,我们势必要切换到内核。但是,我们还要转移特权级。于是,Intel设计了门这样一个机制。中断门和陷阱门的区别就是经过中断门后,可屏蔽中断就被屏蔽了。而过了陷阱门后中断不会被屏蔽。

5.2.1门描述符

上图就是门描述符的结构。不是很麻烦。我来介绍一下:第5字节和段描述符一样,P

DPLS还有Type,我就不介绍了。对了,上图的D表示门的大小,1=32位。上面的保留部分实际上被某些别的门所用到,但是这两个门我们就把它置为0

门的目标地址就是选择子:偏移。知道了这些以后,我们也可以开始写代码了。

我们先写一个宏初始化门:

;=====================================================

; GateDescriptor(u32 offset, u16 selector, u8 count, u8attr);初始化门描述符

;-----------------------------------------------------

; Entry:

;   - arg0 ->偏移

;   - arg1 ->选择子

;   - arg2 ->计数器

;   - arg3 ->属性

; Exit:

;   -填充一个段描述符

%macro GateDescriptor 4

   dw%1&0FFFFh                        ;门偏移低16

   dw%2                             ;选择子

   dw(%3&1Fh)|((%4<<8)&0FF00h)  ;计数器及属性

   dw(%1>>16)&0FFFFh                ;门偏移高16

%endmacro

代码5.2.1门初始化(chapter5/a/boot/include/protect.inc)

由于IDTPointerGDTPointer的结构相同,所以我们直接%define IDTPointer GDTPointer了。

接下来初始化IDTTable

IDTTable:

GateDescriptor    cont, SEL_FLAT_C,0, DA_386IGate

IDT_END:

 

IdtPtr:           IDTPointerIDTTable, IDT_END IDTTable

代码5.2.2 IDT(chapter5/a/kernel/entry.asm)

我们改写了_start函数,里面加入了IDT的加载以及测试代码。

_start:

   mov   esp,0x9FC00 ; Set the stack

   cli

   lgdt  [GdtPtr]

   lidt  [IdtPtr]

   sti

   

   jmp    SEL_FLAT_C:gdtinit

gdtinit:

   int0

 

   cli

   hlt

 

_cont:

contequ _cont-$$+KERNEL_ENTRY

   

   call   cstart

 

   cli

hlt

代码5.2.3测试一下,你就知道(chapter5/a/kernel/entry.asm)

这两段代码吧0号中断映射到了cont函数,如果中断设置成功,通过int 0,应该能成功启动cstart,如果失败,那么虚拟机会崩溃。我们可以执行一下。完全没变化,说明我们的IDT设置成功了。

 

5.3         中断处理

世界上不可能存在一个只能处理一个中断的操作系统。我们势必需要扩展我们的OS。笔者打算把IDT专门放在一个文件中,我把它叫做interrupt.asm。需要注意的是,我们的中断处理函数采用了绝对定位,所以我们必须要能够静态地找出中断处理函数的地址。在NASM中,我们只能知道代码在段中的偏移地址,所以我们只要保证interrupt.asm的段在内存的最开始处就行了。为此,我们要修改ld参数的顺序,然后把_start放到interrupt.c中去。

这部分操作虽然复杂,但是没什么技术含量,读者可以自行去看chapter5/b中的源代码以及Makefile

我们添加了所有的内部中断,目前有32项。

divide_error:

   push  -1

   push  0

jmp    exception

exception:

   call   exception_handler

   add   esp,4*2

hlt

代码5.3.1中断处理程序(chapter5/b/interrupt.asm)

所有的中断处理程序都和这个一样,如果没有错误码就压栈一个-1

我们创建了一个新文件int.c,然后增加了exception_handler函数。

void exception_handler(u_addr vec_no, u_addr err_code, u_addr eip, u_addr cs, u_addr eflags){

   staticchar* err_msg[]={

      "#DEDivide Error",

      "#DBDebug Error",

      "   NMI Interrupt",

      "#BPBreakpoint",

      "#OFOverflow",

      "#BRBOUND Range Exceeded",

      "#UDInvalid Opcode (Undefined Opcode)",

      "#NMDevice Not Available (No Math Coprocessor)",

      "#DFDouble Fault",

      "   Coprocessor Segment Overrun (reserved)",

      "#TSInvalid TSS",

      "#NPSegment Not Present",

      "#SSStack-Segment Fault",

      "#GPGeneral Protection",

      "#PFPage Fault",

      "   Intel Reserved",

      "#MFx87 FPU Floating-Point Error (Math Fault)",

      "#ACAlignment Check",

      "#MCMachine Check",

      "#XFSIMD Floating-Point Exception",

      "#VEVirtualization Exception"

   };

   if(vec_no==-1)vec_no=15;

    puts(err_msg[vec_no]);

   if(err_code!=-1){

       puts("\r\n    Error Code: ");

       dispInt(err_code);

   }

    puts("\r\n     Source: ");

    dispInt(cs);

    puts(" : ");

    dispInt(eip);

    puts("\r\n     EFlags: ");

    dispInt(eflags);

    u8* addr;

   for(addr=(u8*)0xB8001;((u_addr)addr)<0xB8FA0;addr+=2){

      *addr=0x1F;

   }

}

代码5.3.2异常处理(chapter5/b/int.c)

非常地简单,我们显示了错误名称、错误码、以及EFLAGS和出错地址。

为了有趣,我还加上了把整个屏幕变蓝的函数,并在cstart中清空了屏幕。这样我们就制造了一个蓝屏。

Make然后运行。

5.3.1我们的OSVMware下的效果

真是Perfect

 

5.4         8259A

现在的处理器都有一个APIC(高级可编程中断控制器),APIC比较复杂,我们先从最早的中断机制看起。

最早的中断控制机制拥有2片级联的PIC(可编程中断控制器),也就是说最多可以支持

一个外部中断请求信号通过中断请求线IRQ,传输到IMR(中断屏蔽寄存器),IMR根据所设定的中断屏蔽字(OCW1),决定是将其丢弃还是接受。如果可以接受,则8259AIRR(中断请求暂存寄存器)中代表此IRQ的位置位,以表示此IRQ有中断请求信号,并同时向CPUINTR(中断请求)管脚发送一个信号,但CPU这时可能正在执行一条指令,因此CPU不会立即响应,而当这CPU正忙着执行某条指令时,还有可能有其余的IRQ线送来中断请求,这些请求都会接受IMR的挑选,如果没有被屏蔽,那么这些请求也会被放到IRR中,也即IRR中代表它们的IRQ的相应位会被置1

INTR管脚会要求CPU处理中断是否有信号,如果发现有信号,就会转到中断服务,此时,CPU会立即向8259A芯片的INTA(中断应答)管脚发送一个信号。当芯片收到此信号后,它就在IRR中,挑选优先级最高的中断,将中断请求送到ISR(中断服务寄存器),也即将ISR中代表此IRQ的位置位,并将IRR中相应位置零,表明此中断正在接受CPU的处理。同时,将它的编号写入中断向量寄存器IVR的低三位,这时,CPU还会送来第二个INTA信号,当收到此信号后,芯片将IVR中的内容,也就是此中断的中断号送上通向CPU的数据线。

8259A的编程是通过向其相应的端口发送一系列的ICW(初始化命令字)完成的。总共需要发送四个ICW,它们都分别有自己独特的格式,而且必须按次序发送,并且必须发送到相应的端口。在初始化命令字中有一些位是针对80808085CPU的,因为这两种CPU早已不用了,所以,在下面命令字的说明中对这些位不再作进一步的解释。在写命令字时这些位取0

5.4.1 ICW1命令字

对于ICW1命令字,我们使用级联的PIC以及32位中断向量,我们会发送一个ICW4,所以我们应该设置ICW10b00010001=0x11

ICW2用来设置IRQ对应的中断向量号。所有的IRQ最高5位都是一致的,被设置在ICW2中。ICW2的低三位任意,一般设置为0

ICW3 只有在一个系统中包含多片8259A时才有意义。而系统中多片8259A是由ICW1D1SNGL)来指示的,所以只有当SNGL=0时,才设置ICW3  。并且ICW3  在主片和从片中的格式还不一样。在主片中,第几个端口接了从片就把第几位置为1。对于从片来说,ICW3的值等于接在主片上端口号。高5位被保留。

ICW4的结构如下图所示,也比较简单。

5.4.2 ICW4的结构

中断的操作不应该被缓冲,所以我们设置BUFMS位为0SDNM我们也置为0EOI的处理我们将手动进行,所以AEOI也清除。我们的确在80x86模式下运行,因此我们设置μP0。这样一来我们的ICW4设置就完成了,我们将ICW设置为1

接下来是端口。从片和主片的端口是不同的。而且ICW1必须写入在偶数端口,而ICW2-4则写入在奇数端口。

一下是端口对照表:

名称

芯片

用途

端口

INT_MASTER_CTL

主片

ICW1

0x20

INT_MASTER_CTLMASK

主片

ICW2-4,OCW

0x21

INT_SLAVE_CTL

从片

ICW1

0xA0

INT_SLAVE_CTLMASK

从片

ICW2-4,OCW

0xA1

哦对了,OCW是中断设置的控制字,其中OCW1是屏蔽控制字,OCW2是优先级控制字,还有OCW3。。。这些我们就不管了。

OCW1其实很简单,第几位控制第几个端口,设为0的为表示允许中断,设为1的位表示禁止中断。譬如,0b11111101表示禁止第2号中断(主片IRQ2或者从片IRQ10)。

我们把一些宏定义在include/const.inc里面:

INT_MASTER_CTL      equ0x20

INT_MASTER_CTLMASK  equ0x21

INT_SLAVE_CTL    equ0xA0

INT_SLAVE_CTLMASKequ0xA1

 

INT_VECTOR_IRQ0     equ32

INT_VECTOR_IRQ8     equ INT_VECTOR_IRQ0+8

 

INT_8259A_M_ICW1 equ0b00010001

INT_8259A_S_ICW1 equ0b00010001

INT_8259A_M_ICW2 equ INT_VECTOR_IRQ0

INT_8259A_S_ICW2 equ INT_VECTOR_IRQ8

INT_8259A_M_ICW3 equ0b00000100

INT_8259A_S_ICW3 equ2

INT_8259A_M_ICW4 equ0b00000001

INT_8259A_S_ICW4 equ0b00000001

代码5.4.1 8259A相关宏(chapter5/c/kernel/include/const.inc)

接下来我们初始化8259A

init_8259A:

   mov   al,INT_8259A_M_ICW1

   out    INT_MASTER_CTL,al

   mov   al,INT_8259A_M_ICW2

   out    INT_MASTER_CTLMASK,al

   mov   al,INT_8259A_M_ICW3

   out    INT_MASTER_CTLMASK,al

   mov   al,INT_8259A_M_ICW4

   out    INT_MASTER_CTLMASK,al

   

   mov   al,INT_8259A_S_ICW1

   out    INT_SLAVE_CTL,al

   mov   al,INT_8259A_S_ICW2

   out    INT_SLAVE_CTLMASK,al

   mov   al,INT_8259A_S_ICW3

   out    INT_SLAVE_CTLMASK,al

   mov   al,INT_8259A_S_ICW4

   out    INT_SLAVE_CTLMASK,al

   

   mov   al,0b11111101

   out    INT_MASTER_CTLMASK,al

   mov   al,0b11111111

   out    INT_SLAVE_CTLMASK,al

   

ret

代码5.4.2初始化8259A(chapter5/c/kernel/interrupt.asm)

设置完中断后,我们简单处理一下IDT,添加一些表项,这部分代码比较烦,但是不难,不外乎是一些宏,添加方式与之前一样。我们依旧让exception_handler处理这些中断,并让它显示文字“Hardware IRQ”。

更改一下entry.asm的代码,不使用cli,使得我们有机会接受中断:

start:

   mov   esp,0x9FC00 ; Set the stack

 

   call   init_8259A

   

   call   cstart

 

    .loop:

   hlt

   jmp    .loop

代码5.4.3允许接受外来中断(chapter5/b/kernel/entry.asm)

接下来Make一下并运行。好像什么反应都没有。对了,我们没有设备产生中断!其实,如果你仔细看了OCW1的设置,你就会发现我们允许了IRQ1:键盘。我们随便按下一个键。出现了!

至此我们IRQ的设置也完成了。

关于8259A的具体内容我们会在讨论进程时进一步讨论。

         接下来,我们就要进入比较繁杂的一部分——内存管理了。

原创粉丝点击