【操作系统】保护模式工作机理

实验目标

1. 理解x86架构下的段式内存管理

2. 掌握实模式和保护模式下段式寻址的组织方式、关键数据结构、代码组织方式

3. 掌握实模式与保护模式的切换

4. 掌握特权级的概念,以及不同特权之间的转移

5. 了解调用门、任务门的基本概念

实验内容

  1. 认真阅读章节资料,掌握什么是保护模式,弄清关键数据结构:GDT、descriptor、selector、GDTR, 及其之间关系,阅读pm.inc文件中数据结构以及含义,写出对宏Descriptor的分析

  1. 调试代码,/a/ 掌握从实模式到保护模式的基本方法,画出代码流程图,如果代码/a/中,第71行有dword前缀和没有前缀,编译出来的代码有区别么,为什么,请调试截图。

  1. 调试代码,/b/,掌握GDT的构造与切换,从保护模式切换回实模式方法。

  1. 调试代码,/c/,掌握LDT切换。

  1. 调试代码,/d/掌握一致代码段、非一致代码段、数据段的权限访问规则,掌握CPL、DPL、RPL之间关系,以及段间切换的基本方法。

  1. 调试代码,/e/掌握利用调用门进行特权级变换的转移。

实验过程

  1. 认真阅读章节资料,掌握什么是保护模式,弄清关键数据结构:GDT、descriptor、selector、GDTR, 及其之间关系,阅读pm.inc文件中数据结构以及含义,写出对宏Descriptor的分析

GDT即为Global Descriptor Table(全局描述符表),又叫段描述符表,为保护模式下的一个数据结构,其中包含多个descriptor,定义了段的起始地址、界限、属性等。

descriptor为段描述符,包括段基址、段界限、段属性。其结构如图:

selector为选择子,有其数据结构。在pmtest1.asm程序中,其作用就是便宜,对应描述符相对于GDT基址的偏移。

GDTR为GDT寄存器,结构与GdtPtr类似,6字节,前2字节是GDT界限,后4字节是GDT基地址。

关系:

GDT中包含多个descriptor,descriptor包含段的信息,包括段基址、界限、属性等。多个selector包含对应decriptor相对于GDT的偏移,所以selector发挥了类似指向descriptor的作用。而GDTR中包含了GDT的基址与界限。四者综合就可以获得某个descriptor的地址。而保护模式下寻址就先通过GDTR找到GDT,然后通过descriptor找到对应段的地址,然后加上段内偏移offset,就得到某个线性地址。

对宏descriptor的分析:

共8字节。从低地址开始前两字节为段界限1,然后三个字节为段基址1,然后两个字节byte5、byte6包含段属性以及段界限2,最后一个字节为段基址2。由于历史原因,段界限和段基址都分开存放。程序中descriptor由pm.inc中的宏descriptor生成。

代码:

%macro Descriptor 3

dw %2 & 0FFFFh ; 段界限1

dw %1 & 0FFFFh ; 段基址1

db (%1 >> 16) & 0FFh ; 段基址2

dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性1 + 段界限2 + 属性2

db (%1 >> 24) & 0FFh ; 段基址3

macro代表宏开始。宏名为Descriptor,3代表有三个参数。

参数1-3分别为段基址、界限、属性。

比如LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址

利用宏Descriptor定义了基址为0B8000h的段LABEL_DESC_VIDEO

0B8000h为显存首地址,利用该段在屏幕中显示数据。

第一行dw为两字节,%2 & 0FFFFh相当于取段界限的低位,写入这两字节

然后dw、dd取段基址1、2,构成三四节段基址,相当于上面结构图的段基址1

然后dw两字节构成段属性、段界限2

然后dw两字节构成段基址3

其中段基址为该段起始地址,界限为长度

  1. 调试代码,/a/ 掌握从实模式到保护模式的基本方法,画出代码流程图,如果代码/a/中,第71行有dword前缀和没有前缀,编译出来的代码有区别么,为什么,请调试截图。

流程:

(1)定义GDT[SECTION.gdt]

定义了一个空descriptor,一个32位代码段,一个显存descriptor

其中32位代码段只初始化了段界限、段属性

(2)进入[SECTION.s16]16位代码段(实模式)

修改GDT值:修改32位段描述符值

