oslab oranges 一个操做系统的实现 实验二 认识保护模式

 https://github.com/yyu/osfs00git

实验目的:github

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

掌握实模式和保护模式下段式寻址的组织方式、数组

关键数据结构、代码组织方式数据结构

掌握实模式与保护模式的切换架构

掌握特权级的概念,以及不一样特权之间的转移ide

 

实验内容:函数

1. 认真阅读章节资料,掌握什么是保护模式,弄清关键数据结构:oop

GDT、descriptor、selector、GDTR, 及其之间关系,阅读测试

pm.inc文件中数据结构以及含义,写出对宏Descriptor的分析

2. 调试代码,/a/ 掌握从实模式到保护模式的基本方法,画出代码

流程图,若是代码/a/中,第71行有dword前缀和没有前缀,编

译出来的代码有区别么,为何,请调试截图。

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

模式方法

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

5. 调试代码,/d/掌握一致代码段、非一致代码段、数据段的权限

访问规则,掌握CPL、DPL、RPL之间关系,以及段间切换的基

本方法

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

代码对应iso中chapter3 

 

实验解决问题与课后动手改:

1. GDT、Descriptor、Selector、GDTR结构,及其含义是什么?他

们的关联关系如何?pm.inc所定义的宏怎么使用?

2. 从实模式到保护模式,关键步骤有哪些?为何要关中断?为

什么要打开A20地址线?从保护模式切换回实模式,又须要哪些

步骤?

3. 解释不一样权限代码的切换原理,call, jmp,retf使用场景如何,

可以互换吗?

4. 课后动手改:

1. 自定义添加1个GDT代码段、1个LDT代码段,GDT段内要对一个内

存数据结构写入一段字符串,而后LDT段内代码段功能为读取并打

印该GDT的内容;

2. 自定义2个GDT代码段A、B,分属于不一样特权级,功能自定义,要

求实现A-->B的跳转,以及B-->A的跳转。

 

 

实验环境:

 

VMwareWorkstationPro 15.5.0

 

Ubuntu 12.04.5 desktop i386 32位

 

bochs 2.6.9

 

 

 

关键技术:

 

  1. bochs使用
  2. 实模式,保护模式及其关键数据结构GDT,LDT,Descriptor、Selector等
  3. 特权级变换

 

 

 

实验步骤:

 

1.认真阅读章节资料,掌握什么是保护模式,弄清关键数据结构:

 

GDT、descriptor、selector、GDTR, 及其之间关系,阅读

 

pm.inc文件中数据结构以及含义,写出对宏Descriptor的分析

 

 

 

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

 

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

 

 

 

 

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

 

 

 

 

GDTR为GDT寄存器。结构与GDTPTR相似,6字节,前两字节GDT界限,后4字节GDT基地址。

 

 

 

四者关系:

GDT中包含多个descriptor,descriptor包含段的信息,包含段基址,界限属性等。多个selector包含对应descriptor相对于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 ;macro定义宏。 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%endmacro ; 字节

 

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.

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

 

2. 调试代码,/a/ 掌握从实模式到保护模式的基本方法,画出代码

流程图,若是代码/a/中,第71行有dword前缀和没有前缀,编

译出来的代码有区别么,为何,请调试截图。

 

流程图:pmtest1.asm 用文字描述以下

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

以后关中断。

以后打开A20地址线。

修改cr0寄存器:PE位置1。

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

jmp dword SelectorCode32:0 ,进入32位代码段[SECTION .s32]

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

进行屏幕显示操做。

 

调试代码a:

将程序编译为.com文件,使用dos运行。(由于引导扇区只有512字节,程序高于512字节就不方便了)

代码a有dword前缀调试:

(1)准备freedocs.img

(2)bximage生成pm.img

(3)修改bochs

 

 

重点是

floppya: 1_44=freedos.img, status=inserted

floppyb: 1_44=pm.img, status=inserted

boot: a

 

(1)bochs格式化B盘

Sudo bochs

dos format b:

 

 

(5)修改pmtest1,org改成0100h,并编译为pmtest1.com

 

 

 

 

 

(6)pmtest1.com复制到pm.img

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

会出现了错误

mount point /mnt/floppy does not exist

先建立文件夹

 

 

而后
sudo losetup /dev/loop0 pm.img  建立loop设备,而后操做loop设备,就是对pm.img数据的操做了

 

 

sudo mount /dev/loop0/ /mnt/floppy loop设备挂载到/mnt/floppy上

 

 

而后

 sudo cp pmtest1.com /mnt/floppy/ 赋值

而后卸载

sudo umount /mnt/floppy/

 

 

以后再作一次遇到问题

 

 

 

解决,卸载

 

 

 

 另外发现了 sudo cp pmtest2.com /mnt/floppy/ 赋值并非覆盖。也就是说cp了先cp了pmtest1.com,而后不格式化(format b:),直接cp  pmtest2.com,那么两个程序均可以运行。

(7)dos下运行pmtest1.com

Sudo bochs

B:\pmtest1.com 运行

可见右侧出现一个红色的P

 

 

代码a无dword前缀调试:

(1)修改pmtest1.asm,删掉第71行的dword,存为pmtestd.asm,并编译为pmtestd.com

 

(2)dos运行

陷入循环而且无红色的P在屏幕右侧

 

 

失败缘由:

jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs,  //selector16位,dword两字节,高位selector,低位偏移0.(由于声明了这段是16位代码,因此一个字两字节)
; 并跳转到 Code32Selector:0 处

删除dword 后只有16位。cs寄存器没有正确设置,没有跳转到32位代码段,故显示失败

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

模式方法

分析:pmtest2.asm

在前面程序的基础上,新建一个段,这个段以5MB为基址,远远超出实模式下1MB的界限。咱们

先读出开始处8字节的内容,而后写入一个字符串,再从中读出8字节。若是读写成功的话,两次读出的内容应该是不一样的,并且第

二次读出的内容应该是咱们写进的字符串。字符串是保存在数据段中的,也是新增长的。

1)LABEL_DESC_STACK:  Descriptor 为全局堆栈段[SECTION .gs]的descriptor,初始化在[SECTION .gs]和[SECTION.16]完成。Descriptor属性为DA_DRWA+DA_32,DA_32代表是32位堆栈段。

2)LABEL_DESC_DATA:Descriptor 为[SECTION .data1]  ; 数据段的descriptor,初始化在[SECTION .data1] 完成,其中包含了要写入的字符串

3)LABEL_DESC_CODE32: Descriptor 32位代码段(保护模式)[SECTION .s32]. 由实模式跳入.

[SECTION .s32]中咱们改变了ss和esp(代码3.5第174行到177行),这样,在32位代码段中全部的堆栈操做将会在新增的 堆栈段中进行。

