fi3ework's Dairy.

mineucore-x86-32下的中断处理

字数统计: 2k阅读时长: 12 min
2020/02/21 Share

x86-32下的中断处理

1、中断的类型

来自硬件设备的处理请求称为硬中断,或者外部中断。他是异步产生的,即与CPU的执行无关;
异常是非法指令或者其他原因导致当前指令执行失败后的处理请求,也叫内部中断,他的产生方式是同步的;
应用程序主动向操作系统发出的服务请求称为软中断(trap),也叫系统调用 ,一般通过INT n指令实现,可以是同步产生,也可以是异步产生。

INT、IRET指令用于系统调用,系统调用时,存在堆栈切换和特权级切换
CALL、RET用于常规函数调用

2、中断描述符表IDT

每个中断或异常与一个中断服务例程相关联,其关联关系存储在中断描述符表(IDT)中,IDT可以位于内存的任意位置,IDT的起始地址和大小保存在中断描述符表寄存器(IDTR)中,CPU可以通过IDTR找到IDT。

image-20200221225125272

IDT中每一项为门描述符,含有段选择子和偏移。发生中断后,CPU根据中断号在IDT中找到对应的门描述符,通过门描述符中的段选择子去GDT的段描述符中找到该段的基址,加上门描述符中的偏移,从而计算出中断服务例程所在的地址。

image-20200221151425723

中断描述符表IDT是一个8字节数组,每一个表项叫做一个门描述符,“门”的含义是指当中断发生时必须先访问这些“门”,能够“开门”(即将要进行的处理需通过特权检查,符合设定的权限等约束)后,然后才能进入相应的处理程序,而门描述符则描述了“门”的属性(如特权级、段内偏移量等),在IDT中,主要有3种类型的门描述符:

1、中断门描述符(Interrupt-gate descriptor),用于硬中断和异常的处理,其类型码为110,中断门包含了一个外设中断或故障中断的处理程序所在段的选择子和段内偏移量,中断门中的DPL(Descriptor Privilege Level)为0,因此用户态的进程不能访问中断门。所有的中断处理程序都由中断门激活,并全部限制在内核态。控制权通过陷阱门进入处理程序时会清空IF标志,不允许中断嵌套的发生。

2、陷阱门描述符(Trap-gate descriptor),用于系统调用的处理。与中断门类似,其唯一的区别是,控制权通过陷阱门进入处理程序时维持IF标志位不变,也就是说,不关中断。

3、任务门描述符,Intel设置的“任务”切换的手段。

image-20200221225152925

3、对中断向量表(中断描述符表)IDT进行初始化

在保护模式下有256个中断号,0~31是保留中断号,用于处理异常和不可屏蔽中断NMI,32~255由用户定义,可以是设备中断系统调用

1)生成每个中断号对应的中断处理例程地址

vectors.S包含所有中断号对应的中断服务例程地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 由tools/vector.c产生
# handler
.text
.globl __alltraps
.globl vector0
vector0:
pushl $0
pushl $0
jmp __alltraps
.globl vector1
vector1:
pushl $0
pushl $1
jmp __alltraps
......
......
# vector table
.data
.globl __vectors
__vectors:
.long vector0
.long vector1

可以看到所有中断号对应的中断服务例程都会跳转到__alltraps进行处理

下面使用SETGATE将这些地址填充到IDT中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* *
* Set up a normal interrupt/trap gate descriptor
* - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate
* - sel: Code segment selector for interrupt/trap handler
* - off: Offset in code segment for interrupt/trap handler
* - dpl: Descriptor Privilege Level - the privilege level required
* for software to invoke this interrupt/trap gate explicitly
* using an int instruction.
* */
#define SETGATE(gate, istrap, sel, off, dpl) { \
(gate).gd_off_15_0 = (uint32_t)(off) & 0xffff; \
(gate).gd_ss = (sel); \
(gate).gd_args = 0; \
(gate).gd_rsv1 = 0; \
(gate).gd_type = (istrap) ? STS_TG32 : STS_IG32; \
(gate).gd_s = 0; \
(gate).gd_dpl = (dpl); \
(gate).gd_p = 1; \
(gate).gd_off_31_16 = (uint32_t)(off) >> 16; \
}

注意中断门的DPL是0,陷阱门的DPL是3

由上可以初始化IDT

1
2
3
4
5
6
7
8
9
10
11
12
void
idt_init(void) {
extern uintptr_t __vectors[];
int i;
for (i = 0; i < 256; i ++) {
//0为中断门,GD_KTEXT为系统段选择子,DPL_KERNEL=0
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
}
SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);
// 加载LDT
lidt(&idt_pd);
}

