1、Bran 的内核开发指南第 1 页,共 48 页介绍内核开发不是一件简单的工作,它是对你编程技术的一次考验。所谓的内核开发,也就是你要开发一个直接管理硬件的软件。内核是一个操作系统的核心, 它管理着硬件所能提供的资源。内核所要管理的最重要的资源之一就是中央处理器(CPU)。它为特定的操作分配时间,在另一个事件需要发生时中断一项任务或进程。这就是“多任务” 。多任务的内核是非常具有合作性的,在其中,每个程序自身都具有一种叫做“让步”的功能。在必要时,它能将自己的处理时间“慷慨 ”地让给下一个任务或进程。在抢占式多任务内核中,系统时钟(system timer) 被用来中断当前进程,并切换到新的进
2、程这是一种强制性切换,它很大程度地保证了重要的进程能有大量的时间去执行。现在有很多种分配 CPU 时间的调度算法。最简单的一种被叫做 “时间片轮转”(Round Robin) 。它让所有的进程都按照一个列表来运行。更加复杂的算法需要优先级(priorities),这样高优先权的任务就能比低优先权的任务有更多的 CPU 时间去运行。比这还复杂的算法就是实时算法。它能保证特定的进程能拥有至少确定数目的时钟滴答(timer ticks) 来运行。但最根本地,最重要的资源还是时间。下一个重要的资源似乎是显而易见的,那就是内存。曾经有段时间,内存资源比 CPU 时间更宝贵,因为内存是有限的,而 CPU
3、时间是无限的。你可以将内核设计成能内存高效型,但需要大量 CPU 时间。或者 CPU 高效型,但要使用大量的内存空间来做高速缓存。最好的方法还是将两钟算法综合:争取最好的内存使用率,同时节约 CPU 时间。最后一个内核需要管理的资源就是硬件资源。这包括中断请求(IRQ),它是硬件用来告诉 CPU 去执行特定任务或者操纵特定数据的特殊信号。另一个重要的硬件资源是直接存储器存取(DMA)通道。一个 DMA 通道允许设备锁定内存总线并将它的数据直接传输进系统内存,同时无需 CPU 参与。这是一个提高系统性能的好方法:一个支持 DMA 的设备能在不打扰 CPU 的情况下传输数据,然后用 IRQ中断 C
4、PU,告诉它数据传输已经完成。声卡和网卡就同时使用 IRQ 和 DMA 通道。第三种硬件资源是地址,就像内存,但是它是以接口的形式出现在 I/O 总线上。通过 I/O 接口,设备能被配置、读取或给予数据。设备能使用很多 I/0 接口。举例来说,8 到 16 就是典型的接口范围。总览我写这篇指南的目的是要向你展示怎样去建立内核开发最基本的组件。包括:1)建立内核开发环境2)最基本的:设置引导器 GRUB3)连接各个源文件并调用 main()4)在屏幕上显示信息5)建立全局描述表 (GDT)6)建立中断描述表 (IDT)第 2 页,共 48 页7)建立控制中断和 IRQ 的中断服务例程 (ISR)
5、8)重新映射可编程中断控制器(PIC) 到新的 IDT 入口9)安装并管理 IRQ10)管理可编程记时器/系统时钟(PIT)11)管理键盘 IRQ 和键盘数据12)剩下的就看你自己的了!开始内核开发是一项漫长的写代码和调试的工作。这似乎在开始会是很令人居丧的。但是你不必需要大量的写内核的工具。本指南将使用 GRUB 来将你的内核载入内存。GRUB 需要读入一个运行在保护模式下的二进制镜像:这个“ 镜像”就是我们马上就开始建立的内核。在读这篇指南之前,你至少需要具备 C 语言的知识。我强烈建议你能掌握X86 汇编的知识,因为这将对你在后来熟练地使用寄存器有很大帮助。你至少需要这些工具:一个可以产
6、生 32 位代码的编译器,一个 32 位连接器和一个能产生32 位输出的汇编器。至于硬件方面,你必须有一太拥有 386 或者更好的处理器 (包括386、486、5x86、6x86 、 Pentium、 Athlon、 Celeron、 Duron 等) 。你最好能有第二台用来实验的电脑,它应该就在你开发用的电脑旁边。如果你没有,你可以使用虚拟机软件或者就在开发机上测试(虽然这样开发会很耗费时间,且需要你重新启动很多次)。用于测试的硬件配置要求- 一台 IBM 兼容机- 一块 386 或更好的处理器- 大于 4MB 的内存- 一块 VGA 兼容的显卡和一台显示器- 一块键盘- 一个软驱(是的,你
7、甚至不需要一块硬盘)用于开发的硬件配置要求Bran 的内核开发指南第 3 页,共 48 页- 一台 IBM 兼容机- 一块 Pentium II or K6 300MHz 或者更好的处理器- 至少 32MB 内存- 一块 VGA 兼容的显卡和一台显示器- 一块键盘- 一个软驱- 一块有足够空闲空间的硬盘( 用来存储开发工具、开发文档和源代码)- Microsoft Windows, 或者类 Unix(Linux, FreeBSD) 操作系统- 一个因特网浏览器,用来查找资料(强烈建议使用一个鼠标)工具编译器- The Gnu C Compiler (GCC) Unix- DJGPP (GCC
8、for DOS/Windows) Windows汇编器- Netwide Assembler (NASM) Unix/Windows虚拟机软件- VMWare Workstation 4.0.5 Linux/Windows NT/2000/XP- Microsoft VirtualPC Windows NT/2000/XP- Bochs Unix/Windows基本内核在这部分,我们将涉及一点汇编的知识,学习创建使用最基本的连接脚本。最后,我们将学习如何使用批处理文件自动进行汇编、编译和连接这个最最基本的运行在保护模式下的内核。请注意,我将假设你已经安装了 NASM 和 DJGPP在你的操作系
9、统上,并且你已经掌握了最基本的 X86 汇编语言。内核入口第 4 页,共 48 页内核的入口就是当引导器调用内核时最先被执行的那段代码。这部分代码一般总是用汇编语言来写的。这是为 完成很多特定功能,比如建立新堆栈和加载新的 GDT、IDT 或段寄存器。这些都是不能用 C 语言来完成的。在很多入门级的内核,以及一些更大,更专业的内核中,这些汇编代码被写入一个单独的文件,而其它部分则被写入若干 C 语言文件中。如果你对汇编程序有一点了解,这个文件中的代码就将变得非常直观。正如代码所做的,整个文件就是装载一个新的 8KB 堆栈,然后跳转到一个无限循环。堆栈占用很少的内存,但它能被用来存储并传递参数到
10、用 C 语言的函数。它也可被用来存储在你函数中使用的局部变量。其它的全局变量被存储在 Data 和 BSS区域中。在下面代码中“mboot”和“stublet”之间的部分组成了一段特殊的代码,它能让 GRUB 知道这个二进制文件是一个内核。如果你不能理解这个多引导的头部的话,就跳过吧。; 这是内核入口。我们能够在这里调用 main 函数。; 我们也能在这里设置堆栈等其它有趣的东东,比如说,; 建立 GDT。请注意:中断在这里被禁用了的,后面将设置。BITS 32global startstart:mov esp, _sys_stack ; 这里指示堆栈到我们新的堆栈位置jmp stublet;
11、 这里必须是 4byte 的对齐方式, 因此我们用“ALIGN 4” 来处理。ALIGN 4mboot:; 这里定义的多引导宏使后面的某些内容更加的可读。MULTIBOOT_PAGE_ALIGN equ 1/* 你需要亲自将这些代码实践一遍 */unsigned char *memcpy(unsigned char *dest, const unsigned char *src, int count)/* 在这里添加代码,将 count 指定大小的数据从 src 复制到第 8 页,共 48 页* dest,最后返回 dest */unsigned char *memset(unsigned c
12、har *dest, unsigned char val, int count)/* 在这里添加代码,将在 dest 中 count 指定大小的空间用* val 指定的字符代换。最后返回 dest */unsigned short *memsetw(unsigned short *dest, unsigned short val, int count)/* 这里和上面的功能一样,* 在局部变量都是无符号短整型时,* 你可以把上面的代码完整地复制下来就行了。* */int strlen(const char *str)/* 这里需要添加一个履历数列 str 的循环,* 当发现当前位是“0”时,循
13、环结束,这样通过循环计数器就可以得出一个字符串的长度。* 最后返回字符串的长度。 */* 我们在后面将使用这个函数来读取 I/O 端口以获得键盘等设备的数据。* 我们在这里实际上使用的是被称为“ 在线汇编”的开发方式。* */unsigned char inportb (unsigned short _port)unsigned char rv;_asm_ _volatile_ (“inb %1, %0“ : “=a“ (rv) : “dN“ (_port);return rv;/* 我们将使用这个函数来写 I/O 端口以向设备发送数据。This* 这将在下一章中被用来改变字符模式光标位置。*
14、 我们将再次使用在线汇编,因为有些工作是用 C 语言实现不了的。* */void outportb (unsigned short _port, unsigned char _data)_asm_ _volatile_ (“outb %1, %0“ : : “dN“ (_port), “a“ (_data);/* 下面是一个非常简单的 main()函数。它所做的仅仅是一个无穷循环。* 这就像我们的“空闲”循环。*/void main()Bran 的内核开发指南第 9 页,共 48 页/* 你能在这后面添加命令 */* 保留这个回环入口。如果你不小心删除了下面这个循环语句,* 这在“start.
15、asm”中也是一个无穷循环。*/for (;);main.c:我们内核虽小但重要的开始在编译之前,我们需要在“start.asm”中添加两行。我们需要让 NASM 知道main()函数在一个外部文件中,并且我们需要在汇编文件中调用 main()函数。打开“start.asm” ,找到“stublet”所在的行。就在这行的后面,添加如下内容:extern _maincall _main我们停下想想。在 C 语言中,我们描述为“main”,为什么在这里 main 前面要加下划线呢?这是因为 GCC 编译器在编译时要在所有函数和变量名前加一个下划线。而当我们在汇编代码中引用一个 C 中的函数或变量时
16、,我们必须在其前加上一个下划线。现在似乎可以编译了吧,但我们还没有弄“system.h”呢。很简单,你只要创建一个空白的文本文件,取名为“system.h” 。将所有函数 (memcpy, memset, memsetw, strlen, inportb, outportb)的原型添加到其中。在这里使用宏定义来避免使用文件包含是明智的。这还能避免在头文件中多次使用#ifndef, #define, #endif 来做申明。我们将在指南中每个 C 源程序中包含“system.h”,以便你能方便地按需使用内核中函数。你还可以按照实际情况,随意地扩充这个函数库。“system.h”的内容如下:#if
17、ndef _SYSTEM_H#define _SYSTEM_H/* MAIN.C */extern unsigned char *memcpy(unsigned char *dest, const unsigned char *src, int count);extern unsigned char *memset(unsigned char *dest, unsigned char val, int count);extern unsigned short *memsetw(unsigned short *dest, unsigned short val, int count);extern
18、 int strlen(const char *str);extern unsigned char inportb (unsigned short _port);extern void outportb (unsigned short _port, unsigned char _data);#endif我们的全局头文件: system.h下面,我们将学习怎么编译源代码。打开前面章节提到的“build.bat”,添加如下内容。注意,这里假定“system.h” 文件在内核代码的 “include”文件夹中。这个命令将执行 gcc 编译器。在这些传递的参数中,“-Wall”将给你一些关于代码的第
19、10 页,共 48 页提示。“-nostdinc”和“-fno-builtin”的意思是我们使用的不是标准的 C 库函数。“-I./include”告诉编译器我们的头文件是在当前目录的“include”文件夹中。“-c”告诉编译器先别忙着连接。正如前面章节提到的,“-o main.o”是指定编译器产生的文件。简而言之,我们是在使用对内核开发最适宜的选项把“main.c”编译成“main.o”。小贴示:在 windows 系统中。右键单击批处理文件并选择“编辑” 来编辑它。gcc -Wall -O -fstrength-reduce -fomit-frame-pointer -finline-f
20、unctions -nostdinc -fno-builtin -I./include -c -o main.o main.c把上面的内容添加到“build.bat“中不要忘记按照我们在“build.bat”中提到的指令!最后,你需要把 “main.o”添加到需要连接起来构建内核的目标文件列表中。如果你正在奋力地编写像 memcpy 这样有实际效力的函数,可以看看这个示例的解决方案。屏幕显示现在我们将试着在屏幕上显示点东西。为此,我们需要一种管理屏幕滚动的方法。同时,能在屏幕上显示不同的颜色也是一件美妙的事情。 幸运的是,VGA显卡使这很容易实现:为了在显示器上显示内容,显卡给定了一个内存块,
21、我们只需向内存中写入字符和属性对。 VGA 控制器会自动地把更新的内容画到屏幕上。滚动屏幕是由我们的内核软件来维护的。从技术上说,这是我们的第一个驱动程序,现在我们就开始编写。 如上面所提到的,字符缓存只是在我们地址空间中的一块内存。这块缓存在0xB8000 的物理内存位置上。 缓存的类型为“short” ,这意味着缓存中的每一项内容都是由 16 位组成的,而不是我们通常认为的 8 位。 缓存中的每一个 16 位元素,都可以被分为“高 8 位”和“低 8 位”。低 8 位代表需要显示的字符。高 8 位通常定义了这个字符的前景色和背景色。 15 12 11 8 7 0 背景色 前景色 字符 16
22、 位中的高 8 位被称为“属性位” ,低 8 位被称为“字符位” 。正如你在上面的表格中看到的,每一个 16 位元素中,属性位又被分为 2 个 4 位的块:一个代表背景色,另一个代表前景色。 现在因为只用 4 位来表示颜色的原因,最多只可能有16 种不同的颜色可供选择,(使用公式:(位数 2) - 42 = 16 )。以下是 16种颜色表。 值 颜色 值 颜色Bran 的内核开发指南第 11 页,共 48 页0 黑 8 深灰 1 蓝 9 淡蓝 2 绿 10 淡绿 3 青绿 11 淡青绿 4 红 12 淡红 5 品红 13 淡品红 6 棕 14 淡棕 7 淡灰 15 白 最后,为了能处理内存中特
23、定的索引内容,我们需要使用有一个公式。 字符型的内存是一个简单的“ 线性” (或平坦)的内存区域,但是显示控制器使它看起来像一个 80x25 的 16 位矩阵。 在内存中,文字的每一行都是相等的;前后相互连接。 因此我们试着把屏幕变为平行的线。完成这个过程的最好方法是用公式:index = (y_value * width_of_screen) + x_value;如果我们要控制(3,4)位置上的字符,使用这个公式,就得到 4 * 80 + 3 = 323。 也就是说,在屏幕 (3,4)位置上操作,就等同于如下操作:unsigned short *where = (unsigned short
24、 *)0xB8000 + 323;*where = character | (attribute /* 这些内容定义了我们的文字指针,背景和前景颜色(属性),和 xy 坐标。 */unsigned short *textmemptr;int attrib = 0x0F;int csr_x = 0, csr_y = 0;/* 滚动屏幕 */void scroll(void)unsigned blank, temp;第 12 页,共 48 页/* 把空格定义为空白字符.我们也要设置他的背景颜色 */blank = 0x20 | (attrib = 25)/* 把当前的字符块向上移动一行 */tem
25、p = csr_y - 25 + 1;memcpy (textmemptr, textmemptr + temp * 80, (25 - temp) * 80 * 2);/* 最后,我们把最后一行设置为我们定义的空白字符。 */memsetw (textmemptr + (25 - temp) * 80, blank, 80);csr_y = 25 - 1;/* 更新硬件光标: 在输入的字符之后的那一行上显示一个闪烁。 */void move_csr(void)unsigned temp;/* 在线性的内存块中找到索引的公式。表示为:* Index = (y * width) + x */te
26、mp = csr_y * 80 + csr_x;/* 向 VGA 控制器的 CRT 控制寄存器发送 14 和 15 标志。* 他们是索引字符的高位和低位,这个字符显示在硬件光标的闪烁处。* 想知道更多细节,你可以查看 VGA 规范的编程文档。一个相当好的文档在* http:/ */outportb(0x3D4, 14);outportb(0x3D5, temp 8);outportb(0x3D4, 15);outportb(0x3D5, temp);/* 清空屏幕 */void cls()unsigned blank;int i;/* 同样的,我们需要一个用来做空白的short颜色 */bla
27、nk = 0x20 | (attrib = )where = textmemptr + (csr_y * 80 + csr_x);*where = c | att; /* 字符 AND 属性: 颜色 */csr_x+;第 14 页,共 48 页/* 如果光标到达了屏幕宽度的边缘,我们就插入另一行 */if(csr_x = 80)csr_x = 0;csr_y+;/* 如果需要的话,滚动屏幕,并移动光标 */scroll();move_csr();/* 使用以上的方法,输出一个字符串 */void puts(unsigned char *text)int i;for (i = 0; i /* 定
28、义一个 GDT 入口. 我们称之为包装,因为* 他阻止编译器做他认为最好的事:用包装的方法阻止* 编译器进行所谓的“优化” */struct gdt_entryBran 的内核开发指南第 17 页,共 48 页unsigned short limit_low;unsigned short base_low;unsigned char base_middle;unsigned char access;unsigned char granularity;unsigned char base_high; _attribute_(packed);/* 包括如下界限的特殊指针: GDT 开始的最大字节,
29、 负 1.* 同样,这需要被包装 */struct gdt_ptrunsigned short limit;unsigned int base; _attribute_(packed);/* 这是 GDT, 3 个入口, 最后是特殊的 GDT 指针*/struct gdt_entry gdt3;struct gdt_ptr gp;/* 这是 start.asm 的一个方法. 我们用这个方法适当的重载* 新的段寄存器 */extern void gdt_flush();用gdt.c 管理 GDT你会注意到我们对一个并不存在的函数 gdt_flush()加了一条申明. gdt_flush() 函数
30、使用一个特殊的指针来告诉 CPU 新的 GDT 的位置,正如在上面你所看到的。我们需要重新加载新的段寄存器,并且最后跳转到新的代码段。研究下面代码,然后把它添加到 start.asm 中 stublet 后的那个无穷循环后。; 这会建立一个新的片段寄存器. 我们需要做; 一些特别的命令来设置 CS. 我们要做的就称为; far jump. 一个跳转像偏移量一样包括一个段.; 这里用extern void gdt_flush();来申明global _gdt_flush ; 允许 C 程序连接extern _gp ; 表明_gp 在另一个文件里_gdt_flush:lgdt _gp ; 用这个特
31、殊的指针_gp载如 GDTmov ax, 0x10 ; 0x10 is the offset in the GDT to our data segmentmov ds, axmov es, axmov fs, axmov gs, axmov ss, axjmp 0x08:flush2 ; 0x08 is the offset to our code segment: Far jump!flush2:第 18 页,共 48 页ret ; 回到 C 程序!把这些内容添加到 start.asm 中仅仅在内存中为 GDT 保留空间是不够的。我们需要向 GDT 入口里写入值,设置“gp”GDT 指针,然
32、后调用函数 gdt_flush()来更新。下面要介绍的是一个特殊的函数 gdt_set_entry(),它使用简单好用的参数来进行所有移位(shift),以将合适的值填充进 GDT 入口。你必须在 system.h 中添加这两个函数的原型( 我们至少需要 gdt_install),以便我们能在 main.c 中使用它们。 请仔细分析下面这些代码,它们是 gdt.c 的后半部分。/* 在全局描述表(GDT)中建立一个描述符*/void gdt_set_gate(int num, unsigned long base, unsigned long limit, unsigned char acce
33、ss, unsigned char gran)/* 设定描述符的基地址*/gdtnum.base_low = (base gdtnum.base_middle = (base 16) gdtnum.base_high = (base 24) /* 设定描述符的界限 */gdtnum.limit_low = (limit gdtnum.granularity = (limit 16) /* 最后,设定粒度和访问标识*/gdtnum.granularity |= (gran gdtnum.access = access;/* 这里需要被主函数调用。这里要建立特殊的 GDT* 指针, 在 GDT 里
34、建立最开始的 3 个入口, 然后* 为了告诉处理器新的 GDT 在哪并且更新新的段寄 * 存器,我们需要在汇编文件里调用 gdt_flush()*/void gdt_install()/* 设立 GDT 指针和范围*/gp.limit = (sizeof(struct gdt_entry) * 3) - 1;gp.base = /* NULL 描述符 */gdt_set_gate(0, 0, 0, 0, 0);/* 第二个入口就是我们的代码段(Code Segment)。基地址* 是 0, 大小是 4GBytes, 粒度为 4KByte,* 使用 32-bit 操作码,是一个代码段描述符。*
35、请检查本章前面提到的那个表格,以确保每个* 变量的意思正确。*/Bran 的内核开发指南第 19 页,共 48 页gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF);/*第三个入口 是我们的数据段(Data Segment)。它完全和 代码段(Code Segment)* 相同, 但是这个入口的访问标识说明这是一个数据段 */gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF);/* 把旧的 GDT 删除,安装新的更新 ! */gdt_flush();把这些添加到 gdt.c。它从事的是一些和 GDT 相关的肮脏工作!不要忘记在
36、 system.h 中设置函数原型!既然 GDT 加载器基本构架已准备就绪并且我们已经把它编译连接进了内核,我们需要调用 gdt_install()以让它工作。打开 main.c,然后再 main()函数的最开头添加“gdt_install();”。正如你在本章中所学到的,GDT 需要在最开始就被初始化。它是十分重要的。你现在可以编译连接并将内核弄到软盘里来测试了。你不会在屏幕上看到任何变化,因为这是一个内在的变化。接下来,开始学下一章中断描述表(IDT) 吧!中断描述表中断描述表(Interrupt Descriptor Table,IDT)是用来告诉处理器在遇到异常或 “INT”操作码(汇
37、编中)时所应调用的中断服务例程( Interrupt Service Routine,ISR)。在一个设备完成请求并且需要获得服务时,中断请求也调用IDT 入口。更多关于异常和 ISR 的详细内容在本指南的下一节里,点击这里查看。每个 IDT 入口都与一个 GDT 入口类似,它们都有一个基地址,都有一个访问标志,并且总长度都是 32 位的。两种描述符的主要区别是这些域有不同的含义。在 IDT 中,描述符所指定的基地址实际上是中断产生时处理器所应调用的ISR 的地址。IDT 入口本身没有限制,但你需要指定一个给定 ISR 所在的段。这样做的好处是,当处理器处于某个环时(比如一个应用程序正在运行)
38、,依然可以通过一个已经发生的中断使其将控制权交给内核。IDT 入口的访问标志也与 GDT 的类似。有一位用来表示描述符是否真实存在。还有一个域用来存放描述符优先级(Descriptor Privilege Level,DPL),即允许使用给定中断的最大数目的环。主要的区别在于访问标志的剩余部分的定义。访问位的低五位经常设置为二进制的 01110,即十进制中的 14。下面用一个表来更好地图形表示 IDT 入口的访问位。7 6 54 0第 20 页,共 48 页P DPL 常为 01110 (14) P段是否存在?(1 = 是 )DPL描述符所处优先级(0 到 3)在你的内核目录下创建一个名为“i
39、dt.c” 的新文件。在你的 “build.bat”文件中添加一行以使 GCC 同样编译 “idt.c”。最后,把“idt.o”添加到 LD 需要链接以创建自定义内核的不断增长的文件列表中。“idt.c” 将声明一个结构,这个结构定义了每个 IDT 入口、装载 IDT 时必须用到的特定 IDT 指针结构(与装载 GDT 类似,但更简单),并且声明一个包含 256 个 IDT 入口的数组(这就是我们自己的 IDT)。#include /* 定义一个 IDT 入口。 */struct idt_entryunsigned short base_lo;unsigned short sel; /* 我们
40、的内核段从这里开始! */unsigned char always0; /* 这个变量将一直设置为 0! */unsigned char flags; /* 设置使用上面的表! */unsigned short base_hi; _attribute_(packed);struct idt_ptrunsigned short limit;unsigned int base; _attribute_(packed);/* 声明一个有 256 个入口的 IDT,尽管在本指南中我们将只使用前 32 个入口。* 如果访问任何未定义的 IDT 入口,正常情况下将导致一个“ 未知中断”的异* 常。访问任何
41、“存在”位被清除(为 0)的描述符则将产生一个 “无法处理* 中断”的异常。*/struct idt_entry idt256;struct idt_ptr idtp;/* 这存在于 “start.asm”,用来装载我们的 IDT。 */extern void idt_load();这是“idt.c”的开始部分,定义了重要的数据结构!Bran 的内核开发指南第 21 页,共 48 页接下来,就像“gdt.c”,你将注意到这里声明了一个已经在其它文件中实际存在的函数。“idt_load”和“gdt_flush”一样,由汇编语言编写。所有“idt_load”使用我们的特定 IDT 指针来调用“li
42、dt”汇编操作码,我们随后将在“idt_install” 中创建这个指针。 打开“start.asm”,并在对应“_gdt_flush”的“ret” 后面添加下面几行:; 把“_idtp”中定义的 IDT 装载到处理器。; 在 C 语言里,这样的声明应写为“extern void idt_load();” 。global _idt_loadextern _idtp_idt_load:lidt _idtpret把这段添加到“start.asm”设置 IDT 入口比设置 GDT 入口简单很多。我们有一个“idt_set_gate”函数,这个函数接收 IDT 入口数目、我们的 ISR 基地址、我们的
43、内核代码段(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 bas
44、e, unsigned short sel, unsigned char flags)/* 这里留给你去试着编写这个函数:把参数“base”划分为高 16 位和低 16 位,* 并将他们存储到 idtnum.base_hi 和 idtnum.base_lo。idtnum 中* 你还需要设置的其它域,在设置时都是不言而喻的。*/* 安装 IDT */void idt_install()/* 设置特点的 IDT 指针,就像 “gdt.c”中的一样。 */idtp.limit = (sizeof (struct idt_entry) * 256) - 1;idtp.base = /* 清除整个 ID
45、T,初始化其为零。 */memset(/* 使用 idt_set_gate 将任意新的 ISR 添加到 IDT。 */* 将处理器的内部寄存器指向新的 IDT。 */idt_load();第 22 页,共 48 页“idt.c”的剩余部分。请试着写出“idt_set_gate”,很容易的!最后,确保将“idt_set_gate”和“idt_install” 作为函数原型添加到“system.h”中。我们需要在其它文件中调用这些函数,比如“main.c”。在我们的“main.c”中紧接着“gdt_install ”之后调用“idt_install”。你应该可以毫无问题地编译你的内核。用一些时间在
46、你的新内核下做一些试验。如果你试着做一些非法操作,比如除零,你会发现你的计算机重起了!我们可以通过在新 IDT 中安装 ISR 来捕获这些“异常”。 如果你不想自己写“idt_set_gate”,可以在这里找到本节提到的解决方案。中断服务例程中断服务例程(Interrupt Service Routines,ISR )用来保存当前处理器状态,并且在内核的 C 语言级别中断处理程序被调用之前设置好内核模式所需的适当的段寄存器。这些事情可以在大约 15 到 20 行的汇编语句中完成,包括调用我们用C 编写的处理程序。为了适当地处理异常,我们还需要将 IDT 中正确的入口指向正确的 ISR。异常是处
47、理器遇到的特殊情况,此时处理器无法继续正常执行程序。这有点像除零操作:结果是未知的或非实的数,所以处理器将产生一个异常,使内核可以停止程序或任务以防产生任何问题。如果处理器发现程序试图访问一块它不该访问的内存,就会产生一般性保护错误(General Protection Fault)。当你设置分页,处理器产生一个页错误(Page Fault),但这是可恢复的:你可以把内存中的一页映射到出错地址这会在另一个指南中说到。IDT 中的前 32 个入口对应于可能由处理器产生的异常,因此需要加以处理。一些异常将会把另一个值压入栈:一个对应的错误代码值。 异常号 描述 错误代码?0 除零异常(Divisi
48、on By Zero Exception) 没有1 调试异常(Debug Exception) 没有2 非可屏蔽中断异常(Non Maskable Interrupt Exception) 没有3 断点异常(Breakpoint Exception) 没有4 INTO 指令溢出异常(Into Detected Overflow Exception) 没有5 越界异常(Out of Bounds Exception) 没有6 非法操作码异常(Invalid Opcode Exception) 没有7 无协处理器异常(No Coprocessor Exception) 没有Bran 的内核开发指南
49、第 23 页,共 48 页8 双重故障异常(Double Fault Exception) 有9 协处理器段溢出异常(Coprocessor Segment Overrun Exception) 没有10 无效 TSS 异常(Bad TSS Exception) 有11 段不存在异常(Segment Not Present Exception) 有12 栈错误异常(Stack Fault Exception) 有13 一般性保护错误异常(General Protection Fault Exception) 有14 页错误异常(Page Fault Exception) 有15 未知中断异常(Unknown Interrupt Exception) 没有16 协处理器错误异常(Coprocessor Fault Exception) 没有17 队列校验异常(Alignment Check Exception ,486+)