这个段的开头初始化了ds、es和gs,让ds指向新增的数据段,es指向新增的5MB内存 的段,gs指向显存(第167行到第172行)。接着显示一行字符串,以后就开始读写大地址内存了(第198行到第200行)。因为要读 两次相同的内存,咱们把读的过程写进一个函数TestRead,写内存的内容也写进函数TestWrite,这两个函数的入口分别在第206行 和第222行。能够看到,在TestRead中还调用了DispAL和DispReturn这两个函数(第253行和第286行),DispAL将al中的字节用十 六进制数形式显示出来,字的前景色仍然是红色;DispReturn模拟一个回车的显示,其实是让下一个字符显示在下一行的开头 处。要注意的一个细节是,在程序的整个执行过程当中,edi始终指向要显示的下一个字符的位置。因此,若是程序中除显示字符外 还用到edi,须要事先保存它的值,以避免在显示时产生混乱。

4)保护模式中字符串寻址:TestWrite中用到一个常量OffsetStrTest,它的定义在代码3.4第47行。注意,咱们用到这个字符串的时候并无用直接标 号StrTest,而是又定义了一个符号OffsetStrTest,它等于StrTest-$$。$$的含义表明当前 节(section)开始处的地址。因此StrTest-$$表示字符串StrTest相对于本节的开始处(即LABEL_DATA处)的偏移。容易发现数据段的基址即是LABEL_DATA的物理地址。因而OffsetStrTest既是字符串相对LABEL_DATA的偏移,也是其在数据段中的偏移。咱们在保护模式下须要用到的正是这个偏移,而再也不是实模式下的地址。前文中提到过的section的一点妙用指 的即是这里的$$,它不是没有替代品,而是这样作思路会比较清晰。OffsetPMMessage的情形与此相似。

6)返回实模式

概述:

先回忆开中断:加载寄存器,以后关中断。以后打开A20地址线。修改cr0寄存器:PE位置1。此时cs的值仍然是实模式下的值,把代码段的选择子装入cs(修改段界限,段属性。)

 

关中断差很少就是完成上述的逆向操做:

加载一个合适的描述符选择子到有关段寄存器,以使对应段描述符高速缓冲寄存器中含有合适的段界限和属性,从新设置各个段寄存器的值,好比cr0PE位置0.恢复sp(堆栈指针寄存器)的值,修改段界限,段属性,而后关闭A20,打开中断,从新回到原来的样子。

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

为了能从保护模式恢复实模式的寄存器,须要先保存到系统本身的堆栈段。在[SECTION.16]中完成。

mov sp, 0100h

...

而后32位代码段的操做在自定义的堆栈段[SECTION .STACK]完成。两者互不干扰,方便了恢复。

 

 

详述:

从实模式进入保护模式时直接用一个跳转就能够了,可是返回的时候却稍稍复杂一些。由于在准备结束保护模式回到实模 式以前,须要加载一个合适的描述符选择子到有关段寄存器,以使对应段描述符高速缓冲寄存器中含有合适的段界限和属性。而 且,咱们不能从32位代码段返回实模式,只能从16位代码段中返回这是由于没法实现从32位代码段返回时cs高速缓冲寄存器中的 属性符合实模式的要求(实模式不能改变段属性)。

因此,在这里,咱们新增一个Normal描述符(代码3.4第15行)。在返回实模式以前把对应选择子SelectorNormal加载到ds、 es和ss,就是上面所说的这个缘由。

LABEL_DESC_NORMAL: Descriptor对应选择子SelectorNormal。对应段 [SECTION .s16code],16 位代码段. 由 32 位代码段跳入, 跳出后到实模式。

这个段是由[SECTION .s32]中的jmp SelectorCode16:0跳进来的。开头的语句把 SelectorNormal赋给ds、es、fs、gs和ss,完成咱们刚刚提到的使命。而后就清cr0的PE位,接下来的跳转看上去好像不太对,因 为段地址是0。其实这里只是暂时这样写罢了,在程序的一开始处能够看到代码3.8中的这几句。

67 mov ax, cs

...

73 mov [LABEL_GO_BACK_TO_REAL+3], ax

mov [LABEL_GO_BACK_TO_REAL+3], ax的做用就是为回到实模式的这个跳转指令指定正确的段地址,这条指令的机器码如图3.9 所示。 

 

 

3.9告诉咱们,LABEL_GO_BACK_TO_REAL+3刚好就是Segment的地址,而第73行执行以前ax的值已是实模式下的cs(咱们记 作cs_real_mode)了,因此它将把cs保存到Segment的位置,等到jmp指令执行时,它已经再也不是:

jmp 0:LABEL_REAL_ENTRY

而变成了:

jmp cs_real_mode:LABEL_REAL_ENTRY

它将跳转到标号LABEL_REAL_ENTRY处。

在跳回实模式以后,程序从新设置各个段寄存器的值,恢复sp的值,而后关闭A20,打开中断,从新回到原来的样子

144 LABEL_REAL_ENTRY: ; 从保护模式跳回到实模式就到了这里

...

159 int 21h ; / 回到 DOS

 

调试:

编译pmtest2.asm为pmtest2.com

bochs dos 下运行

 

 

 

 

第一行为开始内存5MB处全是零。而后写入了41,42,...48,也就是16进制的A,B,C,D...H,在代码pmtest2.asm中DATA段的写入的str。

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

DOS。

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

分析:

LDT与GDT都是描述符table,L表明Local,局部。简单来讲,LDT是一种描述符表,与GDT差很少,只不过它的选择子的TI位必 须置为1。在运用它时,须要先用lldt指令加载ldtr,lldt的操做数selector是GDT中用来描述LDT的描述符。(也就是说LDT至关于GDT中描述的一个段,对应有特殊的寄存器ldtr,而该段中又有一些描述符描述一些LDT段,只属于这个LDT。)

 

pmtest3.asm中增长了两个节[SCTION .ldt][SECTION .la]。(原来有omtest2.asm中的各个段)。其中[SCTION .ldt]在GDT中有对应的descriptor和selector  LABEL_DESC_LDT: 。而[SECTION .la]是LDT描述的段,在GDT无定义。

[SCTION .ldt]是增长的LDT,其中有一个descriptor,对应[SECTION .la]。

[SECTION .la]中包含显示的字符L,在屏幕显示。实现时调用了GDT中 的SelectorVideo。

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

mov ax, SelectorLDT

lldt ax

加载ldtr(成为当前LDTR),

而后。jmp SelectorLDTCodeA。由于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

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

 

这个LDT很简单,只有一个代码段。咱们还能够在其中增长更多的段,好比数据段、堆栈段等,这样一来,咱们能够把一个单独的任务所用到的全部东西封装在一个LDT中。

 

经过几个简单的例子,咱们对IA32的分段机制大体已经有所了解了。“保护模式”中“保护”二字究竟是什么含义? 在描述符中段基址和段界限定义了一个段的范围,对超越段界限以外的地址的访问是被禁止的,这无疑是对段的一种保护。另外,有点复杂的段属性做为对一个段各个方面的定义规定和限制了段的行为和性质,从功能上来说,这仍然是一种保护。

 

调试:

编译pmtest3.asm为pmtest3.com,在dos运行

 

 

5. 调试代码,/d/掌握一致代码段、非一致代码段、数据段的权限

访问规则,掌握CPL、DPL、RPL之间关系,以及段间切换的基

本方法

分析:

(1)特权级

