《ORANGE-s-一个操作系统实现》保护模式(2-2)

保护模式进阶

前面我们学习了保护模式,对它的一个整体有了了解,其中突出讲解了保护模式下的强大寻址能力,但是保护模式不仅仅有这个优点,下面我们继续学习保护模式。

1.补个坑

前面我们还是把保护模式写在引导扇区,这就限制在了512字节,我们就借用DOS的引导扇区来解除这个限制。
首先,我们按照如下操作进行:

  • 到Bochs官网下载FreeDOs。解压后将其中的a.img复制到工作目录下,并改名为freedos.img。
  • 然后用bximage深层一个软盘映像,命名为pm.img。
  • 在修改bochsrc,为如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    ###############################################################
    # Configuration file for Bochs
    ###############################################################

    # how much memory the emulated machine will have
    megs: 32

    # filename of ROM images
    romimage: file=$BXSHARE/BIOS-bochs-latest
    vgaromimage: file=$BXSHARE/VGABIOS-lgpl-latest

    # what disk images will be used
    floppya: 1_44=freedos.img, status=inserted
    floppyb: 1_44=pm.img, status=inserted

    # choose the boot disk.
    boot: a

    # where do we send log messages?
    # log: bochsout.txt

    # disable the mouse
    mouse: enabled=0

    # enable key mapping, using US layout as default.
    keyboard: keymap=$BXSHARE/keymaps/x11-pc-us.map
  • 然后启动bochs ,初始化b盘,如图:
    mark

  • 再将代码中的07c00h改为0100h,并重新编译如图

    1
    nasm pmtest2.asm -o pmtest2.com
  • 再将pmtest2.com复制到软盘pm.img上,执行如下图命令:

    1
    2
    3
    sudo mount -o loop pm.img /mnt/floppy
    sudo cp pmtest2.com /mnt/floppy/
    sudo umount /mnt/floppy

mark
最后的调试结果如图:
mark

2.保护模式进阶正题

坑补完了,下面正式开始保护模式的进阶学习,前面我们提到实模式下1MB的寻址能力太差了,上一篇为了突出重点,所以最后直接写的死循环,所以想要退出,只能重启电脑,现在我们尝试体验下保护模式强大的寻址能力。
首先实验下读写大地址内存。在前面的代码的基础上,新建一个段段以5MB为基址,远远超出实模式下的1MB界限。
新增的代码段为:

1
2
3
LABEL_DESC_DATA:   Descriptor    0,      DataLen-1, DA_DRW    ; Data
LABEL_DESC_STACK: Descriptor 0, TopOfStack, DA_DRWA+DA_32; Stack, 32 位
LABEL_DESC_TEST: Descriptor 0500000h, 0ffffh, DA_DRW

在上篇代码的这个位置:
mark

1
2
3
SelectorData		equ	LABEL_DESC_DATA		- LABEL_GDT
SelectorStack equ LABEL_DESC_STACK - LABEL_GDT
SelectorTest equ LABEL_DESC_TEST - LABEL_GDT

mark
然后需要添加的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[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]

如下图所示
mark
接下来我就不贴代码了,主要分析代码,理解其中原理,具体代码可以在这里下载密码为:xang

3.代码以及原理分析

我们直接看到第166行,也就是[SECTION .s32]段,这一段首先将ds、es、gs分别初始化,ds指向新增的数据段,es指向新增的5MB内存,gs指向显存。
之后是显示字符串,再然后是开始读写大地址内存了(第198行到200行),由于要读写2次相同的内存,所以我们读写分别用函数TestRead、TestWrite来表示这两个函数的入口分别在206行和222行,其中TestRead还调用了DispAL(将al中的字节用16进制数表示,字的前景色任然是红色)和DispReturn(模拟回车,让下一个字符显示在下一行的开头处)这两个函数。注意edi始终要指向显示的下一个字符的位置,所以,如果程序除了显示字符外还用到edi,需要事先保存它的值,以免在显示时产生混乱。
最后,我们说下如何在保护模式下跳转到实模式。因为我们不能从323位代码返回实模式,只能从16位返回,这是因为无法实现从32位代码段返回时,cs高速缓冲寄存器中的属性符合实模式的要求(实模式不能改变段属性)。所以我们增加了第15行Normal描述符,在返回实模式之前,应该把选择子SelectorNormal加载到ds、es、ss.
下面我们再看看返回到实模式的段[SECTION .s16code],如上所说,把Normal赋给ds、es、fs、gs和ss,然后cr0的PE位置为0,接下来跳转。
这块还不理解,以后理解了再补上!!!
再跳回实模式之后,关闭A20,打开中断,重新回到原来的样子。

