一个简单的32位多任务操作系统的实现(2)
2.保护模式简述
最早的Intel系列的CPU只存在一种操作模式,即现在所说的实模式(Real Mode,以下简称RM)。在Intel推出80286之后,为了增强CPU的处理能力,同时也为了适应当时的软件开发需求,Intel提出了保护模式(Protected Mode,以下简称PM),但在80286下的PM由于CPU本身设计的问题,并没有使其发挥出很大的功效。在80386推出之后,Intel完善了CPU的设计形成了最终的IA-32架构,并提出了另一种模式系统管理模式(System Management Mode)。本章我们的讨论就围绕着这三种模式进行展开,并重点讨论PM.
首先,对这三种模式做一简单概述。
RM:此模式是主机在加电或复位后自动进入的模式,在此模式下其可以执行16位指令,并可以切换到PM或者SMM。
PM:在此模式下,CPU能够支持其自身的32位特性,使自身处于最高性能表现。
这些特性主要包括:
1. 最大可访问4GB内存空间。事实上,在RM下通过一些未公开的特性,也可以达到同样的效果,但其对于代码段 和堆栈空间却是无效的。况且后面的所有特性都是基于PM的,对RM没有效果。
2. 虚拟存储。处于PM下的CPU其内存管理单元(MMU)支持这项特性。前面我已经说到,在PM下CPU最大寻址空 间为4GB,而在实际中,我们并没有如此大的物理内存空间。因此通过MMU,可以将外存设备(如:硬盘)的一部分 空间模拟成物理内存进行使用。
3. 地址映射。即MMU可以在地址使用前对其进行转换,即所谓的映射。
4. 改进的分段机制。本文后面将对此进行重点论述。
5. 内存保护与任务保护。即在PM状态下,引入了权限机制。通过权限控制可以达到保护相关代码和数据的目的。
6. 改进的寻址模式。在RM下,只有常数,BX或BP,SI或DI可以用来形成地址,而在RM下可以通过任意寄存器进行寻 址,并且可以包含一个为2,4或8的比例因子。
7. 多任务支持。在PM下,CPU提供了特殊的机制能够进行快速的上下文切换。
SMM:该模式为操作系统实现特定平台指定的功能提供了一种有效的机制。
值得注意的是,在PM下,CPU允许在受保护的情况下,执行RM程序,这个特性被称为虚拟8086模式(Virtual-8086 Mode),但其本质上却不是真正的RM。
对于三种模式关系的形象解释可以通过下图来描绘:
正如上面所说的,只有在PM下,CPU才能充分发挥其自身的所有特性,而计算机在启动之后,默认的CPU操作模式却是RM。因此摆在我们面前的一个主要问题就是如何在RM与PM之间相互切换。那么如何在RM和PM之间相互切换呢。核心步骤其实很简单,只要改变CPU中的CR0寄存器中PE标志位的值,就可以实现。在PE=1时,CPU进入PM,而在PE = 0时,则进入RM.但这仅仅是整个切换过程中的一小部分,在进入保护模式之前我们还需要做很多事情,其中最关键的就是建立好一个被称为GDT的表。
在谈到GDT之前,我们先回顾一下,在RM中,内存中寻址的方式---段:偏移量。其中段(Segment)表明了一个基地址,其最大长度固定为64KB(FFFFH),即16bit数所能表示的最大数值。而偏移量(Offset),就是指在指定段内的位置。由此可见,通过段+偏移量这种表示方式,就可以表示出内存中的绝对地址。需要指出的是,在CPU实际处理过程中,CPU会将段寄存器的 值左移动4位,再与偏移量相加,形成地址,放入20位的总线当中。
在PM中,对于段模式来讲,上面的寻址方式,在大部分上仍然是适用的。但由于PM是工作在32位下的,因此上面的各个值,也就都相应的变成了32位。与RM不同的是,在PM下,一个段的长度不再固定,其可以在CPU允许的规则下任意设置.并且CPU为段模式提供了保护机制,即增加了对自身的访问权限.因此在PM下,对于一个段,需要有三个变量给于描述,即基地址,段界限和访问权限.
事实上,CPU将这三个值保存为一个64位长的段描述符.但出于兼容性的考虑,Intel并没有将段寄存器改为64位可用--虽然,段寄存器在事实上确是64位,但对于程序来讲,高于16位的部分却是不可见的--因此,我们需要另一种方法去存放这些数据.Intel选择了将这些段描述符统统存入到一个全局数组中的方法,在访问段时,向相应的段寄存器填入该数组的下标值来实现间接引用。这个全局数组就称其为GDT(全局描述符表).由于GDT可以存放在内存中的任何位置,因此要引用它,就必须知道他的入口地址.Intel为我们提供了GDTR寄存器和LGDT指令.其中GDTR寄存器存放的是GDT的入口地址(32位)和其界限(16位),共48位.这里的入口地址是一个线性地址,界限则是表的字节长度减一.可见该表最多可以长达64KB,存储8192条描述符号,而LGDT指令的作用就是将GDT装载到放入GDTR寄存器当中.
顾名思义,GDT是全局描述符,因此其在内存中存在且仅存在一个,并且它的存在对于所有的任务来讲,都是可见的.显然,这种做法对于多任务来讲是不易管理的.因此,Intel又引入了LDT(局部描述符表),该描述符与GDT不同之处在于,LDT在系统中可以有许多个,但每个任务只允许有一个LDT,且其只能该任务自身可见.其与GDT的主要关系在于,每一个LDT都会作为一个段,存入GDT中.由于CPU在任何时刻只能执行一个任务的代码,因此存储LDT所需要的寄存器也就只需要一个,Intel将其命名为LDTR,与GDT相同,Intel为装入LDT设置了LLDT指令。与GDT不同的是,LLDT指令的操作数却是一个16位的段选择子,即前面说到的要装入的LDT在GDT中的索引值。这里需要指出的是,LDT并不是必须的,你的程序可以选择使用,或者不使用它。
前面提到了一个新概念--段选择子。我们说段选择子是要引用段在GDT或LDT中的索引值,其实这种说法并不正确。因为段选择子除了含有索引值以外,它还包含了其他内容。
段选择子的结构如下图:
段选择子是一个16位的数据结构,其包含了三部分内容。其中,其高13位正是前面所说的索引值,TI用来指定是在GDT中索引,还是在LDT中索引(0 = GDT, 1 = LDT),RPL则是用来指明请求特权级的。
谈到这里,我们就已经阐明了在PM的段模式下,如何引用一个内存地址。首先,将段选择子装入相应的段寄存器中,然后CPU会自动根据段选择子找到相应的段描述符,并找出基地址,最后在加上偏移量,就得到了所需要的内存地址。
在本文开始的部分,我已经说过GDT是进入 PM所必需的数据结构,下面就详细的来讨论一下如何设置好GDT,并将其装入相应的寄存器.
首先必须注意的两点是:
1.GDT中的第一个描述符必须是空,即全为0。在程序中这个描述符不能用来进行内存访问,否则将产生General Protection异常。
2.由于GDT中的描述符都是64位长,因此为了让CPU的访问速度达到最快,需要将GDT的入口地址以8字节对齐,即放入8的倍数的位置.
下面,开始设置进入PM后的代码段和数据段的描述符.
其格式如下:
G – 粒度
D/B – 大小(0 = 16位段; 1 = 32位段)
D – 保留
AVL – 用户定义
P – 段是否存在
DPL – 描述符特权级
注意P位,这个位确定了段是否存在.这是什么意思呢.当该位被清除时,如果存在任务要访问这个段.那么CPU会产生一个错误,并会从外存(如:硬盘)中调入该段并再次尝试.当该位被清除时,描述符中的0到39位和48到63位能够包含任意值.你也可以用这些空间来存储该段在磁盘空间中的地址.
还有就是A位,CPU会在对其所在段写入数据后,将该位置1,这样在做段的磁盘交换时,可以决定是否将该段写入磁盘.
下面要说的就是G位.你会发现,在描述符中段界限仅仅为20位.那么其如何能够设置成1MB到4GB之间的范围呢.这里G位其了重要的作用.当G位被清零时,界限域就是段的最大合法偏移.而如果G位被置成1,那么会把描述符中的段界限左移12位形成32位界限,再将低12位全部填1.这样,实际上就能够指定1MB以下的任意长度,和以4K到4GB为单位的长度.
假定要在进入PM后,使代码段和数据段能够访问全部线性空间,于是可将GDT设置为:
gdt dd 00000000h, 00000000h ;空
gdt.Code32 dd 0000ffffh, 00cf9a00h ;代码段 读/执行 4GB空间 基地址 = 0 粒度 = 4096,386
gdt.Data32 dd 0000ffffh, 00cf9200h ;数据段 读/写 4GB空间 基地址 = 0 粒度 = 4096,386
这里你会发现在GDT中不同的描述符指向了同一块内存空间.这在系统中是允许的.在实际应用中,这也是经常要使用到的,例如:操作系统可以 将一个可执行文件装入数据段,然后再从同一位置开始执行.
在设置好GDT以后,需要将其装入相应的寄存器中.前面说过GDTR寄存器包含两段内容,因此我们需要先算出GDT的绝对物理地址.
GDTR的具体内容
gdtr dw gdtr - gdt – 1;界限
dd gdt;前面GDT的地址
实现代码如下:
mov eax, ds
shl eax, 4
add [gdtr+2], eax ; 生成绝对物理地址
lgdt [gdtr] ; 将gdtr装入寄存器
到此,就完成了进入保护模式的最主要工作,可以进入PM模式了.
实现代码如下:
mov eax, cr0
or al, 1
mov cr0, eax ; 修改CR0寄存器,置PE = 1
jmp dword 8:_premain32 ; 8为选择子
你可能会问为什么要在代码的最后添加一个jmp语句.这是因为,我们必须要清除CPU的指令预取队列(流水线),并以此来设置CS段寄存器. 不过这仅仅是其一,还有一个重要的原因就是,我前面谈到的那个段寄存器大于16位的不可见部分.需要指出的是,Intel对于这一点是未公开的,因此我下面对该问题的讨论仅仅是由推断得出来的。 事实上,当我们执行一条装载CS寄存器的指令时,操作数被装入了寄存器的可见部分,而CPU会自动根据操作数,去设置其不可见部分.CS段寄存器所处的状态与当前在哪个模式下并无关系。 在刚刚进PM后,CS仍然认为当前处于16位段,即当前的地址仍是16位地址。因此,必须通过 装载一条32位指令去切换到32位段,这也就是jmp在这里的意义.
虽然程序已经进入了PM,但需要做的事情还远没有做完,因为我们还没有配置IDT,即中断描述符表,而要理解这个表又要牵涉到许多内容,因此,我将在后面的文章中详细介绍,这里就不多谈了。
Intel之所以将这个模式起名为保护模式,其来源就在于特权保护。在PM下,每一个任务都拥有自己的特权级(PL)。Intel将其分为四个级别,由零到三。数字越低级别则越高。例如:PL3级的程序对于一些特定的指令,如:HLT,LGDT,LLDT等,没有执行的权限,并且其也不允许访问拥有高特权级程序的数据。
在PM下,I/O的访问同样也受到了特权保护。在EFLAGS中存在一个IOPL域。这个域的值决定了能够执行I/O操作的最低权限。例如,当IOPL为3的时候,表明所有特权级的程序都能够执行I/O操作。这个域的值仅允许PL0级的程序进行修改,其他级的程序修改无效。
同样,数据访问在PM下也是受到保护的。当数据段寄存器要被加载时,CPU会将该段的描述符特权级(DPL)与一个被称为有效特权级(EPL)的数值进行比较,如果DPL不小于EPL,则允许装载寄存器,否则将会产生一个错误。这里的EPL就是选择子的RPL和程序当前特权级(CPL)的数值较大的那一个。
对于堆栈,则有些不同,访问SS寄存器,其DPL要求必须和CPL相等。
关于保护模式,本篇文章就介绍到的这里。对于保护模式的其他重要特性,如:分页操作,多任务处理等,由于内容很多,几乎每一块内容都能当成一个专题来讲,因此我将在以后的文章中对此进行详细讨论。