操作系统——中断实现(十五)

 操作系统——中断实现(十五)

2020-09-28 18:33:33 hawk


概述

  前面讲了许多关于中断的基础知识,这里我们将在前面的基础上,给操作系统添加上中断处理,并不断进行优化。

  这次仓库链接点此进入。这里面有几个版本,根据log可以回滚到。


简易中断处理程序

  前面一直写理论,把我自己都看吐了,这里首先讲一点实的——Intel 8295A芯片实际上使位于主板上的南桥芯片中的,因此相当于硬件已经给我们提供好了处理中断的必要支持了,因此我们只需要完成8259A的编程操作和中断处理程序的环境构建即可。这里简单描述一下启用中断的大体流程,如下所示

 

 

   

  其中,Init_all函数就是用来初始化所有的色号被以及数据结构的,也就是我们将通过在内核中调用Init_all函数,从而完成初始化工作。而Init_all函数首先调用的,就是Idt_Init函数,用来初始化中断相关的内容——当然,中断的初始化也需要分成几个部分去完成,这里分为了pic_Init和Idt_desc_Init进行的。其中,pic_Init用来初始化可编程中断控制器8259A(pic就是Programmable Interrupt Controller);而Idt_desc_Init,自然的,就是用来初始化中断描述符表IDT的。

  为了更好的演示中断处理程序,我们首先使用汇编语言完成中断处理程序。这其中使用到的新内容就是宏,即一段代码的模板,格式如下所示

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

 

   下面首先是中断处理程序的源代码,如下所示

 

;        主要实现一个简易版本的中断处理程序
;------------------------------------------------------------------------


[bits 32]

%define    ERROR_CODE    nop            ;若在相关的异常中,CPU已经自动压入错误码,为保证栈格式统一,这里不进行操作

%define ZERO    push 0                ;若在相关的异常中,CPU没有自动压入错误码,为保证栈格式统一,手动压入0

extern    put_str                    ;声明外部函数,即方便调用已经实现好的打印函数

SECTION    .data:
    intr_str db "Hawk's Interrupt occur!", 0xa, 0x0

    

%macro    VECTOR    2
    SECTION    .text
    intr%1entry:                    ;第一个参数用来表示在IDT中的索引

        %2                    ;表明第二个参数应该是指令,实际上就是前面定义的ZERO或者ERROR_CODE
        
        push    intr_str
        call    put_str                ;首先输出给定的相关字符串

        add    esp, 4                ;维护栈平衡


        mov    al, 0x20            ;将其看做OCW,除了EOI为1,其余位全部为0,也就是普通EOI结束方式

        out    0xa0, al            ;向从片发送EOI信号
        out    0x20, al            ;向主片发送EOI信号

        add    esp, 4                ;考虑到前面%2,无论如何都确保压入了ERROR_CODE,所以需要跳过,方便进行返回

        iret                    ;依次弹出eip、cs、eflags,并根据特权级是否变化再弹出ss/esp

    SECTION    .data
        %if %1 == 0x00
            global intr_entry_table
            intr_entry_table:
        %endif

        dd    intr%1entry            ;有点类似于got表中的每一个表项,里面保存的都是函数的地址
%endmacro



;    0 ~ 19中断向量是CPU内部固定的异常类型,其是否压入错误码可以根据CPU的规定知道,见书上表7.1
VECTOR 0x00,ZERO
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO 
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO 
VECTOR 0x08,ERROR_CODE
VECTOR 0x09,ZERO
VECTOR 0x0a,ERROR_CODE
VECTOR 0x0b,ERROR_CODE 
VECTOR 0x0c,ERROR_CODE 
VECTOR 0x0d,ERROR_CODE
VECTOR 0x0e,ERROR_CODE
VECTOR 0x0f,ZERO 
VECTOR 0x10,ZERO
VECTOR 0x11,ERROR_CODE
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO 

;    20 ~ 31中断向量是Intel保留的,这里对于是否压入错误码,我是根据网上资料进行修改的
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO 
VECTOR 0x18,ERROR_CODE
VECTOR 0x19,ZERO
VECTOR 0x1a,ERROR_CODE
VECTOR 0x1b,ERROR_CODE 
VECTOR 0x1c,ZERO
VECTOR 0x1d,ERROR_CODE
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO

