一个操作系统的实现:第三篇——保护模式(Protect Mode)

目录

汇编知识:

GDT(Global Descriptor Table)全局描述符表

GDTR 全局描述符寄存器

段选择子(Selector)

描述符(Descriptor)

LDTR 局部描述符寄存器

TR 任务寄存器

门描述符:

进入保护模式的主要步骤:

保护模式下寻址的机制:

一致代码段:

非一致代码段:

特权级:

TSS结构:

什么叫做“页”:

PDE和PTE:

中断描述符表(Interrupt Descriptor Table)

外部中断:

8259A可编程中断控制器:

保护模式下的I/O:

 IOPL:

I/O许可位图(I/O Permission Bitmap):

“保护模式”包含如下几方面的含义:

代码段:


参考链接:

指令对标志寄存器的影响总结:https://wenku.baidu.com/view/4db7b8795f0e7cd185253610.html

GDT,LDT,GDTR,LDTR 详解:http://www.techbulo.com/708.html

两张图看懂GDT、GDTR、LDT、LDTR的关系:https://blog.csdn.net/Six_666A/article/details/80634972

CR0-4寄存器介绍:https://blog.csdn.net/qq_37414405/article/details/84487591

汇编指令: LGDT、LIDT、LLDT、LMSW、LOADALL、LOADALL286、LOCK、LODSB、LODSW、LODSD:https://blog.csdn.net/xboxmicro/article/details/20007647

NASM汇编指令复习:https://www.cnblogs.com/magic-cube/archive/2011/11/01/2232280.html

这个MOV ax,4c00h int 21h是啥作用:https://zhidao.baidu.com/question/1952329676112232388.html

BIOS 中断大全:https://blog.csdn.net/weixin_37656939/article/details/79684611

保护模式下寻址:https://blog.csdn.net/littlehedgehog/article/details/2089504

TSS(任务状态段) TSS描述符 任务寄存器(TR) 任务门描述符 学习总结:https://blog.csdn.net/chen1540524015/article/details/74075252?utm_source=blogxgwz5

 

汇编知识:

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第1张图片

LGDT: 加载全局描述符
LIDT:加载中断描述符
LLDT:加载局部描述符
LTR:装载任务状态段寄存器TR的指令。该指令的操作数是对应TSS段描述符的选择子。
LMSW:加载状态字
LOADALL:加载所有
LOADALL286:加载所有286
LOCK:锁

LODS/LODSB/LODSW/LODSD块装入指令:

将由源变址寄存器DS:E(SI)寻址的一个内存指向的存储器操作数OPS装入到累加器AL/AX/EAX中, 并根据DF之值自动修改地址【当方向标志位D=0时,则SI自动增加;D=1时,SI自动减小。】,为下次传送作准备.LODSB/LODSW /LODSD是不操作数的字符串装入指令.只是LODSB,LODSW,LODSD装入的分别是字节,字 ,双字.

stosb, stosw, stosd字符串处理指令:

这三个指令把al/ ax/ eax的内容存储到es:edi指向的内存单元中,同时edi的值根据方向标志的值增加或者减少。

 JC(Jump if Carry)当运算产生进位标志时,即CF=1时,跳转到目标程序处
JNC,当CF=0时跳转;
JZ,当ZF=1时跳转,
JNZ,当ZF=0时跳转;
JO,当OF=1时跳转,
JNO,当OF=0时跳转;
JP,当PF=1时跳转……

ret、retf、iref的区别
ret  弹出一个参数,给ip ,返回
retf 弹出2个参数,一个给 ip,一个给 cs ,返回
iref 弹出 3个参数,一个给 ip,一个 给 cs ,一个给flag标志位 ,返回

控制寄存器(CR0~CR3)用于控制和确定处理器的操作模式以及当前执行任务的特性。

CR0中含有控制处理器操作模式和状态的系统控制标志;

CR1保留不用;

CR2含有导致页错误的线性地址;

CR3中含有页目录表物理内存基地址,因此该寄存器也被称为页目录基地址寄存器PDBR(Page-Directory Base addressRegister)。

CR0:

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第2张图片

PE:CR0的位0是启用保护(Protection Enable)标志。当设置该位时即开启了保护模式;当复位时即进入实地址模式。
PG:CR0的位31是分页(Paging)标志。当设置该位时即开启了分页机制;当复位时则禁止分页机制,此时所有线性地址等同于物理地址。
WP:对于Intel 80486或以上的CPU,CR0的位16是写保护(Write Proctect)标志。当设置该标志时,处理器会禁止超级用户程序(例如特权级0的程序)向用户级只读页面执行写操作;当该位复位时则反之。
NE:对于Intel 80486或以上的CPU,CR0的位5是协处理器错误(Numeric Error)标志。当设置该标志时,就启用了x87协处理器错误的内部报告机制;若复位该标志,那么就使用PC形式的x87协处理器错误报告机制。