将LABEL_SEG_CODE32的物理地址(即[SECTION.s32]这个段的物理地址)赋给eax,然后把它分成三部分赋给描述符DESC_CODE32中的相应位置。由于DESC_CODE32的段界限和属性已经指定,所以至此,DESC_CODE32的初始化全部完成。

(将寄存器段界限属性由符合实模式要求到符合保护模式要求)

赋值gdtr寄存器:

把GDT的物理地址填充到了GdtPtr这个6字节的数据结构中。

lgdt[GdtPtr]将GdtPtr指示的6字节加载到寄存器gdtr

关中断:cli

打开A20地址线

修改cr0寄存器,将PE位置1

此时cs的值仍然是实模式下的值,把代码段的选择子装入cs:

(3)进入32位代码段[SECTION.s32]

进行屏幕显示操作

调试代码:

将程序编译为.com文件,使用dos运行

代码有dword前缀调试:

准备freedos.img(在此前的学习中获得)

bximage生成pm.img

修改bochsrc

用bochs格式化b盘

修改pmtest1.asm,org改为0100h,并编译为pmtest1.com

挂载pm.img,并将生成的pmtest1.com拷贝到软盘中

sudo mkdir /mnt/floppy
sudo mount -o loop pm.img /mnt/floppy
sudo cp pmtest1.com /mnt/floppy/
sudo umount /mnt/floppy

在dos下运行pmtest1.com,可以看到右侧出现一个红色的P

代码无dword前缀调试:

修改pmtest1.asm,删掉第71行的dword,另存为pmtestd.asm,编译为pmtestd.com

仿照有dword前缀的步骤在dos下运行

采用magic number进行调试在bochsrc中添加magic_break: enabled=1

在程序中添加命令xchg bx,bx

启动bochs,查看jmp指令前后寄存器的变化,进行比较

pmtest1.com:

pmtest1b.com:

比较发现,pmtest1.compmtest1b.com执行指令前后寄存器的值相同,所以dword不影响jmp跳转的地址结果

  1. 调试代码,/b/,掌握GDT的构造与切换,从保护模式切换回实模式方法

调试方式与结果如上,这个程序讲述的是从保护模式切换回实模式的方法。在之前的实验里我们了解了怎么进行从实模式到保护模式的切换,但在实验的结尾我们是以一个死循环结束,从输出也可以看出,他没有弹出一个新的DOS,而是打印完P之后就没有反应了。而在这个实验里,我们添加了一段代码,使得其在执行完之后可以返回到实模式,弹出一个新的DOS。

从这里开始,保护模式执行完成,返回到实模式中。

跳回实模式之后,程序重新设置各个寄存器的值,关闭A20,开中断。

我们看到,程序打印出两行数字,第一行全部是零,说明开始内存地址5MB处都是0,而下一行已经变成了41 42 43…,说明写操作成功。十六进制的41、42、43、…、48正是A、B、C、…、H。

同时看到,程序执行结束后不再像上一个程序那样进入死循环,而是重新出现了DOS 提示符。这说明我们重新回到了实模式下。

  1. 调试代码,/c/,掌握LDT切换

结果如上,编译、调试部分与pmtest1.compmtest2.com类似

转换到LDT的过程:

先由实模式跳转到GDT中的32位代码段[SECTION .s32](保护模式),然后在[SECTION .s32]中

因为SelectorLDTCodeA 的 TI 位为 1,所以系统从当前 LDT 寻找相应描述符。跳转到 LDT 中 descriptor 描述的段 [SECTION .la] 显示 L 后,然后 jmp SelectorCode16:0,跳回 GDT 中描述的 16 位代码段,然后返回实模式。其中 SelectorLDT 在 GDT 中定义,指向 LDT 地址。

[SECTION .s32] 第 217 行到第 220 行,指令 lldt,功能和 lgdt 也差不多, 负责加载 ldtr,它的操作数是一个选择子,这个选择子对应的就是用来描述 LDT 的那个描述符(标号 LABEL_DESC_LDT)。

本例用到的LDT 中只有一个描述符(标号 LABEL_LDT_DESC_CODEA 处),这个描述符跟 GDT 中的描述符没什么分别。选择子却不一样,多出了一个属性 SA_TIL。可以在 pm.inc 中找到它的定义:

