中断描述符表

好了,现在,我们知道了80x86微处理器在硬件级对中断和异常做了些什么,接下来,我们继续关注的是如何初始化中断描述符表。

内核启用中断以前,必须把IDT表的初始地址装到idtr寄存器,并初始化表中的每一项。这项工作是在初始化系统时完成的。

int指令允许用户态进程发出一个中断信号,其值可以是0-255的任意一个向量。因此,为了防止用户通过int指令模拟非法的中断和异常,IDT的初始化必须非常小心。这可以通过把中断或陷阱门描述符的DPL字段设置成0来实现。如果进程试图发出其中的一个中断信号,控制单元将会发现CPL的值与DPL字段有冲突,并且产生一个"General protection”异常。

然而,在少数情况下,用户态进程必须能发出一个编程异常。为此,只要把中断或陷阱门描述符的DPL字段设置成3,即特权级尽可能一样高就足够了。

现在,让我们来看一下Linux是如何实现这种策略的。

1 中断门、陷阱门及系统门


我们先回忆一下前一篇博文的“中断描述符表”,Intel提供了三种类型的中断描述符:任务门、中断门及陷阱门描述符。Linux使用与Intel稍有不同的细目分类和术语,把它们如下进行分五类:

中断门(interrupt gate):用户态的进程不能访问Intel中断门(门的DPL字段为0)。所有的Linux中断处理程序都通过中断门激活,并全部限制在内核态。

系统门(system gate):用户态的进程可以访问Intel陷阱门(门的DPL字段为3)。通过系统门来激活三个Linux异常处理程序,它们的向量是4,5及128,因此,在用户态下,可以发布into、bound及int $0x80三条汇编语言指令。

系统中断门(system interrupt gate):能够被用户态进程访问的Intel中断门(门的DPL字段为3)。与向量3相关的异常处理程序是由系统中断门激活的,因此,在用户态可以使用汇编语言指令int3。

陷阱门(trap gate):用户态的进程不能访问的一个Intel陷阱门(门的DPL字段为0)。大部分Linux异常处理程序都通过陷阱门来激活。

任务门(task gate):不能被用户态进程访问的Intel任务门(门的DPL字段为0)。Linux对“Double fault”异常的处理程序是由任务门激活的。

下列体系结构相关的函数用来在IDT中插入门:

set_intr_gate(n,addr)

在IDT的第n个表项插入一个中断门。门中的段选择符设置成内核代码的段选择符,偏移量设置为中断处理程序的地址addr,DPL字段设置为0。

set_system_gate(n,addr)

在IDT的第n个表项插入一个陷阱门。门中的段选择符设置成内核代码的段选择符,偏移量设置为异常处理程序的地址addr,DPL字段设置为3。

set_system_intr_gate(n,addr)

在IDT的第n个表项插入一个中断门。门中的段选择符设置成内核代码的段选择符,偏移量设置为异常处理程序的地址addr,DPL字段设置为3。

set_trap_gate(n,addr)

与前一个函数类似,只不过DPL的字段设置成0。

set_task_gate(n,gdt)

在IDT的第n个表项中插入一个中断门。门中的段选择符中存放一个TSS的全局描述符表的指针,该TSS中包含要被激活的函数。偏移量设置为0,而DPL字段设置为3。

2 IDT的初步初始化


当计算机还运行在实模式时,IDT就被初始化并由BIOS例程使用。然而,一旦Linux接管,IDT就被移到RAM主存的另一个区域,并进行第二次初始化,因为Linux没有利用任何BIOS的例程。

在源代码中,IDT存放在idt_table表中,有256个表项。6字节的idt_descr变量指定了IDT的大小和它的地址,只有当内核用lidt汇编指令初始化idtr寄存器时才用到这个变量(回忆一下,idtr寄存器存放的是IDT的基址)。

在内核初始化过程中,setup_idt()汇编语言函数用同一个中断门(即指向ignore_int()中断处理程序)来填充所有这256个idt_table表项:
    setup_idt:
        lea ignore_int, %edx
        movl $(_ _KERNEL_CS << 16), %eax
        movw %dx, %ax       /* selector = 0x0010 = cs */
        movw $0x8e00, %dx   /* interrupt gate, dpl=0, present */
        lea idt_table, %edi
        mov $256, %ecx
    rp_sidt:
        movl %eax, (%edi)
        movl %edx, 4(%edi)
        addl $8, %edi
        dec %ecx
        jne rp_sidt
        ret

用汇编语言写成的ignore_int()中断处理程序,可以看作一个空的处理程序,它执行下列动作:

1. 在栈中保存一些寄存器的内容。
2. 调用printk()函数打印“Unknown interrupt”系统消息。
3. 从栈恢复寄存器的内容。
4. 执行iret指令以恢复被中断的程序。

ignore_int()处理程序应该从不被执行。如果在控制台或日志文件中出现的“Unknown interrupt”消息,则标志着要么是出现了一个硬件的问题(一个I/O设备正在产生没有预料到的中断),要么就是出现了一个内核的问题(一个中断或异常未被适当地处理)。

紧接着这个预初始化,内核将在IDT中进行第二遍初始化,用有意义的陷阱和中断处理程序替换这个空处理程序。一旦这个过程完成,对控制单元产生的每个不同的异常,IDT都有一个专门的陷阱或系统门,而对于可编程中断控制器确认的每一个IRQ,IDT都将包含一个专门的中断门。

好了,现在有了对中断硬件环境的了解,以及在得到一个空的IDT的表以后,接下来的博文中,将分别针对异常和中断来详细地说明这个工作是如何完成的。随后将分别为中断和异常举一个实例,一个是进程调度的“心脏”——时钟中断,一个是虚拟存储的核心内容——缺页异常。敬请期待!

你可能感兴趣的:(linux,汇编,table,System,语言,任务)