;    32号中断向量开始,这里才是我们可以设置的最低中断向量号
VECTOR 0x20,ZERO

 

  可以看到,逻辑上是很简单,就是通过宏,来方便的定义中断处理程序——每一个都是简单的输出字符串。这里需要说明几点

  1.  这里我们定义intr_entry_table(类似于got表,里面的每一个表项又是一个函数指针)和课本上的不太一样,因为课本上的我经过编译后,intr_entry_table始终指向.data中的第一个字节,也就是字符串位置,不知道为什么。

  2.  这里面ZERO或者ERROR_CODE与否(前0x20个),是已经定好了的——因为0 - 0x13的中断是CPU内部中断;而0x14 - 0x1f是Intel保留的,所以我们查询资料即可。

 

  下面则是idt的构建——否则我们怎么找到这些中断处理程序?当然,这里按照我们前面的预期,将idt的初始化和8259A一起进行初始化,源代码如下所示

#ifndef __LIB_IO_H
#define    __LIB_IO_H

    #include "stdint.h"
    
    /*
        向端口port写入一个字节
    */
    static inline void outb(uint16_t port, uint8_t data) {
        /*
            b0表示第一个操作数的低1字节,这里就是eax寄存器的低1字节,也就是al
            w1表示第二个操作数的低2字节,这里就是edx寄存器的低2字节,也就是dx

            Nd表示立即数约束,表示0 - 255,这里也就是最多1字节的内容
        */
        asm volatile("\
        outb    %b0, %w1"::"a"(data), "Nd"(port));
    }


    /*
        将addr处起始的word_cnt个字(2字节)写入端口port中
    */
    static inline void outsw(uint16_t port, const void* addr, uint32_t word_cnt) {
        /*
            这里默认已经将ss、ds段寄存器设置为对应的选择子,因此不需要担心选择子。
            outsw将    DS:esi中的字输出到 dl指向的端口
        */
        asm volatile("\
        cld; \
        rep outsw":"+c"(word_cnt), "+S"(addr):"d"(port));
    }


    /*
        将从端口port读入的一个字节返回
    */
    static inline uint8_t inb(uint16_t port) {
        /*
            和前面实际上是类似的

            b0表示第一个操作数的低1字节,这里就是eax寄存器的低1字节,也就是al
            w1表示第二个操作数的低2字节,这里就是edx寄存器的低2字节,也就是dx

            Nd表示立即数约束,表示0 - 255,这里也就是最多1字节的内容
        */
        uint8_t data;
        asm volatile("\
        inb    %w1, %b0":"=a"(data):"Nd"(port));

        return data;
    }


    /*
        将从端口port读入的word_cnt个字(2字节)写入目的地址addr中
    */
    static inline void insw(uint16_t port, const void* addr, uint32_t word_cnt) {
        /*
            这里默认已经将ss、ds段寄存器设置为对应的选择子,因此不需要担心选择子。
            insw将    dl指向的端口中的字 输出到    es:esi指向的内存
        */
        asm volatile("\
        cld;\
        rep insw":"+c"(word_cnt), "+D"(addr):"d"(port):"memory");
    }

#endif

 

#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "print.h"
#include "io.h"


#define    IDT_DESC_CNT    0x21                //目前总共支持的中断数目,其中0x0 - 0x13是CPU内部保留的,0x14 - 0x20是Intel预留的,0x21开始才是我们实现的

#define PIC_M_PORT_20    0x20                //主片的0x20端口
#define PIC_M_PORT_21    0x21                //主片的0x21端口
#define PIC_S_PORT_a0    0xa0                //从片的0xa0端口
#define PIC_S_PORT_a1    0xa1                //从片的0xa1端口



/*
    下面是中断描述符结构体
*/


typedef struct IDT_DESC {
    uint16_t    func_offset_low_word;                //即中断处理程序目标代码段内的偏移的0 - 15位
    uint16_t    func_selector;                    //即中断处理程序目标代码段的选择子

    uint16_t    func_attribute;                    //即中断描述符相关的属性
    
    uint16_t    func_offset_high_word;                //即中断处理程序目标代码段内的偏移的16 - 31位
} *idt_desc;