IA32的分段机制中,特权级总共有4个特权级别,从高到低分别是0、一、二、3。数字越小表示的特权级越大,较为核心的代码和数据,将被放在特权级较高的层级中。处理器将用这样的机制来避免低特权级的任务在不被 容许的状况下访问位于高特权级的段。若是处理器检测到一个访问请求是不合法的,将会产生常规保护错误(#GP)。

 

 

(2)CPL,DPL,RPL

CPL是存寄存器如CS中,

RPL是代码中根据不一样段跳转而肯定,以动态刷新CS里的CPL.

DPL是在GDT/LDT描述符表中,静态的。

一致代码段:

  简单理解,就是操做系统拿出来被共享的代码段,能够被低特权级的用户直接调用访问的代码。一般这些共享代码,是"不访问"受保护的资源和某些类型异常处理。好比一些数学计算函数库,为纯粹的数学运算计算,被做为一致代码段。

一致代码段的限制做用:

特权级高的程序不容许访问特权级低的数据:核心态不容许调用用户态的数据.

特权级低的程序能够访问到特权级高的数据.可是特权级不会改变:用户态仍是用户态.

非一致代码段:

为了不低特权级的访问而被操做系统保护起来的系统代码.

非一致代码段的限制做用

只容许同级间访问.

绝对禁止不一样级访问:核心态不用用户态.用户态也不使用核心态.

 一般低特权代码必须经过"门"来实现对高特权代码的访问和调用。不一样级别代码段之间转移规则,是经过CPL/RPL/DPL来校验。先来理解这几个概念。

 

CPL(Current PrivilegeLevel)

CPL是当前执行的程序或任务的特权级。它被存储在cs和ss的第0位和第1位上。在一般状况下,CPL等于代码所在的段的 特权级。当程序转移到不一样特权级的代码段时,处理器将改变CPL。

在遇到一致代码段时,状况稍稍有点特殊,一致代码段能够被相同或者更低特权级的代码访问。当处理器访问一个与 CPL特权级不一样的一致代码段时,CPL不会被改变。

 DPL(Descriptor Privilege Level)

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的最低特权级(这与数据段的规则是一致的)。(TSS 全称task state segment,是在操做系统进程管理的过程当中,任务(进程)切换时的任务现场信息。)

 

RPL(Requested PrivilegeLevel)

RPL是经过段选择子的第0位和第1位表现出来的。处理器经过检查RPL和CPL来确认一个访问请求是否合法。即使提出访问请求的段有足够的特权级,若是RPL不够也是不行的。也就是说,若是RPL的数字比CPL大(数字越大特权级越低), 那么RPL将会起决定性做用,反之亦然。

操做系统过程每每用RPL来避免低特权级应用程序访问高特权级段内的数据。当操做系统过程(被调用过程)从一个应用程序(调用过程)接收到一个选择子时,将会把选择子的RPL设成调用者的特权级。因而,当操做系统用这个选择子 去访问相应的段时,处理器将会用调用过程的特权级(已经被存到RPL中),而不是更高的操做系统过程的特权级(CPL)进行特权检验。这样,RPL就保证了操做系统不会越俎代庖地表明一个程序去访问一个段,除非这个程序自己是有权限的。

例子:

的数据段的选择子的RPL改成3:

SelectorData equ LABEL_DESC_DATA-LABEL_GDT+SA_RPL3

再运行一下,发生了什么?

Bochs重启了,系统崩溃了,在控制台你能看到这样的字样:

load_seg_reg(DS): RPL & CPL must be <= DPL

容易理解,崩溃的缘由在于咱们违反了特权级的规则,用RPL=3的选择子去访问DPL=1的段,因而引发异常。而咱们又没有相应 的异常处理模块,因而最为严重的状况就发生了。 

(3)不一样特权级代码段间转移

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

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

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

1. 目标操做数包含目标代码段的段选择子。

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

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

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

4 种方式能够看作是两大类,一类是经过jmp和call的直接转移(上述第1种),另外一类是经过某个描述符的间接转移(上述 第二、三、4种)。下面就来分别看一下。

 

(4)经过jmp或call直接转移

若是目标是非一致代码段,要求CPL必须等于目标段的

DPL,同时要求RPL小于等于DPL;若是目标是一致代码段,则要求CPL大于或者等于目标段的DPL,RPL此时不作检查。当转移到一致

代码段中后,CPL会被延续下来,而不会变成目标代码段的DPL。也就是说,经过jmp和call所能进行的代码段间转移是很是有限

的,对于非一致代码段,只能在相同特权级代码段之间转移。遇到一致代码段也最多能从低到高,并且CPL不会改变。若是想自由

地进行不一样特权级之间的转移,显然须要其余几种方式,即运用门描述符或者TSS。

 

(5)基本的调用门进行段转移(先不涉及特权级转换,用门特权级转换见6./e/)

门:门也是一种描述符,门描述符的结构如图3.13

 

 

能够看到,门描述符和咱们前面提到的描述符有很大不一样,它主要是定义了目标代码对应段的选择子、入口地址的偏移和一些 属性等。但是,虽然这样的结构跟代码段以及数据段描述符大不相同,咱们仍然看到,第5个字节(BYTE5)倒是彻底一致的,都表 示属性。在这个字节内,各项内容的含义与前面提到的描述符也别无二致,这显然是必要的,以便识别描述符的类型。在这里,S 位将是0

直观来看,一个门描述了由一个选择子和一个偏移所指定的线性地址,程序正是经过这个地址进 行转移的。门描述符分为4种:

调用门(Call gates)

中断门(Interrupt gates)

陷阱门(Trap gates)

任务门(Task gates)

其中,中断门和陷阱门是特殊的调用门,将会在后面提到,咱们先来介绍调用门。在这个例子中,咱们用到调用门。为简单起见,先不涉及任何特权级变换,而是先来关注它的工做方法。

pmtest3.asm的基础上修改成pmtest4.asm

 

增长一个代码段做为经过调用门转移的目标段

添加[SECTION .sdset]:调用selectvideo在屏幕上显示C。由于打算用call指令调用将要创建的调用门,因此,在这段代码的结尾处调用了一个retf指令。

而后加入该段的descriptor以及selector,并初始化

 

 

而后添加调用门的descriptor以及selector

使用宏GATE(在pm.inc定义)初始化门的descriptor

SelectorCodeDest就是这个调用门要调用的段的selector,也就是咱们刚刚在上面定义的段的selector

 

 

 

 

 

 

而后就准备好了要被调用的段以及调用门

 

下面进行调用

Call 测试调用门后retf,至关于继续运行,从235行开始继续。

调用门准备就绪,它指向的位置是SelectorCodeDest:0,即标号LABEL_SEG_CODE_DEST处的代码

用一个call指令来使用这个调用门是个好主意 :

 

233 ; 测试调用门(无特权级变换),将打印字母'C'

⇒ 234 call SelectorCallGateTest:0

...

241 jmp SelectorLDTCodeA:0 ; 跳入局部任务,将打印字母'L'

 

这个call指令被放在进入局部任务以前,因为咱们新加的代码以指令retf结尾,因此最终代码将会跳回 到call指令的下面继续执行。因此,咱们最终看到的结果应该是在pmtest3.exe执行结果的基础上多出一个红色的字母C。

 

其实调用门本质上只不过是个入口地址,只是增长了若干的属性而已。在咱们的例子中所用到的调用门彻底等同于一个地址,咱们甚至能够把使用调用门进行跳转的指令修改成跳转到调用门内指定的地址的指令:

call SelectorCodeDest:0

运行一下,效果是彻底相同的。(下面是更复杂的状况)

6)使用调用门进行转移时特权级检验的规则。

