购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.5 进入保护模式

从现在开始我们将进入保护模式,为进入IA-32e模式做必要的准备工作,毕竟我们前面说过,要进入IA-32e模式,必须先进入保护模式。和往常一样,要进入保护模式,必须先创建并初始化全局描述符表GDT。要创建和初始化GDT,就必须先确定它的内存位置。

回头看一下图3-4的系统内存布局,明确一下GDT的位置。为了方便,我们在全局定义文件global_defs.wid中将这个地址定义为常量GDT_PHY_ADDR。

为了创建和初始化GDT,需要将GDT所在的位置当成一个段来访问。第198~199行,我们将GDT的物理地址右移4次,生成一个逻辑段地址,然后传送到DS。于是,GDT就起始于这个段内偏移为0的地方。

接下来,我们在GDT内安装段描述符。按照处理器的要求,第一个描述符,也就是0号描述符,必须是空描述符,我们直接跳过这个表项或者说槽位;1号描述符是保护模式下的代码段描述符。这个段的基地址为0,界限值为0xfffff,粒度为4KB,显然,段长度是4GB;2号描述符是保护模式下的数据段和栈段描述符,这个段的基地址为0,界限值为0xfffff,粒度为4KB,显然,段长度也是4GB。在保护模式下,栈段也可以使用普通的、向上扩展的数据段。我们让数据段和栈段使用同一个描述符,只要栈操作和普通的数据访问不发生冲突就行。栈段在访问时向下推进,数据段在访问时向上推进,互不干扰。

描述符的格式我们应该比较熟悉了,所以这里不再详细解释。要进入保护模式,有这两个段描述符就足够了。从这两个描述符我们可以看出,进入保护模式后,系统将工作在平坦模型下。平坦模型用起来非常简单,也方便程序的编写。

最后一个段描述符并不是在保护模式下使用的,而是为IA-32e模式及其64位子模式提前准备的。我们的最终目标是进入64位模式,所以需要提前准备这样一个代码段描述符。在64位模式下,段描述符的大部分内容都不再使用,所以段基地址、段界限、粒度等都不再起作用。唯一需要注意的是,这个段描述符的L位是1。一旦进入这个代码段执行,处理器将切换到64位模式。

按照流程,接下来应该用lgdt指令加载全局描述符表寄存器GDTR。lgdt指令需要从内存里取得GDT的界限值和基地址,为此,我们需要首先把GDT的界限值和基地址保存在某个内存位置。

如图3-5所示,GDT的界限值和基地址保存在系统数据区。在系统数据区内偏移为02的地方是全局描述符表GDT的界限值,长度是1个字;在偏移为04的地方是全局描述符表GDT的基地址,长度是1个双字。

我们知道,系统数据区的物理地址被定义为常量SDA_PHY_ADDR,需要将它作为一个段才能访问。为此,第216~217行将系统数据区的物理地址SDA_PHY_ADDR右移4位,生成一个逻辑段地址,并用这条指令传送到段寄存器DS,然后就可以用DS访问系统数据区了。

第219行,将GDT的界限值31传送到系统数据区内偏移为2的地方。全局描述符表中一共有4个段描述符,每个描述符8字节,一共是32字节,32减1就是界限值。

第220行,将GDT的基地址GDT_PHY_ADDR传送到系统数据区内偏移为4的地方保存起来。

以上两条指令执行之后,第223行的lgdt指令加载全局描述符表寄存器GDTR。这条指令访问段寄存器DS所指向的系统数据区,从偏移为2的地方取出GDT的界限值和基地址并传送到GDTR。

一旦完成了全局描述符表的初始化,接下来,第225~235行,打开处理器的第21根地址线A20,并通过设置控制寄存器CR0的位1来开启保护模式。这段代码你应该非常熟悉,不再多说。

现在我们已经进入保护模式,但是需要刷新代码段寄存器CS。像往常一样,第236行的jmp指令清空流水线并串行化处理器。指令执行时,用描述符选择子0x0008从GDT中选择第二个描述符,并用描述符的内容刷新CS的描述符高速缓存器。在这里,段描述符选择子0x0008选择的是基地址为0的代码段,而后面的LDR_PHY_ADDR+flush是段内偏移量。但,为什么偏移量是LDR_PHY_ADDR+flush呢?

如图3-6所示,目标代码段的基地址为0,指向物理内存起始处;内核加载器程序起始于物理地址LDR_PHY_ADDR。在内核加载器程序中,标号flush代表的汇编地址是它相对于内核加载器程序开头的偏移。如此一来,标号flush相对于目标代码段起始处的偏移量就是LDR_PHY_ADDR+flush。

图3-6 远转移指令的目标位置

从这里开始,后面的指令都是用bits 32编译的,只能在32位保护模式下执行。第240~246行,依次向DS、ES、FS、GS和SS加载平坦模型下的4GB数据段选择子,然后将栈指针设置为0x7c00。如图3-4所示,这个位置紧挨着主引导程序,但它是向下推进的。初始化数据段寄存器时使用的段描述符选择子是0x0010,它选择的是一个4GB的段,段的线性基地址为0,这和代码段寄存器是一样的。换句话说,我们当前是在保护模式下采用平坦模型执行的。

进入保护模式后,我们准备显示一条信息。因为已经工作在保护模式下,无法再调用BIOS例程来显示字符串,毕竟BIOS例程只能在实地址模式下调用。为此,我们特意准备了一个保护模式下的字符串显示例程put_string_flat32,它位于第403行。

这是一个带光标跟随的字符串显示例程,只运行在32位保护模式下,而且只工作在平坦模型下。这个例程我们并不陌生,这是我们在学习保护模式的时候使用的例程,所以不准备多说,只是简单回顾一下。

这个例程显示0终止的字符串并移动光标,在平坦模型下不需要传入段地址,只要求用EBX传入字符串的起始线性地址。和从前一样,字符串显示例程从字符串中取出每个字符,然后调用子例程put_char。在子例程put_char内,获取光标位置,在光标处打印字符,根据需要执行回车、换行或者屏幕滚动,最后要重新设置光标并返回到调用者。

回到当前程序的最前面,第26行,要显示的字符串从标号protect这里开始定义,这个字符串的意思是“已经进入保护模式为IA-32e模式做准备。”

回到第249~250行,字符串的显示是用这两条指令完成的。在此之前,我们刚刚初始化了所有的数据段寄存器,在平坦模型下,它们都指向同一个段,段的基地址为0,要显示的字符串就位于这个段中。我们首先将字符串的线性地址protect+LDR_PHY_ADDR传送到EBX,但这个线性地址为什么是protect+LDR_PHY_ADDR呢?

如图3-7所示,在平坦模型下,所有段寄存器都指向同一个4GB的段,都指向物理内存的最低端。在这个段中,内核加载器程序的起始物理地址为LDR_PHY_ADDR。在内核加载器中,字符串起始于标号protect,标号protect代表的汇编地址是相对于内核加载器程序起始处的距离或者说偏移量。

图3-7 字符串的段内偏移量

尽管字符串位于内核加载器程序中,但是在打印的时候我们把它放在整个4GB的内存段中,由于在平坦模型下段的基地址为0。所以字符串的段内偏移量是内核加载器的起始地址LDR_PHY_ADDR加上protect。我们用protect+LDR_PHY_ADDR得到字符串的段内偏移量,传送到EBX,然后调用put_string_flat32来打印它。 r97YKOR5KBYhxSBOw3ZimwSiPPtgg7NgTI34XyexRlnScGUlKYvVkvWOj1bFSUEF

点击中间区域
呼出菜单
上一章
目录
下一章
×