//这里也就是IDT,即中断描述符表
static struct IDT_DESC idt[IDT_DESC_CNT];


//这里就是汇编中定义的intr_entry_table,相当于got表,里面每一项都是函数指针
extern intr_handler intr_entry_table[IDT_DESC_CNT];


//    创建指向中断处理程序偏移为function的终端描述符体
static void create_idt_desc(idt_desc desc, uint16_t attr, intr_handler function) {
    desc->func_offset_low_word = ((uint32_t)function) & 0x0000ffff;                //获取目标代码段偏移的0 - 15位

    desc->func_selector = SELECTOR_K_CODE;                    
    
    desc->func_attribute = attr;

    desc->func_offset_high_word = ((uint32_t)function & 0xffff0000) >> 16;                //获取目标代码段偏移的16 - 31位
}



//    用来初始化IDT,即安装所有的中断处理程序
static void idt_desc_init(void) {
    put_str("[*] idt_desc_init start\n");

    for(int i = 0; i < IDT_DESC_CNT; ++i) {create_idt_desc(idt + i, IDT_DESC_ATTR_DPL0, intr_entry_table[i]);}

    put_str("[*] idt_desc_init done\n");
}



//    用来初始化可编程中断控制器 Intel 8259A
static void pic_init(void) {
    put_str("[*] pic_init start\n");
    /*
        首先初始化主片,主要是写入1字节的ICW1 - ICW4
    */
    //-------------------------------------------------------------

    /*
        0x11        000_1_0____0___0___1b
                000|1|LTIM|ADI|SNGL|IC4
        即边沿触发、级联的
    */    
    outb(PIC_M_PORT_20, 0x11);

    /*
        当前主片的起始IRQ中断向量号为0x20
    */    
    outb(PIC_M_PORT_21, 0x20);


    /*
        即级联了两片Intel 8259A
        主片的IRQ2与从片进行级联
    */    
    outb(PIC_M_PORT_21, 0x04);


    /*
        0x01        000_0____0___0___0____1
                000|SPNM|BUF|M/S|AEOI|u PM
        即为非缓冲模式,手动结束中断
    */    
    outb(PIC_M_PORT_21, 0x01);




    /*
        接着初始化从片,同样是写入1字节的ICW1 - ICW4
    */
    //-------------------------------------------------------------

    /*
        0x11        000_1_0____0___0___1b
                000|1|LTIM|ADI|SNGL|IC4
        即边沿触发、级联的
    */    
    outb(PIC_S_PORT_a0, 0x11);

    /*
        当前从片的起始IRQ中断向量号为0x28(因为主片已经占据了0x20中断)
    */    
    outb(PIC_S_PORT_a1, 0x28);


    /*
        即级联了两片Intel 8259A
        主片的IRQ2与从片进行级联
        主片中是8bit,每一bit代表一个IRQ
        从片中是3bit, 共同表示IRQ的顺序
    */    
    outb(PIC_S_PORT_a1, 0x02);


    /*
        0x01        000_0____0___0___0____1
                000|SPNM|BUF|M/S|AEOI|u PM
        即为非缓冲模式,手动结束中断
    */    
    outb(PIC_S_PORT_a1, 0x01);




    /*
        仅仅打开主片的IR0,也就是仅仅接受主片的IRQ0的中断信号,也就是时钟中断信号
    */
    outb(PIC_M_PORT_21, 0xfe);
    outb(PIC_S_PORT_a1, 0xff);
    
    put_str("[*] pic_init done\n");
}



