本文前置内容:中断详解结构体对齐
本节对应分支:interrupt

概述

本节我们为操作系统加入中断,初始化中断描述符表,并为 0~0x2f 中断添加对应的中断处理程序。当前的思路是,在 interrupt.s 中定义实际中断例程的入口函数( 通过入口函数转移到实际中断例程 ),并利用汇编宏技术得到所有入口函数的地址,形成入口函数的地址数组 interrupt_entry_table ;然后在 idt.c 中引入该数组,进而我们能够很方便地向中断描述符中填写入口函数的地址。现在读者可能不明白这个思路的具体含义,别急,下面做具体阐述。

代码解析

interrupt.s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
[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 不行嘛?哈哈,您的心情我表示理解。使用汇编来编写此文件,有以下几点原因:

  1. 用汇编处理错误码更加方便。
  2. 汇编能够直接发出 EOI 信号。
  3. 使用汇编的宏技术,所有宏函数直接展开,非常方便。

当然,你也可以使用 C 语言来书写,笔者认为用 C 语言书写此部分应该可以使内核体积更小,毕竟这里的几十个宏函数未来都会被展开,体积就稍微大些。读者可以写两个 C 函数,分别应对有错误码和无错误码的情况。

接下来剖析代码:

  • 第 5 行,interrupt_handler_table 是位于 idt.c 的指针数组,其中的指针指向实际的中断处理函数。

  • 第 7 行,%macro VECTOR 2 ,这是汇编宏技术。前面我们使用过 equ 宏定义,它只能定义单行宏;对于多行宏,就需要使用 %macro 实现,其声明方式如下:

    1
    2
    3
    4
    5
    %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 芯片中断处理结束。这部分内容参见:8259A编程

  • 第 30 行,主动跨过错误码,原因已在前面阐述。

  • 第 86~134 行,定义中断入口数组,即函数指针数组。该数组 interrupt_entry_table[] 会在 dit.c 文件中被引用。

global.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//文件说明: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_typeDPLpresent 字段按位分配而非按字节分配。可别以为声明了 char 就是分配了一个字节。
    2)结构体声明的末尾 __attribute__((packed)) 是在**指示编译器不要进行结构体对齐,这点很重要** 。详细参考结构体对齐
  • xdt_ptr 是 IDTR/GDTR 的结构体。当前我们只使用 IDTR,后续还会使用 GDTR 。

idt.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#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 函数的指针。再次强调,现在虽然每个中断都使用同一个函数,但后期对于某些中断我们会将其专门化,现在只是为中断提供基本的信息,以便产生中断时我们能明白发生了什么中断。

大家可能对各个函数之间的关系感到混乱,下面用一张图来帮助各位理清思绪:


运行截图

本文结束。