总觉得操作系统是每一个计算机人在专业技术领域的终极目标,虽然这种观点有失偏颇,但是对于一个计算机科班出身的人来说,掌握操作系统至少是基本功之一。
于渊的这本《Orange‘S一个操作系统的实现》是一本很不错的操作系统书籍,本科课堂上讲的都是那种理论的东西,做做题还是可以的,但是太抽象的东西终归是不好的,尤其对于工科来说,工科就是要实践。于渊的这本书,是教大家从头开始写操作系统的,从汇编到C,步步深入操作系统的基本原理,虽然只读了前面几章,但是反复读下来,以前的疑惑渐渐散去,基本的概念渐渐落地生根,这是种很奇妙的感觉,一个庞大软件的大幕渐渐拉开。
闲话少说,马上进入正题,今天谈谈书中的第三章,关于保护模式的东西。
保护模式是前朝遗留的产物,用于渊的话说,从实模式到保护模式是一次改朝换代的过程,很形象,因为改朝换代大部分时间意味着进步。实模式是dos时代的产物,16位寄存器,16位数据线,32位的地址线,但是时代在召唤,CPU终究要进化,程序也终究是要变大,因此就对这个操作系统提出了更高的要求。
第一要明白的一点是不是说有了32位的CPU,原来的16位的就要全盘否定,就像苏联不能一边倒地去否定斯大林的模式。于是在这里引入了实模式和保护模式,也就是说,实模式和保护模式的同时出现,就是一个文化的革故鼎新、兼收并蓄的过程。
操作系统的启动过程是首先进入实模式,然后进入保护模式的,保护模式的一节刚开始,于渊就给出了一段跳转的代码。这段代码乍看很晕,晕是好事,晕说明自己还不懂,如果能够克服对未知的恐惧,那么我们就能够有所长进了。
在16位的代码段中,作者初始化了全局描述符,全局描述符是在许多系统级的书籍里面都会提到的概念,在这里先不深究,我们只要记住这个东西是用来提供段式存储机制的就行。回到上面提到的16位的代码段,这个段中,所有的指令都是老系统的,也就是16位的,我们要在这个16位的系统中为将来的保护模式下的32位系统做准备,准备什么?准备全局描述符(GDT)。这一点是很好理解的,因为在32位系统中,基于段的存储机制中,我们必须要借助GDT,也就是说GDT在你进入保护模式的时候必须是成型的,是好的,是直接可以拿来用的,而我们现在开机后必须要经过16位的系统只是历史遗留的问题,那么在这个16位的阶段,我们应该干点什么呢?答案是把32位系统需要的一些东西给准备好。于是有了这段代码:
mov ax, cs
shl eax, 4
add eax, LABEL_SEG_CODE32
mov word [LABEL_DESC_CODE32 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE32 + 4], al
mov byte [LABEL_DESC_CODE32 + 7], ah
这些语句的主要作用就是GDT的初始化,即声明了本程序的代码段在内存中的地址。我们已经知道GDT就是为了段存储机制而设置的一种数据结构,那么GDT表项中一个很重要的内容就是该段的在整个程序空间中的地址。好了这里终于引入地址两个字了,终于到了那些课本上的东西了,终于和自己平常做的题有关了,对,这里从GDT中得到的段地址加上程序中提供的偏移地址就是以前我们学过的线性地址,线性地址不是我们脑中地址链的开头,开头应该是逻辑地址,逻辑地址是什么?逻辑地址就是你查GDT表的输入。好了,现在我们有点头绪了,为了给大家一个直观的解释,我们画张图来表示。
好了,逻辑地址和线性地址大已经家知道他们的关系了,但是逻辑地址的值到底是什么,线性地址的值到底是什么呢?先给出答案吧,逻辑地址里面是【选择子:偏移地址】,线性地址是【段地址:偏移地址】。选择子又是什么?我们好像进入了一个DFS似的,但是不要紧,考点各个击破嘛,不要怕陌生的概念。选择子是一个16位的结构,其中高13位表示的是本段(记住,现在我们已经升级了,开始以段来管理程序了)在GDT中的偏移量,低三位表示的优先级等量的设置,暂时不要管它们了。好了,现在我们明白了,所谓的逻辑地址,就是给出了本段在全局描述符表(GDT)中的偏移量,并给出了在本段中的偏移。好了,现在给你1分钟,自己根据以上提供的知识来推理一下逻辑地址到线性地址的转换吧。
sleep(1000);
理清楚了吗?逻辑地址提供的是选择子和偏移地址,然后根据偏移我们能够找到GDT中本段的表项,GDT中存储的是什么?是本段在整个程序空间的起始地址,也就是段地址,有了段地址,有了逻辑地址上的偏移,这两个量相加就叫线性地址---【段地址:偏移地址】。再联系一下上图中所示的过程,看看是不是清晰很多。
好了,在这个过程当中我们略去了很多的细节,比如GDT的基址存储在寄存器cr0中等等。下面就看看作者在保护模式一节中,是怎样通过编程,让我们更明白这种转换的吧。
下面这段代码和之前贴出的代码一致,为了方便说明,又贴出一遍。
mov ax, cs
shl eax, 4
add eax, LABEL_SEG_CODE32
mov word [LABEL_DESC_CODE32 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE32 + 4], al
mov byte [LABEL_DESC_CODE32 + 7], ah
上面这段代码,出自在实模式下对GDT表的初始化过程。先说明一下,代码中出现的四条mov指令中的后三条是对GDT表项的填充,其他的一些细节可以先不管。好了,我们看一下具体填充的内容:cs寄存器的内容,左移四位,然后加上32位代码段的起始地址,然后将得到的这个地址存入相应的表项中。就这么简单的几句话,完成了对一个表项的填充,恍然大悟:在实模式条件下,地址线的寻址空间是1MB,为了凑够20位的地址,CPU的处理方式正好是,CS左移四位,得到所谓的“段”,然后加上偏移,如此得到的地址我们可以称作是在16位条件下的线性地址。然后把这个地址填入,代码段GDT的地址中,作为了32位代码段的入口地址,也即段地址。
好了,就这样,我们的段的初始化描述结束。然后再看看具体的是怎样通过逻辑地址得到线性地址。
mov ax, SelectorVideo
mov gs, ax
mov edi, (80 * 11 + 79) * 2 ;
mov al, 'P'
mov [gs:edi], ax
好了,至此逻辑地址与线性地址解释完毕,物理地址与线性地址的关系以及更多保护模式的内容会在接下来的日志中一一讲述。