//    完成中断相关的初始化工作
void idt_init(void) {
    put_str("[*] idt_init start\n");

    //即完成Intel 8259A的初始化、IDT的初始化
    pic_init();
    idt_desc_init();

    //下面使用内联汇编实现加载idt
    uint64_t idt_ptr = (sizeof(idt) - 1) | ( ((uint64_t)idt) << 16);                //一共48位,低16位是idt的界限;而高32位是idt的基址

    asm volatile("\
     lidt    %0"::"m"(idt_ptr));                                    //这里相当于直接通过内存中的地址进行访问变量的值,所以对于变量值无所谓(只要字节数大于指令中要求的即可)

    put_str("[*] idt_init done\n");
}

 

  这里逻辑还是很简单的,就是利用前面已经完成好的中断处理程序地址偏移表(intr_entry_table)初始化IDT;同时使用io.h提供的IO接口通信功能完成8259A的初始化即可。另外值得注意的是,为了方便观察,这里仅仅开了时钟中断,屏蔽了其他的中断,方便进行观察。

  最后,则是将这些共同链接,附加一些额外的代码,如下所示

#include "print.h"
#include "init.h"


int main(void) {

    /*
        初始化所有的模块
    */    
    init_all();

    //为了演示中断,我们此时打开中断,使用sti指令,其会将eflags寄存器中的IF置为相关的值
    asm volatile("\
        sti");

    while(1);

    return 0;
}

 

#include "init.h"
#include "print.h"
#include "interrupt.h"

/*
    负责初始化所有的模块
*/

void init_all(void) {
    put_str("[*] init_all start\n");
    idt_init();
    put_str("[*] init_all done\n");
}
    

 

  最后则是编译、链接,构建最后的虚拟硬盘,这里我已经提前写好了makefile,对于不懂得规则可以自己查询一下,如下所示

#############################    KERNEL-15 目录的编译            ############################################
KERNEL-15/mbr.bin : KERNEL-15/mbr.S
    nasm -I KERNEL-15/include/ -o KERNEL-15/mbr.bin KERNEL-15/mbr.S

KERNEL-15/loader.bin: KERNEL-15/loader.S
    nasm -I KERNEL-15/include/ -o KERNEL-15/loader.bin KERNEL-15/loader.S

KERNEL-15/kernel/lib/kernel/print.o: KERNEL-15/kernel/lib/kernel/print.S KERNEL-15/kernel/lib/stdint.h
    nasm -f elf -o KERNEL-15/kernel/lib/kernel/print.o KERNEL-15/kernel/lib/kernel/print.S

KERNEL-15/kernel/interrupt.o: KERNEL-15/kernel/interrupt.c KERNEL-15/kernel/global.h KERNEL-15/kernel/interrupt.h KERNEL-15/kernel/lib/stdint.h KERNEL-15/kernel/lib/kernel/print.h KERNEL-15/kernel/lib/kernel/io.h
    gcc -m32 -w -I KERNEL-15/kernel/ -I KERNEL-15/kernel/lib/ -I KERNEL-15/kernel/lib/kernel/ -I KERNEL-15/kernel/lib/user/ -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes -fno-stack-protector -o KERNEL-15/kernel/interrupt.o KERNEL-15/kernel/interrupt.c


KERNEL-15/kernel/init.o: KERNEL-15/kernel/init.c KERNEL-15/kernel/global.h KERNEL-15/kernel/init.h KERNEL-15/kernel/lib/kernel/print.h KERNEL-15/kernel/interrupt.h
    gcc -m32 -w -I KERNEL-15/kernel/ -I KERNEL-15/kernel/lib/ -I KERNEL-15/kernel/lib/kernel/ -I KERNEL-15/kernel/lib/user/ -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes -fno-stack-protector -o KERNEL-15/kernel/init.o KERNEL-15/kernel/init.c

KERNEL-15/kernel/kernel.o: KERNEL-15/kernel/kernel.S KERNEL-15/kernel/lib/kernel/print.o
    nasm -f elf -o KERNEL-15/kernel/kernel.o KERNEL-15/kernel/kernel.S

KERNEL-15/kernel/main.o: KERNEL-15/kernel/main.c KERNEL-15/kernel/lib/kernel/print.h KERNEL-15/kernel/global.h KERNEL-15/kernel/init.h
    gcc -m32 -w -I KERNEL-15/kernel/ -I KERNEL-15/kernel/lib/ -I KERNEL-15/kernel/lib/kernel/ -I KERNEL-15/kernel/lib/user/ -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes -fno-stack-protector -o KERNEL-15/kernel/main.o KERNEL-15/kernel/main.c

