一个操作系统的实现(5)-关于特权级

这节讲述IA32分段机制中的特权级。包括CPL、DPL、RPL的介绍以及代码实现不同特权级之间的转换。

IA32的分段机制有四种特权级别,从高到低分别是0、1、2、3。数字越小表示的特权级越大。

处理器引入特权级的目的是为了保护核心代码和数据。核心的代码和数据会被放在较高的层级中。从而避免低特权级(外层)的任务在不被允许的情况下访问位于高特权级(内层)的段。

在开始之前,首先介绍一下一致代码段的概念。

一致代码段

关于一致代码段中一致的理解:程序经常会通过call和jmp实现直接转移操作。当转移的目标是一个特权级更高的一致代码段,当前的特权级会被延续下去,而向特权级更高的非一致代码段转移将会引起常规保护错误(general-protection
exception, #GP)。你看完下面CPL的介绍会对这句话有更深刻的理解。

当然是有办法访问特权级更高的非一致代码段的:使用调用门或者任务门。

如何去划分一致代码段与非一致代码段呢?从上面的介绍可以知道,一致代码段的保护较弱,能够被低特权级的代码通过call和jmp访问到。所以,如果系统代码不访问受保护的资源和某些类型的异常处理(比如,除法错误或溢出错误),那么此系统代码可被放在一致代码段中。对于那些为了避免被低特权级的程序访问而保护起来的系统代码应该放到非一致代码段中。

另外,如果目标代码的特权级低,无论它是不是一致代码段,都不能通过call或者jmp转移进去,尝试这样的转移将会导致常规保护性错误。

所有的数据段都是非一致的,这意味着不可能被低特权级的代码访问到。然而,与代码段不同的是,数据段可以被更高特权级的代码访问到,而不需要使用特定的门。

综上,通过call和jmp的转移遵从下表的规则:

引入特权级之后call和jmp能够实现的直接转移类型

  特权级`低->高` 特权级`高->低` 相同特权级之间 适用于何种代码
一致代码段 Yes No Yes 不访问受保护的资源和某些类型的异常处理的系统代码
非一致代码段 No No Yes 避免低特权级的程序访问而被保护器来的系统代码
数据段 No Yes Yes  

举个例子,假设有代码段AB,数据段D。那么,

当A向跳转到B,有下面两种情况:

当B是一致代码段时,A的特权级低于或等于B的时候才有可能通过call和jmp从A转移到B。

当B是非一致代码段时,A的特权级必须等于B才有可能通过call和jmp从A转移到B。

如果A想要访问数据段D,那么A的特权级必须高于或等于D。

上面讲的所有东西都在这个例子中。从这儿可以看出,引入了特权级指令之后,call和jmp指令并不能满足所有的转移情况。比如想转移到高特权级的非一致代码段,call和jmp就无法实现了,但下面将要讲的调用门能够实现。

不过在讲调用门之前,首先需要了解CPL、DPL、RPL。

CPL、DPL、RPL

1 CPL(Current Privilege Level)

CPL代表的是当前执行的程序或任务的特权级。它被存储在csss的第0位和第1位上。

通常情况下,CPL等于代码所在的段的特权级。当程序转移到不同的特权级的代码段时,处理器将改变CPL。

上面说到是通常情况,那么就有一个特例:转移的目标是一致代码段。因为一致代码段可以被相同或者更低特权级的代码访问。所以当处理器访问一个于CPL特权级不同的一致代码段时,CPL不会被改变。

2 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的最低特权级(这与数据段的规则是一致的)。

3 RPL(Requested Privilege Level)

RPL是选择子的特权级,它是通过选择子的第0位和第1位表现出来的。

处理器通过检查RPL和CPL来确认一个访问请求是否合法。即便提出访问请求的段有足够的特权级,如果RPL不够也是不行的。也就是说,如果RPL的数字比CPL大(数字越大特权级越低),那么RPL将会起决定性作用,反之亦然。

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

上面的内容完全出自书本上

介绍完了CPL、DPL、RPL。接下来看看不同特权级代码之间的转移。

不同特权级代码段之间的转移

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

程序控制转移的发生引起原因如下:

指令引起,包括jmpcallretsysentersysexitint
n
iret等指令。

中断和异常引起。

使用jmp和call能够实现的四种转移如下:

  • 目标操作数包含目标代码段的段选择子。
  • 目标操作数指向一个包含目标代码段选择子的调用门描述符。
  • 目标操作数指向一个包含目标代码段选择子的TSS。
  • 目标操作数指向一个任务门,这个任务门指向一个包含目标代码段选择子的TSS。

这四种转移可以看作两大类,一类是通过jmp和call的直接转移(上述第1种),另一类是通过某个描述符的间接转移(上述第2、3、4种)。下面就来分别看一下。

通过jmp和call进行的直接转移

上面介绍了很多通过jmp和call的直接转移。这里总结一下。

目标是非一致代码段的转移条件:CPL==DPLRPL<=DPL

目标是一致代码段的转移条件:CPL>=DPL、RPL此时不做检查

上面已经说过jmp和call进行直接转移的限制条件太多。如果向自由地进行不同特权级之间的转移,需要通过门描述符或者TSS。

门描述符结构

选择子:目标代码的选择子,用来初始化cs。指明转移处的目标代码段。

偏移地址:是用来初始化eip,指明转移到目标代码段的某个偏移处执行。

属性:

BYTE5:与其他描述符完全相同,此时S位为0(代表门描述符)。

BYTE4:转移过程需要从调用者堆栈中将参数复制到被调用者堆栈(新堆栈)中,Param Count指明复制参数的数目。Param Count为0将不会复制参数。

从上面门描述符的结构可以看出,一个门描述了由一个选择子和一个偏移所指定的线性地址(关于虚拟地址,线性地址,物理地址的概念会在后面讨论)。程序就是通过这个地址进行转移的。

门描述符有四种:

  • 调用门(Call gates)
  • 中断门(Interupt gates)
  • 陷阱门(Trap gates)
  • 任务门(Task gates)

下面用代码实现调用门的使用。在下面这个例子中,先不涉及任何特权级的变换,只是实现通过调用门转移代码。

相同特权级下使用调用门

相对于上节的代码,增加如下部分:

通过调用门转移的目标段

265 [SECTION .sdest]; 调用门目标段
266 [BITS   32]
267
268 LABEL_SEG_CODE_DEST:
269         ;jmp    $
270         mov     ax, SelectorVideo
271         mov     gs, ax                  ; 视频段选择子(目的)
272
273         mov     edi, (80 * 12 + 0) * 2  ; 屏幕第 12 行, 第 0 列。
274         mov     ah, 0Ch                 ; 0000: 黑底    1100: 红字
275         mov     al, ‘C‘
276         mov     [gs:edi], ax
277
278         retf
279
280 SegCodeDestLen  equ     $ - LABEL_SEG_CODE_DEST
281 ; END of [SECTION .sdest]

从这里看目标段也比较简单。在屏幕第12行第0列打印一个黑底红色的字符C

因为这里打算用call指令调用将要建立的调用门,所以,在这段代码的结尾处调用了一个retf指令。retf表示段间返回。

上述代码段的描述符选择子初始化描述符的代码

 18 LABEL_DESC_CODE_DEST: Descriptor 0,SegCodeDestLen-1, DA_C+DA_32; 非一致代码段,32
...

 36 SelectorCodeDest        equ     LABEL_DESC_CODE_DEST    - LABEL_GDT
...

103         ; 初始化测试调用门的代码段描述符
104         xor     eax, eax
105         mov     ax, cs
106         shl     eax, 4
107         add     eax, LABEL_SEG_CODE_DEST
108         mov     word [LABEL_DESC_CODE_DEST + 2], ax
109         shr     eax, 16
110         mov     byte [LABEL_DESC_CODE_DEST + 4], al
111         mov     byte [LABEL_DESC_CODE_DEST + 7], ah

调用门

 24 ; 门                               目标选择子,偏移,DCount, 属性
 25 LABEL_CALL_GATE_TEST: Gate SelectorCodeDest,   0,     0, DA_386CGate+DA_DPL0
 26 ; GDT 结束

上面可以看到,门描述符的属性是DA_386CGate+DA_DPL0。DA_386CGate表明他是一个调用门;DPL0指定门描述符的DPL为0。上面指定的选择子是SelectorCodeDest,表明目标代码是刚刚新添加的代码段。偏移地址是0,表明将要跳转到目标代码段的开头处执行。DCount代表的是Param
Count,这里表明转移时不复制参数到被调用者的堆栈。

这里用一个宏Gate来初始化描述符,Gate的定义在pm.inc中,如下:

264 ; 门
265 ; usage: Gate Selector, Offset, DCount, Attr
266 ;        Selector:  dw
267 ;        Offset:    dd
268 ;        DCount:    db
269 ;        Attr:      db
270 %macro Gate 4
271         dw      (%2 & 0FFFFh)                           ; 偏移1
272         dw      %1                                      ; 选择子
273         dw      (%3 & 1Fh) | ((%4 << 8) & 0FF00h)       ; 属性
274         dw      ((%2 >> 16) & 0FFFFh)                   ; 偏移2
275 %endmacro ; 共 8 字节

在认识保护模式那一节我还不确定%1%2%3…这些符号是什么意思。当时的猜测是传递进去的参数,按照位置分别是1,2,3。与shell中的位置变量类似。最近仔细看了上面的定义。现在可以肯定我的猜测是对的了。如果下次再有这种疑问,可以先猜测,也许在接下来的某天重新看会豁然开朗。

调用门对应的选择子

 42 SelectorCallGateTest    equ     LABEL_CALL_GATE_TEST    - LABEL_GDT

好了,现在调用门准备就绪,它指向的位置是SelectorCodeDest:0,即标号LABEL_SEG_DESC_DEST处的代码。

下面,使用call指令来使用调用门。

使用调用门

233         ; 测试调用门(无特权级变换),将打印字母 ‘C‘
234         call    SelectorCallGateTest:0
...

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

call指令放在jmp之前。因为目标代码以retf结尾,所以call调用结束之后会返回到call下面的那条代码继续执行。因此此段代码最终的结果是在上一节的基础上多了一个字母C。

其实调用门这种听起来很可怕的东西本质上只不过是个入口地址,只是增加了若干的属性而已。在我们的例子中所用到的调用门完全等同于一个地址,我们甚至可以把使用调用门进行跳转的指令修改为跳转到调用门内指定的地址的指令:

call   SelectorCodeDest:0

运行一下,效果是完全相同的。

看起来引入调用门有一点多此一举,但事实上并不是。下面将用他来实现不同特权级的代码之间的转移。不过首先你需要知道使用调用门进行转移时的特权级检验规则。

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

  call jmp
目标是一致代码段 CPL <= DPL_GRPL <= DPL_GDPL_B <= CPL
目标是非一致代码段 CPL <= DPL_GRPL <= DPL_GDPL_B <= CPL CPL <= DPL_GRPL <= DPL_GDPL_B == CPL

从上表我们能够看出,通过调用门和call指令,可以实现从低特权级到高特权级的转移,无论目标代码段是一致的还是非一致的。

说到这里,你一定又跃跃欲试了,写一个程序实现一个特权级变换应该是件有趣的事情。可是你可能突然发现,调用门只能实现特权级由低到高的转移,而我们的程序一直是在最高的特权级下的。也就是说,我们需要先到相对低一点的特权级下,才可能有机会对调用门亲自实践一番。那么,如何才能到低一点的特权级下呢?先不要慌,调用门的故事还没有讲完。

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

使用调用门时的堆栈变化

首先回忆一下8086汇编语言的长跳转和短跳转。

长跳转相当于:

push CS
push IP
jmp far ptr 标号

短跳转相当于:

push IP
jmp near ptr 标号

call的返回过程弹出IP(或IP与CS)

从上面可以看出,call指令是影响堆栈的。

我们的调用门转移是通过长调用(长跳转)call指令来实现的。上面已经知道,call指令会压栈与出栈。但是在第7小结说过,特权级变化的时候,堆栈也要变化。因此call指令执行前后的堆栈已经不是同一个了。这样一来问题出现了,我们在堆栈A中压入参数和返回地址,等到需要使用他们的时候堆栈已经变成B了,如何解决这个问题呢?

Intel提供了这样一种机制:将堆栈A的诸多内容复制到堆栈B中。

由于每一个任务最多都可能在4个特权级间转移,所以,每个任务实际上需要4个堆栈。可是,我们只有一个ss和一个esp,那么当发生堆栈切换,我们该从哪里获得其余堆栈的ss和esp呢?实际上,这里涉及一样新事物TSS(Task-State Stack),它是一个数据结构,里面包含多个字段,32位TSS如下图所示:

解释一下TSS的4-27字段的使用方法:比如,我们当前所在的是ring3,当转移至ring1时,堆栈将被自动切换到由ss1和esp1指定的位置。由于只是在由外层到内层(低特权级到高特权级)切换时新堆栈才会从TSS中取得,所以TSS中没有位于最外层的ring3的堆栈信息。

到这里堆栈会变化的问题也解决了,接下来让我们看一下整个的转移过程是怎样的。下面就是CPU的整个过程所做的工作:

通过调用门转移的过程中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,开始执行被调用者过程。

上面就是CPU在整个过程中所做的工作。调用过程结束后会通过ret(retf)返回,那么返回过程中CPU做了哪些工作呢?看下一小节:

调用门转移结束后通过ret返回时CPU所做的工作

  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指令来实现。

接下来我们就用ret指令实现由高特权级到低特权级的转移:

通过ret指令从ring0进入ring3

通过上面的分析我们知道,在ret指令执行前,堆栈中应该已经准备好了目标代码的cseipssesp,另外还可能有参数。这些可以是处理器压入栈的,当然,也可以由我们自己压栈。在接下来的例子中,ret前的堆栈如下图所示:

这样,ret执行之后就可以转移到低特权级代码中了。接下来用代码实现如下:

在原来的代码上添加如下内容:

ring3的代码段ring3的堆栈段

 19 LABEL_DESC_CODE_RING3: Descriptor 0,SegCodeRing3Len-1, DA_C+DA_32+DA_DPL3
...

 22 LABEL_DESC_STACK3:     Descriptor 0,      TopOfStack3, DA_DRWA+DA_32+DA_DPL3
...

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

 40 SelectorCodeRing3       equ     LABEL_DESC_CODE_RING3   - LABEL_GDT + SA_RPL3
...

 43 SelectorStack3          equ     LABEL_DESC_STACK3       - LABEL_GDT + SA_RPL3
...

 75 ; 堆栈段ring3
 76 [SECTION .s3]
 77 ALIGN   32
 78 [BITS   32]
 79 LABEL_STACK3:
 80         times 512 db 0
 81 TopOfStack3     equ     $ - LABEL_STACK3 - 1
 82 ; END of [SECTION .s3]
...

379 ; CodeRing3
380 [SECTION .ring3]
381 ALIGN   32
382 [BITS   32]
383 LABEL_CODE_RING3:
384         mov     ax, SelectorVideo
385         mov     gs, ax
386
387         mov     edi, (80 * 14 + 0) * 2
388         mov     ah, 0Ch
389         mov     al, ‘3‘
390         mov     [gs:edi], ax
391
392         jmp     $
393 SegCodeRing3Len equ     $ - LABEL_CODE_RING3
394 ; END of [SECTION .ring3]

ring3堆栈段与ring3代码段的描述符的初始化代码

146         ; 初始化堆栈段描述符(Ring3)
147         xor     eax, eax
148         mov     ax, ds
149         shl     eax, 4
150         add     eax, LABEL_STACK3
151         mov     word [LABEL_DESC_STACK3 + 2], ax
152         shr     eax, 16
153         mov     byte [LABEL_DESC_STACK3 + 4], al
154         mov     byte [LABEL_DESC_STACK3 + 7], ah
...

176         ; 初始化Ring3描述符
177         xor     eax, eax
178         mov     ax, ds
179         shl     eax, 4
180         add     eax, LABEL_CODE_RING3
181         mov     word [LABEL_DESC_CODE_RING3 + 2], ax
182         shr     eax, 16
183         mov     byte [LABEL_DESC_CODE_RING3 + 4], al
184         mov     byte [LABEL_DESC_CODE_RING3 + 7], ah

由于这段代码运行在ring3,而在其中由于要写显存而访问到了VIDEO段,为了不会产生错误,我们把VIDEO段的DPL修改为3(第25行)。依据上面所说的规则,RPL不需要修改。

上面代码段和数据段都已经初始化好了。接下来将ssespcseip依次压栈,并且执行retf指令。

266         push    SelectorStack3
267         push    TopOfStack3
268         push    SelectorCodeRing3
269         push    0
270         retf

查看结果,如果出现了红色的3并且不返回到DOS(因为新添加的代码段最后是jmp $),说明我们已经成功进入ring3。

上面就是从ring0到ring3的过程。接下来开始使用调用门实现ring3到ring0的转移

通过调用门进行有特权级变换的转移

上面已经进入ring3了,接下来通过调用门重新进入ring0。将上面ring3的代码修改如下:

 28 LABEL_CALL_GATE_TEST: Gate SelectorCodeDest,   0,     0, DA_386CGate+DA_DPL3
...

 47 SelectorCallGateTest    equ     LABEL_CALL_GATE_TEST    - LABEL_GDT + SA_RPL3
...

379 ; CodeRing3
380 [SECTION .ring3]
381 ALIGN   32
382 [BITS   32]
383 LABEL_CODE_RING3:
384         mov     ax, SelectorVideo
385         mov     gs, ax
386         mov     edi, (80 * 14 + 0) * 2
387         mov     ah, 0Ch
388         mov     al, ‘3‘
389         mov     [gs:edi], ax
390
391         call    SelectorCallGateTest:0
392
393         jmp     $
394 SegCodeRing3Len equ     $ - LABEL_CODE_RING3
395 ; END of [SECTION .ring3]

jmp $之前,增加了使用调用门的指令,这个调用门是之前已经定义好了的。修改描述符和选择子是为了满足CPL和RPL都小于等于调用门DPL的条件。

不要忘记,从低特权级到高特权级转移的时候,需要用到TSS。因此接下来需要人工准备一个TSS:

 24 LABEL_DESC_TSS:        Descriptor 0,          TSSLen-1, DA_386TSS
...

 45 SelectorTSS             equ     LABEL_DESC_TSS          - LABEL_GDT
...

 85 ; TSS
 86 [SECTION .tss]
 87 ALIGN   32
 88 [BITS   32]
 89 LABEL_TSS:
 90                 DD      0                       ; Back
 91                 DD      TopOfStack              ; 0 级堆栈
 92                 DD      SelectorStack           ;
 93                 DD      0                       ; 1 级堆栈
 94                 DD      0                       ;
 95                 DD      0                       ; 2 级堆栈
 96                 DD      0                       ;
 97                 DD      0                       ; CR3
 98                 DD      0                       ; EIP
 99                 DD      0                       ; EFLAGS
100                 DD      0                       ; EAX
101                 DD      0                       ; ECX
102                 DD      0                       ; EDX
103                 DD      0                       ; EBX
104                 DD      0                       ; ESP
105                 DD      0                       ; EBP
106                 DD      0                       ; ESI
107                 DD      0                       ; EDI
108                 DD      0                       ; ES
109                 DD      0                       ; CS
110                 DD      0                       ; SS
111                 DD      0                       ; DS
112                 DD      0                       ; FS
113                 DD      0                       ; GS
114                 DD      0                       ; LDT
115                 DW      0                       ; 调试陷阱标志
116                 DW      $ - LABEL_TSS + 2       ; I/O位图基址
117                 DB      0ffh                    ; I/O位图结束标志
118 TSSLen          equ     $ - LABEL_TSS

因为这里只进入ring0,所以在这里先只初始化0级堆栈。

接下来初始化TSS描述符:

223         ; 初始化 TSS 描述符
224         xor     eax, eax
225         mov     ax, ds
226         shl     eax, 4
227         add     eax, LABEL_TSS
228         mov     word [LABEL_DESC_TSS + 2], ax
229         shr     eax, 16
230         mov     byte [LABEL_DESC_TSS + 4], al
231         mov     byte [LABEL_DESC_TSS + 7], ah

最后需要在特权级变换之前加载TSS:

313         mov     ax, SelectorTSS
314         ltr     ax

接下来开始运行,运行结果如下:

初始的32位代码段(ring0)打印一串字符串,ring3代码段打印数字3,调用门的目标代码段(ring0)打印字母C.因此到这里,我们实现了从rong0到ring3,然后再返回ring0的整个过程。接下来做最后一步,就是使程序顺利返回实模式,只需要将调用局部任务的代码加入到调用门的目标代码([SECTION .sdest])。最后,程序将由这里进入局部任务,然后由原路返回实模式。代码如下:

346 [SECTION .sdest]; 调用门目标段
347 [BITS   32]
348
349 LABEL_SEG_CODE_DEST:
350         mov     ax, SelectorVideo
351         mov     gs, ax                  ; 视频段选择子(目的)
352
353         mov     edi, (80 * 12 + 0) * 2  ; 屏幕第 12 行, 第 0 列。
354         mov     ah, 0Ch                 ; 0000: 黑底    1100: 红字
355         mov     al, ‘C‘
356         mov     [gs:edi], ax
357
358         ; Load LDT
359         mov     ax, SelectorLDT
360         lldt    ax
361
362         jmp     SelectorLDTCodeA:0      ; 跳入局部任务,将打印字母 ‘L‘。
363
364         ;retf
365
366 SegCodeDestLen  equ     $ - LABEL_SEG_CODE_DEST
367 ; END of [SECTION .sdest]

从上面的运行结果我们能够知道。到这里就实现了DOS->ring0->ring3->ring0->DOS的整个过程。

源代码

; ==========================================
; pmtest5.asm
; 编译方法:nasm pmtest5.asm -o pmtest5.com
; ==========================================

%include    "pm.inc"    ; 常量, 宏, 以及一些说明

org    0100h
    jmp    LABEL_BEGIN

[SECTION .gdt]
; GDT
;                            段基址,           段界限     , 属性
LABEL_GDT:             Descriptor 0,                 0, 0           ;空描述符
LABEL_DESC_NORMAL:     Descriptor 0,            0ffffh, DA_DRW           ;Normal描述符
LABEL_DESC_CODE32:     Descriptor 0,    SegCode32Len-1, DA_C+DA_32       ;非一致,32
LABEL_DESC_CODE16:     Descriptor 0,            0ffffh, DA_C           ;非一致,16
LABEL_DESC_CODE_DEST:  Descriptor 0,  SegCodeDestLen-1, DA_C+DA_32       ;非一致,32
LABEL_DESC_CODE_RING3: Descriptor 0, SegCodeRing3Len-1, DA_C+DA_32+DA_DPL3
LABEL_DESC_DATA:       Descriptor 0,         DataLen-1, DA_DRW             ;Data
LABEL_DESC_STACK:      Descriptor 0,        TopOfStack, DA_DRWA+DA_32       ;Stack,32
LABEL_DESC_STACK3:     Descriptor 0,       TopOfStack3, DA_DRWA+DA_32+DA_DPL3
LABEL_DESC_LDT:        Descriptor 0,          LDTLen-1, DA_LDT           ;LDT
LABEL_DESC_TSS:        Descriptor 0,          TSSLen-1, DA_386TSS       ;TSS
LABEL_DESC_VIDEO:      Descriptor 0B8000h,      0ffffh, DA_DRW+DA_DPL3

; 门                                            目标选择子,       偏移, DCount, 属性
LABEL_CALL_GATE_TEST:    Gate          SelectorCodeDest,          0,      0, DA_386CGate + DA_DPL3
; GDT 结束

GdtLen        equ    $ - LABEL_GDT    ; GDT长度
GdtPtr        dw    GdtLen - 1    ; GDT界限
        dd    0        ; GDT基地址

; GDT 选择子
SelectorNormal        equ    LABEL_DESC_NORMAL    - LABEL_GDT
SelectorCode32        equ    LABEL_DESC_CODE32    - LABEL_GDT
SelectorCode16        equ    LABEL_DESC_CODE16    - LABEL_GDT
SelectorCodeDest    equ    LABEL_DESC_CODE_DEST    - LABEL_GDT
SelectorCodeRing3    equ    LABEL_DESC_CODE_RING3    - LABEL_GDT + SA_RPL3
SelectorData        equ    LABEL_DESC_DATA        - LABEL_GDT
SelectorStack        equ    LABEL_DESC_STACK    - LABEL_GDT
SelectorStack3        equ    LABEL_DESC_STACK3    - LABEL_GDT + SA_RPL3
SelectorLDT        equ    LABEL_DESC_LDT        - LABEL_GDT
SelectorTSS        equ    LABEL_DESC_TSS        - LABEL_GDT
SelectorVideo        equ    LABEL_DESC_VIDEO    - LABEL_GDT

SelectorCallGateTest    equ    LABEL_CALL_GATE_TEST    - LABEL_GDT + SA_RPL3
; END of [SECTION .gdt]

[SECTION .data1]     ; 数据段
ALIGN    32
[BITS    32]
LABEL_DATA:
SPValueInRealMode    dw    0
; 字符串
PMMessage:        db    "In Protect Mode now. ^-^", 0    ; 进入保护模式后显示此字符串
OffsetPMMessage        equ    PMMessage - $$
StrTest:        db    "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0
OffsetStrTest        equ    StrTest - $$
DataLen            equ    $ - LABEL_DATA
; END of [SECTION .data1]

; 全局堆栈段
[SECTION .gs]
ALIGN    32
[BITS    32]
LABEL_STACK:
    times 512 db 0
TopOfStack    equ    $ - LABEL_STACK - 1
; END of [SECTION .gs]

; 堆栈段ring3
[SECTION .s3]
ALIGN    32
[BITS    32]
LABEL_STACK3:
    times 512 db 0
TopOfStack3    equ    $ - LABEL_STACK3 - 1
; END of [SECTION .s3]

; TSS ---------------------------------------------------------------------------------------------
[SECTION .tss]
ALIGN    32
[BITS    32]
LABEL_TSS:
        DD    0            ; Back
        DD    TopOfStack        ; 0 级堆栈
        DD    SelectorStack        ;
        DD    0            ; 1 级堆栈
        DD    0            ;
        DD    0            ; 2 级堆栈
        DD    0            ;
        DD    0            ; CR3
        DD    0            ; EIP
        DD    0            ; EFLAGS
        DD    0            ; EAX
        DD    0            ; ECX
        DD    0            ; EDX
        DD    0            ; EBX
        DD    0            ; ESP
        DD    0            ; EBP
        DD    0            ; ESI
        DD    0            ; EDI
        DD    0            ; ES
        DD    0            ; CS
        DD    0            ; SS
        DD    0            ; DS
        DD    0            ; FS
        DD    0            ; GS
        DD    0            ; LDT
        DW    0            ; 调试陷阱标志
        DW    $ - LABEL_TSS + 2    ; I/O位图基址
        DB    0ffh            ; I/O位图结束标志
TSSLen        equ    $ - LABEL_TSS
; TSS ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

[SECTION .s16]
[BITS    16]
LABEL_BEGIN:
    mov    ax, cs
    mov    ds, ax
    mov    es, ax
    mov    ss, ax
    mov    sp, 0100h

    mov    [LABEL_GO_BACK_TO_REAL+3], ax
    mov    [SPValueInRealMode], sp

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

    ; 初始化 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

    ; 初始化测试调用门的代码段描述符
    xor    eax, eax
    mov    ax, cs
    shl    eax, 4
    add    eax, LABEL_SEG_CODE_DEST
    mov    word [LABEL_DESC_CODE_DEST + 2], ax
    shr    eax, 16
    mov    byte [LABEL_DESC_CODE_DEST + 4], al
    mov    byte [LABEL_DESC_CODE_DEST + 7], ah

    ; 初始化数据段描述符
    xor    eax, eax
    mov    ax, ds
    shl    eax, 4
    add    eax, LABEL_DATA
    mov    word [LABEL_DESC_DATA + 2], ax
    shr    eax, 16
    mov    byte [LABEL_DESC_DATA + 4], al
    mov    byte [LABEL_DESC_DATA + 7], ah

    ; 初始化堆栈段描述符
    xor    eax, eax
    mov    ax, ds
    shl    eax, 4
    add    eax, LABEL_STACK
    mov    word [LABEL_DESC_STACK + 2], ax
    shr    eax, 16
    mov    byte [LABEL_DESC_STACK + 4], al
    mov    byte [LABEL_DESC_STACK + 7], ah

    ; 初始化堆栈段描述符(ring3)
    xor    eax, eax
    mov    ax, ds
    shl    eax, 4
    add    eax, LABEL_STACK3
    mov    word [LABEL_DESC_STACK3 + 2], ax
    shr    eax, 16
    mov    byte [LABEL_DESC_STACK3 + 4], al
    mov    byte [LABEL_DESC_STACK3 + 7], ah

    ; 初始化 LDT 在 GDT 中的描述符
    xor    eax, eax
    mov    ax, ds
    shl    eax, 4
    add    eax, LABEL_LDT
    mov    word [LABEL_DESC_LDT + 2], ax
    shr    eax, 16
    mov    byte [LABEL_DESC_LDT + 4], al
    mov    byte [LABEL_DESC_LDT + 7], ah

    ; 初始化 LDT 中的描述符
    xor    eax, eax
    mov    ax, ds
    shl    eax, 4
    add    eax, LABEL_CODE_A
    mov    word [LABEL_LDT_DESC_CODEA + 2], ax
    shr    eax, 16
    mov    byte [LABEL_LDT_DESC_CODEA + 4], al
    mov    byte [LABEL_LDT_DESC_CODEA + 7], ah

    ; 初始化Ring3描述符
    xor    eax, eax
    mov    ax, ds
    shl    eax, 4
    add    eax, LABEL_CODE_RING3
    mov    word [LABEL_DESC_CODE_RING3 + 2], ax
    shr    eax, 16
    mov    byte [LABEL_DESC_CODE_RING3 + 4], al
    mov    byte [LABEL_DESC_CODE_RING3 + 7], ah

    ; 初始化 TSS 描述符
    xor    eax, eax
    mov    ax, ds
    shl    eax, 4
    add    eax, LABEL_TSS
    mov    word [LABEL_DESC_TSS + 2], ax
    shr    eax, 16
    mov    byte [LABEL_DESC_TSS + 4], al
    mov    byte [LABEL_DESC_TSS + 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, 并跳转到 Code32Selector:0  处

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

LABEL_REAL_ENTRY:        ; 从保护模式跳回到实模式就到了这里
    mov    ax, cs
    mov    ds, ax
    mov    es, ax
    mov    ss, ax

    mov    sp, [SPValueInRealMode]

    in    al, 92h        ; ┓
    and    al, 11111101b    ; ┣ 关闭 A20 地址线
    out    92h, al        ; ┛

    sti            ; 开中断

    mov    ax, 4c00h    ; ┓
    int    21h        ; ┛回到 DOS
; END of [SECTION .s16]

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

LABEL_SEG_CODE32:
    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, OffsetPMMessage    ; 源数据偏移
    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:    ; 显示完毕

    call    DispReturn

    ; Load TSS
    mov    ax, SelectorTSS
    ltr    ax
    ;在任务内发生特权级变换时要切换堆栈,而内层堆栈的指针存放在当前任务的TSS中,所以要设置任务状态段寄存器TR。

    push    SelectorStack3
    push    TopOfStack3
    push    SelectorCodeRing3
    push    0
    retf        ; Ring0 -> Ring3,历史性转移!将打印数字 ‘3‘。

; ------------------------------------------------------------------------
DispReturn:
    push    eax
    push    ebx
    mov    eax, edi
    mov    bl, 160
    div    bl
    and    eax, 0FFh
    inc    eax
    mov    bl, 160
    mul    bl
    mov    edi, eax
    pop    ebx
    pop    eax

    ret
; DispReturn 结束---------------------------------------------------------

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

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

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

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

    ; Load LDT
    mov    ax, SelectorLDT
    lldt    ax

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

    ;retf

SegCodeDestLen    equ    $ - LABEL_SEG_CODE_DEST
; END of [SECTION .sdest]

; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式
[SECTION .s16code]
ALIGN    32
[BITS    16]
LABEL_SEG_CODE16:
    ; 跳回实模式:
    mov    ax, SelectorNormal
    mov    ds, ax
    mov    es, ax
    mov    fs, ax
    mov    gs, ax
    mov    ss, ax

    mov    eax, cr0
    and    al, 11111110b
    mov    cr0, eax

LABEL_GO_BACK_TO_REAL:
    jmp    0:LABEL_REAL_ENTRY    ; 段地址会在程序开始处被设置成正确的值

Code16Len    equ    $ - LABEL_SEG_CODE16

; END of [SECTION .s16code]

; LDT
[SECTION .ldt]
ALIGN    32
LABEL_LDT:
;                                         段基址       段界限     ,   属性
LABEL_LDT_DESC_CODEA:    Descriptor           0,     CodeALen - 1,   DA_C + DA_32    ; Code, 32 位

LDTLen        equ    $ - LABEL_LDT

; LDT 选择子
SelectorLDTCodeA    equ    LABEL_LDT_DESC_CODEA    - LABEL_LDT + SA_TIL
; END of [SECTION .ldt]

; CodeA (LDT, 32 位代码段)
[SECTION .la]
ALIGN    32
[BITS    32]
LABEL_CODE_A:
    mov    ax, SelectorVideo
    mov    gs, ax            ; 视频段选择子(目的)

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

    ; 准备经由16位代码段跳回实模式
    jmp    SelectorCode16:0
CodeALen    equ    $ - LABEL_CODE_A
; END of [SECTION .la]

; CodeRing3
[SECTION .ring3]
ALIGN    32
[BITS    32]
LABEL_CODE_RING3:
    mov    ax, SelectorVideo
    mov    gs, ax            ; 视频段选择子(目的)

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

    call    SelectorCallGateTest:0    ; 测试调用门(有特权级变换),将打印字母 ‘C‘。
    jmp    $
SegCodeRing3Len    equ    $ - LABEL_CODE_RING3
; END of [SECTION .ring3]
时间: 2024-11-05 13:40:03

一个操作系统的实现(5)-关于特权级的相关文章

一个操作系统的实现——笔记4

在2^k*2^k个方格组成的棋盘中,有一个方格被占用,用下图的4种L型骨牌覆盖所有棋盘上的其余所有方格,不能重叠. 代码如下: def chess(tr,tc,pr,pc,size): global mark global table mark+=1 count=mark if size==1: return half=size//2 if pr<tr+half and pc<tc+half: chess(tr,tc,pr,pc,half) else: table[tr+half-1][tc+

《Orange&#39;S:一个操作系统的实现》笔记(一)

感觉自己对于操作系统始终没有一个清楚的概念,尤其最近困扰于实模式.保护模式以及寻址方式等一些概念.转而一想,所有的程序,最终都是操作的计算机资源,需要和操作系统打交道,所以操作系统有必要深入了解一下.最终想要自己动手编写一个简单的版本,上网查.网友对于于渊的<Orange'S:一个操作系统的实现>和<30天自制操作系统>评价挺高的,先选<orange>为学习手册.<30>为参考手册,开始自己的操作系统之旅. 首先是平台的搭建问题,首先因本人编程一般都是在自己

一个操作系统的实现(1)-准备工作

今天开始看<Orange'S:一个操作系统的实现>一书.这里是ubuntu 16.04下开发环境的搭建以及实现一个最小的操作系统(准确地说应该是一个引导扇区). 工欲善其事,必先利其器.自制一个操作系统需要的工具如下: 汇编编译器NASM 虚拟机算计Bochs 软盘绝对扇区读写工具(这里直接使用强大的dd) 现在就来安装这些工具 安装NASM 通过源代码安装,官网下载最新版源代码,当前最新的是nasm-2.12.01版本,下面的内容以此来演示. 个人软件一般安装到/usr/local下,所以压

一个操作系统的实现(4)-认识LDT

看到这里,你应该已经很了解GDT了,如果还不了解GDT.请看这篇文章:一个操作系统的实现(2)-认识保护模式,认识保护模式那篇文章的最后详细介绍了由16位寻址升级到32位寻址而引入的GDT. LDT(Local Descriptor Table):从名字上面就可以看出来它与GDT(Gobal Descriptor Table)的区别.GDT是全局描述符表,LDT是局部描述符表(相对于GDT). 下面仍然是从代码的角度讲解什么是LDT.主要讲解在上一节的基础上增加的代码.在文章的最后会附上所有代码

《ORANGE&#39;S一个操作系统的实现》第7章 TTY与键盘输入的关系。

背景:我感觉这块部分有一些逻辑上的复杂,于是我把它的关系结构画了一张图来表述,并且在图上解答了我自己想到的几个问题. 关系如图所示: <ORANGE'S一个操作系统的实现>第7章 TTY与键盘输入的关系. 原文地址:https://www.cnblogs.com/vizdl/p/12178667.html

一个操作系统的实现中jmp dword SelectorCode32:0的理解

; 准备切换到保护模式 mov eax, cr0 or eax, 1 mov cr0, eax ; 真正进入保护模式 jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs, ; 并跳转到 Code32Selector:0 处 这段时间在一个操作系统的实现 好书啊,感谢作者 这两天一直纠结在    ; jmp    dword SelectorCode32:0    ; 这句话,看了些资料,现将自己理解的分享一下,不对的地方,大家指点一

一个操作系统的实现(9)-中断和异常

这节讲了中断与异常的一些基本概念.然后通过代码实现一个显示字符的中断和时钟中断. 实模式与保护模式下的中断有区别 保护模式下的中断与实模式下的中断有几点不同. 实模式下的中断向量表在保护模式下被IDT取代 实模式下可以使用BIOS中断,而保护模式下不能用 这里面出现了一个新的名词IDT,接下来就介绍什么是IDT. 中断描述符表(IDT,Interrupt Descriptor Table) 中断描述符表的作用 与GDT和LDT一样,IDT也是一个描述符表,IDT的描述符可以是下面三种之一: 中断

《一个操作系统的实现》读书笔记--第三章---不同特权级代码段之间的跳转

http://blog.csdn.net/begginghard/article/details/7262901 1.特权级 2.一致代码段和非一致代码段 3.DPL.RPL.CPL分别代表的含义,存储在什么位置,以及它们之间的关系 4.不同特权级数据段之间的访问规则 5.不同特权级代码段之间的转移 6.代码段之间的转移对堆栈的影响 7.结合pmtest5.asm来见证不同特权级代码段之间的跳转 一.特权级在IA32的分段机制下,特权级总共有4个特权级别,从高到低分别是0.1.2.3.数字越小表

一个操作系统的实现(11)-让操作系统进入保护模式

这节首先介绍了突破引导扇区只有512字节的原理,然后介绍了FAT12文件系统,最后通过实验加载loader并将控制权交给loader来实现突破512字节的束缚. 突破512字节的限制 前面所用的引导扇区只有512字节.然而实际上操作系统在启动过程需要做的事情是很多的.所以需要通过某种方法突破512字节的限制. 那么如何突破512字节的限制呢?一种方法是再建立一个文件,通过引导扇区把它加载到内存,然后把控制权教给它.这样,512字节的束缚就没有了. 这里被引导扇区加载进内存的并不是操作系统的内核.