代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。
我们接下来要完成的是在汇编版本的 intrXXentry中调用C语言版本的中断处理函数。这样汇编中的intrXXentry就变成了中断的entry,即入口。
对于C的idt_table[i],汇编调用它是用call [idt_table + i*4],因为idt_table是函数数组,里面的元素为函数指针,即函数的地址。在32位系统中函数地址为32位,所以是4字节。
intr_entry_table 数组元素,也就是每个中断入口 intrXXentry,都相当于用自己的中断向量号作为 idt_table中的索引, 于是intr_entry_table数组中的每个元素均与idt_table中的每个元素对等,相当于intr_entry_table[i]调用idt_table[i]。汇编版本的中断入口程序 intrXXentry 相当于路由器,中断到达时,根据自己所属的中断向量号把中断路由到对应的C版本中断处理程序。

/kernel/interrupt.c修改后完整代码如下:
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "io.h"
#include "print.h"
#define IDT_DESC_CNT 0x21 //目前共支持的中断数,33
#define PIC_M_CTRL 0x20 //主片的控制端口是0x20
#define PIC_M_DATA 0x21 //主片的数据端口是0x21
#define PIC_S_CTRL 0xa0 //从片的控制端口是0xa0
#define PIC_S_DATA 0xa1 //从片的数据端口是0xa1
/*中断门描述符结构体*/
struct gate_desc{
uint16_t func_offset_low_word; //低32位——0~15位:中断处理程序在目标代码段内的偏移量的第15~0位
uint16_t selector; //低32位——16~31位:目标CS段选择子
uint8_t dcount; //高32位——0~7位:此项为双字计数字段,是门描述符中的第4字节,为固定值
uint8_t attribute; //高32位——8~15位:P+DPL+S+TYPE
uint16_t func_offset_high_word; //高32位——16~31位:中断处理程序在目标代码段内的偏移量的第16~31位
};
//静态函数声明,非必须
static void make_idt_desc(struct gate_desc* p_gdesc,uint8_t attr,intr_handler function);
static struct gate_desc idt[IDT_DESC_CNT]; //IDT是中描述符表,实际上是中断门描述符数组
extern intr_handler intr_entry_table[IDT_DESC_CNT]; //指针格式应该与数组类型一致,这里intr_entry_table中的元素类型就是function,就是.text的地址。所以用intr_handle来引用。声明引用定义在kernel.S中的中断处理函数入口数组
char* intr_name[IDT_DESC_CNT]; //用于保存异常的名字
intr_handler idt_table[IDT_DESC_CNT]; //idt_table为函数数组,里面保持了中断处理函数的指针
/*创建中断门描述符*/
static void make_idt_desc(struct gate_desc* p_gdesc,uint8_t attr,intr_handler function){ //intr_handler是个空指针类型,仅用来表示地址
//中断门描述符的指针、中断描述符内的属性、中断描述符内对应的中断处理函数
p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000ffff;
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t)function & 0xffff0000) >> 16;
}
/*初始化中断描述符表*/
static void idt_desc_init(void){
int i;
for(i = 0;i < IDT_DESC_CNT;i++){
make_idt_desc(&idt[i],IDT_DESC_ATTR_DPL0,intr_entry_table[i]);
}
put_str(" idt_desc_init done\n");
}
/*初始化可编程中断控制器8259A*/
static void pic_init(void){
/*初始化主片*/
outb(PIC_M_CTRL,0x11); //ICW1:边沿触发,级联8259,需要ICW4
outb(PIC_M_DATA,0x20); //ICW2:起始中断向量号为0x20,也就是IR[0-7]为0x20~0x27
outb(PIC_M_DATA,0x04); //ICW3:IR2接从片
outb(PIC_M_DATA,0x01); //ICW4:8086模式,正常EOI
/*初始化从片*/
outb(PIC_S_CTRL,0x11); //ICW1:边沿触发,级联8259,需要ICW4
outb(PIC_S_DATA,0x28); //ICW2:起始中断向量号为0x28,也就是IR[8-15]为0x28~0x2F
outb(PIC_S_DATA,0x02); //ICW3:设置从片连接到主片的IR2引脚
outb(PIC_S_DATA,0x01); //ICW4:8086模式,正常EOI
/*打开主片上IR0,也就是目前只接受时钟产生的中断*/
outb(PIC_M_DATA,0xfe);
outb(PIC_S_DATA,0xff);
put_str(" pic_init done\n");
}
/*通用的中断处理函数,一般用在异常出现时的处理*/
static void general_intr_handler(uint8_t vec_nr){
if(vec_nr == 0x27 || vec_nr == 0x2f){ //IRQ7和IRQ5会产生伪中断,无需处理;0x2f是从片8259A的最后一个IRQ引脚,保留项
return;
}
put_str("int vector : 0x");
put_int(vec_nr);
put_char('\n');
}
/*初始化idt_table*/
static void exception_init(void){
//将idt_table的元素都指向通用的中断处理函数,名称为unknown
int i;
for(i = 0;i < IDT_DESC_CNT;i++){
idt_table[i] = general_intr_handler; //默认为general_intr_handler,以后会由register_handler来注册具体函数
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]是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";
}
/*完成所有有关中断的初始化工作*/
void idt_init(){
put_str("idt_init start\n");
idt_desc_init(); //初始化中断描述符表
exception_init();
pic_init(); //初始化8259A
/*加载idt*/
uint64_t idt_operand = ((sizeof(idt)-1) | ((uint64_t)((uint32_t)idt << 16))); //低16位是idt的大小,高48位是IDT的基址。因为idt是32位,左移16位后会丢失高16位,所以先转换为64位再左移
asm volatile("lidt %0" : : "m" (idt_operand)); //加载IDT,IDT的0~15位是表界限,16~47位是表基址
put_str("idt_init done\n");
}对其的修改如下:
先定义中断异常名数组intr_name,它的数组长度是IDT_DESC_CNT,用来记录每一项异常的名字。
char* intr_name[IDT_DESC_CNT]; //用于保存异常的名字
另外还定义了中断处理函数数组 idt_table,元素个数为 IDT_DESC_CNT,这与要处理的中断数量对应。此数组中的元素即目标中断处理函数地址先由函数exception_init初始化(之所以是“先”,这是因为以后会在初始化后,再由专门的注册函数来修改),其中的数组元素将由intrXXentry来调用。