KERNEL-15/kernel/kernel.bin: KERNEL-15/kernel/main.o KERNEL-15/kernel/lib/kernel/print.o KERNEL-15/kernel/init.o KERNEL-15/kernel/interrupt.o KERNEL-15/kernel/kernel.o KERNEL-15/kernel/init.o
    ld -melf_i386 -Ttext 0xc0002000 -e main -o KERNEL-15/kernel/kernel.bin KERNEL-15/kernel/main.o KERNEL-15/kernel/init.o KERNEL-15/kernel/interrupt.o KERNEL-15/kernel/kernel.o KERNEL-15/kernel/lib/kernel/print.o


KERNEL-15 : KERNEL-15/mbr.bin KERNEL-15/loader.bin KERNEL-15/kernel/kernel.bin
    dd if=KERNEL-15/mbr.bin of=./hawk.img bs=512 count=1 conv=notrunc;
    dd if=KERNEL-15/loader.bin of=./hawk.img bs=512 seek=1 count=9 conv=notrunc;
    dd if=KERNEL-15/kernel/kernel.bin of=./hawk.img bs=512 seek=10 count=200 conv=notrunc;
    
    rm -rf KERNEL-15/mbr.bin;
    rm -rf KERNEL-15/loader.bin;
    rm -rf KERNEL-15/kernel/main.o KERNEL-15/kernel/kernel.bin KERNEL-15/kernel/init.o KERNEL-15/kernel/interrupt.o KERNEL-15/kernel/kernel.o;
    rm -rf KERNEL-15/kernel/lib/kernel/print.o

 

  然后我们简单的执行如下指令,即可完成所有的编译工作。如下所示

make -f makefile KERNEL-15

 

 

  然后我们运行虚拟机进行验证,命令如下所示

~/bochs/bin/bochs -f bochsrc.disk 

 

  结果如图所示

 

 

 

   可以看到,操作系统正确的加载了idt。同时CPU正确接受了中断信号,并且执行了正确的中断处理程序。


改进中断处理程序

  前面我们实现的中断处理程序过于简单,每一个中断都输出相同的字符串。下面我们将在c语言中实现对应的中断处理程序。这里仍然简单的介绍一下大体思路,这里需要修改的只有两个文件——kernel.S和interrupt.c文件。对于kernel.S文件来说,为了保持其尽可能的精简,但是有为了实现对应的功能,所以我们在保持原有框架不修改的情况下,让其宏修改为保存上下文、call 函数、恢复上下文,这样子其实际上仅仅需要的源代码就很精简了,仅仅是push,call和pop等组合,而至于最后的函数位置,实际上是在interrupt.c中的,这里我们首先给出修改后的kernel.S源代码,如下所示

;        主要实现一个简易版本的中断处理程序
;------------------------------------------------------------------------


[bits 32]

%define    ERROR_CODE    nop            ;若在相关的异常中,CPU已经自动压入错误码,为保证栈格式统一,这里不进行操作

%define ZERO    push 0                ;若在相关的异常中,CPU没有自动压入错误码,为保证栈格式统一,手动压入0

extern    put_str                    ;声明外部函数,即方便调用已经实现好的打印函数
extern    idt_table                ;声明外部数据,即实际上中断处理程序最后实际的处理部分的函数指针,通过call进行调用即可

    

%macro    VECTOR    2
    SECTION    .text
    intr%1entry:                    ;第一个参数用来表示在IDT中的索引

        %2                    ;表明第二个参数应该是指令,实际上就是前面定义的ZERO或者ERROR_CODE

;        下面进行保存上下文——保存段寄存器和通用寄存器,因为使用c程序可能破坏掉这些环境
        push    ds
        push    es
        push    fs
        push    gs
        pushad                    ;这里是将所有寄存器双字寄存器入栈,先后入栈顺序为eax、ecx、edx、ebx、原始esp、ebp、esi和edi
    
        mov    al, 0x20            ;将其看做OCW,除了EOI为1,其余位全部为0,也就是普通EOI结束方式
        out    0xa0, al            ;向从片发送EOI信号
        out    0x20, al            ;向主片发送EOI信号