假设咱们想由代码A转移到代码B,运用一个调用门G,即调用门G中的目标选择子指向代码B的段。实际上,咱们涉及了这么几个要素:CPL、RPL、代码B的DPL(记作DPL_B)、调用门G的DPL(记作DPL_G)。根据3.2.3.1中提到的,A访问G这个调用门时,规则至关于访问一个数据段,要求CPL和RPL都小于或者等于DPL_G。换句话说,CPL和RPL需在更高的特权级上。

除了这一步要符合要求以外,系统还将比较CPL和DPL_B。若是是一致代码段的话,要求DPL_B≤CPL;若是是非一致代码段的话,call指令和jmp指令又有所不一样。在用call指令时,要求DPL_B≤CPL;在用jmp指令时,只能是DPL_B=CPL。

综上所述,调用门使用时特权检验的规则如表所示。

 

 

 

也就是说,经过调用门和call指令,能够实现从低特权级到高特权级的转移,不管目标代码段是一致的仍是非一致的。

 

 

调试:

编译pmtest4.asm为pmtest4.com,在dos运行

pmtest3.asm的基础上又多显示了C。是调用门调用的段的输出

 

 

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

分析:

(1)跳转与堆栈

经过调用门和call指令,能够实现从低特权级到高特权级的转移,不管目标代码段是一致的仍是非一致的。 那么如何进行高特权级向低特权级转换?

有特权级变换的转移的复杂之处,不但在于严格的特权级检验,还在于特权级变化的时候,堆栈也要发生变化。处理器的这种 机制避免了高特权级的过程因为栈空间不足而崩溃。并且,若是不一样特权级共享同一个堆栈的话,高特权级的程序可能所以受到有意或无心的干扰。

在咱们的程序中,指令call DispReturn和call SelectorCodeDest:0显然不一样。与在实模式下相似,若是一个调用或跳转指 令是在段间而不是段内进行的,那么咱们称之为“长”的(Far jmp/call),反之,若是在段内则是“短”的(Near jmp/call)。  (与windows不一样)

那么长的和短的jmp或call有什么分别呢?对于jmp而言,仅仅是结果不一样罢了,短跳转对应段内,而长跳转对应段间;而call 则稍微复杂一些,由于call指令是会影响堆栈的,长调用和短调用对堆栈的影响是不一样的。咱们下面的讨论只考虑32位的状况.

 

对于短调用来讲,call指令执行时下一条指令的eip压栈,到ret指令执行时,这个eip会被从堆栈中弹出,如图所示。

先从右向左压栈参数,而后压栈下一条指令eip,(从高地址到低地址压栈)eip寄存器存储着咱们cpu要读取指令的地址每次cpu执行都要先读取eip寄存器的值,而后定位eip指向的内存地址。Esp是当前堆栈的指针寄存器,指向当前堆栈的底部位置。

 

 

能够看出,调用者的eip被压栈,而在此以前参数已经入栈。图中的“调用者

eip”对应nop指令地址。而在函数foo调用最后一条指令ret(带有参数)返回以前和以后,堆栈的变化如图所示。可见esp指向的内存中,存放着call后下一条指令的地址(nop)

 

 

长调用的状况与此相似,容易想到,返回的时候跟调用的时候同样也是“长”转移,因此返回的时候也需

要调用者的cs,因而call指令执行时被压栈的就不只有eip,还应该有cs,如图所示。

 

 

带参数的ret指令执行先后的情形如图所示。

 

 

(2)经过调用门进行特权级转换

call一个调用门也是长调用,状况跟上面 所说的长调用差很少。但是因为一些缘由堆栈发生了切换,也就是说,call指令执行先后的堆栈已经 再也不是同一个。咱们在堆栈A中压入参数和返回时地址,等到须要使用它们的时候堆栈已经变成B了。Intel提供了这样一种机制,将堆栈A的诸多内容复制到堆栈B中,如图所示。 

 

 

事实上,因为每个任务最多均可能在4个特权级间转移,因此,每一个任务实际上须要4个堆栈。可 是,咱们只有一个ss和一个esp,那么当发生堆栈切换,咱们该从哪里得到其他堆栈的ss和esp呢?这里涉及同样TSS(Task-State Stack),它是一个数据结构,里面包含多个字段,32位TSS如图所示。

 

 

能够看出,TSS包含不少个字段,可是在这里,咱们只关注偏移4到偏移27的3个ss和3个esp。当发生堆栈切换时,内层的ss和 esp就是从这里取得的。

好比,咱们当前所在的是ring3,当转移至ring1时,堆栈将被自动切换到由ss1和esp1指定的位置。因为只是在由外层到内层 (低特权级到高特权级)切换时新堆栈才会从TSS中取得,因此TSS中没有位于最外层的ring3的堆栈信息。

 

新堆栈的问题已经解决,下面就是CPU在整个过程当中所作的工做:

1. 根据目标代码段的DPL(新的CPL)从TSS中选择应该切换至哪一个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是零的话,将不会复制参数。

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

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

 

在第7步中,解释了调用门中Param Count的做用,Param Count只有5位,也就是说,最多只能复制31个参数。若是参数多于31个该怎么办呢?这时可让其中的某个参数变成指向一 个数据结构的指针,或者经过保存在新堆栈里的ss和esp来访问旧堆栈中的参数。

 

此刻结合TSS结构和上述步骤,能够理解经过调用门进行由外层到内层调用的全过程。那么,正如call指令对 应ret,调用门也面临返回的问题。经过长短call和ret的堆栈变化这两组对比,咱们发现,ret基本上是call的反过程,只

是带参数的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(此规则不适用于一致代码段),那么一个空描述符会被加载到该寄存器。

如图所示

 

 

综上所述,使用调用门的过程实际上分为两个部分,一部分是从低特权级到高特权级,经过调用门和call指令来实现;另外一部

分则是从高特权级到低特权级,经过ret指令来实现。

(3)进入ring3

ret指令执行前,堆栈中应该已经准备好了目标代码段的cs、eip,以及ss和esp,另外,还可能有参数。这些能够是处理器压入栈的,也能够由咱们本身压栈。在咱们的例子中,在ret前的堆栈如图3.22所示。 

 

 

这样,执行ret以后就能够转移到低特权级代码中了。在(pmtest4.asm)基础上作一下修改(造成 pmtest5a.asm)。如上面的图3.22所示,咱们至少要添加一个ring3的代码段和一个ring3的堆栈段。

 

(4)pmtest5a.asm 由ring0到ring3转移

首先,咱们以前的代码都运行在ring0!

添加一个ring3代码段[SECTION .ring3],一个ring3堆栈段[SECTION .s3]

这个ring3代码段很是简单,跟[SECTION .la]和[SECTION .sdest]的内容差很少,一样是打印一个字符。