CR3:
        cr3又叫做PDBR(Page-Directory Base Register)。用来存放最高级页目录地址(物理地址),各级页表项中存放的也是物理地址。它的高20位将是页目录表首地址的高20位,页目录表首地址的低12位会是零,也就是说,页目录表会是4KB对齐的。类似地,PDE中的页表基址(Page- Table Base Address)以及PTE中的页基址(Page Base Address)也是用高20位来表示4KB对齐的页表和页。格式如下:

 

GDT(Global Descriptor Table)全局描述符表

在整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT),GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。GDTR中存放的是GDT在内存中的基地址和其表长界限。GDT的作用是用来提供段式存储机制,这种机制是通过段寄存器和GDT中的描述符共同提供的。

在IA32下,CPU有两种工作模式:实模式和保护模式。直观地看,当我们打开自己的PC,开始时CPU是工作在实模式下的,经过某种机制之后,才进入保护模式。在保护模式下,CPU有着巨大的寻址能力,并为强大的32位操作系统提供了更好的硬件保障。如果你还是不明白,我们不妨这样类比,实模式到保护模式的转换类似于政权的更替,开机时是在实模式下,就好像皇帝A在执政,他有他的一套政策,你需要遵从他订立的规则,否则就可能被杀头。后来通过一种转换,类似于革命,新皇帝B登基,登基的那一刻类似于程序中那个历史性的jmp(我们后面会有专门的介绍)。之后,B又有他的一套完全不同的新政策。当然,新政策比老政策好得多,对人民来说有更广阔的自由度,虽然它要复杂得多,这套新政策就是保护模式。我们要学习的,就是新政策是什么,我们在新政策下应该怎样做事。