;        下面开始调用真正处理中断的程序,则我们需要将中断向量号传递过去
        push    %1                ;压入中断向量号

        call    [idt_table + 4 * %1];        ;因为实际上idt_table也是一个函数指针数组,其每一个元素都是对应中断处理程序的函数指针
        
    
        add    esp, 4                ;恢复栈平衡

                            ;首先回复保护的上下文环境
        popad
        pop    gs
        pop    fs
        pop    es
        pop    ds


        add    esp, 4                ;跳过error_code
        iretd                    ;最终完成中断处理程序


    SECTION    .data
        %if %1 == 0x00
            global intr_entry_table
            intr_entry_table:
        %endif

        dd    intr%1entry            ;有点类似于got表中的每一个表项,里面保存的都是函数的地址
%endmacro



;    0 ~ 19中断向量是CPU内部固定的异常类型,其是否压入错误码可以根据CPU的规定知道,见书上表7.1
VECTOR 0x00,ZERO
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO 
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO 
VECTOR 0x08,ERROR_CODE
VECTOR 0x09,ZERO
VECTOR 0x0a,ERROR_CODE
VECTOR 0x0b,ERROR_CODE 
VECTOR 0x0c,ERROR_CODE 
VECTOR 0x0d,ERROR_CODE
VECTOR 0x0e,ERROR_CODE
VECTOR 0x0f,ZERO 
VECTOR 0x10,ZERO
VECTOR 0x11,ERROR_CODE
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO 

;    20 ~ 31中断向量是Intel保留的,这里对于是否压入错误码,我是根据网上资料进行修改的
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO 
VECTOR 0x18,ERROR_CODE
VECTOR 0x19,ZERO
VECTOR 0x1a,ERROR_CODE
VECTOR 0x1b,ERROR_CODE 
VECTOR 0x1c,ZERO
VECTOR 0x1d,ERROR_CODE
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO

;    32号中断向量开始,这里才是我们可以设置的最低中断向量号
VECTOR 0x20,ZERO

  实际上,根据代码可以看出来,最后中断处理程序的地址是存储在idt_table数组中的,而idt_table数组又是位于interrupt.c中的。实际上这很神奇——我们在interrupt.c中需要安装中断处理程序,所以引用到了kernel.S中的intr_entry_table数组,这里面每个元素都是对应的中断向量号的中断处理程序的地址,而这些地址最终调用的函数地址,仍然是位于interrupt.c中的,其修改部分如下所示

 

//    这里用来存储中断名称
char* intr_name[IDT_DESC_CNT];

//    这里就是汇编中定义的intr_entry_table,相当于got表,里面每一项都是函数指针,实际上最终调用的仍然是在下面定义的ide_table中的程序
//    其多余的代码主要是保存上下文环境等作用
extern intr_handler intr_entry_table[IDT_DESC_CNT];

//    这里就是idt_table,里面包含了实际的中断处理函数
intr_handler idt_table[IDT_DESC_CNT];


//    这里是通用的中断处理程序,一般用于在异常出现时的处理
static void general_intr_handler(uint8_t vec_nr) {

    /*
        主片的IRQ7和从片的IRQ7会产生伪中断(spurious interrupt),无需进行处理
    */
    if( vec_nr == 0x27 || vec_nr == 0x2f) {return;}

    put_str("int vector : 0x");
    put_uHex(vec_nr);

    put_char(' ');    put_str(intr_name[vec_nr]);
    put_char('\n');
}