须要注意,因为这段代码运行在ring3,而在其中因为要写显存而访问到了VIDEO段,为了避免会产生错误,咱们把VIDEO段的DPL 修改成3。

25 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW + DA_DPL3

 

392行让程序再也不继续执行。392 jmp $

之因此这样作,是为了先验证一下由ring0到ring3的转移是否成功。若是屏幕上出 现红色的3,而且停住不动,再也不返回DOS,则说明转移成功。

 

新段对应的描述符LABEL_DESC_CODE_RING3的属性加上了DA_DPL3,让它的DPL变成了3

相应选择子SelectorCodeRing3的SA_RPL3将RPL也设成了3。

同时有堆栈段的descriptor LABEL_DESC_STACK3以及selector SelectorStack3,以及初始化,在此略去。

 

这样,代码段和堆栈段都已经准备好了。让咱们将ss、esp、cs、eip依次压栈,而且执行retf指令。

266 push SelectorStack3

267 push TopOfStack3

107268 push SelectorCodeRing3

269 push 0

270 retf

此段代码放在显示完字符串“In Protect Mode now.”后当即执行。

编译,运行。

会看到了红色的3在“In Protect Mode now.”下方显示。在这代表咱们由ring0到ring3的转移成功完成。

(5)pmtest5b.asm 在ring3中使用调用门

修改pmtest4中提到的调用门的selectorSelectorCallGateTest以及descriptorLABEL_CALL_GATE_TEST:的DPL,RPL

而后修改[SECTION .ring3]代码,在死循环前添加

call SelectorCallGateTest:0。

修改描述符和选择子是为了知足CPL和RPL 都小于等于调用门DPL的条件。

编译运行

出现错误。由于从低特权级到高特权级转移的时候,须要用到 TSS。

 

(6)pmtest5c.asm 添加TSS,在ring3中使用调用门

由于从低特权级到高特权级转移的时候,须要用到 TSS,在pmtest5c.asm中准备一个TSS

TSS做为数据结构有其descriptor LABEL_DESC_TSS,selector SelectorTSS以及段[SECTION .TSS]。定义及初始化见代码

能够看出,除了0级堆栈以外,其余各个字段咱们都没作任何初始化。由于在本例中,咱们只用到这一部分。

添加初始化TSS描述符的代码以后,TSS就准备好了,咱们须要在特权级变换以前加载它

311 call DispReturn

312

⇒ 313 mov ax, SelectorTSS

⇒ 314 ltr ax

315

316 push SelectorStack3

317 push TopOfStack3

318 push SelectorCodeRing3

319 push 0

320 retf

以后编译运行,成功。显示call调用门的C以及ring3段的3.

 

(7)pmtest5.asm 返回实模式

到目前为止,咱们已经成功实现了两次从高特权级到低特权级以及一次从低特权级到高特权级的转移(ring0-ring3-ring-0-ring3,ring0打印“In protect mode”,而后到ring3打印3,而后ring3callgate到ring0打印L,而后返回ring3),最终在低特权级的代码[SECTION .ring3] 中让程序停住。咱们已经具有了在各类特权级下进行转移的能力,而且熟悉了调用门这种典型门描述符的用法。

 

为了让咱们的程序可以顺利地返回实模式,咱们将调用局部任务的代码加入到调用门的目标代码([SECTION .sdest])。最后,程序将由这里进入局部任务,而后经由原路返回实模式。(ring3打印3,调用门,调用门打印C,调用局部任务LDT打印L,而后在局部任务jmp SelectorCode16:0返回16位代码段,以后返回实模式)

346 [SECTION .sdest]; 调用门目标段

347 [BITS 32]

...

⇒ 359 mov ax, SelectorLDT

⇒ 360 lldt ax

361

⇒ 362 jmp SelectorLDTCodeA:0 ; 跳入局部任务,将打印字母'L'

编译运行,结果应为显示in protect mode ,3,c,l,而后返回实模式能够继续运行

 

调试:

编译为.com文件运行

pmtest5a

 

 

pmtest5b

 

 

 

pmtest5c

 

 

pmtest5

 

 

7.课后手动改:

1)自定义添加1个GDT代码段、1个LDT代码段,GDT段内要对一个内

存数据结构写入一段字符串,而后LDT段内代码段功能为读取并打印该GDT的内容;

参考pmtest3.com

修改[SECTION .data1],修改字符串为StrTest: db "JUST MONIKA", 0

修改[SECTION .s32]; 32 位代码段. 由实模式跳入.

改成以下,至关于直接跳到LDT中的descriptor

.........................

[SECTION .s32]; 32 位代码段. 由实模式跳入.

[BITS 32]

LABEL_SEG_CODE32:

; Load LDT

mov ax, SelectorLDT

lldt ax

jmp SelectorLDTCodeA:0 ; 跳入局部任务

SegCode32Len equ $ - LABEL_SEG_CODE32

; END of [SECTION .s32]

........................

 

修改LDT中的段; CodeA (LDT, 32 位代码段)[SECTION .la]

功能改成显示GDT中[SECTION .DATA]段的字符串StrTest

........................

; CodeA (LDT, 32 位代码段)

[SECTION .la]

ALIGN 32

[BITS 32]

LABEL_CODE_A:

mov ax, SelectorData

mov ds, ax ; 数据段选择子

mov ax, SelectorVideo

mov gs, ax ; 视频段选择子

mov ax, SelectorStack

mov ss, ax ; 堆栈段选择子

mov esp, TopOfStack

; 下面显示一个字符串

mov ah, 0Ch ; 0000: 黑底    1100: 红字

xor esi, esi

xor edi, edi

mov esi, OffsetStrTest ; 源数据偏移

mov edi, (80 * 10 + 0) * 2 ; 目的数据偏移。屏幕第 10 行, 第 0 列。

cld

.1:

lodsb

test al, al

jz .2

mov [gs:edi], ax

add edi, 2

jmp .1

.2: ; 显示完毕

jmp SelectorCode16:0

CodeALen equ $ - LABEL_CODE_A

; END of [SECTION .la]

.........................

而后编译运行。运行时顺序为实模式跳转保护模式[SECTION .s32],而后[SECTION .s32]加载LDT的ldtr,而后跳转LDT的[SECTION .la]段,该段中先在屏幕显示[SECTION .DATA]段的字符串StrTest,而后跳回实模式

代码保存为pmtestmy.asm,编译为pmtestmy.com。

编译运行

(2)自定义2个GDT代码段A、B,分属于不一样特权级,功能自定义,要求实现A-->B的跳转,以及B-->A的跳转。

参考pmtest5,实现了ring0->ring3->ring0->ring3d的跳转

 

 

 

二.是书上内容的节选,代码里有一点注释。再翻翻书的保护模式那一章吧

 

x86 CPU的基本模式:实模式、保护模式

 

– 实模式

 

• 地址总线宽度:20bit

 

• 寄存器和数据总线宽度:16bit

 

• 寻址空间是多少?

 

• 实模式:PA=Segment*16+Offset

 

 

pmtest1.asm

; ========================================== ; pmtest1.asm ; 编译方法:nasm pmtest1.asm -o pmtest1.bin ; ==========================================

