中断描述符表
中断描述符表(Interrupt Descriptor Table,IDT)是用来告诉处理器在遇到异常或 “INT”操作码(汇编中)时所应调用的中断服务例程( Interrupt Service Routine,ISR)。在一个设备完成请求并且需要获得服务时,中断请求也调用IDT入口。更多关于异常和ISR的详细内容在本指南的下一节里,点击这里查看。
每个IDT入口都与一个GDT入口类似,它们都有一个基地址,都有一个访问标志,并且总长度都是32位的。两种描述符的主要区别是这些域有不同的含义。在IDT中,描述符所指定的基地址实际上是中断产生时处理器所应调用的ISR的地址。IDT入口本身没有限制,但你需要指定一个给定ISR所在的段。这样做的好处是,当处理器处于某个环时(比如一个应用程序正在运行),依然可以通过一个已经发生的中断使其将控制权交给内核。
IDT入口的访问标志也与GDT的类似。有一位用来表示描述符是否真实存在。还有一个域用来存放描述符优先级(Descriptor Privilege Level,DPL),即允许使用给定中断的最大数目的环。主要的区别在于访问标志的剩余部分的定义。访问位的低五位经常设置为二进制的01110,即十进制中的14。下面用一个表来更好地图形表示IDT入口的访问位。
| |||||
| |||||
P-段是否存在?(1 = 是) |
在你的内核目录下创建一个名为“idt.c”的新文件。在你的“build.bat”文件中添加一行以使GCC同样编译“idt.c”。最后,把“idt.o”添加到LD需要链接以创建自定义内核的不断增长的文件列表中。“idt.c”将声明一个结构,这个结构定义了每个IDT入口、装载IDT时必须用到的特定IDT指针结构(与装载GDT类似,但更简单),并且声明一个包含256个IDT入口的数组(这就是我们自己的IDT)。
#include < system.h > /* 定义一个IDT入口。 */ struct idt_entry { unsigned short base_lo; unsigned short sel; /* 我们的内核段从这里开始! */ unsigned char always0; /* 这个变量将一直设置为0! */ unsigned char flags; /* 设置使用上面的表! */ unsigned short base_hi; } __attribute__((packed)); struct idt_ptr { unsigned short limit; unsigned int base; } __attribute__((packed)); /* 声明一个有256个入口的IDT,尽管在本指南中我们将只使用前32个入口。 * 如果访问任何未定义的IDT入口,正常情况下将导致一个“未知中断”的异 * 常。访问任何“存在”位被清除(为0)的描述符则将产生一个“无法处理 * 中断”的异常。 */ struct idt_entry idt[256]; struct idt_ptr idtp; /* 这存在于“start.asm”,用来装载我们的IDT。 */ extern void idt_load(); |
这是“idt.c”的开始部分,定义了重要的数据结构! |
接下来,就像“gdt.c”,你将注意到这里声明了一个已经在其它文件中实际存在的函数。“idt_load”和“gdt_flush”一样,由汇编语言编写。所有“idt_load”使用我们的特定 IDT指针来调用“lidt”汇编操作码,我们随后将在“idt_install”中创建这个指针。 打开“start.asm”,并在对应“_gdt_flush”的“ret”后面添加下面几行:
; 把“_idtp”中定义的IDT装载到处理器。 ; 在C语言里,这样的声明应写为“extern void idt_load();”。 global _idt_load extern _idtp _idt_load: lidt [_idtp] ret |
把这段添加到“start.asm” |
设置IDT入口比设置GDT入口简单很多。我们有一个“idt_set_gate”函数,这个函数接收IDT入口数目、我们的ISR基地址、我们的内核代码段(Kernel Code Segment)、以及我们在前面的介绍中用表描述的访问标志。接下来,我们有一个“idt_install”函数,这个函数用来设置我们的特定IDT指针,同时将IDT设置为 默认的已知的清除状态。最后,我们将通过调用“idt_load”装载IDT。请注意,在IDT装载之后,你仍可以在任意时刻将ISR添加到你的IDT中。更多关于ISR的内容将在下一节中讲解。
/* 用这个函数来设置IDT中的一个入口。同样比GDT中的简单。 */ void idt_set_gate(unsigned char num, unsigned long base, unsigned short sel, unsigned char flags) { /* 这里留给你去试着编写这个函数:把参数“base”划分为高16位和低16位, * 并将他们存储到idt[num].base_hi和idt[num].base_lo。idt[num]中 * 你还需要设置的其它域,在设置时都是不言而喻的。 */ } /* 安装IDT */ void idt_install() { /* 设置特点的IDT指针,就像“gdt.c”中的一样。 */ idtp.limit = (sizeof (struct idt_entry) * 256) - 1; idtp.base = &idt; /* 清除整个IDT,初始化其为零。 */ memset(&idt, 0, sizeof(struct idt_entry) * 256); /* 使用idt_set_gate将任意新的ISR添加到IDT。 */ /* 将处理器的内部寄存器指向新的IDT。 */ idt_load(); } |
“idt.c”的剩余部分。请试着写出“idt_set_gate”,很容易的! |
最后,确保将“idt_set_gate”和“idt_install”作为函数原型添加到“system.h”中。我们需要在其它文件中调用这些函数,比如“main.c”。在我们的“main.c”中紧接着“gdt_install ”之后调用“idt_install”。你应该可以毫无问题地编译你的内核。用一些时间在你的新内核下做一些试验。如果你试着做一些非法操作,比如除零,你会发现你的计算机 重启了!我们可以通过在新IDT中安装ISR来捕获这些“异常”。