加入中断-代码剖析
概述
本节我们为操作系统加入中断,初始化中断描述符表,并为 0~0x2f 中断添加对应的中断处理程序。当前的思路是,在 interrupt.s
中定义实际中断例程的入口函数( 通过入口函数转移到实际中断例程 ),并利用汇编宏技术得到所有入口函数的地址,形成入口函数的地址数组 interrupt_entry_table
;然后在 idt.c
中引入该数组,进而我们能够很方便地向中断描述符中填写入口函数的地址。现在读者可能不明白这个思路的具体含义,别急,下面做具体阐述。
代码解析
interrupt.s
[bits 32]
%define ERROR_CODE nop ; 若在相关的异常中cpu已经自动压入了错误码,为保持栈中格式统一,这里不做操作.
%define ZERO push 0 ; 若在相关的异常中cpu没有压入错误码,为了统一栈中格式,就手工压入一个0
extern interrupt_handler_table ;声明中断处理函数的指针数组
%macro VECTOR 2
INTERRUPT_ENTRY_%1: ;中断处理entry
%2
push ds
push es
push fs
push gs
pushad
push dword %1
call [interrupt_handler_table + %1*4] ;进入实际中断函数
add esp, 4 ;外平栈
popad
pop gs
pop fs
pop es
pop ds
mov al,0x20 ;中断结束命令EOI
out 0xa0,al ;向从片发送
out 0x20,al ;向主片发送
add esp,4 ;跨过error_code,以保持堆栈平衡
iret ;从中断返回,32位下等同指令iretd
%endmacro
;;;;;;;;;以下代码利用宏来定义函数;;;;;;;;;;;;;;;
VECTOR 0x00, ZERO ;divide by zero
VECTOR 0x01, ZERO ;debug
VECTOR 0x02, ZERO ;non maskable interrupt
VECTOR 0x03, ZERO ;breakpoint
VECTOR 0x04, ZERO ;overflow
VECTOR 0x05, ZERO ;bound range exceeded
VECTOR 0x06, ZERO ;invalid opcode
VECTOR 0x07, ZERO ;device not avilable
VECTOR 0x08, ERROR_CODE ;double fault
VECTOR 0x09, ZERO ;coprocessor segment overrun
VECTOR 0x0a, ERROR_CODE ;invalid TSS
VECTOR 0x0b, ERROR_CODE ;segment not present
VECTOR 0x0c, ZERO ;stack segment fault
VECTOR 0x0d, ERROR_CODE ;general protection fault
VECTOR 0x0e, ERROR_CODE ;page fault
VECTOR 0x0f, ZERO ;reserved
VECTOR 0x10, ZERO ;x87 floating point exception
VECTOR 0x11, ERROR_CODE ;alignment check
VECTOR 0x12, ZERO ;machine check
VECTOR 0x13, ZERO ;SIMD Floating - Point Exception
VECTOR 0x14, ZERO ;Virtualization Exception
VECTOR 0x15, ZERO ;Control Protection Exception
VECTOR 0x16, ZERO ;reserved
VECTOR 0x17, ZERO ;reserved
VECTOR 0x18, ERROR_CODE ;reserved
VECTOR 0x19, ZERO ;reserved
VECTOR 0x1a, ERROR_CODE ;reserved
VECTOR 0x1b, ERROR_CODE ;reserved
VECTOR 0x1c, ZERO ;reserved
VECTOR 0x1d, ERROR_CODE ;reserved
VECTOR 0x1e, ERROR_CODE ;reserved
VECTOR 0x1f, ZERO ;reserved
VECTOR 0x20, ZERO ;clock 时钟中断
VECTOR 0x21, ZERO ;键盘中断
VECTOR 0x22, ZERO ;级联用的
VECTOR 0x23, ZERO ;串口2对应的入口
VECTOR 0x24, ZERO ;串口1对应的入口
VECTOR 0x25, ZERO ;并口2对应的入口
VECTOR 0x26, ZERO ;软盘对应的入口
VECTOR 0x27, ZERO ;并口1对应的入口
VECTOR 0x28, ZERO ;rtc实时时钟
VECTOR 0x29, ZERO ;重定向
VECTOR 0x2a, ZERO ;保留
VECTOR 0x2b, ZERO ;保留
VECTOR 0x2c, ZERO ;ps/2鼠标
VECTOR 0x2d, ZERO ;fpu浮点单元异常
VECTOR 0x2e, ZERO ;硬盘
VECTOR 0x2f, ZERO ;保留
;;;;;;;;;中断函数地址表;;;;;;;;;;
global interrupt_entry_table
interrupt_entry_table:
dd INTERRUPT_ENTRY_0x00
dd INTERRUPT_ENTRY_0x01
dd INTERRUPT_ENTRY_0x02
dd INTERRUPT_ENTRY_0x03
dd INTERRUPT_ENTRY_0x04
dd INTERRUPT_ENTRY_0x05
dd INTERRUPT_ENTRY_0x06
dd INTERRUPT_ENTRY_0x07
dd INTERRUPT_ENTRY_0x08
dd INTERRUPT_ENTRY_0x09
dd INTERRUPT_ENTRY_0x0a
dd INTERRUPT_ENTRY_0x0b
dd INTERRUPT_ENTRY_0x0c
dd INTERRUPT_ENTRY_0x0d
dd INTERRUPT_ENTRY_0x0e
dd INTERRUPT_ENTRY_0x0f
dd INTERRUPT_ENTRY_0x10
dd INTERRUPT_ENTRY_0x11
dd INTERRUPT_ENTRY_0x12
dd INTERRUPT_ENTRY_0x13
dd INTERRUPT_ENTRY_0x14
dd INTERRUPT_ENTRY_0x15
dd INTERRUPT_ENTRY_0x16
dd INTERRUPT_ENTRY_0x17
dd INTERRUPT_ENTRY_0x18
dd INTERRUPT_ENTRY_0x19
dd INTERRUPT_ENTRY_0x1a
dd INTERRUPT_ENTRY_0x1b
dd INTERRUPT_ENTRY_0x1c
dd INTERRUPT_ENTRY_0x1d
dd INTERRUPT_ENTRY_0x1e
dd INTERRUPT_ENTRY_0x1f
dd INTERRUPT_ENTRY_0x20
dd INTERRUPT_ENTRY_0x21
dd INTERRUPT_ENTRY_0x22
dd INTERRUPT_ENTRY_0x23
dd INTERRUPT_ENTRY_0x24
dd INTERRUPT_ENTRY_0x25
dd INTERRUPT_ENTRY_0x26
dd INTERRUPT_ENTRY_0x27
dd INTERRUPT_ENTRY_0x28
dd INTERRUPT_ENTRY_0x29
dd INTERRUPT_ENTRY_0x2a
dd INTERRUPT_ENTRY_0x2b
dd INTERRUPT_ENTRY_0x2c
dd INTERRUPT_ENTRY_0x2d
dd INTERRUPT_ENTRY_0x2e
dd INTERRUPT_ENTRY_0x2f
读者可能又会泄气,怎么又用汇编?能用 C 尽量用 C 不行嘛?哈哈,您的心情我表示理解。使用汇编来编写此文件,有以下几点原因:
- 用汇编处理错误码更加方便。
- 汇编能够直接发出 EOI 信号。
- 使用汇编的宏技术,所有宏函数直接展开,非常方便。
当然,你也可以使用 C 语言来书写,笔者认为用 C 语言书写此部分应该可以使内核体积更小,毕竟这里的几十个宏函数未来都会被展开,体积就稍微大些。读者可以写两个 C 函数,分别应对有错误码和无错误码的情况。
接下来剖析代码:
-
第 5 行,
interrupt_handler_table
是位于idt.c
的指针数组,其中的指针指向实际的中断处理函数。 -
第 7 行,
%macro VECTOR 2
,这是汇编宏技术。前面我们使用过equ
宏定义,它只能定义单行宏;对于多行宏,就需要使用%macro
实现,其声明方式如下:%macro 宏名 参数个数 ........ 代码体 ........ %endmacro
如果在代码体中想引用某个参数,则必须用
%数字
的方式来引用,参见第 8 行与第 9 行。我们将宏名定义为 VECTOR,并引入了两个参数。怎么压入参数呢?看第 35~82 行,直接在宏名后接上两个参数即可,参数直接用逗号隔开。注意,宏定义属于预处理指令(伪指令),这些宏会在编译期展开,也就是说,编译后,interrupt.s
中会有 0x30 个第 8~31 行这样的代码段 。 -
第 8 行为中断入口标号,代表入口函数的地址,下面定义函数指针的数组时会使用这些标号。
-
第 9 行,该行有两种情况,一种是
ZERO
宏对应的push 0
,另一种是ERROR_CODE
宏对应的nop
指令,具体是哪种情况取决于利用宏定义函数时压入的什么参数,参见 35~82 行。注意,对于有错误码的中断,CPU 会自动压入错误码;对于没有错误码的中断,CPU 则不进行动作(nop);然而,对于前者,CPU 在函数返回时主动弹出错误码,必须由我们手动弹出错误码,这点尤其重要! 为了方便操作,有错误码的中断我们不做处理,无错误码的我们就压入 0,这样就统一了各中断函数的弹栈行为,无需特殊处理。关于错误码,参见中断详解。 -
第 10~14 行,保存当前寄存器环境。由于在 17 行,我们调用了 C 语言编写的实际的中断处理函数,这必将破坏当前的寄存器环境,因此需要保存段寄存器和通用寄存器。其他寄存器会由 CPU 自动保存,关于这部分还请参见中断详解。
-
第 16 行,压入中断号,这是实际中断处理函数的参数。在我们的系统中,大多数异常我们不做处理,但发生异常时我们需要知道抛出了哪个异常,因此需要通过中断号来定位错误源。
-
第 17 行,interrupt_handler_table 是 idt.c 中的数组,该数组中装载的是实际中断处理函数的地址。因为是指针数组,指针大小为 4 字节,因此需要用序号乘 4 才能找到函数的地址。
-
第 18 行进行平栈,关于平栈请参见函数调用约定。
-
第 26~28 行,发送 EOI 信号,通知 8259A 芯片中断处理结束。这部分内容参考《操作系统-真相还原》。
-
第 30 行,主动跨过错误码,原因已在前面阐述。
-
第 86~134 行,定义中断入口数组,即函数指针数组。该数组
interrupt_entry_table[]
会在dit.c
文件中被引用。
global.h
//文件说明:global.h
#ifndef OSLEARNING_GLOBAL_H
#define OSLEARNING_GLOBAL_H
#define RPL0 0
#define RPL1 1
#define RPL2 2
#define RPL3 3
#define TI_GDT 0
#define TI_LDT 1
#define SELECTOR_K_CODE ((1 << 3) + (TI_GDT << 2) + RPL0)
#define SELECTOR_K_DATA ((2 << 3) + (TI_GDT << 2) + RPL0)
#define SELECTOR_K_STACK SELECTOR_K_DATA
#define SELECTOR_K_VIDEO ((3 << 3) + (TI_GDT << 2) + RPL0)
//================IDT描述符P================
#define IDT_DESC_P_ON 1
#define IDT_DESC_P_OFF 0
//================IDT描述符DPL==============
#define IDT_DESC_DPL0 0 //为什么只有0和3?
#define IDT_DESC_DPL3 3
//==========中断门的s位与type位=================
#define IDT_DESC_GATE 0xE // S=0(系统段),TYPE=1110(32位中断门)
//============================================
struct gate_desc
{
short offset_L; // 段内偏移 0 ~ 15 位
short selector; // 代码段选择子
char reserved ; // 保留不用
char s_type :5; // 系统段:任务门/中断门/陷阱门/调用门
char DPL :2; // 使用 int 指令访问的最低权限
char present:1; // 是否有效
short offset_H; // 段内偏移 16 ~ 31 位
} __attribute__((packed));//声明不要进行对齐
//=================GDT/IDT指针=================
struct xdt_ptr
{
unsigned short limit;
unsigned int base;
}__attribute__((packed));
//======加载GDT/LDT指针的函数,直接内联===========
static inline void load_xdt(struct xdt_ptr* p, unsigned short limit, unsigned int base)
{
p->base=base;
p->limit=limit;
}
#endif //OSLEARNING_GLOBAL_H
- 结构体
gate_desc
是中断描述符结构。该结构体有两点需要注意:
1)使用了位域,即为s_type
,DPL
,present
字段按位分配而非按字节分配。可别以为声明了 char 就是分配了一个字节。
2)结构体声明的末尾__attribute__((packed))
是在**指示编译器不要进行结构体对齐,这点很重要** 。详细参考结构体对齐 。 xdt_ptr
是 IDTR/GDTR 的结构体。当前我们只使用 IDTR,后续还会使用 GDTR 。
idt.c
#include "../include/interrupt.h"
#include "../include/global.h"
#include "../include/print.h"
#include "../include/system.h"
static struct xdt_ptr idt_ptr ;
static char* interrupt_name[IDT_DESC_CNT];
static struct gate_desc idt[IDT_DESC_CNT]; //idt-中断描述符表
extern intr_handler interrupt_entry_table[IDT_DESC_CNT]; //引用interrupt.s中的中断处理函数入口数组,注意,这是一个指针数组
intr_handler interrupt_handler_table[IDT_DESC_CNT]; //实际中断处理例程的地址
void make_idt_desc(struct gate_desc* p_desc, unsigned char DPL, intr_handler function) {
p_desc->offset_L = (unsigned int)function & 0x0000FFFF; //低16位赋值给offset_L,高位丢弃
p_desc->offset_H = ((unsigned int)function>>16) & 0x0000FFFF; //低16位赋值给offset_L,高位丢弃
p_desc->selector = SELECTOR_K_CODE;
p_desc->reserved = 0;
p_desc->s_type = IDT_DESC_GATE;
p_desc->DPL = DPL;
p_desc->present = IDT_DESC_P_ON;
}
void idt_desc_init() {
for (int i = 0; i < IDT_DESC_CNT; i++)
{
make_idt_desc(&idt[i], IDT_DESC_DPL0, interrupt_entry_table[i]);
}
put_str("idt is done\n",BG_BLACK+FT_YELLOW);
}
/* 初始化可编程中断控制器8259A */
void pic_init() {
/* 初始化主片 */
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",BG_BLACK+FT_RED);
}
void idt_init() {
put_str("idt_init start\n",BG_BLACK+FT_YELLOW);
idt_desc_init(); //初始化中断描述符表
general_handler_regist(); //默认中断函数注册
pic_init(); //初始化8259A
load_xdt(&idt_ptr,IDT_DESC_CNT*8-1,idt); //注意,limit=size-1,书中代码有误
/* 加载idt */
asm volatile("lidt idt_ptr");
put_str("idt_init done\n",BG_BLACK+FT_YELLOW);
}
void general_intr_handler(unsigned char vec_num)
{
if(vec_num==0x27 || vec_num==0x2f)
return;
put_str("\ninterrupt ",BG_BLACK+FT_RED);
put_int(vec_num, BG_BLACK+FT_RED,HEX);
put_str(" occur: ",BG_BLACK+FT_RED);
put_str(interrupt_name[vec_num],BG_BLACK+FT_RED);
put_int(time, BG_BLACK+FT_RED,DEC);
}
void general_handler_regist()
{
for(int i=0;i<IDT_DESC_CNT;i++)
{
interrupt_handler_table[i]= general_intr_handler; //将一般函数的地址安装到中断函数表中
interrupt_name[i]="unknown";
}
interrupt_name[0] = "Divide Error\n";
interrupt_name[1] = "Debug Exception\n";
interrupt_name[2] = "NMI Interrupt\n";
interrupt_name[3] = "Breakpoint Exception\n";
interrupt_name[4] = "Overflow Exception\n";
interrupt_name[5] = "BOUND Range Exceeded Exception\n";
interrupt_name[6] = "Invalid Opcode Exception\n";
interrupt_name[7] = "Device Not Available Exception\n";
interrupt_name[8] = "Double Fault Exception\n";
interrupt_name[9] = "Coprocessor Segment Overrun\n";
interrupt_name[0xa] = "Invalid TSS Exception\n";
interrupt_name[0xb] = "Segment Not Present\n";
interrupt_name[0xc] = "Stack Fault Exception\n";
interrupt_name[0xd] = "General Protection Exception\n";
interrupt_name[0xe] = "Page-Fault Exception\n";
//interrupt_name[15] 第15项是intel保留项,未使用
interrupt_name[0x10] = "x87 FPU Floating-Point Error\n";
interrupt_name[0x11] = "Alignment Check Exception\n";
interrupt_name[0x12] = "Machine-Check Exception\n";
interrupt_name[0x13] = "SIMD Floating-Point Exception\n";
interrupt_name[0x14] = "Virtualization Exception\n";
interrupt_name[0x15] = "Control Protection Exception\n";
interrupt_name[0x16] = "reserved interrupt-unknown\n";
interrupt_name[0x17] = "reserved interrupt-unknown\n";
interrupt_name[0x18] = "reserved interrupt-unknown\n";
interrupt_name[0x19] = "reserved interrupt-unknown\n";
interrupt_name[0x1a] = "reserved interrupt-unknown\n";
interrupt_name[0x1b] = "reserved interrupt-unknown\n";
interrupt_name[0x1c] = "reserved interrupt-unknown\n";
interrupt_name[0x1d] = "reserved interrupt-unknown\n";
interrupt_name[0x1e] = "reserved interrupt-unknown\n";
interrupt_name[0x1f] = "reserved interrupt-unknown\n";
interrupt_name[0x20] = "Clock interrupt\n";
interrupt_name[0x21] = "Keyboard interrupt\n";
interrupt_name[0x22] = "Clock interrupt\n";
interrupt_name[0x23] = "Cascade\n";
interrupt_name[0x24] = "Unknown\n";
interrupt_name[0x25] = "Unknown\n";
interrupt_name[0x26] = "Unknown\n";
interrupt_name[0x27] = "Unknown\n";
interrupt_name[0x28] = "RTC\n";
interrupt_name[0x29] = "Relocation\n";
interrupt_name[0x2a] = "Reserved\n";
interrupt_name[0x2b] = "Reserved\n";
interrupt_name[0x2c] = "Unknown\n";
interrupt_name[0x2d] = "FPU Exception\n";
interrupt_name[0x2e] = "Disk interrupt\n";
interrupt_name[0x2f] = "Reserved\n";
}
- make_idt_desc() 函数用于构造单个中断门描述符。注意第三个参数,是中断入口函数的指针,
typedef void* intr_handler
,该声明在 interrupt.h 中。 - idt_desc_init() 函数用来构造整个 IDT 表。
- pic_init() 函数用来初始化 8259A 芯片,并将当前设置为只接收时钟中断 。其中还用到了 outb() 函数,该函数用汇编书写,在
port_io.s
文件中。端口号的宏在interrupt.h
中。 - general_intr_handler() 便是便是我们期待已久的实际中断程序,不过现在它很简陋。先统一将所有的中断处理程序都设置为该函数,未来我们会使用 register_handler() 来注册专门的中断程序。另外,0x27 和 0x2f 无需处理 。
- 最后,在 general_handler_regist() 中将 interrupt_handler_table 数组的每一个元素赋值为 general_intr_handler 函数的指针。再次强调,现在虽然每个中断都使用同一个函数,但后期对于某些中断我们会将其专门化,现在只是为中断提供基本的信息,以便产生中断时我们能明白发生了什么中断。
大家可能对各个函数之间的关系感到混乱,下面用一张图来帮助各位理清思绪:
本文结束。