操作系统——中断实现(十五)
操作系统——中断实现(十五)
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,如下所示