4.LDT(Local Descriptor Table)

- 感性认识

LDT是局部描述符表的建成,我们先还是通过代码来对它产生感性认识。在这里,我就不再贴代码了,仅仅把重要的代码写出来,方便理解。下面有这个部分的完整可执行代码下载。
下面是代码执行完的结果:
mark
[这里下载]https://pan.baidu.com/s/1mkj6GPi)
密码为:uzja

- 代码分析

在pmtest3.asm中,从11行开始到134行,是对LDT的初始化,包括对选择子的创建。
接着是新增的两个节,其中一个是新的描述符表,也就是LDT,另一个是代码段,对应新增的LDT中的一个描述符。
然后在217行到220行,是加载ldte,这里和GDT很相似,但是在选择子上,多了一个SA_TIL,这个属性在pm.inc中可以看到,是:

1
SA_TIL EQU 4

mark
从上图可知这个属性将SelectorLDTCodeA的TI位 置为1.
这一位是区别GDT的选择子和LDT的选择子的关键。如果TI被置位,那么系统将从LDT中寻找相应描述符。
总结下这部分内容,我们已经看到,在描述符中段基址段界限定义了一个段的范围,这无疑是一种对段的保护。所以,不知不觉,我们已经接触到了一些保护机制。接下来,我们将加深对“保护”的理解,下面,我们即将介绍的是特权级。

5.特权级

  • 特权级概述
    在IA32的分段机制中,特权级总共有4个特权级别,从高到低分别为0、1、2、3.数字越小特权级越大。
    所以我们也将高特权的称为内层,而把低特权级称为外层。
  • CPL、DPL、RPL
    CPU通过识别以上三种特权级进行特权级检验。
    首先,CPL(Current Priviliege Level)。它是当前只想能够的程序或者任务的特权级,被存储在cs和ss的第0位和第1位上。在通常情况下,CPL等于代码所在的段的特权级。在遇到一致代码段时,可以被相同或者更低特权的代码访问。否则不改变CPL。
    其次,DPL(Descriptor Privilege Level)。DPL表示段或者门的特权级。它被存储在段描述符或者门描述符的DPL字段中。根据门或者段的类型不同,DPL将会被区别对待。在数据段中,DPL规定了可以访问此段的最低特级权;在非一致代码段,DPL规定访问此段的特权级;在调用门,DPL规定了当前执行的程序或任务可以访问此调用门的最低特权级,和数据段一样。
    最后,RPL(Requested Privilege Level)。RPL是通过段选择子的第0位和第1位表现出来的。CPU通过检测RPL和CPl来确认一个访问请求是否合法。也就是说,如果RPL的数字比CPL大,呢么RPL将会起决定性作用。
    操作系统往往用RPL来避免低特权应用访问高特权段内的数据。
  • 一个特权级检验实验
    由上面我们很容易知道,对于数据的访问,特权级检验还是比较简单的,只要CPL和RPL都小于被访问的数据段的DPL就可以了。
    我们现在就开始实验:
    首先,将先前例子中的数据段描述符的DPL修改一下,将LABEL_DESC_DATA对应的段描述符的DPL修改为1:
    1
    LABEL_DESC_DATA:   Descriptor       0,       DataLen - 1, DA_DRW+DA_DPL1	; Data

运行后结果不变。
接着,继续将刚修改过的数据段的选择子RPL改为3

1
SelectorData		equ	LABEL_DESC_DATA		- LABEL_GDT+SA_RPL3

