Linux 内核--硬件中断初始化及中断描述符表
阅读原文时间:2021年04月20日阅读:1

本文分析基于Linux 0.11内核,转载请标明出处http://blog.csdn.net/yming0221/archive/2011/06/01/6459119.aspx

以下是硬件初始化函数

void trap_init(void) { int i; set_trap_gate(0,÷_error); set_trap_gate(1,&debug); set_trap_gate(2,&nmi); set_system_gate(3,&int3); /* int3-5 can be called from all */ set_system_gate(4,&overflow); set_system_gate(5,&bounds); set_trap_gate(6,&invalid_op); set_trap_gate(7,&device_not_available); set_trap_gate(8,&double_fault); set_trap_gate(9,&coprocessor_segment_overrun); set_trap_gate(10,&invalid_TSS); set_trap_gate(11,&segment_not_present); set_trap_gate(12,&stack_segment); set_trap_gate(13,&general_protection); set_trap_gate(14,&page_fault); set_trap_gate(15,&reserved); set_trap_gate(16,&coprocessor_error); for (i=17;i<48;i++) set_trap_gate(i,&reserved); set_trap_gate(45,&irq13); outb_p(inb_p(0x21)&0xfb,0x21); outb(inb_p(0xA1)&0xdf,0xA1); set_trap_gate(39,¶llel_interrupt); } 

其中set_trap_gate是宏定义

定义如下

设置门描述符宏函数。 // 参数:gate_addr -描述符地址;type -描述符中类型域值;dpl -描述符特权层值;addr -偏移地址。 // %0 - (由dpl,type 组合成的类型标志字);%1 - (描述符低4 字节地址); // %2 - (描述符高4 字节地址);%3 - edx(程序偏移地址addr);%4 - eax(高字中含有段选择符)。 #define _set_gate(gate_addr,type,dpl,addr) / __asm__ ( "movw %%dx,%%ax/n/t" / // 将偏移地址低字与选择符组合成描述符低4 字节(eax)。 "movw %0,%%dx/n/t" / // 将类型标志字与偏移高字组合成描述符高4 字节(edx)。 "movl %%eax,%1/n/t" / // 分别设置门描述符的低4 字节和高4 字节。 "movl %%edx,%2": :"i" ((short) (0x8000 + (dpl << 13) + (type << 8))), "o" (*((char *) (gate_addr))), "o" (*(4 + (char *) (gate_addr))), "d" ((char *) (addr)), "a" (0x00080000)) 设置中断门函数。 // 参数:n - 中断号;addr - 中断程序偏移地址。 // &idt[n]对应中断号在中断描述符表中的偏移值;中断描述符的类型是14,特权级是0。 #define set_intr_gate(n,addr) / _set_gate(&idt[n],14,0,addr) 设置陷阱门函数。 // 参数:n - 中断号;addr - 中断程序偏移地址。 // &idt[n]对应中断号在中断描述符表中的偏移值;中断描述符的类型是15,特权级是0。 #define set_trap_gate(n,addr) / _set_gate(&idt[n],15,0,addr) 设置系统调用门函数。 // 参数:n - 中断号;addr - 中断程序偏移地址。 // &idt[n]对应中断号在中断描述符表中的偏移值;中断描述符的类型是15,特权级是3。 #define set_system_gate(n,addr) / _set_gate(&idt[n],15,3,addr) 

中断描述符定义如下:

在实地址模式中,CPU把内存中从0开始的1K字节作为一个中断向量表。表中的每个表项占四个字节,由两个字节的段地址和两个字节的偏移量组成,这样构成的地址便是相应中断处理程序的入口地址。但是,在保护模式下,由四字节的表项构成的中断向量表显然满足不了要求。这是因为,除了两个字节的段描述符,偏移量必用四字节来表示;‚要有反映模式切换的信息。因此,在保护模式下,中断向量表中的表项由8个字节组成,如图3.2所示,中断向量表也改叫做中断描述符表IDT(Interrupt Descriptor Table)。其中的每个表项叫做一个门描述符(gate descriptor),“门”的含义是当中断发生时必须先通过这些门,然后才能进入相应的处理程序。