4、通过中断实现在内核态和用户态的切换

需要注意的是,发生中断时,有一部分压栈操作是由硬件参与的,而且根据是否存在特权级的切换,压入不同的参数。

所以我们重新梳理下发生中断后的操作。发生中断后,若发生特权级的切换(如从用户态到内核态),将SS、ESP依次压入堆栈。接着压入EFLAGS、CS、EIP、err_code,上述这些压栈操作是由硬件实现。接着CPU根据中断号在IDT中找到相应的表项,将中断号压栈后,跳转到 __alltraps__alltraps将原来的段寄存器全部压栈,并将当前ESP作为第一个参数调用trap函数,tap函数将参数解析成trapframe结构体,根据不同的中断号做进一步处理。trap执行完后会调用__trapret,将栈上的数值弹回寄存器,硬件压栈的参数由iret弹回。所以当所有参数存在栈上时进行修改,在退出中断时,相应的寄存器的值就会修改,从而完成用户态和内核态的切换

实现内核态到用户态的切换:

此时不存在特权级的切换,发生中断时当前栈空间如下(从上到下为低地址到高地址)

1
2
3
4
5
6
7
8
9
10
uint16_t tf_gs;
uint16_t tf_fs;
uint16_t tf_es;
uint16_t tf_ds;
uint32_t tf_trapno;
/* below here defined by x86 hardware */
uint32_t tf_err;
uintptr_t tf_eip;
uint16_t tf_cs;
uint32_t tf_eflags;

中断服务例程代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
case T_SWITCH_TOU:
if (tf->tf_cs != USER_CS) {
//当前在内核态,需要建立切换到用户态所需的trapframe结构的数据switchk2u,即修改栈上的段寄存器为用户态段寄存器
switchk2u = *tf;
switchk2u.tf_cs = USER_CS;
switchk2u.tf_ds = switchk2u.tf_es = switchk2u.tf_ss = USER_DS;
switchk2u.tf_esp = (uint32_t)tf + sizeof(struct trapframe) - 8;
//设置EFLAG的I/O特权位,使得在用户态可使用in/out指令
switchk2u.tf_eflags |= (3 << 12);
//设置临时栈,指向switchk2u,这样iret返回时,CPU会从switchk2u恢复数据,
//而不是从现有栈恢复数据。
*((uint32_t *)tf - 1) = (uint32_t)&switchk2u;
}

实现用户态到内核态的切换:

此时发生了特权级的变化,发生中断时当前栈空间如下(从上到下为低地址到高地址)

1
2
3
4
5
6
7
8
9
10
11
12
uint16_t tf_gs;
uint16_t tf_fs;
uint16_t tf_es;
uint16_t tf_ds;
uint32_t tf_trapno;
/* below here defined by x86 hardware */
uint32_t tf_err;
uintptr_t tf_eip;
uint16_t tf_cs;
uint32_t tf_eflags;
uintptr_t tf_esp;
uint16_t tf_ss;

中断服务例程代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
case T_SWITCH_TOK:
if (tf->tf_cs != KERNEL_CS) {
//发出中断时,CPU处于用户态,我们希望处理完此中断后,CPU继续在内核态运行,
//修改栈上的段寄存器值为内核代码段和内核数据段
tf->tf_cs = KERNEL_CS;
tf->tf_ds = tf->tf_es = KERNEL_DS;
//设置EFLAGS,让用户态不能执行in/out指令
tf->tf_eflags &= ~(3 << 12);
switchu2k = (struct trapframe *)(tf->tf_esp - (sizeof(struct trapframe) - 8));
//设置临时栈,指向switchu2k,这样iret返回时,CPU会从switchu2k恢复数据,
//而不是从现有栈恢复数据。
memmove(switchu2k, tf, sizeof(struct trapframe) - 8);
*((uint32_t *)tf - 1) = (uint32_t)switchu2k;
}

最终演示结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0: @ring 0
0: cs = 8
0: ds = 10
0: es = 10
0: ss = 10
+++ switch to user mode +++
1: @ring 3
1: cs = 1b
1: ds = 23
1: es = 23
1: ss = 23
+++ switch to kernel mode +++
2: @ring 0
2: cs = 8
2: ds = 10
2: es = 10
2: ss = 10
CATALOG
  1. 1. x86-32下的中断处理
    1. 1.1. 1、中断的类型
    2. 1.2. 2、中断描述符表IDT
    3. 1.3. 3、对中断向量表(中断描述符表)IDT进行初始化
  2. 2. 4、通过中断实现在内核态和用户态的切换