SA_TIL EQU 4

SA_TIL 将选择子 SelectorLDTCodeA 的 TI 位置为 1。实际上,这一位便是区别 GDT 的选择子和 LDT 的选择子的关键所在。如果 TI 被置位,那么系统将从当前 LDT 中寻找相应描 述符。也就是说,当代码 pmtest3.asm中用到SelectorLDTCodeA 时,系统会从 LDT 中找到 LABEL_LDT_DESC_CODEA 描述符,并跳转到相应的段中。

这个 LDT 很简单,只有一个代码段。我们还可以在其中增加更多的段,比如数据段、堆栈段等,这样一来,我们可以把一个单独的任务所用到的所有东西封装在一个 LDT 中。

  1. 调试代码,/d/掌握一致代码段、非一致代码段、数据段的权限访问规则,掌握CPL、DPL、RPL之间关系,以及段间切换的基本方法

调试结果如下:

CPL

CPL是当前执行的程序或任务的特权级。在通常情况下,CPL等于代码所在的段的特权级。在遇到一致代码段时,情况稍稍有点特殊,一致代码段可以被相同或者更低特权级的代码访问。当处理器访问一个与CPL特权级不同的一致代码段时,CPL不会被改变。

DPL

DPL表示段或者门的特权级。它被存储在段描述符或者门描述符的DPL字段中,正如我们先前所看到的那样。当当前代码段试图访问一个段或者门时,DPL将会和CPL以及段或门选择子的RPL相比较,根据段或者门类型的不同,DPL将会被区别对待:

数据段:DPL规定了可以访问此段的最低特权级。比如,一个数据段的DPL是1,那么只有运行在CPL为0或者1的程序才有权访问它。

非一致代码段(不使用调用门的情况下):DPL规定访问此段的特权级。比如,一个非一致代码段的特权级为0,那么只有CPL为0的程序才可以访问它。

调用门:DPL规定了当前执行的程序或任务可以访问此调用门的最低特权级(这与数据段的规则是一致的)。

一致代码段和通过调用门访问的非一致代码段:DPL规定了访问此段的最高特权级。比如,一个一致代码段的DPL是2,那么CPL为0和1的程序将无法访问此段。

TSS:DPL规定了可以访问此TSS的最低特权级(这与数据段的规则是一致的)。

RPL

RPL是通过段选择子的第0位和第1位表现出来的。处理器通过检查RPL和CPL来确认一个访问请求是否合法。即便提出访问请求的段有足够的特权级,如果RPL不够也是不行的。操作系统过程往往用RPL来避免低特权级应用程序访问高特权级段内的数据。当操作系统过程(被调用过程)从一个应用程序(调用过程)接收到一个选择子时,将会把选择子的RPL设成调用者的特权级。于是,当操作系统用这个选择子去访问相应的段时,处理器将会用调用过程的特权级(已经被存到RPL中),而不是更高的操作系统过程的特权级(CPL)进行特权检验。这样,RPL就保证了操作系统不会越俎代庖地代表一个程序去访问一个段,除非这个程序本身是有权限的。

段间切换:

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

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

使用jmp 或call指令可以实现下列 4 种转移:

(1)目标操作数包含目标代码段的段选择子。

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

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

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

  1. 调试代码,/e/掌握利用调用门进行特权级变换的转移的基本方法

调试结果如下图:

这个阶段的实验目的为利用门进行有特权级变换的转移。在分析代码之后我们发现出现了一个新的段ring3。

先来看看ring3的定义,高亮部分的代码很明显是打印一个‘3’字符,并使得程序停止。

定义过后,依次将ss,esp,cs,eip压栈,执行retf指令。

我们看到了一个红色的3,说明我们成功进入了ring3!

成功进入ring3之后,我们来试验一下调用门的使用。

在此之前别忘了TSS

准备完成后,在进行特权级变换之前加载TSS。

运行之后,我们可以看到在字符‘3’之前是存在其他字符的,这就意味着在ring3下对门的调用也是成功的。

最后,我们将调用局部任务的代码加入到调用门的目标代码,使得我们的程序能成功返回实模式,这也就是最终结果上显示的3个字符的原因。

总结

写了实验报告里自己负责的部分

做实验真不容易啊,心累QAQ