中断
中断介绍
中断机制的本质是来了一个中断信号后,调用相应的中断处理程序。所以, CPU 不管有多少种类型的中断,为了统一中断管理,把来自外部设备、内部指令的各种中断类型统统归结为一种管理方式,即为每个中断信号分配 个整数,用此整数作为中断的ID,而这个整数就是所谓的中断向量,然后用此为中断描述符表中的索引,这样就能找到对应的表项,进而从中找到对应的中断处理程序。
外部中斷
来自CPU外部的中断,中断源必须是某个硬件,因此由称为硬件中断.CPU提供两条信号线(INTR和NMI)来接受中断.
INTR接受的中断为可屏蔽中断,其中断程序分为上下两部分,上部分需要限时执行(通常是中断应答或者硬件复位等重要紧迫工作),下部分则是在开中断的情况下执行,可能会被打断.
NMI接受的为不可屏蔽中断,他表示系统产生了致命的错误.
内部中断
内部中断分为软中断和异常.
软中断
软中断就是由软件主动发起的中断源于软件,他是主观上的,而非客观上的某种内部错误.
可发起中断的指令
“int 8位立即数” => 可利用其进行系统调用
“int3”=>断点调试指令,其所触发的中断向量号是3.我们用gbd或bochs调试程序时,实际上就是调试器fork了一个子进程.子进程用于运行被调试的程序,调试器中经常要设置断点,其原理就是父进程用int3修改了子进程的指令.
“into” =>断溢出指令,触发的中断向量号为4,只有在eflag标志寄存器中的of位为1时才能触发.
“bound”=>检查数组索引越界指令,触发5号中断,
“ud2”=>未定义指令,触发6号中断,该指令表示指令无效,CPU无法识别.
异常![该表我们写IDT要用的](https://i-blog.csdnimg.cn/blog_migrate/b9023ccf4e9ce4a0e542be2700f82df4.png)
异常时执行期间CPU内部错误产生的
可分为三类:
(1)Fault,故障.这种错误可被修复,它给软件一次"改过自新"的机会.
(2)Trap,常用于调试中.
(3)Abort,终止.一旦出现,由于错误无法修复,程序将无法继续运行,只能将程序从进程表中去掉.
中断描述符表
保护模式下用于存储中断处理程序入口的表,当CPU接收一个中断是,需要用中断向量在此表中检索对应的描述符,找到该中断处理程序的起始地址,然后执行中断处理程序.
值得注意的是
中断描述符表中不仅有中断描述符,还有任务门描述符和陷阱门描述符.描述符的高4节中的第8~12位用来表述描述符的类型。
任务门
任务门和任务状态段是Intel处理器在硬件一级提供的任务切换机制。所以任务门需要和TSS配合使用。
中断门
中断门包含了中断处理程序所在断的段选择子和段内偏移地址。通过此方式进入中断后,标志寄存器eflag中的IF位自动置0,即进入中断后自动关闭中断,避免中断嵌套。
陷阱门
陷阱门和中断门十分相似,区别是由陷阱门进入中断后,标志寄存器eflag中的IF位不会自动置0,陷阱门只允许存在IDT中。
调用门
调用是提供给用户进程进入特权0级的方式,其DPL为3.调用门记录例程的地址,它不能用int指令调用,只能用jmp和call指令,调用门可安装在GDT和LDT中。
中断处理过程及保护
完整的中断过程分为CPU外和CPU内两部分
CPU外:外部设备的中断由中断代理芯片接收,处理后该中断的中断向量号发个CPU(后面的8259A芯片)
CPU内:CPU执行该中断向量号。
CPU内处理过程
(1)处理器根据中断向量号定位中断门描述符
(2)处理器进行特权检查
中断门的特权检查同调用门类似,对于软件主动发起的软中断,当前特权级CPL必须在门描述符DPL和门中目标代码段DPL之间,这是为了防止位于3特权级下的用户程序主动调用某些只为内核服务的例程。
(3)执行中断处理程序
将门描述符目标代码段选择子加载到代码段寄存器CS中,把门描述符中中断处理程序的偏移地址加载到EIP
中断错误码
其格式和选择子很像,其实其本质上就是个描述符选择子。
EXT表示EXTernal event,即外部事件用来指明中断源是否来源自处理器外部。
IDT表示选择子是否指向中断描述符IDT,IDT位为1,则表示此选择子指向中断描述符符,否则指向全局描述符表GDT或者局部描述符表LDT
TI和选择子的TI一个意思
可编程中断控制器8259A
8259A中断代理芯片,用于管理和控制可屏蔽中断,它表现在屏蔽外设中断,对它们实行优先级判决。向CPU提供中断向量号的等功能。
8259A的编程
开机后的实模式下,8259A的IRQ0~7已经被BIOS分配了0x8 ~ 0xf的中断向量号。中断向量号是逻辑上的东西,它在物理上是 8259A 上的 IRQ 接口号。
8259A内部有两种寄存器,一组是初始化命令寄存器组,用来保存初始化命令字ICW,
另外一组寄存器是操作命令寄存器组,用来保存操作命令字OCW。
ICW1
需要写入到主片的0x20端口和从片的0xA0端口
IC4表示是否需要写入ICW4
SNGL为1表示单片
ADI用来设置时间间隔
LTIM表示
第4位1是固定的
ICW2
用来设置起始中断向量号(IRQ0),其他IRQ接口对应的中断向量号回自动排下去。
我们只负责填写高5位T3~T7,ID0 ~ID2这三位不用我们负责。
ICW3
仅在级联的方式下才需要。用来设置主片和从片用哪个IRQ接口互联。
主片:指明自己的IRQ接口
从片:指明连接到主片的IRQ(主片的)接口
ICW4
ICW4用于设置8259A的工作模式,当ICW1的IC4为1时才需要ICW4
第5~7位未定义,直接置0
SFNM表示特殊全嵌套模式,为0表示全嵌套模式
BUF表示本8259A芯片是否工作在缓冲模式。
M/S用了规定主从片
AEOI表示自动结束中断
uPM表示微处理器的类型
OCW1
用来屏蔽连接在8259A上的外部设备的中断信号
M7 ~ M0对应IRQ0 ~ IRQ7
OCW2
用来设置中断结束方式和优先级模式
SL:开关位针对某个特定优先级的中断操作,SL为1,可以用OWC2的低三位来指定位于ISR寄存器的哪一个中断被终止,其为0时,不起作用。
R:设置优先级控制方式
EOI:为中断结束命令位
4~3位:OWC2的标识
L2~L0:设置优先级编码
OCW3
要写入主片的0x20以及从片的0xA0端口
第7位 没用到
ESMM和SMM是组合一起用的,用来启动或禁止特殊屏蔽模式,EMSS是特殊模式允许位
,SMM是特殊屏蔽模式位。
第3~4位是OCW3的标识
P为查询命令,P为1时,设置8259A为中断查询方式
RIS读取中断寄存器选择位。
ICWI OCW2, OCW3 是用偶地址端口 Ox20 (主片〉或 Ox.AO (从片)写入。
ICW2 ICW4 OCWl 是用奇地址端口 Ox21 (主片)或 Ox.Al (从片)写入
要保证一定次序写入。
程序
init.c
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
void init_all(){
put_str("init_all\n");
idt_init();
timer_init();
}
init.h
#ifndef __KERNEL_INIT_H
#define __KERNEL_INIT_H
void init_all(void);
#endif
global.h
#ifndef _KERNEL_GLOBAL_H
#define _KERNEL_GLOBAL_H
#include "stdint.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_GS ((3<<3)+(TI_GDT<<2)+RPL0)
//------------IDT描述符属性-----------
#define IDT_DESC_P 1
#define IDT_DESC_DPL0 0
#define IDT_DESC_DPL3 3
#define IDT_DESC_32_TYPE 0xE //32位门
#define IDT_DESC_16_TYPE 0x6 //16位门
#define IDT_DESC_ATTR_DPL0 \
((IDT_DESC_P<<7)+(IDT_DESC_DPL0<<5)+IDT_DESC_32_TYPE)
#define IDT_DESC_ATTR_DPL3 \
((IDT_DESC_P<<7)+(IDT_DESC_DPL3<<5)+IDT_DESC_32_TYPE)
#endif
main.c
#include "print.h"
#include "init.h"
void main(void){
put_str("I am kernel\n");
init_all();
asm volatile("sti");
while(1);
}
interrupt.c
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "io.h"
#include "print.h"
#define PIC_M_CTRL 0x20 // 这里用的可编程中断控制器是8259A,主片的控制端口是0x20
#define PIC_M_DATA 0x21 // 主片的数据端口是0x21
#define PIC_S_CTRL 0xa0 // 从片的控制端口是0xa0
#define PIC_S_DATA 0xa1 // 从片的数据端口是0xa1
#define IDT_DESC_CNT 0x21 // 目前总共支持的中断数
/*中断门描述符结构体*/
struct gate_desc {
uint16_t func_offset_low_word;
uint16_t selector;
uint8_t dcount; //此项为双字计数字段,是门描述符中的第4字节。此项固定值,不用考虑
uint8_t attribute;
uint16_t func_offset_high_word;
};
// 静态函数声明,非必须
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是中断描述符表,本质上就是个中断门描述符数组
char* intr_name[IDT_DESC_CNT]; // 用于保存异常的名字
/******** 定义中断处理程序数组 ********
* 在kernel.S中定义的intrXXentry只是中断处理程序的入口,
* 最终调用的是ide_table中的处理程序*/
intr_handler idt_table[IDT_DESC_CNT];
/********************************************/
extern intr_handler intr_entry_table[IDT_DESC_CNT]; // 声明引用定义在kernel.S中的中断处理函数入口数组
/* 初始化可编程中断控制器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
outb (PIC_M_DATA, 0xfe);
outb (PIC_S_DATA, 0xff);
put_str(" pic_init done\n");
}
/* 创建中断门描述符 */
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) {
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, lastindex = IDT_DESC_CNT - 1;
for (i = 0; i < IDT_DESC_CNT; i++) {
make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
}
/* 单独处理系统调用,系统调用对应的中断门dpl为3,
* 中断处理程序为单独的syscall_handler */
put_str(" idt_desc_init done\n");
}
/* 通用的中断处理函数,一般用在异常出现时的处理 */
static void general_intr_handler(uint8_t vec_nr) {
if (vec_nr == 0x27 || vec_nr == 0x2f) { // 0x2f是从片8259A上的最后一个irq引脚,保留
return; //IRQ7和IRQ15会产生伪中断(spurious interrupt),无须处理。
}
put_str("int vector : 0x");
put_int(vec_nr);
put_char('\n');
}
/* 完成一般中断处理函数注册及异常名称注册 */
static void exception_init(void) { // 完成一般中断处理函数注册及异常名称注册
int i;
for (i = 0; i < IDT_DESC_CNT; i++) {
/* idt_table数组中的函数是在进入中断后根据中断向量号调用的,
* 见kernel/kernel.S的call [idt_table + %1*4] */
idt_table[i] = general_intr_handler; // 默认为general_intr_handler。
// 以后会由register_handler来注册具体处理函数。
intr_name[i] = "unknown"; // 先统一赋值为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] 第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));
asm volatile("lidt %0" : : "m" (idt_operand));
put_str("idt_init done\n");
}
interrupt.h
#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"
typedef void* intr_handler;
void idt_init(void);
#endif
io.h
/*机器模式
* b--输出寄存器QImage名称,即寄存器的最低8位
* w--输出寄存器HImode名称,即寄存器中2字节的部分
* HImod
* "Half-Integer"模式,表示一个两字节的整数
* QImod
* “Quarter-Interger”模式表示一个一字节的整数
*/
#ifndef _LIB_IO_H
#define _LIB_IO_H
#include "stdint.h"
/*向端口port写入一个字节*/
static inline void outb(uint16_t port,uint8_t data){
/*对端口指定N表示0-255,d表示用dx储存端口号,%b0表示对应al,%wl表示对应dx*/
asm volatile("outb %b0,%w1"::"a"(data),"Nd"(port));
}
/*将addr处起始的word_cnt个字写入端口port */
static inline void outsw(uint16_t port,const void* addr,uint32_t word_cnt){
/*+表示此限制输入,又作输出,outsw是把ds:esi处的16位内容写入port端口,我们在设置段描述符时,已经将ds,es,ss段选择子设置为相同的值,此时不用担心数据错乱*/
asm volatile("cld;rep outsw":"+S"(addr),"+c"(word_cnt):"d"(port));
}
/*将端口port读入的一个字节返回*/
static inline uint8_t inb(uint16_t port){
uint8_t data;
asm volatile("inb %w1,%b0":"=a"(data):"Nd"(port));
return data;
}
/*将端口port读入的word——cnt个字写入addr*/
static inline void insw(uint16_t port,void* addr,uint32_t word_cnt){
/*
insw是将端口port处读入的16位内容写入es:edi指向的内存,我们在设置段描述符时,已经将ds,es,ss段选择子设置为相同的值,此时不用担心数据错乱
*/
asm volatile("cld;rep insw":"+D"(addr),"+c"(word_cnt):"d"(port):"memory");
}
#endif
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:
%macro VECTOR 2
section .text
intr%1entry: ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少
%2 ; 中断若有错误码会压在eip后面
; 以下是保存上下文环境
push ds
push es
push fs
push gs
pushad ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI
; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI
mov al,0x20 ; 中断结束命令EOI
out 0xa0,al ; 向从片发送
out 0x20,al ; 向主片发送
push %1 ; 不管idt_table中的目标程序是否需要参数,都一律压入中断向量号,调试时很方便
call [idt_table + %1*4] ; 调用idt_table中的C版本中断处理函数
jmp intr_exit
section .data
dd intr%1entry ; 存储各个中断入口程序的地址,形成intr_entry_table数组
%endmacro
section .text
global intr_exit
intr_exit:
; 以下是恢复上下文环境
add esp, 4 ; 跳过中断号
popad
pop gs
pop fs
pop es
pop ds
add esp, 4 ; 跳过error_code
iretd
VECTOR 0x0 ,ZERO
VECTOR 0X1 ,ZERO
VECTOR 0X2 ,ZERO
VECTOR 0x3 ,ZERO
VECTOR 0X4 ,ZERO
VECTOR 0X5 ,ZERO
VECTOR 0x6 ,ZERO
VECTOR 0X7 ,ZERO
VECTOR 0X8 ,ERROR_CODE
VECTOR 0x9 ,ZERO
VECTOR 0XA ,ERROR_CODE
VECTOR 0XB ,ERROR_CODE
VECTOR 0XC ,ERROR_CODE
VECTOR 0XD ,ERROR_CODE
VECTOR 0XE ,ERROR_CODE
VECTOR 0XF ,ZERO
VECTOR 0X10 ,ZERO
VECTOR 0X11 ,ERROR_CODE
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