9 GDT与IDT的初始化(harib02i)
鼠标指针显示出来了,我们想做的第一件事就是去移动它,但鼠标指针却一动不动。那是当然,因为我们还没有做出这个功能。……嗯,无论如何想让它动起来。
要怎么样才能让它动呢?……(思考中)……有办法了!首先要将GDT和IDT初始化。不过在此之前,必须说明一下什么是GDT和IDT。
GDT也好,IDT也好,它们都是与CPU有关的设定。为了让操作系统能够使用32位模式,需要对CPU做各种设定。不过,asmhead.nas里写的程序有点偷工减料,只是随意进行了一些设定。如果这样原封不动的话,就无法做出使用鼠标指针所需要的设定,所以我们要好好重新设置一下。
那为什么要在asmhead.nas里偷工减料呢?最开始就规规矩矩地设定好不行吗?……嗯,这个问题一下子就戳到痛处了。这里因为笔者希望尽可能地不用汇编语言,而用C语言来写,这样大家更容易理解。所以,asmhead.nas里尽可能少写,只做了运行bootpack.c所必需的一些设定。这次为了使用这个文件,必须再进行设定。如果大家有足够能力用汇编语言编写程序,就不用模仿笔者了,从一开始规规矩矩地做好设定更好。
从现在开始,学习内容的难度要增加不小。以后要讲分段呀,中断什么的,都很难懂,很多程序员都是在这些地方受挫的。从难度上考虑,应该在20天以后讲而不是第5天。但如果现在不讲,几乎所有的装置都不能控制,做起来也没什么意思。笔者不想让大家做没有意思的操作系统。
所以请大家坚持着读下去,先懂个大概,然后再回过头来仔细咀嚼。在一天半以后,内容的难度会回到以前的水平,所以这段时间大家就打起精神加油吧!
■■■■■
先来讲一下分段。回想一下仅用汇编语言编程时,有一个指令叫做ORG。如果不用ORG指令明确声明程序要读入的内存地址,就不能写出正确的程序来。如果写着ORG 0x1234,但程序却没读入内存的0x1234号,可就不好办了。
发生这种情况是非常麻烦的。最近的操作系统能同时运行多个程序,这一点也不稀奇。这种时候,如果内存的使用范围重叠了怎么办?这可是一件大事。必须让某个程序放弃执行,同时报出一个“因为内存地址冲突,不能执行”的错误信息。但是,这种错误大家见过吗?没有。所以,肯定有某种方法能解决这个问题。这个方法就是分段。
所谓分段,打个比方说,就是按照自己喜欢的方式,将合计4GB的内存分成很多块(block),每一块的起始地址都看作0来处理。这很方便,有了这个功能,任何程序都可以先写上一句ORG 0。像这样分割出来的块,就称为段(segment)。顺便说一句,如果不用分段而用分页(paging),也能解决问题。不过我们目前还不讨论分页,可以暂时不考虑它。
需要注意的一点是,我们用16位的时候曾经讲解过的段寄存器。这里的分段,使用的就是这个段寄存器。但是16位的时候,如果计算地址,只要将地址乘以16就可以了。但现在已经是32位了,不能再这么用了。如果写成“MOV AL, [DS:EBX]”, CPU会往EBX里加上某个值来计算地址,这个值不是DS的16倍,而是DS所表示的段的起始地址。即使省略段寄存器(segment register)的地址,也会自动认为是指定了DS。这个规则不管是16位模式还是32位模式,都是一样的。
■■■■■
按这种分段方法,为了表示一个段,需要有以下信息。
❏ 段的大小是多少
❏ 段的起始地址在哪里
❏ 段的管理属性(禁止写入,禁止执行,系统专用等)
CPU用8个字节(=64位)的数据来表示这些信息。但是,用于指定段的寄存器只有16位。或许有人会猜想在32位模式下,段寄存器会扩展到64位,但事实上段寄存器仍然是16位。
那该怎么办才好呢?可以模仿图像调色板的做法。也就是说,先有一个段号,存放在段寄存器里。然后预先设定好段号与段的对应关系。
调色板中,色号可以使用0~255的数。段号可以用0~8191的数。因为段寄存器是16位,所以本来应该能够处理0~65535范围的数,但由于CPU设计上的原因,段寄存器的低3位不能使用。因此能够使用的段号只有13位,能够处理的就只有位于0~8191的区域了。
段号怎么设定呢?这是对于CPU的设定,不需要像调色板那样使用io_out(由于不是外部设备,当然没必要)。但因为能够使用0~8191的范围,即可以定义8192个段,所以设定这么多段就需要8192×8=65536字节(64KB)。大家可能会想,CPU没那么大存储能力,不可能存储那么多数据,是不是要写入到内存中去呀。不错,正是这样。这64KB(实际上也可以比这少)的数据就称为GDT。
GDT是“global(segment)descriptor table”的缩写,意思是全局段号记录表。将这些数据整齐地排列在内存的某个地方,然后将内存的起始地址和有效设定个数放在CPU内被称作GDTR的特殊寄存器中,设定就完成了。
■■■■■
另外,IDT是“interrupt descriptor table”的缩写,直译过来就是“中断记录表”。当CPU遇到外部状况变化,或者是内部偶然发生某些错误时,会临时切换过去处理这种突发事件。这就是中断功能。
我们拿电脑的键盘来举个例子。以CPU的速度来看,键盘特别慢,只是偶尔动一动。就算是重复按同一个键,一秒钟也很难输入50个字符。而CPU在1/50秒的时间内,能执行200万条指令(CPU主频100MHz时)。CPU每执行200万条指令,查询一次键盘的状况就已经足够了。如果查询得太慢,用户输入一个字符时电脑就会半天没反应。
要是设备只有键盘,用“查询”这种处理方法还好。但事实上还有鼠标、软驱、硬盘、光驱、网卡、声卡等很多需要定期查看状态的设备。其中,网卡还需要CPU快速响应。响应不及时的话,数据就可能接受失败,而不得不再传送一次。如果因为害怕处理不及时而靠查询的方法轮流查看各个设备状态的话,CPU就会穷于应付,不能完成正常的处理。
正是为解决以上问题,才有了中断机制。各个设备有变化时就产生中断,中断发生后,CPU暂时停止正在处理的任务,并做好接下来能够继续处理的准备,转而执行中断程序。中断程序执行完以后,再调用事先设定好的函数,返回处理中的任务。正是得益于中断机制,CPU可以不用一直查询键盘,鼠标,网卡等设备的状态,将精力集中在处理任务上。
讲了这么长,其实总结来说就是:要使用鼠标,就必须要使用中断。所以,我们必须设定IDT。IDT记录了0~255的中断号码与调用函数的对应关系,比如说发生了123号中断,就调用〇×函数,其设定方法与GDT很相似(或许是因为使用同样的方法能简化CPU的电路)。
如果段的设定还没顺利完成就设定IDT的话,会比较麻烦,所以必须先进行GDT的设定。
■■■■■
虽然说明很长,但程序并没那么长。
本次的*bootpack.c节选
struct SEGMENT_DESCRIPTOR{ short limit_low, base_low; char base_mid, access_right; char limit_high, base_high; }; struct GATE_DESCRIPTOR { short offset_low, selector; char dw_count, access_right; short offset_high; }; void init_gdtidt(void) { struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) 0x00270000; struct GATE_DESCRIPTOR *idt = (struct GATE_DESCRIPTOR *) 0x0026f800; int i; /* GDT的初始化 */ for (i = 0; i < 8192; i++) { set_segmdesc(gdt + i, 0, 0, 0); } set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092); set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a); load_gdtr(0xffff, 0x00270000); /* IDT的初始化 */ for (i = 0; i < 256; i++) { set_gatedesc(idt + i, 0, 0, 0); } load_idtr(0x7ff, 0x0026f800); return; } void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar) { if (limit > 0xfffff) { ar |= 0x8000; /* G_bit = 1 */ limit /= 0x1000; } sd->limit_low = limit & 0xffff; sd->base_low = base & 0xffff; sd->base_mid = (base >> 16) & 0xff; sd->access_right = ar & 0xff; sd->limit_high = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0); sd->base_high = (base >> 24) & 0xff; return; } void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar) { gd->offset_low = offset & 0xffff; gd->selector = selector; gd->dw_count = (ar >> 8) & 0xff; gd->access_right = ar & 0xff; gd->offset_high = (offset >> 16) & 0xffff; return; }
SEGMENT_DESCRIPTOR中存放GDT的8字节的内容,它无非是以CPU的资料为基础,写成了结构体的形式。同样,GATE_DESCRIPTOR中存放IDT的8字节的内容,也是以CPU的资料为基础的。
变量gdt被赋值0x00270000,就是说要将0x270000~0x27ffff设为GDT。至于为什么用这个地址,其实那只是笔者随便作出的决定,并没有特殊的意义。从内存分布图可以看出这一块地方并没有被使用。
变量idt也是一样,IDT被设为了0x26f800~0x26ffff。顺便说一下,0x280000~0x2fffff已经有了bootpack.h。“哎?什么时候?我可没听说过这事哦!”大家可能会有这样的疑问,其实是后面要讲到的“asmhead.nas”帮我们做了这样的处理。
■■■■■
现在继续往下说明。
for (i = 0; i < 8192; i++) { set_segmdesc(gdt + i, 0, 0, 0); }
请注意一下以上几行代码。gdt是0x270000, i从0开始,每次加1,直到8191。这样一来,好像gdt+i最大也只能是0x271fff。但事实上并不是那样。C语言中进行指针的加法运算时,内部还隐含着乘法运算。变量gdt已经声明为指针,指向SEGMENT_DESCRIPTOR这样一个8字节的结构体,所以往gdt里加1,结果却是地址增加了8。
因此这个for语句就完成了对所有8192个段的设定,将它们的上限(limit,指段的字节数-1)、基址(base)、访问权限都设为0。
再往下还有这样的语句:
set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092); set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
以上语句是对段号为1和2的两个段进行的设定。段号为1的段,上限值为0xffffffff即大小正好是4GB),地址是0,它表示的是CPU所能管理的全部内存本身。段的属性设为0x4092,它的含义我们留待明天再说。下面来看看段号为2的段,它的大小是512KB,地址是0x280000。这正好是为bootpack.hrb而准备的。用这个段,就可以执行bootpack.hrb。因为bootpack.hrb是以ORG 0为前提翻译成的机器语言。
■■■■■
下一个语句是:
load_gdtr(0xffff, 0x00270000);
这是因为依照常规,C语言里不能给GDTR赋值,所以要借助汇编语言的力量,仅此而已。
再往下都是关于IDT的记述,因为跟前面一样,所以应该没什么问题。
在set_segmdesc和set_gatedesc中,使用了新的运算符,下面来介绍一下。首先看看语句“ar |=0x8000; ”,它是“ar = ar |0x8000; ”的省略表现形式。同样还有“limit /= 0x1000; ”,它是“limit =limit/0x1000; ”的省略表现形式。“|”是前面已经出现的或(OR)运算符。“/”是除法运算符。
“>>”是右移位运算符。比如计算00101100>>3,就得到00000101。移位时,舍弃右边溢出的位,而左边不足的3位,要补3个0。
■■■■■
今天到这里就差不多了,访问权属性及IDT的详细说明就留到明天吧。总之,使用本程序的操作系统是做成了。能不能正常运行啊?赶紧试一试吧。“make run ”……还好,能运行。这次只是简单地做了初期设定,所以即使运行成功了,画面上也什么都不显示。
现在haribote.sys变成多少字节了呢?哦,光字体就有4KB,增加了不少,到7632字节了。今天就先到这里吧,大家明天见。