%include    "pm.inc" ; 常量, 宏, 以及一些说明 org 07c00h jmp LABEL_BEGIN [SECTION .gdt] ;定义一个段,段名gdt ; GDT ; 段基址, 段界限 , 属性 LABEL_GDT: Descriptor 0,                0, 0 ; 空描述符 LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32; 非一致代码段 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址 ; GDT 结束 GdtLen equ $ - LABEL_GDT ; GDT长度 :equ至关于起个别名。S为当前位置。s-LABEL_GDT,就是当前位置减去.gdt起始位置,也就是.gdt长度 GdtPtr dw GdtLen - 1 ; GDT界限 。GdtPtr也是个小的数据结构,它有6字节,前2字节是GDT的界限,后4字节是GDT的基地址
 dd 0 ; GDT基地址 ; GDT 选择子 SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT ;直观地看,它好像是DESC_VIDEO这个描述符相对于GDT基址的偏移。实际上有其数据结构,其名选择子 SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT ;

; END of [SECTION .gdt]

[SECTION .s16]
[BITS 16]   ;代表是16位代码
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h

; 初始化 32 位代码段描述符
xor eax, eax
mov ax, cs
shl eax, 4
add eax, LABEL_SEG_CODE32
mov word [LABEL_DESC_CODE32 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE32 + 4], al
mov byte [LABEL_DESC_CODE32 + 7], ah

; 为加载 GDTR 做准备
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ; eax <- gdt 基地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址

; 加载 GDTR
lgdt [GdtPtr]

; 关中断
cli

; 打开地址线A20
in al, 92h
or al, 00000010b
out 92h, al

; 准备切换到保护模式
mov eax, cr0
or eax, 1
mov cr0, eax

; 真正进入保护模式
jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs,  //selector16位,dword两字节,高位selector,低位偏移0.(由于声明了这段是16位代码,因此一个字两字节)
; 并跳转到 Code32Selector:0 处
; END of [SECTION .s16]


[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]

LABEL_SEG_CODE32:
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子(目的)

mov edi, (80 * 11 + 79) * 2 ; 屏幕第 11 行, 第 79 列。
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov al, 'P'
mov [gs:edi], ax

; 到此中止
jmp $

SegCode32Len equ $ - LABEL_SEG_CODE32
; END of [SECTION .s32]

 

 

好了,首先看[SECTION .gdt]这个段,其中的Descriptor是在pm.inc中定义的宏(见代码3.2)。先不要管具体的意义是什
么,看字面咱们能够知道,这个宏表示的不是一段代码,而是一个数据结构,它的大小是8字节。 

 

在段[SECTION.gdt]中并列有3个Descriptor,看上去是个结构数组,你必定猜到了,这个数组的名字叫作GDT。
GdtLen是GDT的长度,GdtPtr也是个小的数据结构,它有6字节,前2字节是GDT的界限,后4字节是GDT的基地址。
另外还定义了两个形如SelectorXXXX的常量,至因而作什么用的,咱们暂且无论它。
再往下到了一个代码段,[BITS 16]明确地指明了它是一个16位代码段。你会发现,这段程序修改了一些GDT中的值,而后执行
了一些不常见的指令,最后经过jmp指令实现一个跳转(第71行)。正如代码注释中所说的,这一句将“真正进入保护模式”。实
际上,它将跳转到第三个section,即[SECTION .s32]中,这个段是32位的,执行最后一小段代码。这段代码看上去是往某个地址
处写入了2字节,而后就进入了无限循环。 
 
能够看到,在屏幕中部右侧,出现了一个红色的字母“P”,而后不再动了。不难猜到,程序的最后一部分代码中写入的两个字节是写进了显存中。
如今,大体的感性认识已经有了,但你必定有一些疑惑,什么是GDT?那些看上去怪怪的指令到底在作什么?如今咱们先来总结一下,在这个程序中,咱们了解到什么,有哪些疑问。
 
咱们了解到的内容以下:
程序定义了一个叫作GDT的数据结构。
后面的16位代码进行了一些与GDT有关的操做。
程序最后跳到32位代码中作了一点操做显存的工做。
 
咱们不明就里的内容以下:
GDT是什么?它是干什么用的?
程序对GDT作了什么?
那个jmp SelectorCode32:0跟咱们从前用过的jmp有什么不一样? 
 
 
在IA32下,CPU有两种工做模式:实模式和保护模式。直观地看,当咱们打开本身的PC,开始时CPU是工做在实模式下的,通过某种机制以后,才进入保护模式。在保护模式下,CPU有着巨大的寻址能力,并为强大的32位操做系统提供了更好的硬件保障。
 
咱们先来回忆一下旧政策。Intel 8086是16位的CPU,它有着16位的寄存器(Register)、16位的数据总线(Data Bus)以及20位的地址总线(Address Bus)和1MB的寻址能力。一个地址是由段和偏移两部分组成的,物理地址遵循这样的计算公式:
物理地址(Physical Address)=段值(Segment)×16+偏移(Offset)
其中,段值和偏移都是16位的。
 
从80386开始,Intel家族的CPU进入32位时代。80386有32位地址线,因此寻址空间能够达到4GB。因此,单从寻址这方面说,使用16位寄存器的方法已经不够用了。这时候,咱们须要新的方法来提供更大的寻址能力。固然,慢慢地你能看到,保护模式的优势不只仅在这一个方面。
 
在实模式下,16位的寄存器须要用“段:偏移”这种方法才能达到1MB的寻址能力,现在咱们有了32位寄存器,一个寄存器就能够寻址4GB的空间,是否是今后段值就被抛弃了呢?实际上并无,新政策下的地址仍然用“段:偏移”这样的形式来表示,只不过保护模式下“段”的概念发生了根本性的变化。
 
实模式下,段值仍是能够看作是地址的一部分的,段值为XXXXh表示以XXXX0h开始的一段内存。
 
而保护模式下,虽然段值仍然由原来16位的cs、ds等寄存器表示,但此时它仅仅变成了一个索引,这个索引指向一个数据结构的一个表项,表项中详细定义了段的起始地址、界限、属性等内容。这个数据结构,就是GDT(实际上还多是LDT,这个之后再介绍)。GDT中的表项也有一个专门的名字,叫作描述符(Descriptor)。
也就是说,GDT的做用是用来提供段式存储机制,这种机制是经过段寄存器和GDT中的描述符共同提供的。为了全面地了解它,
咱们来看一下图3.4所示的描述符的结构。
 
 
 

 

 

 

 

这个示意图表示的是代码段和数据段描述符,此外,描述符的种类还有系统段描述符和门描述符,下文会有介绍。
除了BYTE5和BTYE6中的一堆属性看上去有点复杂之外,其余三个部分倒还容易理解,它们分别定义了一个段的基址和界限。不过,因为历史问题,它们都被拆开存放。
至于那些属性,咱们暂时先无论它。
好了,咱们回头再来看看代码3.1,Descriptor这个宏用比较自动化的方法把段基址、段界限和段属性安排在一个描述符中合适的位置,有兴趣的读者能够研究这个宏的具体内容。
本例的GDT中共有3个描述符,为方便起见,在这里咱们分别称它们为DESC_DUMMY、DESC_CODE32和DESC_VIDEO。
其中DESC_VIDEO的段基址是0B8000h,顾名思义,这个描述符指向的正是显存。
如今咱们已经知道,GDT中的每个描述符定义一个段,那么cs、ds等段寄存器是如何和这些段对应起来的呢?你可能注意到了,在[SECTION.s32]这个段中有两句代码是这样的(第80行和第81行):
 