我们先来回忆一下旧政策。Intel 8086是16位的CPU,它有着16位的寄存器(Register)、16位的数据总线(Data Bus)以及20位的地址总线(Address Bus)和1MB的寻址能力。一个地址是由段和偏移两部分组成的,物理地址遵循这样的计算公式:
物理地址(PhysicalAddress)=段值(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中的描述符共同提供的。

GDTR 全局描述符寄存器

48位,高32位存放GDT基址,低16为存放GDT限长。

段选择子(Selector)

由GDTR访问全局描述符表是通过“段选择子”(实模式下的段寄存器)来完成的。段选择子是一个16位的寄存器(同实模式下的段寄存器相同)

RPL(Requested Privilege Level): 请求特权级,用于特权检查。
TI(Table Indicator): 引用描述符表指示位
    TI=0 指示从全局描述符表GDT中读取描述符;
    TI=1 指示从局部描述符表LDT中读取描述符。

描述符(Descriptor)

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第3张图片

P位 存在(Present)位。P=1表示段在内存中存在;P=0表示段在内存中不存在。

DPL 描述符特权级(Descriptor Privilege Level)。特权级可以是0、1、2或者3。数字越小特权级越大。

S位 指明描述符是数据段/代码段描述符(S=1)还是系统段/门描述符(S=0)。

TYPE 描述符类型。
S=1 时TYPE中的4个二进制位情况:
     3             2            1           0
   执行位 一致位 读写位 访问位
执行位:置1时表示可执行,置0时表示不可执行;
一致位:置1时表示一致码段,置0时表示非一致码段;
读写位:置1时表示可读可写,置0时表示只读;
访问位:置1时表示已访问,置0时表示未访问。
所以一致代码段和非一致代码段的意思就是指这个一致位是否置1,置1就是一致代码段,置0就为非一致代码段。

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第4张图片

G位 段界限粒度(Granularity)位。G=0时段界限粒度为字节;G=1时段界限粒度为4KB。

D/B位 这一位比较复杂,分三种情况。
        在可执行代码段描述符中,这一位叫做D位。D=1时,在默认情况下指令使用32位地址及32位或8位操作数;D=0时,在默认情况下使用16位地址及16位或8位操作数。
       在向下扩展数据段的描述符中,这一位叫做B位。B=1时,段的上部界限为4GB;B=0时,段的上部界限为64KB。
       在描述堆栈段(由ss寄存器指向的段)的描述符中,这一位叫做B位。B=1时,隐式的堆栈访问指令(如push、pop和call)使用32位堆栈指针寄存器esp;D=0时,隐式的堆栈访问指令(如push、pop和call)使用16位堆栈指针寄存器sp。

AVL位 保留位,可以被系统软件使用。

LDT(Local Descriptor Table)局部描述符表

局部描述符表可以有若干张,每个任务可以有一张。我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。

A-32处理器仍然使用xxxx:yyyyyyyy(段选择器:偏移量)逻辑方式表示一个线性地址,那么是怎么得到段的基址呢?在上面说明中我们知道,要得到段的基址首先通过段选择符xxxx中TI位指定的段描述符所在位置: 当 TI=0时表示段描述符在GDT中,如下图所示:

① 先从GDTR寄存器中获得GDT基址。

② 然后再GDT中以段选择符高13位位置索引值得到段描述符。

③ 段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第5张图片

当TI=1时表示段描述符在LDT中,如下图所示:

① 还是先从GDTR寄存器中获得GDT基址。

② 从LDTR寄存器中获取LDT所在段的位置索引(LDTR高13位)。

③ 以这个位置索引在GDT中得到LDT段描述符从而得到LDT段基址。

④ 用段选择符高13位位置索引值从LDT段中得到段描述符。

⑤ 段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第6张图片

LDTR 局部描述符寄存器

16位,高13为存放LDT在GET中的索引值。参考段选择子结构。

IDTR 中断描述符表寄存器

与GDTR的作用类似,IDTR寄存器用于存放中断描述符表IDT的32位线性基地址和16位表长度值。指令LIDT和SIDT分别用于加载和保存IDTR寄存器的内容。在机器刚加电或处理器复位后,基地址被默认地设置为0,而长度值被设置成0xFFFF。

TR 任务寄存器

TR用于寻址一个特殊的任务状态段(TaskState Segment,TSS)。TSS中包含着当前执行任务的重要信息。

TR寄存器用于存放当前任务TSS段的16位段选择符、32位基地址、16位段长度和描述符属性值。它引用GDT表中的一个TSS类型的描述符。指令LTR和STR分别用于加载和保存TR寄存器的段选择符部分。当使用LTR指令把选择符加载进任务寄存器时,TSS描述符中的段基地址、段限长度以及描述符属性会被自动加载到任务寄存器中。当执行任务切换时,处理器会把新任务的TSS的段选择符和段描述符自动加载进任务寄存器TR中。

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第7张图片

门描述符:

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第8张图片

门描述符的结构就是这样的,直观来看,一个门描述了由一个选择子和一个偏移所指定的线性地址,程序正是通过这个地址进行转移的。门描述符分为4种:
调用门(Call gates)
中断门(Interrupt gates)
陷阱门(Trap gates)
任务门(Task gates)

 

进入保护模式的主要步骤:

1. 准备GDT。
2. 用lgdt加载gdtr。
3. 打开A20。
4. 置cr0的PE位。
5. 跳转,进入保护模式。

保护模式下寻址的机制:

1、段寄存器中存放段选择子Selector
2、GDTR中存放着段描述符表的首地址
3、通过选择子根据GDTR中的首地址,就能找到对应的段描述符
4、段描述符中有段的物理首地址,就得到段在内存中的首地址
5、加上偏移量,就找到在这个段中存放的数据的真正物理地址。

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第9张图片

一致代码段:

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

一致代码段的限制作用:
1.特权级高的程序不允许访问特权级低的数据:核心态不允许调用用户态的数据.
2.特权级低的程序可以访问到特权级高的数据.但是特权级不会改变:用户态还是用户态.

非一致代码段:

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

非一致代码段的限制作用:
1.只允许同级间访问.
2.绝对禁止不同级访问:核心态不用用户态.用户态也不使用核心态.

通常低特权代码必须通过"门"来实现对高特权代码的访问和调用.
不同级别代码段之间转移规则,是通过CPL/RPL/DPL来校验.

        “一致码段”中“一致”这个词比较令人费解。“一致”的意思是这样的,当转移的目标是一个特权级更高的一致代码段,当前的特权级会被延续下去,而向特权级更高的非一致代码段的转移会引起常规保护错误(general-protection exception,#GP),除非使用调用门或者任务门。如果系统代码不访问受保护的资源和某些类型的异常处理(比如,除法错误或溢出错误),它可以被放在一致代码段中。为避免低特权级的程序访问而被保护起来的系统代码则应放到非一致代码段中。
        要注意的是,如果目标代码的特权级低的话,无论它是不是一致代码段,都不能通过call或者jmp转移进去,尝试这样的转移将会导致常规保护错误。
        所有的数据段都是非一致的,这意味着不可能被低特权级的代码访问到。
        然而,与代码段不同的是,数据段可以被更高特权级的代码访问到,而不需要使用特定的门。

特权级:

CPL(Current Privilege Level)
        CPL是当前执行的程序或任务的特权级。它被存储在cs和ss的第0位和第1位上。在通常情况下,CPL等于代码所在的段的特权级。当程序转移到不同特权级的代码段时,处理器将改变CPL。
        在遇到一致代码段时,情况稍稍有点特殊,一致代码段可以被相同或者更低特权级的代码访问。当处理器访问一个与CPL特权级不同的一致代码段时,CPL不会被改变。

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

RPL(RequestedPrivilege Level)
        RPL是通过段选择子的第0位和第1位表现出来的。处理器通过检查RPL和CPL来确认一个访问请求是否合法。即便提出访问请求的段有足够的特权级,如果RPL不够也是不行的。也就是说,如果RPL 的数字比CPL大(数字越大特权级越低),那么RPL将会起决定性作用,反之亦然。
        操作系统过程往往用RPL来避免低特权级应用程序访问高特权级段内的数据。当操作系统过程(被调用过程)从一个应用程序(调用过程)接收到一个选择子时,将会把选择子的RPL设成调用者的特权级。于是,当操作系统用这个选择子去访问相应的段时,处理器将会用调用过程的特权级(已经被存到RPL中),而不是更高的操作系统过程的特权级(CPL)进行特权检验。这样,RPL就保证了操作系统不会越俎代庖地代表一个程序去访问一个段,除非这个程序本身是有权限的。
        RPL说明的是进程对段访问的请求权限(Request Privilege Level),是对于段选择子而言的,每个段选择子有自己的RPL,它说明的是进程对段访问的请求权限,有点像函数参数。而且RPL对每个段来说不是固定的,两次访问同一段时的RPL可以不同。RPL可能会削弱CPL的作用,例如当前CPL=0的进程要访问一个数据段,它把段选择符中的RPL设为3,这样虽然它对该段仍然只有特权为3的访问权限。 (个人认为是以CPL来访问段DPL所出示的“证件(RPL)”,如出示的“证件”权级范围在CPL之内且满足DPL的特权检查规则:DPL >= max{CPL,RPL},就能正常通过DPL;反之则不会通过还会发生错误)

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第10张图片

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

        程序从一个代码段转移到另一个代码段之前,目标代码段的选择子会被加载到cs中。作为加载过程的一部分,处理器将会检查描述符的界限、类型、特权级等内容。如果检验成功,cs将被加载,程序控制
将转移到新的代码段中,从eip指示的位置开始执行。
        程序控制转移的发生,可以是由指令jmp、call、ret、sysenter、sysexit、int n或iret引起的,也可以由中断和异常机制引起。
使用jmp或call指令可以实现下列4种转移:
1. 目标操作数包含目标代码段的段选择子。
2. 目标操作数指向一个包含目标代码段选择子的调用门描述符。
3. 目标操作数指向一个包含目标代码段选择子的TSS。
4. 目标操作数指向一个任务门,这个任务门指向一个包含目标代码段选择子的TSS。
这4 种方式可以看做是两大类,一类是通过jmp和call的直接转移(上述第1种),另一类是通过某个描述符的间接转移(上述第2、3、4种)。

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第11张图片

关于堆栈:
        如果一个调用或跳转指令是在段间而不是段内进行的,那么我们称之为“长”的(Far jmp/call),反之,如果在段内则是“短”的
(Near jmp/call)。
        那么长的和短的jmp或call有什么分别呢?对于jmp而言,仅仅是结果不同罢了,短跳转对应段内,而长跳转对应段间;而call则稍微复杂一些,因为call指令是会影响堆栈的,长调用和短调用对堆栈的影响是不同的。我们下面的讨论只考虑32位的情况,对于短调用来说,call指令执行时下一条指令的eip压栈,到ret指令执行时,这个eip会被从堆栈中弹出,如下图。

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第12张图片

      上图显示了call指令执行前后堆栈的变化,可以看出,调用者的eip被压栈,而在此之前参数已经入栈。图中的“调用者eip”对应nop指令地址。而在函数foo调用最后一条指令ret(带有参数)返回之前和之后,堆栈的变化如下图所示。

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第13张图片

        这是短调用的情况,长调用的情况与此类似,容易想到,返回的时候跟调用的时候一样也是“长”转移,所以返回的时候也需要调用者的cs,于是call指令执行时被压栈的就不仅有eip,还应该有cs,如下图所示。

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第14张图片

        相应地,带参数的ret指令执行前后的情形如下图。

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第15张图片

有特权级变换的转移时堆栈变化:

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第16张图片

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

TSS结构:

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第17张图片

        可以看出,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. 从调用者堆栈中将参数复制到被调用者堆栈(新堆栈)中,复制参数的数目由调用门中ParamCount一项来决定。如果ParamCount是零的话,将不会复制参数。
8. 将当前的cs和eip压栈。
9. 加载调用门中指定的新的cs和eip,开始执行被调用者过程。

由被调用者到调用者的返回过程中,处理器的工作包含以下步骤:
1. 检查保存的cs中的RPL以判断返回时是否要变换特权级。
2. 加载被调用者堆栈上的cs和eip(此时会进行代码段描述符和选择子类型和特权级检验)。
3. 如果ret指令含有参数,则增加esp的值以跳过参数,然后esp将指向被保存过的调用者ss和esp。注意,ret的参数必须对应调用门中的ParamCount 的值。
4. 加载ss和esp,切换到调用者堆栈,被调用者的ss和esp被丢弃。在这里将会进行ss描述符、esp以及ss段描述符的检验。
5. 如果ret指令含有参数,增加esp的值以跳过参数(此时已经在调用者堆栈中)。
6. 检查ds、es、fs、gs的值,如果其中哪一个寄存器指向的段的DPL小于CPL(此规则不适用于一致代码段),那么一个空描述符会被加载到该寄存器。
下图可以比较形象地表示出这个过程。

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第18张图片

使用调用门的过程实际上分为两个部分,一部分是从低特权级到高特权级,通过调用门和call指令来实现;另一部分则是从高特权级到低特权级,通过ret指令来实现。通过ret指令可以实现由高特权级到低特权级的转移。

什么叫做“页”:

所谓“页”,就是一块内存,在80386中,页的大小是固定的4096字节(4KB)。在Pentium中,页的大小还可以是2MB或者4MB,并且可以访问到多于4GB的内存,在此我们不予讨论。

逻辑地址、线性地址、物理地址:
        在未打开分页机制时,线性地址等同于物理地址,于是可以认为,逻辑地址通过分段机制直接转换成物理地址。但当分页开启时,情况发生变化,分段机制将逻辑地址转换成线性地址,线性地址再通过分页机制转换成物理地址。这可以用下图来说明。

分页机制示意图:

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第19张图片

        如图所示,转换使用两级页表,第一级叫做页目录,大小为4KB,存储在一个物理页中,每个表项4字节长,共有1024个表项。每个表项对应第二级的一个页表,第二级的每一个页表也有1024个表项,每一个表项对应一个物理页。页目录表的表项简称PDE(Page DirectoryEntry),页表的表项简称PTE(Page Table Entry)。
        进行转换时,先是从由寄存器cr3指定的页目录中根据线性地址的高10位得到页表地址,然后在页表中根据线性地址的第12到21位得到物理页首地址,将这个首地址加上线性地址低12位便得到了物理地址。
        分页机制是否生效的开关位于cr0的最高位PG位。如果PG=1,则分页机制生效。所以,当我们准备好了页目录表和页表,并将cr3指向页目录表之后,只需要置PG位,分页机制就开始工作了。

PDE和PTE:

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第20张图片

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第21张图片

PDE和PTE中各位的解释如下:
        P存在位,表示当前条目所指向的页或页表是否在物理内存中。P=0表示页不在内存中,如果处理器试图访问此页,将会产生页异常(page-faultexception,#PF);P=1表示页在内存中。
        R/W指定一个页或者一组页(比如,此条目是指向页表的页目录条目)的读写权限。此位与U/S位和寄存器cr0中的WP位相互作用。R/W=0表示只读;R/W=1表示可读并可写。
        U/S 指定一个页或者一组页(比如,此条目是指向页表的页目录条目)的特权级。此位与R/W位和寄存器cr0中的WP位相互作用。U/S=0表示系统级别(Supervisor Privilege Level),如果CPL为0、1或2,那么它便是在此级别;U/S=1表示用户级别(User Privilege Level),如果CPL为3,那么它便是在此级别。
        如果cr0中的WP位为0,那么即便用户级(User P.L.)页面的R/W=0,系统级(Supervisor P.L.)程序仍然具备写权限;如果WP位为1,那么系统级(Supervisor P.L.)程序也不能写入用户级(UserP.L.)只读页。
        PWT 用于控制对单个页或者页表的缓冲策略。PWT=0时使用Write-back缓冲策略;PWT=1时使用Write-through缓冲策略。当cr0寄存器的CD(Cache-Disable)位被设置时会被忽略。
        PCD 用于控制对单个页或者页表的缓冲。PCD=0时页或页表可以被缓冲;PCD=1时页或页表不可以被缓冲。当cr0寄存器的CD(Cache-Disable)位被设置时会被忽略。
        A 指示页或页表是否被访问。此位往往在页或页表刚刚被加载到物理内存中时被内存管理程序清零,处理器会在第一次访问此页或页面时设置此位。而且,处理器并不会自动清除此位,只有软件能清除它。
        D 指示页或页表是否被写入。此位往往在页或页表刚刚被加载到物理内存中时被内存管理程序清零,处理器会在第一次写入此页或页面时设置此位。而且,处理器并不会自动清除此位,只有软件能清除它。
        A位和D位都是被内存管理程序用来管理页和页表从物理内存中换入和换出的。
        PS 决定页大小。PS=0时页大小为4KB,PDE指向页表。
        PAT 选择PAT(Page Attribute Table)条目。PentiumIII以后的CPU开始支持此位,在此不予讨论,并在我们的程序中设为0。
        G 指示全局页。如果此位被设置,同时cr4中的PGE位被置,那么此页的页表或页目录条目不会在TLB中变得无效,即便cr3被加载或者任务切换时也是如此。
        处理器会将最近常用的页目录和页表项保存在一个叫做TLB(Translation Lookaside Buffer)的缓冲区中。只有在TLB中找不到被请求页的转换信息时,才会到内存中去寻找。这样就大大加快了访问页目录和页表的时间。
        当页目录或页表项被更改时,操作系统应该马上使TLB中对应的条目无效,以便下次用到此条目时让它获得更新。
        当cr3被加载时,所有TLB都会自动无效,除非页或页表条目的G位被设置。

如何获取内存:

那么程序如何知道机器有多少内存呢?实际上方法不止一个,在此我们仅介绍一种通用性比较强的方法,那就是利用中断15h。
在调用中断15h之前,需要填充如下寄存器:
eax int 15h可完成许多工作,主要由ax的值决定,我们想要获取内存信息,需要将ax赋值为0E820h。
ebx 放置着“后续值(continuation value)”,第一次调用时ebx必须为0。
es:di 指向一个地址范围描述符结构ARDS(Address Range Descriptor Structure),BIOS将会填充此结构。
ecx es:di所指向的地址范围描述符结构的大小,以字节为单位。无论es:di所指向的结构如何设置,BIOS最多将会填充ecx个字节。不过,通常情况下无论ecx为多大,BIOS只填充20字节,有些BIOS忽略ecx的值,总是填充20字节。
edx 0534D4150h('SMAP')──BIOS将会使用此标志,对调用者将要请求的系统映像信息进行校验,这些信息会被BIOS放置到es:di所指向的结构中。
中断调用之后,结果存放于下列寄存器之中。
CF CF=0表示没有错误,否则存在错误。
eax 0534D4150h('SMAP')。
es:di 返回的地址范围描述符结构指针,和输入值相同。
ecx BIOS填充在地址范围描述符中的字节数量,被BIOS所返回的最小值是20字节。
ebx 这里放置着为等到下一个地址描述符所需要的后续值,这个值的实际形势依赖于具体的BIOS的实现,调用者不必关心它的具体形式,只需在下次迭代时将其原封不动地放置到ebx中,就可以通过它获取下一个地址范围描述符。如果它的值为0,并且CF没有进位,表示它是最后一个地址范围描述符。

上面提到的地址范围描述符结构(Address Range Descriptor Structure)如表3.5所示。

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第22张图片

其中,Type的取值及其意义如下图所示。

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第23张图片

中断描述符表(Interrupt Descriptor Table)

IDT的作用是将每一个中断向量和一个描述符对应起来。IDT中的描述符可以是下面三种之一:
中断门描述符
陷阱门描述符
任务门描述符

中断向量到中断处理程序的对应过程:

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第24张图片

        联系调用门我们知道,其实中断门和陷阱门的作用机理几乎是一样的,只不过使用调用门时使用call指令,而这里我们使用int指令。IDT中可以有中断门、陷阱门或者任务门。但任务门在有些操作系统中根本就没有用到(比如Linux)。

中断门和陷阱门:

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第25张图片

        对比调用门的结构我们知道,在中断门和陷阱门中BYTE4的低5位变成了保留位,而不再是ParamCount。而且,表示TYPE的4位也将变为0xE(中断门)或0xF(陷阱门)。当然,S位仍将是0。

保护模式中的中断和异常:

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第26张图片

         Fault 是一种可被更正的异常,而且一旦被更正,程序可以不失连续性地继续执行。当一个fault发生时,处理器会把产生fault的指令之前的状态保存起来。异常处理程序的返回地址将会是产成fault的指令,而不是其后的那条指令。
         Trap是一种在发生trap的指令执行之后立即被报告的异常,它也允许程序或任务不失连续性地继续执行。异常处理程序的返回地址将会是产成trap的指令之后的那条指令。
         Abort 是一种不总是报告精确异常发生位置的异常,它不允许程序或任务继续执行,而是用来报告严重错误的。

外部中断:

外部中断分为不可屏蔽中断(NMI)和可屏蔽中断两种,分别由CPU的两根引脚NMI和INTR来接收,如下图所示。

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第27张图片

        NMI不可屏蔽,因为它与IF是否被设置无关。NMI中断对应的中断向量号为2。
        可屏蔽中断与CPU的关系是通过对可编程中断控制器8259A建立起来的。如果你是第一次听说8259A,那么你可以认为它是中断机制中所有外围设备的一个代理,这个代理不但可以根据优先级在同时发生中断的设备中选择应该处理的请求,而且可以通过对其寄存器的设置来屏蔽或打开相应的中断。
        由图我们知道,与CPU相连的不是一片,而是两片级联的8259A,每个8259A有8根中断信号线,于是两片级联总共可以挂接15个不同的外部设备。那么,这些设备发出的中断请求如何与中断向量对应起来呢?就是通过对8259A的设置完成的。在BIOS初始化它的时候,IRQ0~IRQ7被设置为对应向量号08h~0Fh。

8259A可编程中断控制器:

对它的设置并不复杂,是通过向相应的端口写入特定的ICW(Initialization Command Word)来实现的。主8259A对应的端口地址是20h和21h,从8259A对应的端口地址是A0h和A1h。ICW共有4个,每一个都是具有特定格式的字节。现在,先来看一下初始化过程:
1. 往端口20h(主片)或A0h(从片)写入ICW1。
2. 往端口21h(主片)或A1h(从片)写入ICW2。
3. 往端口21h(主片)或A1h(从片)写入ICW3。
4. 往端口21h(主片)或A1h(从片)写入ICW4。
这4步的顺序是不能颠倒的。
我们现在来看一下4个如下图所示的ICW的格式。

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第28张图片

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第29张图片

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第30张图片

        OCW(Operation Control Word)。OCW共有3个,OCW1、OCW2和OCW3。由于我们只在两种情况下用到它,因此并不需要了解所有的内容。这两种情况是:
        屏蔽或打开外部中断。
        发送EOI给8259A以通知它中断处理结束。

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第31张图片

        可见,若想屏蔽某一个中断,将对应那一位设成1就可以了。实际上,OCW1是被写入了中断屏蔽寄存器(IMR,全称Interrupt Mask Register)中,当一个中断到达,IMR会判断此中断是否应被丢弃。
        说起EOI,如果你有过在实模式下的汇编经验,那么对它应该不会陌生。当每一次中断处理结束,需要发送一个EOI给8259A,以便继续接收中断。而发送EOI是通过往端口20h或A0h写OCW2来实现的。
        OCW2的格式如下图所示。

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第32张图片

 中断或异常发生时的堆栈变化:

一个操作系统的实现:第三篇——保护模式(Protect Mode)_第33张图片

        如果中断或异常发生时没有特权级变换,那么eflags、cs、eip将依次被压入堆栈,如果有出错码的话,出错码将在最后被压栈。有特权级变换的情况下同样会发生堆栈切换,此时,ss和esp将被压入内层堆栈,然后是eflags、cs、eip、出错码(如果有的话)。
        从中断或异常返回时必须使用指令iretd,它与ret很相似,只是它同时会改变eflags的值。需要注意的是,只有当CPL为0时,eflags中的IOPL域才会改变,而且只有当CPL6IOPL时,IF才会被改变。
        另外,iretd执行时Error Code不会被自动从堆栈中弹出,所以,执行它之前要先将它从栈中清除掉。

保护模式下的I/O:

毫无疑问,对I/O的控制权限是很重要的一项内容,保护模式对此也做了限制,用户进程如果不被许可是无法进行I/O操作的。这种限制通过两个方面来实现,它们就是IOPL和I/O许可位图。

 IOPL:

它是I/O保护机制的关键之一,位于寄存器eflags的第12、13位,如下图所示。

指令in、ins、out、outs、cli、sti只有在CPL<=IOPL时才能执行。
这些指令被称为I/O敏感指令(I/O SensitiveInstructions)。如果低特权级的指令试图访问这些I/O敏感指令将会导致常规保护错误(#GP)。
可以改变IOPL的指令只有popf和iretd,但只有运行在ring0的程序才能将其改变。运行在低特权级下的程序无法改变IOPL,不过,如果试图那样做的话并不会产生任何异常,只是IOPL不会改变,仍然保持原样。
指令popf同样可以用来改变IF(就好像执行了cli和sti)。然而,在这种情况下,popf也变成了I/O敏感指令。只有CPL<=IOPL时,popf才可以成功将IF改变,否则IF将维持原值,不会产生任何异常。

I/O许可位图(I/O Permission Bitmap):

它是一个以TSS的地址为基址的偏移,指向的便是I/O许可位图。之所以叫做位图,是因为它的每一位表示一个字节的端口地址是否可用。如果某一位为0,则表示此位对应的端口号可用,为1则不可用。由于每一个任务都可以有单独的TSS,所以每一个任务可以有它单独的I/O许可位图。
        比如,有一个任务的TSS是这样的:由于I/O许可位图开始有12字节内容为0FFh,即有12×8=96位被置为1,所以从端口00h到5Fh共96个端口地址对此任务不可用。同理,接下来的1字节只有第1位(从0开始数)是0,表示这一位对应的端口(61h)可用。
        I/O许可位图必须以0FFh结尾。
        如果I/O位图基址大于或等于TSS段界限,就表示没有I/O许可位图,如果CPL>IOPL,则所有I/O指令都会引起异常。I/O许可位图的使用使得即便在同一特权级下不同的任务也可以有不同的I/O访问权限。

“保护模式”包含如下几方面的含义:

1、在GDT、LDT以及IDT中,每一个描述符都有自己的界限和属性等内容,是对描述符所描述对象的一种限定和保护。
2、分页机制中的PDE和PTE都含有R/W以及U/S位,提供了页级保护。
3、页式存储的使用使应用程序使用的是线性地址空间而不是物理地址,于是物理内存就被保护起来。
4、中断不再像实模式下一样使用,也提供特权检验等内容。
5、I/O指令不再随便使用,于是端口被保护起来。
6、在程序运行过程中,如果遇到不同特权级间的访问等情况,会对CPL、RPL、DPL、IOPL等内容进行非常严格的检验,同时可能伴随堆栈的切换,这都对不同层级的程序进行了保护。

代码段:

pmtest:

	; 关中断,之所以关中断,是因为保护模式下中断处理的机制是不同的,不关掉中断将会出现错误。
	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

那么什么是A20呢?这又是一个历史问题。8086中,“段:偏移”这样的模式能表示的最大内存是FFFF:FFFF,即10FFEFh。可是8086只有20位的地址总线,只能寻址到1MB,那么如果试图访问超过1MB的地址时会怎样呢?实际上系统并不会发生异常,而是回卷(wrap)回去,重新从地址零开始寻址。可是,到了80286时,真的可以访问到1MB以上的内存了,如果遇到同样的情况,系统不会再回卷寻址,这就造成了向上不兼容,为了保证百分之百兼容,IBM想出一个办法,使用8042键盘控制器来控制第20个(从零开始数)地址位,这就是A20地址线,如果不被打开,第20个地址位将会总是零。
显然,为了访问所有的内存,我们需要把A20打开,开机时它默认是关闭的。

一般来讲,DOS程序结束有三种方法
一,是用int 20h 来终止程序,但有条件,在结束时cs必须跟程序开始时一致,否则要死机。在DOS中常用于*.com文件。
二,是用ret来终止程序,如下:
...
code segment
start proc far ;注意,这里有 far,表示是远调用,主要影响ret指令,编译后为RETF
push cs
mov ax,0
push ax
......
...... ;应用户程序
......
ret ;返回DOS
start endp
ends
end start
应用这种退出机制,千万注意堆栈一个都不能错,否则死机。在DOS中常用于*.exe文件。
三,int 21h
mov ax,4c00h
int 21h
用它返回是不需任何条件,还可顺便帮你关闭你打开后忘记关闭的文件。并返回寄存器al的值。在DOS中可用于*.com或*.exe文件。
具体来说:
DOS系统提供给用户很多应用,比如文件读写、时间读写,显示等等。int 21h是DOS系统的系统调用的入口,ah为功能号,就是本问题中的4c,比如
mov ah,9
mov dl,‘a'
int 21h
表示要在屏幕上显示英文字母a
本问题中mov ax,4c00h表明应用程序要退出,并为调用本程序的程序返回00,传递退出信息。
ax取值范围是4c00h---4cffh。

​​​​​​​pmtest8.asm:

	; pmtest8.asm
	; 在此假设内存是大于 8M 的
	mov	eax, LinearAddrDemo         ;mov eax,0x00401000,转化为2进制为:0000,0000,0100,0000,0001,0000,0000,0000
	shr	eax, 22                     ;shr eax,0x16. eax变为0x00000001 索引项PTE所在页表之前的页表数,及PDE值
	mov	ebx, 4096                   ;mov ebx,0x00001000
	mul	ebx                         ;mul eax,ebx;eax变为0x00001000
	mov	ecx, eax                    ;ecx=0x00001000 索引项PTE所在页表之前的页表
	mov	eax, LinearAddrDemo         ;eax=0x00401000,转化为2进制为:0000,0000,0100,0000,0001,0000,0000,0000
	shr	eax, 12                     ;eax=0x00000401
	and	eax, 03FFh	; 1111111111b (10 bits) eax=0x00000001 页表中的索引数,及PTE
	mov	ebx, 4                      ;ebx=0x00000004
	mul	ebx                         ;eax=0x00000004
	add	eax, ecx                    ;eax=0x00001004
	add	eax, PageTblBase1           ;add eax,0x0021100,eax变为0x00212004 及0x00401000所在页表项的内存位置
	mov	dword [es:eax], ProcBar | PG_P | PG_USU | PG_RWW        ;es指向SelectorFlatRW的0x00212004,这句肯定把原来函数地址变了。
                                                                ;相对于ox00211000页表首地址,多了1004h,0x00211000对应的是00000000h,
                                                                ;多了1004就是401000h物理地址,对于这个地址赋值 ProcBar的首地址。

 

你可能感兴趣的:(一个操作系统的实现:第三篇——保护模式(Protect Mode))