本章节所有代码托管在miniOS_32
章节任务介绍
上一节,我们完善了mbr主引导程序的功能——加载loader程序到指定位置运行
但是上一节我们只是为了测试我们的mbr程序是否能够成功加载loader,因此loader程序仅仅只是在屏幕上打印出我们想要的字符串
在本节,我们的主要任务就是完善loader程序的功能——开启保护模式,让后续的内核工作在保护模式下
如何开启保护模式?
- 打开A20
- 加载GDT
- 在实模式下构造需要的段描述符(代码段、数据段、栈段、显存段)
- 将四种段描述符装填进GDT,从而构造GDT
- 构造四种内存段的段选择子
- 构造GDTR(存储GDT基址的寄存器),从而让操作系统运行时将GDT加载进内存
- 置cr0寄存器的PE位为1
进入保护模式之前,先在实模式下测试一段在屏幕上显示信息的代码,即loader_start,这段代码使用实模式的逻辑编写,因此,如果运行成功,则说明在进入保护模式之前我们的代码是在实模式下运行的
保护模式测试
- 在进入保护模式之后写一段屏幕显示信息,如果显示成功说明成功进入了保护模式
前置知识
保护模式概述
一般来说,操作系统负责整个计算机软、硬件的管理,它做任何事情都是可以的。但是,用户程序却应当有所限制,只允许它访问属于自己的数据,即使是转移,也只允许在自己的各个代码段之间进行。
问题在于,在实模式下,用户程序对内存的访问非常自由,没有任何限制,随随便便就可以修改任何一个内存单元
比如以下代码片段,这个程序首先将段地址设置到0xb800,传统上,这是文本模式下的显存。所以,它通过指令向显存写入一个字符H。然后,它又将段地址切换到0x8000,向这个段内偏移地址为6的地方写入一字节0xc7。紧接着,又将段地址切换到0,向段内偏移地址为0x30的地方写入一字节0。
事实上我们知道,段地址为0的这1KB内存是中断向量表,它这样做实际上是破坏了中断向量表的内容,但是它这样做是不受限制的,没有人可以阻止。最后,它又向端口0x60发送一字节的数据,用来控制设备。
通过这一段程序可以看出,在实模式下,程序是可以“为所欲为”的。它想访问内存的哪一部分,都可以很轻松地通过设置段地址和偏移地址来办到,即使某个内存位置不属于当前程序,它照样可以切换到那里,并随意修改其中的内容。
在多用户、多任务时代,内存中会有多个用户(应用)程序在同时运行。为了使它们彼此隔离,防止因某个程序的编写错误或者崩溃而影响到操作系统和其他用户程序,使用保护模式是非常有必要的。
全局描述符表
我们知道,为了让程序在内存中能自由浮动而又不影响它的正常执行,处理器将内存划分成逻辑上的段,并在指令中使用段内偏移。
与实模式相同,在保护模式下,对内存的访问仍然使用段地址和偏移地址,但是,在每个段能够访问之前,必须先进行登记。
为此,我们有以下概念
- 和一个段有关的信息需要8字节来描述,称为段描述符(Segment Descriptor),每个段都需要一个描述符。
- 为了存放这些描述符,需要在内存中开辟出一段空间。在这段空间里,所有的描述符都是挨在一起集中存放的,这就构成了一个描述符表。
- 最主要的描述符表是全局描述符表(Global Descriptor Table, GDT),所谓全局,意味着该表是为整个软硬件系统服务的。在进入保护模式前,必须要定义全局描述符表。
- 为了跟踪全局描述符表,处理器内部有一个48位的寄存器,称为全局描述符表寄存器(GDTR)。
- GDTR分为两部分,分别是32位的线性地址和16位的边界。32位的处理器具有32根地址线,可以访问的地址范围是0x00000000到0xFFFFFFFF,共2^32字节的内存,即4GB内存。
- GDTR的32位线性基地址部分保存的是全局描述符表在内存中的起始线性地址,16位边界部分保存的是全局描述符表的边界(界限),其在数值上等于表的大小(总字节数)减一
- 因为GDT的界限是16位的,所以,该表最大是216字节,也就是65536字节(64KB)。又因为一个描述符占8字节,故最多可以定义8192个描述符
如下是全局描述表寄存器(GDTR)的格式
理论上,全局描述符表可以位于内存中的任何地方。但是,
- 由于在进入保护模式之后,处理器要立即按新的内存访问模式工作,所以,必须在进入保护模式之前定义GDT。
- 此外,由于在实模式下只能访问1MB的内存,故GDT通常都定义在1MB以下的内存范围中。当然,允许在进入保护模式之后换个位置重新定义GDT。
以下是GDT和GDTR的关系图
总结来说,
- GDT相当于一个数组,数组中的元素是各个段描述符
- GDTR这个寄存器中存放着GDT的首地址
- GDTR的访问方式:lgdt 48位内存数据
段描述符
段描述符是一个专门用来描述一个内存段(比如代码段、数据段、栈段、显存段等等)属性的结构,如前所述,该结构的大小占据8个字节
以下是段描述符与内存段之间的关系
以下是段描述符的格式,每个描述符在GDT中占8字节,也就是2个双字,或者说是64位。图中,下面是低32位(低双字),上面是高32位(高双字)。
从上图可以看出
- 描述符中指定了32位的段起始地址(段基址),以及20位的段边界。
- 在实模式下,段地址并非真实的物理地址,在计算物理地址时,还要左移4位(乘以16)。
- 而在32位保护模式下,段地址是32位的线性地址,如果未开启分页功能,该线性地址就是物理地址。
此外,我们给出段描述符中各个属性位的详细解释
- 段界限:表示一个段的最大大小
- 此段界限是一个单位量,其单位要么是字节,要么是4KB,具体可由G位进行指定
- 最终段界限=此段界限值*单位
- 实际段界限计算公式:(此段界限值+1)*(G位)-1
- G:粒度位,用于解释段界限的单位
- 为0:表示段界限的单位表示为字节
- 为1:表示段界限的单位表示为4KB
- S:描述符类型
- 为0,表示系统段
- 为1,表示非系统段(代码段或者数据段)
- type:用于指示描述符的子类型
- DPL:Descriptor Privilege Level,即描述符的特权级,用于指定要访问该段所必须具有的最低特权级。如果这里的数值是2,那么,只有特权级别为0、1和2的程序才能访问该段,而特权级为3的程序访问该段时,处理器会予以阻止。
- 特权级共有0、1、2、3,用于解释段界限的含义,其中0是最高特权级别,3是最低特权级别。
- 特权级是一个数字,可以赋给一个程序,用来决定该程序能够执行哪些指令,或者能够访问哪些系统资源;也可以赋给系统资源,用来决定哪些程序可以访问它们。
- 刚进入保护模式时执行的代码具有最高特权级0(可以看成从实模式那里继承来的),这些代码通常都是操作系统代码,因此它的特权级别最高。
- 每当操作系统加载一个用户程序时,它通常都会指定一个稍低的特权级,比如3特权级。不同特权级别的程序是互相隔离的,其互访是严格限制的,而且有些处理器指令(特权指令)只能由0特权级的程序来执行,为的就是安全。
- P:段存在位,用于指示描述符所对应的段是否存在
- P位是由处理器负责检查的。每当通过描述符访问内存中的段时,如果P位是“0”,处理器就会产生一个异常中断
- 该中断处理过程是由操作系统提供的,该处理过程的任务是负责将该段从硬盘换回内存,并将P位置1
- D/B:标志位,为了能够在32位处理器上兼容运行16位保护模式的程序。该标志位对不同的段有不同的效果。
- 对于代码段,此位称作“D”位,用于指示指令中默认的有效地址和操作数尺寸;
- D=0,表示指令中的有效地址或者操作数是16位的;
- D=1,指示32位的有效地址或者操作数。
- 对于栈段和向下扩展的数据段来说,该位被叫作“B”位,用于指定在进行隐式的栈操作时,是使用寄存器SP还是寄存器ESP,隐式的栈操作指令包括push、pop和call等。
- 如果该位是“0”,在访问那个段时,使用寄存器SP;
- 否则就是使用寄存器ESP。
- AVL:表示软件是否可以使用(Available),通常由操作系统来用,处理器并不使用它。
- L:用于设置是否是64位代码段
- 为1,表示64位代码段
- 为0,表示32位代码段
选择子
段寄存器 CS、DS、ES、FS、GS、SS,在实模式下时,段中存储的是段基地址,即内存段的起始地址。
而在保护模式下时,在段寄存器中存入的是一个叫作选择子的东西–selector。选择子可以看做是个索引值,用于在段描述符表中索引相应的段描述符,这样,便在段描述符中得到了内存段的起始地址和段界限值等相关信息。
以下是选择子结构各属性说明
- RPL:请求特权级
- TI(table indicator):表示在GDT(全局描述符表)还是在LDT(局部描述符表)中索引描述符
- 为0,在GDT中进行索引
- 为1,在LDT中进行索引
- 3 ~ 15位,具体描述符的索引值,2^13=8192,故最多可以索引8192个段
我们知道
- 在实模式下,段地址并非真实的物理地址,在计算物理地址时,还要左移4位(乘以16),而在32位保护模式下,段地址是32位的线性地址,如果未开启分页功能,该线性地址就是物理地址。
- 所以在保护模式下,直接用选择子对应的“段描述符中的段基址”加上“段内偏移地址”就是要访问的内存地址
具体来说,例如选择子是 0x8(1000),将其加载到 ds寄存器后,访问ds:0x9这样的内存,其过程是
- 0x8的低2位是RPL,其值为 00。
- 第2位是TI,其值0,表示是在 GDT中索引段描述符。
- 0x8的高13 位0x1负责在 GDT中索引,也就是GDT中的第1个段描述符(GDT中第0个段描述符不可用)。
- 假设此时第1个段描述符中的3个段基址部分,其值为 0x1234。CPU将0x1234作为段基址,与段内偏移地址 0x9相加,0x1234+0x9=0x123d.用所得的和0x123d作为访存地址。
控制寄存器CR0
控制寄存器是CPU的窗口,既可以用来展示CPU的内部状态,也可以用于控制CPU的运行机制
其中CR0的PE位是保护模式的开关
- 为0时,表示在实模式下运行
- 为1时表示在保护模式下运行
进入保护模式
代码目录结构
|
|
代码
boot.inc
|
|
原书勘误,显存描述符应该为的最后应该是0x0b,而不是0x00
|
|
loader.S
|
|
mbr.S
由于loader程序已经超过了512字节,因此我们要把mbr.S代码中从磁盘加载loader的读入扇区数增大,原来是读入1个扇区,我们直接修改为读入4个扇区,如下所示
|
|
完整的mbr.S代码如下
|
|
编译
|
|
|
|
磁盘写入
注意loader.S写入磁盘时count参数为2
|
|
|
|
运行
- 左下角的字符串“2 loader inreal”是在实模式下用 BIOS 中断 0x10 打印的。
- 左上角第2行的字符P,这是在保护模式下输出的。一个程序历经两种模式,各模式下都打印了字符,为了区别实模式下的打印,所以字符串中含有“inreal”。
输出p说明GDT成功建立,如下调试所示
|
|