2020-03-23-Linux内核19-中断描述符表IDT的初始化
阅读原文时间:2021年04月20日阅读:1

layout

title

subtitle

date

author

header-img

catalog

tags

post

Linux内核19-中断描述符表IDT的初始化

内核使用IDT表管理所有的中断和异常,那么它是如何初始化的呢?

2020-03-23

Tupelo Shen

img/post-bg-unix-linux.jpg

true

Linux

Linux内核

中断

异常

IDT

至此,我们已经理解了X86架构如何在硬件层面如何处理中断和异常,那么接下来,我们看看Linux内核管理这些中断和异常。

同所有的设备一样,我们在使能硬件之前,必须先初始化其相关的数据结构。而Linux使用中断描述符表IDT记录管理所有的中断和异常。那么,首先,Linux内核应该把IDT的起始地址写入idtr寄存器,然后初始化所有的表项。这一步在初始化系统时完成。

因为汇编指令int允许用户进程发送任意编号的中断(0-255)。为此,IDT的初始化必须考虑阻止由用户进程int指令引发的非法中断和异常。可以通过将中断描述符表中的DPL域设为0来实现。如果用户进程试图发送非法中断信号,CPU控制单元比较CPL和DPL的值,发出常规保护的异常。

但是,大部分时候,用户态进程必须能够发送可编程异常。那么把相应的中断或陷阱门描述符的DPL域设为3即可。比如系统调用。

让我们看看Linux如何实现这种策略。

中断、陷阱和系统门

在之前的文章中,我们已经介绍过,Intel提供了三种类型的中断描述符:任务,中断和陷阱门描述符。Linux的分类有些不同,它们如下所示:

  1. 中断门

    和Intel的中断门相同。所有的用户进程不能访问(该门的DPL设为0)。所有Linux的中断处理程序都是通过中断门激活的,也就是说只能在内核态访问。

  2. 系统门

    属于Intel的陷阱门,可以被用户态进程访问(该门的DPL设为3)。三个Linux异常处理程序对应的中断号分别是4、5和128,分别使用intoboundint $0x80三条汇编指令发出对应的中断信号。

  3. 系统中断门

    属于Intel的中断门,用户态进程可以访问(该门的DPL域设为3)。中断号为3的异常处理程序通过系统中断门激活,可以使用在用户态使用int3指令实现。

  4. 陷阱门

    属于Intel陷阱门,不能被用户态程序访问(该门的DPL设为0)。用来访问大部分的异常处理程序。

  5. 任务门

    属于Intel任务门,用户态进程不能访问(该门的DPL设为0)。专门访问处理Double fault异常的处理程序。

对应上面的5种分类,分别有相应的函数可以初始化IDT(这些函数与硬件架构息息相关),如下所示:

  • set_intr_gate(n,addr)

    插入中断门。该门内的端选择器设为内核态代码所在的段。Offset被设为addr,就是中断处理程序的地址。DPL域设为0。

  • set_system_gate(n,addr)

    插入系统门。其余描述与上面的函数相同。

  • set_system_intr_gate(n,addr)

    插入系统中断门。该门内的端选择器设为内核态代码所在的段。Offset被设为addr,就是异常处理程序的地址。DPL域设为3。

  • set_trap_gate(n,addr)

    插入陷阱门,DPL被设为3。其余与上面函数相同。

  • set_task_gate(n,gdt)

    插入任务门。段选择器设为要执行的函数所在的段。Offset设为0,而DPL设为3。

IDT第一次初始化

其实,IDT被初始化两次。第一次是在BIOS程序中,此时CPU还工作在实模式下。一旦Linux启动,IDT会被搬运到RAM的受保护区域并被第二次初始化,因为Linux不会使用任何BIOS程序。

IDT结构被存储在idt_table表中,包含256项。idt_descr变量存储IDT的大小和它的地址,在系统的初始化阶段,内核用来设置idtr寄存器,专用汇编指令是lidt。

内核初始化的时候,汇编函数setup_idt()用相同的中断门填充idt_table表的所有项,都指向ignore_int()中断处理函数:

setup_idt:
    lea ignore_int, %edx
    movl $(__KERNEL_CS << 16), %eax
    movw %dx, %ax           /*  = 0x0010 = cs */
    movw $0x8e00, %dx       /* 中断门,DPL=0 */
    lea idt_table, %edi     /* 加载idt表的地址到寄存器edi中 */
    mov $256, %ecx
rp_sidt:
    movl %eax, (%edi)       /* 设置中断处理函数 */
    movl %edx, 4(%edi)      /* 设置段描述符 */
    addl $8, %edi           /* 跳转到IDT表的下一项 */
    dec %ecx                /* 自减 */
    jne rp_sidt
    ret

中断处理函数ignore_int(),也是一个汇编语言编写的函数,相当于一个null函数,它执行:

  1. 保存一些寄存器到堆栈中。
  2. 调用printk()函数打印Unknown interrupt系统消息`。
  3. 从堆栈中恢复寄存器的内容。
  4. 执行iret指令回到调用处。

正常情况下,此时的中断处理函数ignore_int()是不应该被执行的。如果在console或者log日志中出现Unknown interrupt的消息,说明发生硬件错误或者内核错误。

完成这次IDT表的初始化之后,内核还会进行第二次初始化,用真正的trap或中断处理函数代替刚才的null函数。一旦这两步初始化都完成,IDT表就包含具体的中断、陷阱和系统门,用以控制每个中断请求。

对于IDT表的第二次初始化过程,我们将分别以异常和中断的视角分开阐述。请参考后面的文章。