mov ax, SelectorVideo
mov gs, ax
 
看上去,段寄存器gs的值变成了SelectorVideo,咱们在上文中能够看到,SelectorVideo是这样定义的(第25行):
SelectorVideo equ LABEL_DESC_VIDEO-LABEL_GDT
直观地看,它好像是DESC_VIDEO这个描述符相对于GDT基址的偏移。实际上,它有一个专门的名称,叫作选择子(Selector),它也不是一个偏移,而是稍稍复杂一些,它的结构如图3.5所示。

 

 

 

 

不难理解,当TI和RPL都为零时,选择子就变成了对应描述符相对于GDT基址的偏移,就好像咱们程序中那样。
看到这里,读者确定已经明白了第86行的意思,gs值为SelectorVideo,它指示对应显存的描述符DESC_VIDEO,这条指令将把
ax的值写入显存中偏移位edi的位置。
总之,整个的寻址方式如图3.6所示。

 

 

 

注意图3.6中“段:偏移”形式的逻辑地址(Logical Address)通过段机制转化成“线性地址”(Linear Address),而不是“物理地址”(Physical Address),
其中的缘由咱们之后会提到。在上面的程序中,线性地址就是物理地址。另外,包含描述符的,不只能够是GDT,也能够是LDT。 

 

明白了这些,离明白整个程序的距离已经只剩一层窗纸了。由于只剩下[SECTION .s16]这一段尚未分析。不过,既然[SECTION .s32]是32位的程序,而且在保护模式下执行,那么[SECTION .s16]的任务必定是从实模式向保护模式跳转了。下面咱们就来看一下实模式是如何转换到保护模式的。

 

让咱们到[SECTION .s16]这段,先看一下初始化32位代码段描述符的这一段,代码首先将LABEL_SEG_CODE32的物理地址(即
[SECTION .s32]这个段的物理地址)赋给eax,而后把它分红三部分赋给描述符DESC_CODE32中的相应位置。因为DESC_CODE32的段
界限和属性已经指定,因此至此,DESC_CODE32的初始化所有完成。
 
接下来的动做把GDT的物理地址填充到了GdtPtr这个6字节的数据结构中,而后执行了一条指令(第55行):
 
lgdt [GdtPtr]

 

这一句的做用是将GdtPtr指示的6字节加载到寄存器gdtr,gdtr的结构如图3.7所示。

 

 

 

 

 

 

 

 

 

 

 

pm.inc

