在前面我们知道CPU在接收到一个中断信号或异常信号时就会停下正在执行的任务并转入相应的中断处理程序。这些处理程序我们称作中断服务程序(ISR)。当中断服务程序执行完毕之后再回到之前执行的程序中继续执行。如何让CPU知道不同的中断使用什么程序来处理呢?CPU可以在内存中设置一个叫作中断描述符表(IDT)的信息,其中包括了中断向量与ISR的对应关系。当一个中断信号被CPU接收之后,CPU就会到IDT中查找这个中断的中断向量所对应的ISR,并跳转到这个ISR中执行中断处理,当ISR程序执行完毕再回到原任务。下面来看一下IDT的格式:
IDT与前面所学习的GDT类似:
Offset 0-15:中断服务程序ISR的偏移地址0-15位。
Selector:中断服务程序ISR的选择子。
DPL:中断服务程序运行等级。
P:存在标志(segment-present flag)。
Offset 16-31:中断服务程序ISR的领航地址16-31位。
可以通过设置多个IDT表,并让它们在内存中连续排列,这有点像C语言中的数组,每个IDT在数组中的下标号就是这个ISR程序所对应的中断向量。先创建一个int.S的汇编代码文件,并在其中定义所有的ISR服务程序:
//全局过程函数 .global _int_default, _isr //数据段 .section .data //中断响应函数 _isr: .long _int_0x00, _int_0x01, _int_0x02, _int_0x03, _int_0x04, … … //system call .long _int_0x80, _int_0x81, _int_0x82, _int_0x83, _int_0x84, … … //代码段 .section .text //默认中断程序 _int_default: iret _int_0x00: iret _int_0x01: iret … … _int_0x80: iret _int_0x81: iret … …
这些中断服务程序_int_0x00, _int_0x01, _int_0x02以及_int_0x80, _int_0x81就是ISR,需要将这些ISR的地址转换为IDT之后CPU才能够通过中断向量找到对应的ISR。定义一些重要的宏:
//全局中断符 #define IDT_MAX_SIZE (0xff) //内核代码选择子 #define DT_INDEX_KERNEL_CS (0x8) //内核数据选择子 #define GDT_INDEX_KERNEL_DS (0x10) //中断程序数 #define ISR_COUNT (0x30) //跳过空的中断娄 #define ISR_EMPTY (0x50) //系统中断数 #define ISR_SYSCALL_COUNT (0x20) //系统中断开始于 #define ISR_SYSCALL_START (ISR_COUNT + ISR_EMPTY)
再编写两函数,一个叫addr_to_idt,用来将ISR的地址加入到IDT中。另一个叫install_idt用来将0x0到0x2f的ISR加入到IDT中,并让CPU载入IDTP:
/* * addr_to_idt : 将32位物理地址转为IDT描述符 * - u16 selector : 选择子 * - u32 addr : 中断程序所在的物理地址 * - s_idt *idt : 中断描述符 * return : void */ void addr_to_idt(u16 selector, u32 addr, s_idt *idt) { //设置ISR地址的0-15位 idt->offset = addr; //设置ISR选择子 idt->selector = selector; //保留 idt->bbb_no_use = 0x00; //P与DPL idt->p_dpl_bbbbb = 0x8e; //设置ISR地址的16-31位 idt->offset2 = addr >> 16; } /* * install_idt : 安装IDT全局描述符 * return : void */ void install_idt() { //地址 u32 addr; //设置所有中断程序为_int_default addr = (u32) &_int_default; for (int i = 0; i < IDT_MAX_SIZE; i++) { addr_to_idt(DT_INDEX_KERNEL_CS, addr, &idts[i]); } //设置0x0 - 0x2f的中断程序 for (int i = 0; i < ISR_COUNT; i++) { addr = (u32) _isr[i]; addr_to_idt(DT_INDEX_KERNEL_CS, addr, &idts[i]); } //从0x80开始为系统中断 for (int i = ISR_SYSCALL_START; i < ISR_SYSCALL_START + ISR_SYSCALL_COUNT; i++) { addr = (u32) _isr[i - ISR_EMPTY]; addr_to_idt(DT_INDEX_KERNEL_CS, addr, &idts[i]); } //设置IDT中断描述符 idtp.idt_lenth = IDT_MAX_SIZE * sizeof(s_idt) - 1; idtp.idt_addr2 = ((u32) idts) >> 16; idtp.idt_addr = (u32) idts; //载入IDT中断描述符 load_idt(idtp); }
载入IDT中断描述符所用到的load_idt是一个宏,它用于将IDT描述符载入到内存中,作用与load_gdt类似。它的定义如下:
#define load_idt(idtp) \ ({ \ __asm__ volatile("lidt %0"::"m"(idtp)); \ })
下面来实现一个异常的ISR程序,除零错这个异常的ISR实现如下:
//除零错 _int_0x00: //关中断 cli //C函数中参数和变量所用的栈 //暂时不用,但以后会慢慢用到 push %ebp mov %esp, %ebp //调用C函数来处理这个异常 call int_div_error leave //开中断 sti //中断返回 iret
这里的汇编指令调用了一个C语言的函数int_div_error,这个函数只有两个功能:
显示一个除零错的信息Div error.
执行hlt指令使CPU处于休眠状态。
函数实现如下:
void int_div_error() { printf("Div error.\n"); hlt(); }
准备工作完成之后在start_kernel中加入安装PIC和IDT的代码:
//安装8259A install_pic(); //安装ISR中断服务程序 install_idt(); //开中断,在进入保护模式前已经关闭了中断这时需要将其打开 sti();
最后来执行一段会出现“除零错”的代码:
int a = 1, b = 0, c; c = a / b;
修改Makefile文件,加入对新文件的编译代码,然后编译并运行:
可以看到,当程序出现“除零错”的时候,就会触发一个中断向量为0x0的中断信号,这时CPU转入_isr_0x00中执行相应的处理程序。在这个ISR中显示了一个错误提示信息“Div error.”然后将CPU转入休眠状态。
值得注意的是:当c = a / b;这条语句执行时出现了“除零错”,这时CPU的IP寄存器还是指向这条指令(只有在当前语句执行结束之后IP才指向下一条指令)。但是现在出现了“除零错”,也就是说当前的除法指令并没有执行结束,所以IP寄存器还是指向这条除法指令。这时CPU将CS、IP等寄存器压栈,并开始执行“除零错”的ISR服务程序,当服务程序执行完毕时返回原程序时将CS、IP等寄存器出栈并继续执行,这是的IP寄存器还是指向了c = a / b;这条除法指令,于是又会产生一个“除零错”的异常,CPU又会去执行这个异常的ISR……于是程序陷入了无限“除零错”状态,所以在处理“除零错”的int_div_error函数中执行了一个hlt指令,使CPU进入休眠状态。至于如何避免让CPU进入无限“除零错”状态的内容会在后续的章节中介绍。
源代码的下载地址为:
https https://github.com/magicworldos/lidqos.git git git@github.com:magicworldos/lidqos.git subverion https://github.com/magicworldos/lidqos branch v0.11
Copyright © 2015-2023 问渠网 辽ICP备15013245号