intr_handler idt_table[IDT_DESC_CNT]; //idt_table为函数数组,里面保持了中断处理函数的指针
exception_init的实现。初始化是由for循环完成的,此循环遍历IDT_DESC_CNT个中断,将中断处理函数数组idt_table中的所有元素初始化,先都指向general_intr_handler函数,用来做未处理的中断或通用的中断处理程序。然后初始化了intr_name数组中的每个元素,此数组用来记录异常的名字,由于intr_name是用来记录IDT_DESC_CNT (33)个的名称,但异常只有20个,所以先一律赋值为”unknown”。这样intr_name[20~32]就不指空了。接下来在循环体外单独为0~19这20个异常赋予正确的异常名称。
/*初始化idt_table*/
static void exception_init(void){
//将idt_table的元素都指向通用的中断处理函数,名称为unknown
int i;
for(i = 0;i < IDT_DESC_CNT;i++){
idt_table[i] = general_intr_handler; //默认为general_intr_handler,以后会由register_handler来注册具体函数
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]是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";
}
相关中断若没有定义具体的中断处理函数时将调用general_intr_handler。general_intr_handler只接受一个参数就是中断向量号。
先单独处理伪中断(spurious interrupt),伪中断并不是真正的中断,属于某种不希望发生的硬件中断。 产生伪中断的原因很多,如中断线路上电气信号异常,或是中断请求设备本身有问题。 它经常由IRQ7和IRQ15产生,IRQ7是并口1, IRQ15是保留的。 由于它们无法通过IMR寄存器屏蔽,所以在这里单独处理它们。 直接通过return返回。
然后打印中断向量号,正常情况下将会打印”int vector:向量号换行”,时钟中断将会触发0x20的中断向量。
/*通用的中断处理函数,一般用在异常出现时的处理*/
static void general_intr_handler(uint8_t vec_nr){
if(vec_nr == 0x27 || vec_nr == 0x2f){ //IRQ7和IRQ5会产生伪中断,无需处理;0x2f是从片8259A的最后一个IRQ引脚,保留项
return;
}
put_str("int vector : 0x");
put_int(vec_nr);
put_char('\n');
}
/kernel/kernel.S修改如下:
[bits 32]
%define ERROR_CODE nop ;若在相关的异常中CPU已经自动压入了错误码,为保持栈中格式同一,这里不做操作
%define ZERO push 0 ;若在相关异常中CPU没有压入错误码,为了格式统一,就手工压入一个0
extern put_str ;声明外部函数
extern idt_table
section .data ;代码段开始
global intr_entry_table ;intr_entry_table位于data段,链接时会和宏中的data段放在一起
intr_entry_table: ;构造intr_entry_table数组,只是构造数组,并不调用中断,中断由硬件触发,目前由时钟触发
;宏开始
%macro VECTOR 2 ;定义了一个叫VECTOR的宏,接收两个参数。一个参数是中断向量号,第二个参数也是个宏
section .text
intr%1entry: ;%1表示第一个参数,不是英文L。中断处理程序的起始地址;每个中队处理程序都要压入中断向量号,所以一个中断类型一个向量号。自己知道自己的向量号是多少
%2 ;压入错误码,中断若有错误码会压入到eip后
;保存上下文环境,保存ds,es,fs,gs和8个32位通用寄存器
push ds
push es
push fs
push gs
pushad
;发送EOI信号的目的是告知硬件中断控制器8259A,当前中断已经处理完成,可以继续处理其他中断请求。
;如果是从片中进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI
mov al,0x20 ;中断结束命令EOI。OCW2的第5位是EOI位,其余都是0,所以是0x20。我们要向8259A发送结束标志,不然手动结束的8259A不知道中断结束了。
out 0xa0,al ;向主片发送
out 0x20,al ;向从片发送
;处理中断,即调用中断处理函数
push %1 ;不管idt_table中的目标程序是否需要参数,都一律压入中断向量号
call [idt_table + %1*4]
;中断结束,返回原进程
jmp intr_exit
section .data ;表示上一个代码段结束
dd intr%1entry ;dd用来定义数组元素的宽度,元素值为intr%lentry;存储各个中断入口程序的地址,形成intr_entry_table数组。这样每个宏调用都会在数组中产生新的地址元素
%endmacro
;宏结束
section .text
global intr_exit
intr_exit:
;跳过中断号
add esp,4
;恢复上下文环境
popad
pop gs
pop fs
pop es
pop ds
;跳过error_code
add esp,4
;返回主程序,iretd用于从中断服务例程(ISR)返回到被中断的程序。
iretd
VECTOR 0x00,ZERO ;预处理时会将其展开为宏,intr0x00entry为一个符号(符号就是地址),function为.text,该符号会被写入intr_entry_table数组中。
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO
VECTOR 0x08,ZERO
VECTOR 0x09,ZERO
VECTOR 0x0a,ZERO
VECTOR 0x0b,ZERO
VECTOR 0x0c,ZERO
VECTOR 0x0d,ZERO
VECTOR 0x0e,ZERO
VECTOR 0x0f,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ZERO
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ZERO
VECTOR 0x19,ZERO
VECTOR 0x1a,ZERO
VECTOR 0x1b,ZERO
VECTOR 0x1c,ZERO
VECTOR 0x1d,ZERO
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO
VECTOR 0x20,ZERO上面的修改主要是:
多了保护进程上下文的代码。 因为在此汇编文件中要调用C程序,一定会使当前寄存器环境破坏,所以要保存当前所使用的寄存器环境。所以在程序中先把ds、es、fs和gs这4个16位段寄存器压栈,虽然是16位,但在32位下段寄存器压栈有点特殊,入栈后要占用4字节(而位于其他寄存器和内存中的值若为16位,则入栈后只占2字节)。 然后再通过pushad (push all double word register)指令压入8个通用寄存器。
这8个寄存器入栈顺序是 EAX->ECX->EDX->EBX->ESP->EBP->ESI-> EDI,最先入栈的是 EAX。

