手写OS系列之中断
1.中断相关概念
1.什么是中断捏?
cpu获知到计算机中发生的某些事,暂停正在执行的程序,转而去执行处理该事件的程序,执行完毕后,继续执行刚才的程序,整个过程就叫做中断处理,也就是中断。
中断机制本质就是接收到一个中断信号后,调用相应的中断处理程序。一个中断信号就对应一个整数,作为中断信号的id,这个数字就是中断向量 ,用于在中断描述符表(IDT)中索引,进而可以找到对应的中断处理程序。
2.中断分类
- 外部中断:
- 可屏蔽中断:通过INTR信号线进入cpu(外部设备)
- 不可屏蔽中断:通过NMI信号线进入cpu(灾难性错误)
- 内部中断:
- 软中断:由软件主动发出的中断,分类:
- int 8位立即数
- int3:断点调试的原理
- into:中断溢出指令
- bound:检查数组索引越界指令
- ud2:未定义指令
- 异常:有些异常会产生单独的错误码(error code),在进入中断,cpu会压入栈中。分类
- Fault:故障,最轻的异常
- Trap:陷阱,例如int3指令引起的异常
- Abort:终止,最终的异常
3.中断描述符表
IDT是保护模式下用于存储中断处理程序入口 的表,每个表项都是终端描述符,也叫做中断门,大小为8byte,也就是64位
IDT结构:
name |
bit |
中断处理程序在目标代码段的偏移量的0~15位(低16位) |
低32位的0~15位 |
中断处理程序目标代码段的选择子(16位) |
低32位的16~31位 |
未使用(4位) |
高32位的0~4位 |
默认为0(3位) |
高32位的5~7位 |
TYPE(IDT的这四位默认为011D) |
高32位的8~11位 |
S字段(为0,代表是系统段) |
高32位的12位 |
DPL(中断门的描述符特权级,将用作后续的特权级检查) |
高32位的13~14位 |
P字段(表示段是否存在) |
高32位的15位 |
中断描述符在目标代码段的偏移量的16~31位(高32位) |
高32位的16~31位 |
cpu内部有个中断描述符表寄存器(IDTR),0~15位为表界限,16~47位为IDT基地址。
加载IDTR指令:
4.中断处理流程
- 通过中断向量索引到idt中对应中断描述符。
- 进行特权级检查:目标代码段DPL>=CPL>=中断门DPL,数值上目标代码段DPL<=CPL<=中断门DPL。
- 执行中断处理程序:将目标代码段选择子加载到CS中,将32位的目标代码段偏移地址加载到EIP中,开始执行处理程序。
处理器提供了专门控制IF位的的指令:cli使IF为0,关中断,sti使IF为1,开中断。
IF位只能限制外部设备的中断(可屏蔽中断)
5.中断发生时的压栈情况
1.特权级变化时,CPL<目标代码段DPL:
- 压入旧的ss,低16位,高16位0补齐
- 压入旧的esp
- 压入EFLAGS
- 压入旧的cs,低16位,高16位0补齐
- 压入旧的eip
- 压入错误码ERROR_CODE
2.无特权级变化,CPL=目标代码段DPL:
- 压入EFLAGS
- 压入旧的cs,低16位,高16位0补齐
- 压入旧的eip
- 压入错误码ERROR_CODE
当栈中压入了错误码,cpu不会主动跳过,需要我们手动跳过
2.可编程中断控制器8259A
8259A就是中断代理,用于管理和控制可屏蔽中断(外设中断),决定哪个中断优先被cpu执行。
1.结构
通过级联的方式,将多片串连到一起,分为一片主片master和其余的从片slave,从片的中断传递给主片,再由主片传递给cpu。
每个独立运行的外部设备都是一个中断源,它们发出的中断只有在IRQ(Interrupt ReQuest)信号线上才能被cpu知晓。
具体结构不赘述
2.编程
内部分为两组寄存器:1.初始化命令寄存器组,用来保存初始化命令字ICW(initialization command word),共四个ICW1~ICW4,2.操作命令寄存器组,用来保存操作命令字(operation command word),共四个OCW1~OCW4。所以我们的编程分为初始化和操作:
- 用ICW做初始化:是否需要级联,设置起始中断向量号,设置中断结束模式
- 用OCW来操作8259A
具体结构不赘述
操作端口:
- ICW1,OCW2,OCW3是偶地址0x20(master),0xA0(slave)
- ICW2,ICW3,ICW4,OCW1是奇地址0x21(master),0xA1(slave)
3.编写中断处理程序
kernel/kernel.S:
interrupt.c中定义的idt_table才是中断处理函数的数组,保存了函数地址。intrXXentry通过call 数组的元素,来调用中断处理函数。
定义了一个全局数组intr_entry_table,因为后面定义了数据段(section .data),所以此数组元素为每个中断处理程序的地址
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
|
[bits 32]
%define ERROR_CODE nop ;if the cpu automic to push the error code so that don't do anything
%define ZERO push 0 ;if the cpu wasn't automic push, so push 0 by ourself
extern idt_table ;in c file
section .data
global intr_entry_table
intr_entry_table:
%macro VECTOR 2
section .text
intr%1entry: ;%1 is the first param
%2 ;%2 is the second param
;save the environment
push ds
push es
push fs
push gs
pushad
mov al,0x20 ;interrupt end command:EOI
out 0xa0,al
out 0x20,al
push %1 ;push the interrupt vector number
call [idt_table+ %1 * 4 ] ;call the interrupt function in c file
jmp intr_exit
section .data
dd intr%1entry ;in order to make the all element of intr_entry_table is the address of every handle function
%endmacro
section .text
global intr_exit
intr_exit:
add esp,4 ;skip the param(interrupt vector number)
popad
pop gs
pop fs
pop es
pop ds
add esp,4 ;skip the error code
iretd
VECTOR 0x00,ZERO
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO
VECTOR 0x08,ERROR_CODE
VECTOR 0x09,ZERO
VECTOR 0x0a,ERROR_CODE
VECTOR 0x0b,ERROR_CODE
VECTOR 0x0c,ERROR_CODE
VECTOR 0x0d,ERROR_CODE
VECTOR 0x0e,ERROR_CODE
VECTOR 0x0f,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
|
kernel/interrupt.c:
用于创建IDT,安装中断程序
先初始化idt,一般中断处理函数注册和异常函数名称注册,然后初始化8259A,最后加载IDT
注意这里我们初始化8259A完成后,仅开启了时钟中断,屏蔽了其他中断。
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
|
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "io.h"
#define PIC_M_CTRL 0x20 //master chip contrl port
#define PIC_M_DATA 0x21 //master chip data port
#define PIC_S_CTRL 0xa0 //slave chip contrl port
#define PIC_S_DATA 0xa1 //slave chip data port
//the number of interrupt
#define IDT_DESC_CNT 0x21
// struct of interrupt gate describtor
struct gate_desc{
uint16_t func_offset_low_word;
uint16_t selector;
uint8_t dcount;
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);
//interrupt descriptor table
static struct gate_desc idt[IDT_DESC_CNT];
//save the name
char* intr_name[IDT_DESC_CNT];
//save the interrupt function
intr_handler idt_table[IDT_DESC_CNT];
extern intr_handler intr_entry_table[IDT_DESC_CNT];
static void pic_init(void){
//initial main chip
outb(PIC_M_CTRL , 0x11); //ICW1
outb(PIC_M_DATA , 0x20); //ICW2
outb(PIC_M_DATA , 0x04); //ICW3
outb(PIC_M_DATA , 0x01); //ICW4
//initial slave chip
outb(PIC_S_CTRL , 0x11); //ICW1
outb(PIC_S_DATA , 0x28); //ICW2
outb(PIC_S_DATA , 0x02); //ICW3
outb(PIC_S_DATA , 0x01); //ICW4
//open the IR0 in the main chip , mean that just accept the clock interrupt
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;
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");
}
// general interrupt function
static void general_intr_handler(uint8_t vec_nr){
//IRQ7 and IRQ15 will make spurious interrupt ,don't need handle
//0x2f is the slave chip 's last IRQ ,is reserved items
if (vec_nr == 0x27 || vec_nr == 0x2f){
return ;
}
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[i] = general_intr_handler; //here set the interrupt function is the default general_intr_handler
intr_name[i] = "unknown";
}
//initial exception name
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 Availible 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 is the reserved items,not used
intr_name[16] = "#MF x87 FPU Floationg-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(); //initial interrupt descriptor table
exception_init();
pic_init(); //initial 8259A
//load 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");
}
|
kernel/interrupt.h:
1
2
3
4
|
#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
typedef void* intr_handler;
#endif
|
kernel/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
|
#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)
//-------------------------------the attribute of IDT descriptor-----------
#define IDT_DESC_P 1
#define IDT_DESC_DPL0 0
#define IDT_DESC_DPL3 3
#define IDT_DESC_32_TYPE 0xE //32 bit
#define IDT_DESC_16_TYPE 0x6 //16 bit , won't use ,just for compare
#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
|
lib/kernel/io.h:
使用内联汇编实现端口操作,这里用到了c语法static inline:建议cpu将函数编译成内嵌形式,在函数调用处原封不动展开,编译后的代码中不再包含call等指令,减少了函数调用和返回时的现场保护和恢复过程,提高了效率。
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
|
/*-------------------------mathine_mode--------------------
b --(QImode)low 8bit of register:[a-d]l
w --(HImode) 2 bit of register :[a-d]x
-------------------------------------------------------------*/
#ifndef __LIB_IO_H
#define __LIB_IO_H
#include "stdint.h"
//write a byte into port
static inline void outb(uint16_t port,uint8_t data){
//N is represent 0~255, d represent dx,%b0 represent al , %w1 represent dx
asm volatile("outb %b0,%w1" : : "a" (data),"Nd" (port));
}
//write word_cnt byte from the begin of addr into port
static inline void outsw(uint16_t port,const void* addr, uint32_t word_cnt){
// + is represent read and write ;outsw is write the 16 bit content in ds:esi into port
asm volatile("cld;rep outsw": "+S" (addr) , "+c" (word_cnt) : "d" (port));
}
//read a byte from port
static inline uint8_t inb(uint16_t port) {
uint8_t data;
asm volatile ("inb %w1,%b0" : "=a"(data) :"Nd"(port));
return data;
}
//read wrod_cnt byte from port into addr
static inline void insw(uint16_t port,const void* addr , uint32_t word_cnt){
asm volatile("cld;rep insw" :"+D" (addr) , "+c"(word_cnt):"d"(port) :"memory");
}
#endif
|
device/timer.c:
由于IRQ0引脚上的时钟中断频率太低,通过对8253定时器编程,加快时钟中断的频率.
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
|
#include "timer.h"
#include "io.h"
#include "print.h"
#define IRQ0_FREQUENCY 100
#define INPUT_FREQUENCY 1193180
#define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY //the initial value of counter 0
#define COUNTER0_PORT 0x40
#define COUNTER0_NO 0 //No.1 counter
#define COUNTER0_MODE 2
#define READ_WRITE_LATCH 3
#define PIT_CONTROL_PORT 0x43
//write the control word into the control word register and set the initial value
static void frequency_set(uint8_t counter_port, uint8_t counter_no, uint8_t rwl, uint8_t counter_mode, uint16_t counter_value){
//write into control word register
outb(PIT_CONTROL_PORT,(uint8_t)(counter_no << 6 | rwl << 4 | counter_mode << 1 ));
//write the low bit of counter_value
outb(counter_port,(uint8_t)(counter_value));
//writhe the hight bit of counter_value
outb(counter_port,(uint8_t)(counter_value >> 8));
}
//initial PIT8253
void timer_init(){
put_str("timer_init start \n");
//set the frequency of counter
frequency_set(COUNTER0_PORT,COUNTER0_NO,READ_WRITE_LATCH, COUNTER0_MODE, COUNTER0_VALUE);
put_str("timer_init done \n");
}
|
device/timer.h:
1
2
3
4
|
#ifndef __DEVICE_TIME_H
#define __DEVICE_TIME_H
void timer_init();
#endif
|
kernel/init.c:
init_all负责所有的初始化工作
1
2
3
4
5
6
7
8
9
10
11
|
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
//initial all module
void init_all(){
put_str("init_all\n");
idt_init();
timer_init();
}
|
kernel/init.h:
1
2
3
4
|
#ifndef __KERNEL_INIT_H
#define __KERNEL_INIT_H
void init_all();
#endif
|
kernel/main.c:
这里我们开启中断查看效果,之前我们开启了时钟中断
1
2
3
4
5
6
7
8
9
|
#include "print.h"
#include "init.h"
int main(void){
put_str("Kernel Starting!\n");
init_all();
asm volatile ("sti");
while(1);
return 0;
}
|
4.流程总结
- main函数调用init_all,初始化所有
- 此时先初始化idt,然后加强时钟中断timer_init
- idt的初始化中:
- 先通过kernel.S中的全局数组intr_entry_table提供中断处理函数的入口,初始化中断描述符表
- 再初始化一般处理函数和异常函数名称注册。这里的函数才是真正的中断处理函数,intr_entry_table变项对应的函数中只是调用这里初始化的处理函数,所以只是中断处理函数的入口
- 然后开始8259A(中断代理)的初始化
- 使用内联汇编加载idt
- timer_init加强时钟中断,通过编程8253,增加时钟中断频率
- 初始化结束