//    完成一般的中断处理函数注册以及异常名称的注册
static void exception_init(void) {

    for(int i = 0; i < IDT_DESC_CNT; ++i) {
        //    在kernel.S中,在intr_entry_table中,最后通过call [idt_table + 4 * %1]调用
        idt_table[i] = general_intr_handler;
        
        //    先统一初始化为unknown,方便之后发现没有被注册的异常名称
        intr_name[i] = "unknown";
        }

    //    完成异常名称的注册,下面都是约定好的,网上查阅资料即可找到
    intr_name[0] = "#DE Divide Error";
    intr_name[1] = "#DB Debug Exception";
    intr_name[2] = "NMI Interrupt";
    intr_name[3] = "#BP Breakpoint Exception";
    intr_name[4] = "#OF Overflow Exception";
    intr_name[5] = "#BR BOUND Range Exceeded Exception";
    intr_name[6] = "#UD Invalid Opcode Exception";
    intr_name[7] = "#NM Device Not Available Exception";
    intr_name[8] = "#DF Double Fault Exception";
    intr_name[9] = "Coprocessor Segment Overrun";
    intr_name[10] = "#TS Invalid TSS Exception";
    intr_name[11] = "#NP Segment Not Present";
    intr_name[12] = "#SS Stack Fault Exception";
    intr_name[13] = "#GP General Protection Exception";
    intr_name[14] = "#PF Page-Fault Exception";
    //    intr_name[15] 第15项是intel保留项,未使用
    intr_name[16] = "#MF x87 FPU Floating-Point Error";
    intr_name[17] = "#AC Alignment Check Exception";
    intr_name[18] = "#MC Machine-Check Exception";
    intr_name[19] = "#XF SIMD Floating-Point Exception";
}

 

   看起来似乎是多此一举,实际上我认为还是有一定道理的——因为在汇编语言中,可以方便的进行寄存器等的操作。因此方便我们实现保存上下文、回复上下文等的操作。而c语言由于是高级语言,所以其更容易实现复杂的功能,所以用来进行安装中断程序、实现中断程序功能等。因此,由于中断处理程序一开始就需要保存上下文等,所以中断处理程序的入口一定由汇编语言定义,而中断处理程序如果比较复杂的话,则一定需要由c语言进行实现,则汇编语言最后一定会调用c定义好的中断处理程序。因此这样就是合情合理的。下面我们同样按照上面分析过的,通过make命令进行编译、构建等,最后在虚拟机上进行测试,如下所示

 

 

  可以看到,按照预期,中断处理程序输出了中断向量号以及中断名称。下面我们会调试整个程序,从而完成对于中断全过程的分析。

 

 

 

  首先,我们按照书上的方法,可以获取到发生时间中断时执行的总的指令数,然后重新再次进行断电,并观察发生中断前的上下文环境,如下所示

 

  然后我们继续执行一步,此时会发生中断,然后我们查看对应的指令,如下所示

 

  也就是kernel.S中的保存上下文环境部分。当然,在发生中断的瞬间,CPU应该已经自动的将栈(特权级变化的话)、eflags、cs段和eip压入栈了,我们查看一下当前的栈,如下所示

 

  可以比较两次栈的变化,其压入了eflags、cs和eip,以及后面的错误代码、ds、es、fs、gs。可以看到,实际上和我们的预期是完全一样的。下面就是一些代码的执行,这里我们略过,直接快进到返回时候的地方,如下所示

   可以看到,此时快要推出了,此时栈中应该是除了CPU自动压入的eflags寄存器、cs和eip外,还有中断处理程序压入的error_code信息,其余通用寄存器应该已经恢复到原始情况,我们查看一下

   然后继续执行后,则略过错误代码,将栈顶指向了eip,从而执行iret即可恢复中断发生前的情况,如下所示

 

  可以看到,确实和一开始是一样的。


更快的中断

  最后,我们在这里引入可编程计数器/定时器8253,从而让时钟中断发生的更快一些。后面也会使用到这个,因此这里接着中断处理这个机会,提前接触一下这些概念。

  首先是时钟,计算机中各个设备通过时钟进行同步通信过程。而其大体上可以分为两类——内部时钟和外部时钟。对于内部时钟来说,其指的是CPU内部元件的工作时序,用于控制、同步内部工作过程的步调。内部时序由晶体振荡器产生的,其频率经过分频后就是主板的外频,将外频乘以某个倍数则成为主频。外部时钟是指CPU与外部设备或外部设备之间进行通信时采用的一种时序,其时钟的时间单位粒度会较大。

  而由于外部时钟和内部时钟往往是两套独立的定时体系,因此我们的解决思路是通过定时器解决时序配合问题。当定时器到达了所计数的时间时,其会向CPU发送中断。我们下面接触到的是可编程定时器PIT(Programmable Interval Timer) Intel 8253。实际上8253的介绍和上面8295A的介绍十分相似——虽然我们使用到的部分很少,但为了完全理解,需要介绍很多额外的知识。首先我们介绍一下对应的结构,如图所示

 

   我们给出计数器内重要的IO接口的端口信息

