x86-32下的中断处理 1、中断的类型 来自硬件设备的处理请求称为硬中断,或者外部中断。他是异步产生的,即与CPU的执行无关; 异常是非法指令或者其他原因导致当前指令执行失败后的处理请求,也叫内部中断,他的产生方式是同步的; 应用程序主动向操作系统发出的服务请求称为软中断(trap),也叫系统调用 ,一般通过INT n指令实现,可以是同步产生,也可以是异步产生。
INT、IRET指令用于系统调用,系统调用时,存在堆栈切换和特权级切换 CALL、RET用于常规函数调用
2、中断描述符表IDT 每个中断或异常与一个中断服务例程相关联,其关联关系存储在中断描述符表(IDT)中,IDT可以位于内存的任意位置,IDT的起始地址和大小保存在中断描述符表寄存器(IDTR)中,CPU可以通过IDTR找到IDT。
IDT中每一项为门描述符,含有段选择子和偏移。发生中断后,CPU根据中断号在IDT中找到对应的门描述符,通过门描述符中的段选择子去GDT的段描述符中找到该段的基址,加上门描述符中的偏移,从而计算出中断服务例程所在的地址。
中断描述符表IDT是一个8字节数组,每一个表项叫做一个门描述符,“门”的含义是指当中断发生时必须先访问这些“门”,能够“开门”(即将要进行的处理需通过特权检查,符合设定的权限等约束)后,然后才能进入相应的处理程序,而门描述符则描述了“门”的属性(如特权级、段内偏移量等),在IDT中,主要有3种类型的门描述符:
1、中断门描述符 (Interrupt-gate descriptor),用于硬中断和异常的处理,其类型码为110,中断门包含了一个外设中断或故障中断的处理程序所在段的选择子和段内偏移量,中断门中的DPL(Descriptor Privilege Level)为0,因此用户态的进程不能访问中断门。所有的中断处理程序都由中断门激活,并全部限制在内核态。控制权通过陷阱门进入处理程序时会清空IF标志,不允许中断嵌套的发生。
2、陷阱门描述符 (Trap-gate descriptor),用于系统调用的处理。与中断门类似,其唯一的区别是,控制权通过陷阱门进入处理程序时维持IF标志位不变,也就是说,不关中断。
3、任务门描述符,Intel设置的“任务”切换的手段。
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 #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 ++) { SETGATE(idt[i], 0 , GD_KTEXT, __vectors[i], DPL_KERNEL); } SETGATE(idt[T_SWITCH_TOK], 0 , GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER); lidt(&idt_pd); }
4、通过中断实现在内核态和用户态的切换 需要注意的是,发生中断时,有一部分压栈操作是由硬件参与的,而且根据是否存在特权级的切换,压入不同的参数。
所以我们重新梳理下发生中断后的操作 。发生中断后,若发生特权级的切换(如从用户态到内核态),首先从任务状态栈TSS中获得内核态的SS、ESP,进入内核态,将用户态的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;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) { 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 ; switchk2u.tf_eflags |= (3 << 12 ); *((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;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) { tf->tf_cs = KERNEL_CS; tf->tf_ds = tf->tf_es = KERNEL_DS; tf->tf_eflags &= ~(3 << 12 ); switchu2k = (struct trapframe *)(tf->tf_esp - (sizeof (struct trapframe) - 8 )); 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