文章目录
引言
中断处理是操作系统内核的核心功能之一,它使得操作系统能够响应硬件事件和异常情况。本文将详细分析一个32位操作系统中断处理机制的完整实现,包括GDT(全局描述符表)的作用、中断描述符表的构建、8259A中断控制器的配置,以及中断处理的完整流程。下文代码片段引自《操作系统真象还原》第七章第六节:编写中断处理程序
1. 系统启动与中断初始化
1.1 系统启动流程
操作系统从main.c开始执行,首先进行一系列初始化操作:
// main.c中的启动流程
int main() {
put_str("I am kernel\n");
init_all(); // 初始化所有模块
while(1);
}
init_all()函数负责初始化系统的各个模块,其中中断系统的初始化是关键环节:
static void init_all() {
idt_init(); // 初始化中断描述符表
}
1.2 中断初始化过程
idt_init()函数是中断系统的核心初始化函数,它完成了中断描述符表的构建和加载:
static void idt_init() {
put_str("idt_init start\n");
idt_desc_init(); // 初始化idt
pic_init(); // 初始化8259A
// 加载idt
uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 32));
asm volatile("lidt %0" : : "m" (idt_operand));
put_str("idt_init done\n");
}
2. GDT表的核心作用
2.1 GDT表的结构与定义
GDT(全局描述符表)是x86保护模式下实现内存分段管理的核心数据结构。在这个操作系统中,GDT定义了4个关键段:
; GDT表定义(loader.S第18-29行)
gdt:
.long 0x00000000 ; 第0个描述符:空描述符(必须存在)
.long 0x00000000
.long 0x0000ffff ; 第1个描述符:代码段描述符
.long DESC_CODE_HIGH4
.long 0x0000ffff ; 第2个描述符:数据段和栈段描述符
.long DESC_DATA_HIGH4
.long 0x80000007 ; 第3个描述符:视频段描述符(用于显存访问)
.long DESC_VIDEO_HIGH4
2.2 段描述符的具体功能
代码段描述符(第1个)
基地址:0x00000000(整个内存空间)
段界限:0xfffff(4GB空间,配合4K粒度)
属性:可执行、32位、DPL=0特权级
作用:为内核代码提供执行环境
数据段描述符(第2个)
基地址:0x00000000(整个内存空间)
段界限:0xfffff(4GB空间)
属性:可读写、32位、DPL=0特权级
作用:为数据访问和栈操作提供空间
视频段描述符(第3个)
基地址:0x80000000(显存物理地址)
段界限:0x00007(32KB显存空间)
属性:可读写、32位、DPL=0特权级
作用:专门用于屏幕显示操作
2.3 选择子机制
; 选择子定义(loader.S第32-35行)
SELECTOR_CODE equ (0x01 << 3) + TI_GDT + RPL0 ; 0x08
SELECTOR_DATA equ (0x02 << 3) + TI_GDT + RPL0 ; 0x10
SELECTOR_VIDEO equ (0x03 << 3) + TI_GDT + RPL0 ; 0x18
每个选择子包含:
Index字段:指向GDT中的具体描述符
TI字段:指示使用GDT(0)还是LDT(1)
RPL字段:请求特权级(0为内核级)
2.4 保护模式转换的关键步骤
; 进入保护模式的核心代码(loader.S第95-100行)
mov eax, [gdt_ptr] ; 加载GDT指针
lgdt [eax] ; 将GDT地址加载到GDTR寄存器
mov eax, cr0
or eax, 0x00000001 ; 设置PE位,开启保护模式
mov cr0, eax
jmp dword SELECTOR_CODE:protect_mode_entry ; 远跳转,加载CS选择子
2.5 GDT在中断处理中的作用
GDT在中断处理中发挥关键作用:
// interrupt.c第59行
p_gdesc->selector = SELECTOR_K_CODE; // 中断门使用内核代码段选择子
当中断发生时,CPU通过中断门描述符中的选择子(SELECTOR_K_CODE)查找GDT,加载相应的代码段描述符到CS寄存器,确保中断处理程序在内核态正确执行。
3. 中断描述符表的构建
3.1 中断门描述符结构体
中断描述符表(IDT)是中断处理的核心数据结构。每个中断门描述符占用8字节,其结构定义如下:
// interrupt.c第5-14行
struct gate_desc {
uint16_t offset_low; // 处理程序地址低16位
uint16_t selector; // 段选择子
uint8_t dcount; // 占用字节数
uint8_t attribute; // 属性
uint16_t offset_high; // 处理程序地址高16位
};
3.2 中断门描述符的创建
make_idt_desc()函数负责创建中断门描述符:
// interrupt.c第16-24行
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) {
p_gdesc->offset_low = (uint32_t)function & 0x0000FFFF;
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->offset_high = ((uint32_t)function & 0xFFFF0000) >> 16;
}
3.3 IDT表的初始化
idt_desc_init()函数初始化整个IDT表:
// interrupt.c第26-36行
static void idt_desc_init() {
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");
}
IDT_DESC_CNT定义为0x21,支持0x00-0x20共33个中断向量号,覆盖了所有CPU异常和基本硬件中断。
3.4 SELECTOR_K_CODE选择子的作用
在make_idt_desc()函数中,p_gdesc->selector = SELECTOR_K_CODE;这一行代码至关重要。SELECTOR_K_CODE在global.h中定义为:
// global.h中的定义
#define SELECTOR_K_CODE (1 << 3) // 内核代码段选择子
这个选择子的结构分析:
Index字段:1(指向GDT第1个描述符,即内核代码段)
TI字段:0(使用GDT)
RPL字段:0(内核级特权)
使用SELECTOR_K_CODE的原因:
确保中断处理程序在内核态执行:中断处理程序需要最高特权级
提供正确的代码段上下文:确保中断处理程序能够访问内核资源
符合x86保护模式的设计要求:硬件通过选择子查找段描述符
4. 8259A中断控制器的配置
4.1 8259A初始化函数
pic_init()函数负责初始化8259A可编程中断控制器:
// interrupt.c第38-53行
static void pic_init() {
// 初始化主片
outb (PIC_M_CTRL, 0x11); // ICW1: 边沿触发+级联
outb (PIC_M_DATA, 0x20); // ICW2: 起始中断向量号0x20
outb (PIC_M_DATA, 0x04); // ICW3: IR2接从片
outb (PIC_M_DATA, 0x01); // ICW4: 8086模式
// 初始化从片
outb (PIC_S_CTRL, 0x11); // ICW1: 边沿触发+级联
outb (PIC_S_DATA, 0x28); // ICW2: 起始中断向量号0x28
outb (PIC_S_DATA, 0x02); // ICW3: 从片连接主片的IR2
outb (PIC_S_DATA, 0x01); // ICW4: 8086模式
// 打开主片IR0(时钟中断),其他全部屏蔽
outb (PIC_M_DATA, 0xfe);
outb (PIC_S_DATA, 0xff);
put_str(" pic_init done\n");
}
4.2 8259A配置详解
ICW1(初始化命令字1)
0x11:边沿触发模式、级联方式、需要ICW4
ICW2(初始化命令字2)
主片:0x20(中断向量号0x20-0x27)
从片:0x28(中断向量号0x28-0x2F)
ICW3(初始化命令字3)
主片:0x04(IR2连接从片)
从片:0x02(连接主片的IR2)
ICW4(初始化命令字4)
0x01:8086模式、正常EOI
中断屏蔽
主片:0xfe(仅开启IR0,即时钟中断)
从片:0xff(全部屏蔽)
5. 中断处理程序的生成
5.1 VECTOR宏机制
kernel.S使用宏批量生成中断处理程序:
%macro VECTOR 2
section .text
intr%1entry: ; 中断处理程序入口
%2 ; 处理错误码
push intr_str
call put_str
add esp,4 ; 清理栈
mov al,0x20 ; EOI命令
out 0xa0,al ; 向从片发送EOI
out 0x20,al ; 向主片发送EOI
add esp,4 ; 清理错误码或压入的0
iret ; 中断返回
section .data
dd intr%1entry ; 存储中断入口地址
%endmacro
宏参数说明:
%1:中断向量号
%2:ZERO或ERROR_CODE,控制是否压入0
5.2 中断处理程序生成
通过调用VECTOR宏生成33个中断处理程序:
VECTOR 0x00,ZERO ; 除法错误
VECTOR 0x01,ZERO ; 调试异常
VECTOR 0x02,ZERO ; NMI中断
// ... 其他异常
VECTOR 0x20,ZERO ; 时钟中断
6. 中断响应流程
6.1 中断响应的完整步骤
中断发生:硬件设备(如8253定时器)产生中断信号
中断识别:8259A接收中断信号,向CPU发送INTR
中断响应:CPU响应中断,发送INTA信号
向量获取:8259A返回中断向量号(如时钟中断为0x20)
IDT查找:CPU根据向量号查找IDT表
门描述符检查:检查中断门描述符的权限和属性
上下文保存:自动保存EFLAGS、CS、EIP到栈中
特权级切换:如果需要,切换到内核特权级
段选择子加载:加载中断门中的选择子到CS寄存器
跳转执行:跳转到中断处理程序执行:调用put_str打印"interrupt occur!"消息,向8259A发送EOI(End Of Interrupt)信号,执行iret指令返回
6.2 选择子解析与GDT查找
当中断向量号为0x20时:
IDT索引:0x20 × 8 = 0x100(IDT表中的偏移)
读取中断门:获取offset和selector字段
选择子解析:SELECTOR_K_CODE = 0x08
Index = 0x08 >> 3 = 1
TI = (0x08 >> 2) & 0x1 = 0(使用GDT)
RPL = 0x08 & 0x3 = 0(内核级)
GDT查找:GDT基地址 + 1 × 8 = 内核代码段描述符地址
段描述符加载:将段基地址等信息加载到CS的隐藏部分
6.3 中断处理执行流程
; Intel风格的中断处理程序执行流程(基于kernel.S实际代码)
intr_entry:
; 1. 保存现场(程序执行)
; 根据中断类型压入错误码或0
push DWORD [error_code_or_zero] ; %2 宏展开的结果
; 2. 保存字符串地址(程序执行)
push OFFSET intr_str ; push intr_str
; 3. 执行中断处理逻辑(程序执行)
call put_str ; 调用打印函数
; 4. 清理栈中的字符串地址(程序执行)
add esp, 4 ; add esp,4
; 5. 发送EOI信号(程序执行)
mov al, 20h ; mov al,20h
out 0A0h, al ; out 0xa0,al (从片EOI)
out 020h, al ; out 0x20,al (主片EOI)
; 6. 清理栈中的错误码或0(程序执行)
add esp, 4 ; add esp,4
; 7. 中断返回(程序调用,硬件自动执行)
iret ; iret
在中断处理程序的执行流程中,有些操作是由CPU硬件自动执行的,有些是由程序代码显式执行的。详细分析一下:
CPU硬件自动执行的操作
- 中断响应阶段(硬件自动)
当中断发生时,CPU硬件会自动执行以下操作:
; 硬件自动执行,无需程序代码
1. CPU检测到中断信号
2. 如果IF=1,则响应中断
3. 自动保存当前现场到栈中:
- push EFLAGS ; 保存标志寄存器
- push CS ; 保存代码段选择子
- push EIP ; 保存指令指针
4. 如果是特权级变化,还会:
- push SS ; 保存栈段选择子
- push ESP ; 保存栈指针
5. 根据中断向量号查找IDT表
6. 自动加载新的CS和EIP(从中断门描述符中获取)
7. 跳转到中断处理程序执行
- 段选择子加载(硬件自动)
// 硬件自动执行,程序只需设置选择子
p_gdesc->selector = SELECTOR_K_CODE; // 程序设置
// CPU硬件会自动:
// 1. 从选择子中提取Index=1
// 2. 计算GDT地址:GDT基地址 + 1*8
// 3. 读取段描述符
// 4. 加载段基地址到CS的隐藏部分
// 5. 加载段界限和属性到CS的隐藏部分
- iret指令执行(硬件自动)
; 硬件自动执行iret的完整流程
iret
; CPU硬件会自动:
; 1. 从栈中弹出EIP(恢复指令指针)
; 2. 从栈中弹出CS(恢复代码段选择子)
; 3. 从栈中弹出EFLAGS(恢复标志寄存器)
; 4. 如果是特权级变化,还会:
; - 从栈中弹出ESP(恢复栈指针)
; - 从栈中弹出SS(恢复栈段选择子)
; 5. 恢复到中断前的执行状态
程序代码显式执行的操作
- 保存现场(程序执行)
; 程序显式执行(根据kernel.S实际代码)
intr%1entry: ; 中断处理程序入口
%2 ; 程序执行:根据中断类型压入错误码或0
push intr_str ; 程序执行:保存字符串地址到栈中
call put_str ; 程序执行:调用打印函数
add esp,4 ; 程序执行:清理栈中的字符串地址参数
- 中断处理逻辑(程序执行)
; 程序显式执行(根据kernel.S实际代码)
; 在call put_str中执行打印"interrupt occur!"
; 这里没有段寄存器切换操作,直接使用当前段环境
- EOI信号发送(程序执行)
; 程序显式执行(根据kernel.S实际代码)
mov al,20h ; 程序设置:EOI命令码
out 0a0h,al ; 程序执行:向从片8259A发送EOI
out 020h,al ; 程序执行:向主片8259A发送EOI
- 恢复现场(程序执行)
; 程序显式执行(根据kernel.S实际代码)
add esp,4 ; 程序执行:清理栈中的错误码或0
iret ; 程序调用:触发硬件自动恢复
完整的执行时序对比
硬件自动执行时序:
中断发生 → CPU检测中断 → 保存EFLAGS/CS/EIP → 查找IDT → 加载CS/EIP → 跳转处理程序
↓
程序执行中断处理 ← iret恢复现场 ← 程序发送EOI ← 程序处理逻辑 ← 程序保存现场
程序执行时序:
进入intr%1entry → %2(压入错误码/0) → push intr_str → call put_str → add esp,4 → mov al,20h → out 0a0h,al → out 020h,al → add esp,4 → iret

