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

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:

    int 0

 

    cli

    hlt

 

_cont:

cont equ _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){

    static char* 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       equ 0x20

INT_MASTER_CTLMASK   equ 0x21

INT_SLAVE_CTL     equ 0xA0

INT_SLAVE_CTLMASK equ 0xA1

 

INT_VECTOR_IRQ0      equ 32

INT_VECTOR_IRQ8      equ INT_VECTOR_IRQ0 + 8

 

INT_8259A_M_ICW1  equ 0b00010001

INT_8259A_S_ICW1  equ 0b00010001

INT_8259A_M_ICW2  equ INT_VECTOR_IRQ0

INT_8259A_S_ICW2  equ INT_VECTOR_IRQ8

INT_8259A_M_ICW3  equ 0b00000100

INT_8259A_S_ICW3  equ 2

INT_8259A_M_ICW4  equ 0b00000001

INT_8259A_S_ICW4  equ 0b00000001

代码 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的具体内容我们会在讨论进程时进一步讨论。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值