这时,出现错误了
mark
这个错误很好理解,我们违反了特权级的规则,用RPL=3的选择子去访问DPL=1的段,浴室引起异常。而我们又没有相应的异常处理模块,于是出错。
下面,我们再看看不同特权级之间的转移情况是怎样的。

  • 不同特权级代码之间的转移
    程序从一个代码段到另一个代码段之前,目标代码的选择子会被加载到cs中。作为加载过程的一部分,处理器会坚持描述符的界限、类型、特权级等。
    程序控制转移的发生,可以是由指令jmp、call、ret、sysenter、sysexit、int或iret引起,也可以由中断和异常机制引起。

    6.特权级转移

  • 首先,同门先来看看通过jmp或者call进行直接转移
    对通过jmp或call进行直接转移,如果目标是非一致性代码段,要求CPL必须等于目标段的DPL,同时要求RPL小雨等于DPL;如果是一致代码段,则要求CPL大于或者等于目标段的DPL,此时RPL不做检查。
    所以想要自由的进行不同特权级之间的转移,就需要用门或者TSS.
    -接着, “门”的初体验
    门也是一种描述符,它的结构如下:
    mark
    门描述符的结构就是这样子,直观可以看出,一个门描述了有一个选择子和一个偏移所指定的线性地址,程序正是同这个地址进行转移的。门描述符分为4种:
    (1)调用门(Call Gates)
    (2)中断门(Interrupt Gates)
    (3)陷阱门(Trap Gates)
    (4)任务门(Task Gates)
    我们先看看调用门,值关注它的工作方式,在代码段pmtest3.asm的基础上增加一个代码段作为调用门转移的目标段:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    [SECTION .sdest]; 调用门目标段
    [BITS 32]

    LABEL_SEG_CODE_DEST:
    ;jmp $
    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

    retf

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

我们带酸用call指令掉哦也难怪将要建立的调用门,所以,在这段代码的结尾调用了一个retf指令。
接着,计入这个代码段的描述符,选择子以及初始化这个描述符的代码。

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

1
SelectorCodeDest	equ	LABEL_DESC_CODE_DEST	- LABEL_GDT
1
2
3
4
5
6
7
8
9
; 初始化测试调用门的代码段描述符
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

初始化描述符已经能够很熟悉了,以后就不再赘述了。
下面添加调用门:

1
2
3

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

这个,我们用了一个宏Gate来初始化,这个宏可以在pm.inc中找到。他和Descriptor宏有点类似,也是将描述符的构成要素分别安置在相应的位置,是代码看起来非常清晰。
我们的门描述符的属性是DA_386CGate,表示它是一个调研那个门。里面指定的选择子是SelectorCodeDest,表明代码段是刚刚新调价的代码段。偏移地址是0,表示将跳转到目标代码段的开头出。另外,我们把其DPL指定为0.
好了,现在调用门准备就绪了,它指向的位置是SelectorCodeDest:0,即标号LABEL_SEG_CODE_DEST处的代码。
到这里,我们就可以用call指令来使用这个调用门了,这个call指令被放进局部任务之前,由于我们新加的代码以指令retf结尾,所以代码最终将会跳回call指令的下面继续执行,所以,我们最终会看到在pmtest3执行结果的基础上,多出一个红色的字母C。如图所示:
mark
总结起来,调用门听起来很可怕,本质上只不过是个入口地址,只是增加了若干的属性而已。
我们接下来,激昂要用它来实现不同特权级的代码之间的转移,下图是特权检测的规则:
mark
从上可知,通过调用门和call指令,可以实现从低特权级到高特权级的转移。

下面,我们看一下整个转移过程是怎样的。

  • 根据目标代码段的DPL从TSS中选择应该切换至哪个ss和esp
  • 从TSS中读取新的ss和esp。在这个过程中如果发现ss、esp或者TSS界限错误都会导致无效TSS异常
  • 对ss描述符进行检验,若异常,同样产生异常
  • 暂时性地保存当前ss和esp的值
  • 加载新的ss和esp
  • 将刚刚保存起来的ss和esp的值压入新栈
  • 从调用者堆栈中将参数复制到被调用者堆栈中,复制参数的数目由调用门中Param Count一项来决定。
  • 将当前的cs和eip压栈。
  • 加载调用门中指定的心得cs和eip,开始执行被调用者过程。
    综上,使用调用门的过程实际上分为两个部分,一部分是从低特权级到高特权级,通过调用门和call指令来实现;另一部分是从高特权级到di低特权级,通过ret指令来实现。
您的支持将鼓励我继续创作!