设计优势
这种硬件与软件分工的设计具有以下优势:
硬件保证原子性:关键的状态保存和恢复由硬件自动完成,确保原子性
软件灵活性:具体的中断处理逻辑由软件实现,具有灵活性
性能优化:硬件自动执行的关键路径性能最优
安全性:硬件自动进行的权限检查和状态切换更加安全
理解硬件与软件的分工对于深入理解中断机制至关重要,这也是x86架构设计的精髓所在。
代码特点
简化的现场管理:只管理必要的栈数据,不保存通用寄存器
无段寄存器切换:直接使用进入中断时的段环境
最小化开销:只保存和恢复中断处理必需的数据
硬件优化:充分利用CPU硬件的自动保存/恢复机制
7. "interrupt occur!"重复输出现象分析
7.1 现象描述
在系统运行过程中,屏幕会持续输出"interrupt occur!"字符串,这是时钟中断持续触发的正常现象。
7.2 原因分析
时钟中断配置:8259A仅开启了主片的IR0(时钟中断)
中断处理程序:所有中断处理程序都调用put_str输出信息
周期性触发:8253定时器以固定频率产生时钟中断
循环执行:内核主循环中持续响应中断
7.3 执行流程分析
内核启动
↓
初始化中断系统
↓
执行sti指令开启中断
↓
进入主循环while(1)
↓
时钟中断触发(约每秒多次)
↓
执行intr20_entry处理程序
↓
调用put_str输出"interrupt occur!"
↓
发送EOI信号
↓
iret返回主循环
↓
等待下一次时钟中断
↓
重复上述过程
8. 中断门属性与权限控制
8.1 中断门属性定义
// global.h中的属性定义
#define IDT_DESC_P 1
#define IDT_DESC_DPL0 0
#define IDT_DESC_32_TYPE 0xE
#define IDT_DESC_ATTR_DPL0 (IDT_DESC_P | (IDT_DESC_DPL0 << 5) | IDT_DESC_32_TYPE)
8.2 权限检查机制
x86 CPU在处理中断时会进行严格的权限检查:
CPL(当前特权级):当前执行代码的特权级
RPL(请求特权级):选择子中的请求特权级
DPL(描述符特权级):段描述符或门描述符的特权级
中断门的权限规则:
CPL ≤ 门描述符的DPL:允许通过中断门
CPL ≤ 目标代码段的DPL:允许跳转到目标代码段
内核中断门的特权级:
门描述符DPL:0(只有内核可以访问)
目标代码段DPL:0(内核代码段)
SELECTOR_K_CODE的RPL:0(请求内核级特权)
这种设计确保:
用户程序不能直接访问中断门
中断处理程序在内核态执行
系统调用需要通过门机制进入内核
1572

被折叠的 条评论
为什么被折叠?



