《一个操作系统的实现》(三):2.保护模式进阶

在之前的代码中,程序从实模式跳到保护模式之后就开始死循环。这节会实现在程序结束时跳回实模式,也会实现对大地址内存的读写。

新建的段以5MB为基址。而且每增加段都要用Descriptor描述它的段基址、段界限、属性,而且还要加上对应的选择子,比如如下两行:

LABEL_DESC_DATA:    Descriptor 0, DataLen-1, DA_DRW    ;Data

SelectorData    equ    LABEL_DESC_DATA - LABEL_GDT

需要注意的是:在程序的整个执行过程中,edi始终指向要显示的下一个字符的位置。所以如果程序中除显示字符外还用到edi,需要事先保存它的值,以免在显示时出现混乱。

保护模式跳回到实模式比实模式跳转到保护模式复杂一些。在准备结束保护模式回到实模式之前,需要加载一个合适的描述符选择子到有关段寄存器,以使对应段描述符高速缓冲寄存器中含有合适的段界限和属性。而且不能从32位代码段返回实模式,只能从16位代码段中返回。这是因为无法实现从32位代码段返回时cs高速缓冲寄存器中的属性符合实模式的要求(实模式不能改变段属性)。

若要跳回到实模式,则段寄存器需要修改指向(将SelectorNormal加载到各段寄存器,然后清cr0的PE位,给跳转回实模式的指令指定正确的段地址。这样就可以跳回实模式了。但是还没有完,程序还需要重新设置各个段寄存器的值,恢复sp的值,关闭A20,打开中断,然后就OK了。

编译运行整个程序~

nasm pmtest2.asm -o pmtest2.com

bochs -f bochsrc

然后输入c跑起来,然后又要开另一个终端了:

sudo mount -o loop pm.img /mnt/floppy

sudo cp pmtest2.com /mnt/floppy/

sudo umount /mnt/floppy

然后到Bochs中执行b:\pmtest2.com,得到了预期结果~


这节还讲了一下LDT,与GDT的区别就在于LDT是局部的(Local),而GDT是全局的(Global)。

它们两个的选择子一大区别就是TI位置,LDT为1而GDT为0。lldt是针对LDT的,lgdt针对GDT,它们均负责加载描述符,操作数是对应描述LDT的描述符的选择子(这句话有点绕- -)

而且GDT中需要添加描述LDT的描述符。

以后可以在LDT中添加更多段(数据段、堆栈段等),这样可以把一个单独的任务所用到的所有东西封装在一个LDT中

增加一个用LDT描述的简单任务的步骤:

1.增加一个32位代码段

2.增加一个段,内容是一个描述符表(LDT),可以只有一个代码段描述符,也可以添加更多的描述符描述更多的段,涉及的选择子的TI位应该为1.

3.在GDT中增加一个描述符,用以描述这个LDT,同时要定义其描述符。

4.增加新添的描述符的初始化代码主要针对段基址。

5.用新加的LDT描述的局部任务准备完毕。

6.使用前用lldt指令加载ldtr,用jmp指令跳转等方式运行。


下面对保护模式中的“保护”做更深的了解。描述符中段基址和段界限定义了一个段的范围,对超越段界限之外的地址的访问是被禁止的;另一方面有点复杂的段属性作为对一个段各方面的定义规定和限制了段的行为和性质,从功能上来讲也是一种保护。

关于特权级——

IA32的分段机制中,从高到低有0,1,2,3四个特权级,其中内核为level0,服务为level1和2,应用程序为level3。处理器通过识别CPL、DPL、RPL这三种特权级进行特权级检验。

CPL(current)为当前执行的程序或任务的特权级,存储在cs和ss的第0位和第1位上,CPL随着程序转移到不同特权级代码段而被处理器改变。但一致代码段可以被相同或更低特权级的代码访问,所以当处理器访问一个与CPL特权级不同的一致代码段时,CPL不会被改变。

DPL(descriptor)为段或门的特权级。对于数据段、调用门、TSS(任务状态段,task state segment),DPL规定可访问此段的最低等级;对于不使用调用门情况下的非一致代码段,DPL规定可访问此段的特权级(仅一个等级,不存在最低或最高等级);对于一致代码段和通过调用门访问的非一致代码段,DPL规定访问此段的最高特权级。

RPL(requested)通过段选择子的第0位和第1位表现出来。处理器通过检查RPL和CPL确认一个访问请求是否合法,即RPL和CPL同时都必须有足够特权级。操作系统过程通常用RPL避免低特权级应用程序访问高特权级段内的数据。

程序从一个代码段转移到另一个代码段之前,目标代码段的选择子会被加载到cs中。作为加载过程的一部分,处理器会检查描述符的界限、类型、特权级等内容。如果检验成功,cs将被加载,程序控制将转移到新的代码段中,从eip指示的位置开始执行。

指令jmp、call、ret、sysenter、sysexit、int n或iret或者中断和异常机制可引起程序控制转移发生。

jmp或call指令实现的4种转移:

1.目标操作数包含目标代码段的段选择子(直接转移,下面三种均为通过某描述符的间接转移)

2.目标操作数指向一个包含目标代码段选择子的调用门描述符

3.目标操作数指向一个包含目标代码段选择子的TSS

4.目标操作数指向一个任务门,这个任务门指向一个包含目标代码段选择子的TSS

上面也讲到转移是有限制的,要判断特权级是否符合。如果要自由在不同特权级之间进行转移,则要使用门描述符或者TSS

其实门也是一种描述符,定义入口地址的偏移,属性,选择子。直观地看,一个门描述了由一个选择子和一个偏移所指定的线性地址,程序正是通过这个地址进行转移的。门描述符共四种:调用门、中断门、陷阱门、任务门。调用门本质上是一个增加了若干属性的入口地址。下表为调用门的特权级规则,其中DPL_G为调用门的DPL,DPL_B为转移目的的DPL:

调用门特权级规则
  call jmp
目标是一致代码段 CPL≤DPL_G,
RPL≤DPL_G,
DPL_B≤CPL
同call
目标是非一致代码段 CPL≤DPL_G,
RPL≤DPL_G,
DPL_B≤CPL
CPL≤DPL_G,
RPL≤DPL_G,
DPL_B=CPL
可以总结得出,通过调用门和call指令,可以实现从低特权级到高特权级的转移,无论目标代码段是否一致。

特权级变化时堆栈也要变化,这种机制避免了高特权级的过程由于栈空间不足而崩溃,而且如果不同特权级共享同一个堆栈的话,高特权级的程序可能因此受到有意或无意的干扰。

关于jmp和call的段内和段间跳转:对于jmp,长调用和短调用只是结果不同;而对于call,长调用和短调用对堆栈的影响不同。因为返回时需要调用者的cs,所以call指令执行时被压栈的除了eip还应该有cs。

Intel提供复制堆栈内容的机制。由于每一个任务最多都可能在4个特权级间转移,所以每个任务实际上需要4个堆栈。堆栈切换时,我们可以从TSS中获得其余堆栈的ss和esp。书上称TSS为Task-State Stack,但是网上TSS的全称为Task State Segment,任务状态段,除了涉及本书的内容之外也没有“任务状态栈”这个东西。但是只有在低特权级向高特权级切换时,新堆栈才从TSS中取得。整个的转移过程CPU所做工作如下:

1.根据目标代码段的DPL(新的CPL)从TTS中选择应该切换至哪个ss和esp。

2.从TSS中读取新的ss和esp。在这过程中如果发现ss、esp或者TSS界限错误都会导致无效TSS异常(#TS)。

3.对ss描述符进行检验,如果发生错误,同样产生#TS异常。

4.暂时保存当前ss和esp的值。

5.加载新的ss和esp。

6.将刚保存起来的ss和esp的值压入新栈。

7.从调用者堆栈中将参数复制到被调用者堆栈(新堆栈)中,复制参数的数目由调用门中Param Count一项来决定。如果Param Count是0,则不会复制参数。

8.将当前的cs和eip压栈。

9.加载调用门中指定的新的cs和eip,开始执行被调用者过程。

如果参数多于Param Count的位数,则可以让其中的某个参数变成指向一个数据结构的指针,或者通过保存在新堆栈里的ss和esp来访问旧堆栈中的参数。

ret基本上是call的反过程,只是带参数的ret指令会同时释放事先被压栈的参数。事实上,ret不仅可以实现短返回和长返回,而且可以实现带有特权级变换的长返回。通过ret指令可以实现由高特权级到低特权级的转移。由被调用者到调用者的返回过程中,处理器的工作包含以下步骤:

1.检查保存的cs中的RPL以判断返回时是否要变换特权级。

2.加载被调用者堆栈上的cs和eip(此时会进行代码段描述符和选择子类型和特权级检验)。

3.如果ret指令含有参数,则增加esp的值以跳过参数,然后esp将指向被保存过的调用者ss和esp。注意,ret的参数必须对应调用门中的Param Count的值。

4.加载ss和esp,切换到调用者堆栈,被调用者的ss和esp被丢弃。在这里将会进行ss描述符、esp以及ss段描述符的检验。

5.如果ret指令含有参数,增加esp的值以跳过参数(此时已经在调用者堆栈中)。

6.检查ds、es、fs、gs的值,如果其中哪一个寄存器指向的段的DPL小于CPL(此规则不适用于一致代码段),那么一个空描述符会被加载到该寄存器。


本节到此结束~~

你可能感兴趣的:(操作系统)