然后%1即中断向量号压入栈中作为idt_table数组中某元素所指向的中断处理程序的参数。 我们知道idt_table数组中记录的是C语言下编写的中断处理程序,这些处理程序其实就是函数,函数就有可能需要参数,一个中断对应一个中断向量,同理一个中断向量对应一个中断处理函数,在 interrupt.c 中的通用中断处理函数 general_intr_handler 作为默认的中断处理函数注册到了idt_table数组中,只有当没有新的中断处理函数覆盖它时,它才会有效,然而这必然要用到中断向量号,然后用该向量号作为intr_name数组的索引,这样就能找到相应异常的名字。
“call [idt_table +%1*4]”便调用了对应的C语言编写的中断处理程序。 其中“[idt_table+%1*4]”是32位下的基址变址寻址,由于idt_table中的每个元素都是32位地址,故占用4字节大小,所以将向量号乘以4,再加上idt_table数组的起始地址,便得到了下标为中断向量号%1的数组元素地址,再对该地址通过中括号取值,便得到了该数组元素中所指向C语言编写的中断处理程序,也就是之前在exception_init函数中注册过的general_intr_handler,或者其他处理。
由于是用call指令调用的中断处理程序,所以在相应的中断处理程序执行完成后,程序流程会回到jmp intr_exit,接着该恢复程序的上下文了,就是之前进入中断入口程序intrXXentry时保护的那几个寄存器。
intr_exit其实现是先用add esp,4,跳过压入的中断处理程序的参数:中断向量号。 接下来再以寄存器入栈的相反顺序依次弹出栈恢复到寄存器。 之前用pushad一次性压入8个通用寄存器,与之对应的将8个通用寄存器一次性出栈指令是popad(pop all double word register。
处理器提供了push和pop指令,这两个指令在执行时自动维护栈顶指针esp。 pop操作都会让esp的值自减一个操作数大小,popad也是一样,无非就是使esp自加8个操作数大小,esp会自减8*4-32字节,若栈中旧esp的值弹到esp寄存器就错了,所以popad在执行弹栈时,栈中esp的值会被忽略,栈中其他7个32位通用寄存器的值会被弹进相应的寄存器,于是在intr_exit中先用popad指令将8个32位寄存器出栈,除esp外,其余7个都恢复到各自寄存器中,栈中esp的值弹出后被忽略。 然后再将栈中gs、fs、es、ds的值依次出栈恢复到gs、fs、es、ds寄存器中。此时栈指针指向栈中error_code的位置,当然若中断无错误码,此处是0,所以需要将其跳过,这样后面的iretd指令执行时,栈顶指针esp才能指向栈中eip,iretd才能在栈中弹出正确的值到各寄存器。 跳过的方法同跳过参数中断号一样,就是通过 add 指令把 esp 加 4。
运行结果如下:

参考
郑钢著操作系统真象还原
田宇著一个64位操作系统的设计与实现
丁渊著ORANGE’S:一个操作系统的实现