计数器名称 端口 作用
计数器0 0x40 用于产生实时信号,其就是连接到8259A的主片的IRQ0的时钟
计数器1 0x41 用于DRAM的定时刷新
计数器2 0x42 用于内部扬声器产生不同音调的声音
控制字寄存器 0x43 设置所指定的计数器的工作方式、读写格式以及数制等信息

  

  实际上我们需要改变的就是计数器0中的设置,从而改变本实验中时钟中断的频率,而这需要通过设置控制字寄存器和计数器0完成。我们首先介绍一下控制字寄存器中的数据结构,如下所示

 

  这里就主要介绍一下工作方式相关的信息。实际上计数器开始计数需要两个条件——GATE为高电平;计数器初值已经写入计数器中的减法计数器中。当条件具备后,计数器将在下一个信号时钟的CLK的下降沿开始计时。这里我们结合这个信息,最后给出8253的工作方式的小结,如下表所示

工作方式 计数启动方式 中止计数方法 循环计数 特点
0 写入计数初值 GATE=0 用来实现定时器或外部事件计数
1 GATE上升沿 - 用来产生单稳脉冲
2 写入计数初值 GATE=0 用来实现对时钟脉冲CLK的N分频
3 写入计数初值 GATE=0 用来产生连续的方波,或对时钟脉冲CLK的N分频
4 写入计数初值 GATE=0  
5 GATE上升沿 -  

  实际上,三个计数器的工作频率均为1.19318MHz,即1秒1193180此脉冲信号。那么,如果我们采取工作模式2,则中断的频率很容易计算,如下公式所示

1193180 / 计数器0的初始计数值 = 中断信号的频率

 

  下面就是对于中断处理程序最后的改进了——也就是修改中断的频率。默认情况下是0,也就是65536(2字,字节),约等于18.206hz。下面我们将其提高到100Hz,从而计数器0的初始计数值为

1193180 / 100 = 11931。这里我们给出设备timer.c的源代码,如下所示

 

#include "timer.h"
#include "io.h"
#include "print.h"


#define IRQ0_FREQUENCY        (100)        //即时钟中断的频率是100hz
#define PIT_FREQUENCY        (1193180)        //即8253的工作频率
#define PIT_PORT        (0x43)


#define COUNT0_VALUE        (PIT_FREQUENCY / IRQ0_FREQUENCY)    //计数器0的初始值

#define COUNT0_PORT         (0x40)


#define    PIT_OCW_SC        (0x0)        //即8253控制寄存器 选择计数器0
#define    PIT_OCW_RW        (0x3)        //即8253控制寄存器 选择先读写低字节,后读写高字节
#define    PIT_OCW_M        (0x2)        //即8253控制寄存器 选择工作模式2
#define    PIT_OCW_BCD        (0x0)        //即8253控制寄存器 选择二进制数值



//    即初始化8253中指定计数器的设置
static void frequency_set(uint8_t counter_port, uint8_t sc, uint8_t rw, uint8_t m, uint8_t bcd, uint16_t counter_value) {
    
    //    首先将设置好的控制字输出到 8253中的端口上
    outb(PIT_PORT, (sc << 6) | (rw << 4) + (m << 1) + bcd);
    //    然后将初始值发送到对应的计数器端口上
    outb(counter_port, counter_value & 0xff);
    outb(counter_port, counter_value >> 8);
}



//    初始化8253的计数器0
void timer_init(void) {
    put_str("timer_init start\n");
    
    frequency_set(COUNT0_PORT, PIT_OCW_SC, PIT_OCW_RW, PIT_OCW_M, PIT_OCW_BCD, COUNT0_VALUE);

    put_str("timer_init done\n");
}

 

  就是通过对于端口的写,从而完成8253的设置即可。最后,将其放置在idt_init()之后,从而完成中断频率的设置。下面是演示的gif,如下所示

 

posted @ 2020-10-01 19:40  hawkJW  阅读(788)  评论(0编辑  收藏  举报