; 描述符图示 ; 图示一 ; ; ------ ┏━━┳━━┓高地址 ; ┃ 7 ┃ 段 ┃ ; ┣━━┫ ┃ ; 基 ; 字节 7 ┆ ┆ ┆ ; 址 ; ┣━━┫ ② ┃ ; ┃ 0 ┃ ┃ ; ------ ┣━━╋━━┫ ; ┃ 7 ┃ G ┃ ; ┣━━╉──┨ ; ┃ 6 ┃ D ┃ ; ┣━━╉──┨ ; ┃ 50 ┃ ; ┣━━╉──┨ ; ┃ 4 ┃ AVL┃ ; 字节 6 ┣━━╉──┨ ; ┃ 3 ┃ ┃ ; ┣━━┫ 段 ┃ ; ┃ 2 ┃ 界 ┃ ; ┣━━┫ 限 ┃ ; ┃ 1 ┃ ┃ ; ┣━━┫ ② ┃ ; ┃ 0 ┃ ┃ ; ------ ┣━━╋━━┫ ; ┃ 7 ┃ P ┃ ; ┣━━╉──┨ ; ┃ 6 ┃ ┃ ; ┣━━┫ DPL┃ ; ┃ 5 ┃ ┃ ; ┣━━╉──┨ ; ┃ 4 ┃ S ┃ ; 字节 5 ┣━━╉──┨ ; ┃ 3 ┃ ┃ ; ┣━━┫ T ┃ ; ┃ 2 ┃ Y ┃ ; ┣━━┫ P ┃ ; ┃ 1 ┃ E ┃ ; ┣━━┫ ┃ ; ┃ 0 ┃ ┃ ; ------ ┣━━╋━━┫ ; ┃ 23 ┃ ┃ ; ┣━━┫ ┃ ; ┃ 22 ┃ ┃ ; ┣━━┫ 段 ┃ ; ; 字节 ┆ ┆ 基 ┆ ; 2, 3, 4 ; ┣━━┫ 址 ┃ ; ┃ 1 ┃ ① ┃ ; ┣━━┫ ┃ ; ┃ 0 ┃ ┃ ; ------ ┣━━╋━━┫ ; ┃ 15 ┃ ┃ ; ┣━━┫ ┃ ; ┃ 14 ┃ ┃ ; ┣━━┫ 段 ┃ ; ; 字节 0,1┆ ┆ 界 ┆ ; ; ┣━━┫ 限 ┃ ; ┃ 1 ┃ ① ┃ ; ┣━━┫ ┃ ; ┃ 0 ┃ ┃ ; ------ ┗━━┻━━┛低地址 ; ; 图示二 ; 高地址………………………………………………………………………低地址 ; |   7   |   6   |   5   |   4   |   3   |   2   |   1   |   0    | ; |7654321076543210765432107654321076543210765432107654321076543210|    <- 共 8 字节 ; |--------========--------========--------========--------========| ; ┏━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━┓ ; ┃31..24┃   (见下图)   ┃     段基址(23..0)    ┃ 段界限(15..0)┃ ; ┃ ┃ ┃ ┃ ┃ ; ┃ 基址2┃③│②│ ①┃基址1b│ 基址1a ┃ 段界限1 ┃ ; ┣━━━╋━━━┳━━━╋━━━━━━━━━━━╋━━━━━━━┫ ; ┃ %6 ┃  %5  ┃  %4  ┃  %3  ┃     %2       ┃       %1 ┃ ; ┗━━━┻━━━┻━━━┻━━━┻━━━━━━━┻━━━━━━━┛ ; │ \_________ ; │ \__________________ ; │ \________________________________________________ ; │ \ ; ┏━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┓ ; ┃ 7654321076543210 ┃ ; ┣━━╋━━╋━━╋━━╋━━┻━━┻━━┻━━╋━━╋━━┻━━╋━━╋━━┻━━┻━━┻━━┫ ; ┃ G ┃ D ┃ 0  ┃ AVL┃   段界限 2 (19..16) ┃ P ┃ DPL ┃ S ┃ TYPE ┃ ; ┣━━┻━━┻━━┻━━╋━━━━━━━━━━━╋━━┻━━━━━┻━━┻━━━━━━━━━━━┫ ; ┃ ③: 属性 2      ┃    ②: 段界限 2 ┃ ①: 属性1 ┃ ; ┗━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━┛ ; 高地址 低地址 ; ; ; 说明: ; ; (1) P: 存在(Present)位。 ; P=1 表示描述符对地址转换是有效的,或者说该描述符所描述的段存在,即在内存中; ; P=0 表示描述符对地址转换无效,即该段不存在。使用该描述符进行内存访问时会引发异常。 ; ; (2) DPL: 表示描述符特权级(Descriptor Privilege level),共2位。它规定了所描述段的特权级,用于特权检查,以决定对该段可否访问。 ; ; (3) S: 说明描述符的类型。 ; 对于存储段描述符而言,S=1,以区别与系统段描述符和门描述符(S=0)。 ; ; (4) TYPE: 说明存储段描述符所描述的存储段的具体属性。 ; ; ; 数据段类型 类型值 说明 ; ---------------------------------- ; 0 只读 ; 1 只读、已访问 ; 2        读/写 ; 3        读/写、已访问 ; 4 只读、向下扩展 ; 5 只读、向下扩展、已访问 ; 6        读/写、向下扩展 ; 7        读/写、向下扩展、已访问 ; ; ; 类型值 说明 ; 代码段类型 ---------------------------------- ; 8 只执行 ; 9 只执行、已访问 ; A 执行/读 ; B 执行/读、已访问 ; C 只执行、一致码段 ; D 只执行、一致码段、已访问 ; E 执行/读、一致码段 ; F 执行/读、一致码段、已访问 ; ; ; 系统段类型 类型编码 说明 ; ---------------------------------- ; 0        <未定义> ; 1 可用286TSS ; 2 LDT ; 3 忙的286TSS ; 4 286调用门 ; 5 任务门 ; 6 286中断门 ; 7 286陷阱门 ; 8 未定义 ; 9 可用386TSS ; A <未定义> ; B 忙的386TSS ; C 386调用门 ; D <未定义> ; E 386中断门 ; F 386陷阱门 ; ; (5) G: 段界限粒度(Granularity)位。 ; G=0 表示界限粒度为字节; ; G=1 表示界限粒度为4K 字节。 ; 注意,界限粒度只对段界限有效,对段基地址无效,段基地址老是以字节为单位。 ; ; (6) D: D位是一个很特殊的位,在描述可执行段、向下扩展数据段或由SS寄存器寻址的段(一般是堆栈段)的三种描述符中的意义各不相同。 ; ⑴ 在描述可执行段的描述符中,D位决定了指令使用的地址及操做数所默认的大小。 ; ① D=1表示默认状况下指令使用32位地址及32位或8位操做数,这样的代码段也称为32位代码段; ; ② D=0 表示默认状况下,使用16位地址及16位或8位操做数,这样的代码段也称为16位代码段,它与80286兼容。可使用地址大小前缀和操做数大小前缀分别改变默认的地址或操做数的大小。 ; ⑵ 在向下扩展数据段的描述符中,D位决定段的上部边界。 ; ① D=1表示段的上部界限为4G; ; ② D=0表示段的上部界限为64K,这是为了与80286兼容。 ; ⑶ 在描述由SS寄存器寻址的段描述符中,D位决定隐式的堆栈访问指令(如PUSH和POP指令)使用何种堆栈指针寄存器。 ; ① D=1表示使用32位堆栈指针寄存器ESP; ; ② D=0表示使用16位堆栈指针寄存器SP,这与80286兼容。 ; ; (7) AVL: 软件可利用位。80386对该位的使用未左规定,Intel公司也保证从此开发生产的处理器只要与80386兼容,就不会对该位的使用作任何定义或规定。 ; ;---------------------------------------------------------------------------- ; 在下列类型值命名中: ; DA_ : Descriptor Attribute ; D : 数据段 ; C : 代码段 ; S : 系统段 ; R : 只读 ; RW : 读写 ; A : 已访问 ; 其它 : 可按照字面意思理解 ;---------------------------------------------------------------------------- ; 描述符类型 DA_32 EQU 4000h ; 32 位段 DA_DPL0 EQU 00h ; DPL = 0 DA_DPL1 EQU 20h ; DPL = 1 DA_DPL2 EQU 40h ; DPL = 2 DA_DPL3 EQU 60h ; DPL = 3 ; 存储段描述符类型 DA_DR EQU 90h ; 存在的只读数据段类型值 DA_DRW EQU 92h ; 存在的可读写数据段属性值 DA_DRWA EQU 93h ; 存在的已访问可读写数据段类型值 DA_C EQU 98h ; 存在的只执行代码段属性值 DA_CR EQU 9Ah ; 存在的可执行可读代码段属性值 DA_CCO EQU 9Ch ; 存在的只执行一致代码段属性值 DA_CCOR EQU 9Eh ; 存在的可执行可读一致代码段属性值 ; 系统段描述符类型 DA_LDT EQU 82h ; 局部描述符表段类型值 DA_TaskGate EQU 85h ; 任务门类型值 DA_386TSS EQU 89h ; 可用 386 任务状态段类型值 DA_386CGate EQU 8Ch ; 386 调用门类型值 DA_386IGate EQU 8Eh ; 386 中断门类型值 DA_386TGate EQU 8Fh ; 386 陷阱门类型值 ; 选择子图示: ; ┏━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┓ ; ┃ 1514131211109876543210 ┃ ; ┣━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━╋━━╋━━┻━━┫ ; ┃ 描述符索引 ┃ TI ┃ RPL ┃ ; ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━┻━━━━━┛ ; ; RPL(Requested Privilege Level): 请求特权级,用于特权检查。 ; ; TI(Table Indicator): 引用描述符表指示位 ; TI=0 指示从全局描述符表GDT中读取描述符; ; TI=1 指示从局部描述符表LDT中读取描述符。 ; ;---------------------------------------------------------------------------- ; 选择子类型值说明 ; 其中: ; SA_ : Selector Attribute SA_RPL0 EQU 0 ; ┓ SA_RPL1 EQU 1 ; ┣ RPL SA_RPL2 EQU 2 ; ┃ SA_RPL3 EQU 3 ; ┛ SA_TIG EQU 0 ; ┓TI SA_TIL EQU 4 ; ┛ ;---------------------------------------------------------------------------- ; 宏 ------------------------------------------------------------------------------------------------------ ; ; 描述符 ; usage: Descriptor Base, Limit, Attr ; Base: dd ;段基址 ; Limit: dd (low 20 bits available) ;段界限 ; Attr: dw (lower 4 bits of higher byte are always 0) ;段属性 %macro Descriptor 3 ;macro定义宏。 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 %endmacro ; 共 8 字节 ; ; 门 ; usage: Gate Selector, Offset, DCount, Attr ; Selector: dw ; Offset: dd ; DCount: db ; Attr: db %macro Gate 4 dw (%2 & 0FFFFh) ; 偏移1 dw %1 ; 选择子 dw (%3 & 1Fh) | ((%4 << 8) & 0FF00h) ; 属性 dw ((%2 >> 16) & 0FFFFh) ; 偏移2 %endmacro ; 共 8 字节 ; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
相关文章
相关标签/搜索