图3.2门描述符的一般格式

其中类型占3位,表示门描述符的类型,主要门描述符是:

· 中断门(Interrupt gate)

其类型码为110,中断门包含了一个中断或异常处理程序所在段的选择符和段内偏移量。当控制权通过中断门进入中断处理程序时,处理器清IF标志,即关中断,以避免嵌套中断的发生。中断门中的DPL(Descriptor Privilege Level)为0,因此,用户态的进程不能访问Intel的中断门。所有的中断处理程序都由中断门激活,并全部限制在内核态。

· 陷阱门(Trap gate)

其类型码为111,与中断门类似,其唯一的区别是,控制权通过陷阱门进入处理程序时维持IF标志位不变,也就是说,不关中断。

· 系统门(System gate)

这是Linux内核特别设置的,用来让用户态的进程访问Intel的陷阱门,因此,门描述符的DPL为3。通过系统门来激活4个Linux异常处理程序,它们的向量是3、4、5及128,也就是说,在用户态下,可以使用int3、into、bound 及int0x80四条汇编指令。

最后,在保护模式下,中断描述符表在内存的位置不再限于从地址0开始的地方,而是可以放在内存的任何地方。为此,CPU中增设了一个中断描述符表寄存器IDTR,用来存放中断描述符表在内存的起始地址。中断描述符表寄存器IDTR是一个48位的寄存器,其低16位保存中断描述符表的大小,高32位保存IDT的基址,如图3.3所示。

 | 32位基地址|界限(16位)  共48位

图3.3

其中的函数声明

void page_exception(void); void divide_error(void); void debug(void); void nmi(void); void int3(void); void overflow(void); void bounds(void); void invalid_op(void); void device_not_available(void); void double_fault(void); void coprocessor_segment_overrun(void); void invalid_TSS(void); void segment_not_present(void); void stack_segment(void); void general_protection(void); void page_fault(void); void coprocessor_error(void); void reserved(void); void parallel_interrupt(void); void irq13(void);  

它们是通过汇编实现的/kernel/asm.s或/kernel/system_call.s

就拿divide_error函数来说明

asm.s中的汇编代码:

_divide_error: pushl $_do_divide_error # 首先把将要调用的函数地址入栈。这段程序的出错号为0。 no_error_code: # 这里是无出错号处理的入口处,见下面第55 行等。 xchgl %eax,(%esp) # _do_divide_error 的地址 ?? eax,eax 被交换入栈。 pushl %ebx pushl %ecx pushl %edx pushl %edi pushl %esi pushl %ebp push %ds # !!16 位的段寄存器入栈后也要占用4 个字节。 push %es push %fs pushl $0 # "error code" # 将出错码入栈。 lea 44(%esp),%edx # 取原调用返回地址处堆栈指针位置,并压入堆栈。 pushl %edx movl $0x10,%edx # 内核代码数据段选择符。 mov %dx,%ds mov %dx,%es mov %dx,%fs call *%eax # 调用C 函数do_divide_error()。 addl $8,%esp # 让堆栈指针重新指向寄存器fs 入栈处。 pop %fs pop %es pop %ds popl %ebp popl %esi popl %edi popl %edx popl %ecx popl %ebx popl %eax # 弹出原来eax 中的内容。 iret  

堆栈使用情况如下图

开始时,堆栈指针esp指向中断返回地址的一栏(esp0),eip是指令寄存器,它的内容就是下一条指令的地址。然后通过pushl,将C函数

入栈,此时栈顶指针指向esp1处,通过交换指令xchgl %eax,(%esp),将eax和esp地址的内容(C函数地址)交换。而栈的大小没有发生

改变。当有数据入栈,esp寄存器大小将相应的减小。