4.1系统寄存器和系统指令
标志寄存器
内存管理寄存器
GDTR、LDTR、IDRL、TR
用于指定分段内存管理所使用的系统表的基地址。
控制寄存器
CR0
控制处理器操作模式和状态的系统控制标志
CR1
保留不用
CR2
含有导致页错误的线性地址
CR3
含有页目录物理内存基地址,也被称为页目录基地址寄存器PDBR
系统指令
4.2保护模式内存管理
1.80X86对内存中存储数据的寻址方式一般是:段基地址+段内偏移
段基地址:被存放在段寄存器(16位)中的端选择符指定
段内偏移
2.物理内存就是地址总线宽度决定的那个可以寻址的范围
3.分段
4.逻辑地址,线性地址,物理地址,地址空间
逻辑地址:很简单,就是你源程序里使用的地址,或者源代码经过编译以后编译器将一些标号,变量转换成的地址,或者相对于当前段的偏移地址。
线性地址:这个地址很重要,也很不容易理解。分段机制下CPU寻址是二维的地址即,段地址:偏移地址,CPU不可能认识二维地址,因此需要转化成一维地址即,段地址*16+偏移地址,这样得到的地址便是线性地址(在未开启分页机制的情况下也是物理地址)。这样有什么意义呢?或者说这个一维地址的计算方法随便一个学计算机的人都知道,但是你真的理解它的意思吗?要想理解它的意思,必须要知道什么是地址空间,下文详述。
物理地址:很简单,将内存条看出一个大的数组,下标从0开始到0xFFFFFFFF,其中任意一个下标标记一个内存条上的一个字节的存储空间,物理地址的大小由地址总线的位宽决定
虚拟地址:虚拟地址就是逻辑地址,又叫虚地址
地址空间:这个很重要,不理解地址空间,你就不理解进程,不理解用户空间,不理解内核空间,不理解虚拟存储,不理解分页机制,你就没学过计算机。操作系统为了支持多任务,保护各个任务合理的共享和隔离一些数据和代码,还为了其他很多原因,必须要让每个任务都有自己的地址空间,就是说你在给编写每个程序代码的时候可以随意读写地址空间内的数据,而不用担心会不会读写到其他程序的代码块中去了。32位OS中,每个程序都有4G的内存空间,就是说A程序可以往0X12345中写一个值,B程序也可以往0X12345中写一个值,两个值虽然表面上写到一个地方去了,但是你根本不用担心,它们根本没有写到一个地方去了,它们都是写在自己的用户空间中,经过几次地址映射就映射到不同的物理地址上去了。4G内存空间的地址就是线性地址,也就是说线性地址就是用来标识这个4G的虚拟的用户空间的。
用户空间:每个程序都有4G内存空间,但是分为两个部分,0-3G是用户空间,3-4G是内核空间。
保护
80X86提供两类保护
- 任务间的保护
- 特权级保护
6.任务间的保护
把每个任务放在不同的虚拟地址空间中,也就是同一个逻辑地址,被映射到不同的物理块上,实现隔离。具体的实现方法是,每个任务有自己的段表和页表,这样映射的函数就不同了。也因此,处理器切换任务时,关键的一步就是切换到新任务的变换表。
问:变换表存储在哪里?怎么切换的呢?
- 既然每个任务的地址空间不同,那么操作系统如何实现被不同任务共享呢?
只需为每个任务划分相同的一块虚拟地址空间,并且将这块虚拟地址空间映射到同一物理地址空间,在其中存储内核,就行啦。这个所有任务都具有的相同虚拟地址空间部分被称为全局地址空间。
其他每个任务独有到的虚拟地址空间部分被称为局部地址空间。存放私有代码和数据,这样OS可以给每个任务相同的虚拟地址,但实际上映射到不同物理地址,隔离了任务。
7.特权级保护
当前活动代码段的特权级CPL:指明当前所执行程序的特权级
- CPL存储在哪?
CPL保存在CS中的最低两位,是针对CS而言的。当选择子成功装入CS寄存器后,相应的选择子中的RPL就变成了CPL。因为它的位置变了,已经被装入到CS寄存器中了,所表达的意思也发生了变——原来的要求等级已经得到了满足,就是当前自己的等级。
选择子可以有许多个,因此RPL也就有许多个。而CPL就不同了,正在执行的代码在某一时刻就只有这个值唯一的代表程序的CPL.
任务切换时,虚拟地址空间会切换(不同的段),特权级会改变,堆栈会切换。
8.分段机制
作用:小到保护程序的平坦模型,大到可用分段机制创建一个可同时可靠地运行多个程序(或任务)的环境的多段模型。
- 平坦模型:80X86提供4GB的线性空间和4GB物理地址空间,都是从0到0xFFFFFFFF。
多段模型:每个程序(任务)有自己的段描述符表和自己的段。对所有段的访问或对系统上运行程序各自执行环境的访问,都由硬件控制。(非法访问则CPU产生一般保护性异常)
非法包括:引用了高特权级的段(非法访问操作系统程序)、对段内数据执行了不允许的操作(写只读段)、引用了段长限制之外的位置
每个段由三个参数定义:段基地址、段限长、段属性
- 段基地址:线性地址空间中段的开始地址。是线性地址,段中0偏移处。
- 段限长:段的最大偏移
- 段属性
段的线性地址空间即段基地址~段基地址+段限长
多个段映射到的线性地址空间可以重叠,比如一个任务的代码段和数据段映射到线性地址完全相同而重叠的区域上
以上三个参数存储在段描述符。段描述符存储在段描述符表中。定位段描述符通过段选择符和段描述符表的基地址。
逻辑地址转换为线性地址的过程:
- 段选择符 定位 段描述符
- 利用段描述符,检查访问权限和范围
- 段选择符中取出段基地址,加上偏移量获得线性地址
线性地址空间中含有为系统定义的所有段和系统表
段描述符表
1.是存放段描述符的可变数组,最多包含8192个8字节描述符。
2.分为两种GDT、LDT
3.虚拟地址空间共214个段,GDT和LDT各映射213个段。
4.任务切换时,LDT会更换成新任务的LDT,GDT不会改变。因此GDT映射的段是任务共享的,这样的段包括操作系统的段以及所有任务各自包含的LDT段。
- 图中,任务A和任务B的GDT的段共享,用于映射OS内核;但LDT各自不同,映射自己的私有空间。且两个任务的LDT段也是通过GDT映射的(GDT指向LDT的箭头)。当任务A运行时,任务B的LDTB映射的段时不可访问的。A运行时,一半的虚拟地址空间映射到了共享的段,另一半映射的只是A独占的段,因此A访问不到B的内存。
5.OS也让可以多任务共享一个LDT
GDT本身不是一个段,而是线性地址空间中的一个数据结构。
LDT不同,LDT存放在LDT类型的系统段中。此时GDT必含有LDT的段描述符。
GDT寻址方式是通过GDTR寄存器,里边会载入GDT的基线性地址和长度值(限长值)。
LDT寻址方式是通过GDTR寄存器,里边会载入LDT段(注意是LDT段不,是每个任务独有的LDT映射的段)的段选择符、基地址、段限长以及访问权限。
处理器不使用GDT中的第一个描述符。该段描述符对应的段选择符(除了CS、SS)如果被加载进段寄存器并不会产生异常,但如果使用该寄存器访问内存,就肯定会产生一般保护性异常。
段选择符(段选择子)
16位,包含3个字段内容:
- 请求特权级RPL
- 表指示标志TI:说明这个段的描述符在GDT中还是LDT中
- 索引值Index:段表中的索引号13位,对应LDT(GDT)有2^13个段
对于应用程序,段选择符作为指针变量的一部分而可见,但其值通常由链接编辑器或链接加载程序进行设置,而非应用程序。
段寄存器
为减少地址转换时间和编程复杂性,处理器提供6个段寄存器(若不考虑效率,只实现分段,则提供一个段寄存器就可以,但这样每次切换段都要加一步指令——将选择符加载至寄存器),这样访问一些常用段时就不用每次都加载它的选择符了。
同时有了隐藏部分,进行地址转换时可以不去查段描述符。(同时为了保证描述符更新时,对应的段寄存器的隐藏部分及时更新,每次段描述符改变之后都会立刻重新加载6个段寄存器,加载寄存器的指令见下)
- 3个每个程序原则上都应有的代码段(CS)、数据段(DS)、堆栈段(SS)寄存器
- 3个数据段寄存器ES、FS、GS。
程序要访问某个段,必须先将端选择符加载至段寄存器。因此尽管程序可以定义很多段,但同时只有6个段可供立即访问,其他段要先加载选择符至段寄存器。
加载寄存器的指令分两类:
- MOV、POP、LDS、LES、LSS、LGS、LFS:这些指令显式引用段寄存器
- 隐式加载指令。CALL、JMP、RET、IRET、INTn、INTO、INT3,这些指令会附带改变CS寄存器(和某些其他段寄存器)的内容
段描述符
8字节
三个字段:
- 段基地址
- 段限长
- 段属性
段描述符和段选择符一样通常由编译器、链接器、加载器或操作系统创建,但绝不是应用程序。
-
段限长字段:两部分拼接成20位值。根据颗粒度标志G指定段限长Limit值的实际含义。G=0,Limit范围为1字节1MB字节,单位为字节;G=1,4KB4GB,单位为4KB。根据段扩展方向标志E,以两种不同方式使用段限长,一种是向上扩展的段(简称上扩段),即偏移值范围为0~Limit;另一种是向下扩展的段(下扩段),根据默认栈指针大小标志B的设置,偏移值范围由Limit到0xFFFFFFFF或0xFFFF。上扩段与下扩段区别在于Limit增时,上扩段往地址大处扩展,下扩段往地址小处扩展(很适合堆栈的段);Limit减时同理。
-
基地址字段BASE:3部分拼接为32位值,定义4GB线性空间中一个段的字节0所处的位置。推荐对齐16字节边界,会有最佳性能。
-
描述符类型标志S:S=0→系统描述符;S=1→代码或数据段描述符。
-
段类型字段TYPE:指明段(门)的类型、段的访问种类以及段的拓展方向。TYPE字段的解释依赖S字段,即其编码的解释对代码、数据或系统操作符都不同。
-
描述符特权级字段DPL:指明描述符的特权级,0~3,控制对段的访问。
-
段存在标志P:P=1→段在内存中;P=0,段不在内存中
-
D/B(默认操作大小/默认栈指针大小和/或上界限)标志:
对于32位段,标志设置为1;对于16位段,标志设置为0.
对于32位机和16位机,地址位数、默认操作数长度、栈段界限是不同的,D/B字段主要指明这些。
-
颗粒度标志G
确定Limit值的单位
-
可用和保留比特位
第二个双字的位20可供系统软件使用;位21是保留为且总设置0.
关于TYPE字段
-
第2个双字的位11:
数据段置0,代码段置1
-
位8,9,10:
对于数据段分别表示,已访问A、可写W、拓展方向E
对于代码段分别标志,已访问A、可读R、一致的C
注:
-
数据段W位置1/0分别表示可读可写/只读
代码段R位置1/0分别表示可执行可读/只能执行
-
系统描述符类型S=0
分为
- LDT的段描述符
- 状态任务段描述符
- 调用门描述符
- 中断门描述符
- 陷阱门描述符
- 任务门描述符
分为两大类:系统段描述符和门描述符
系统段描述符指向系统段(如LDT和TSS段)
门描述符并不描述某种内存段,而是描述控制转移的入口点。这种描述符好比一个同向另一代码段的门。通过这种门,可实现任务内特权级的变换和任务间的切换。
分页机制
4.5保护
4.5.1段级保护
-
段限长检查
-
段类型检查
-
特权级检查
-
可寻址范围限制
-
过程入口点限制
-
指令集限制
4.5.1.1段限长检查
段限长Limit范围0~0xFFFFF(1MB)
当颗粒度G=1时,范围为0xFFF~0xFFFFFFFF(4GB)。
当颗粒度G=0时,范围为0x0~0xFFFFF(4GB)。
注:
- G=1时,0~0xFFF仍然有效
- 除了下扩段以外,有效Limit的值是段中允许被访问的最后一个地址,比段长度小一个字节。(类比数组)
- 对于下扩段,范围为,B=1,(有效段偏移+1)— 0xFFFF FFFF;B=0,(有效段偏移+1)— 0xFFFF。段限长指定了段中最后一个不允许访问的地址。
- 除了检查段限长,处理器也会检查描述符表的长度,以免引用到表外描述符(类比数组越界)。GDTR、IDTR和LDTR包含16位限长值,指明了表中最后一个有效字节。因为每个描述符8字节,故N个描述符项的表限长值为8N-1
4.5.1.2段类型TYPE检查
主要有两种情况,检查类型信息
-
段寄存器所能容纳的段选择符类型的限制
- CS寄存器只能被加载可执行段的选择符
- 不可读可执行段的描述符不能进数据段寄存器
- 只有可写数据段的选择符能被加载进SS寄存器
-
可对段进行的指令的限制
- 任何指令不能写一个可执行段
- 任何指令不能写一个可写位为0的数据段
- 任何指令不能读一个可执行段,除非可执行段设置了可读位
4.5.1.3特权级检查
主要涉及代码段之间转移控制的过程
参考http://www.codebelief.com/article/2018/01/operating-system-privilege-mechanism-detailed-explanation/
数据段
对数据段的检查发生在数据段选择符加载进数据段寄存器之时,满足条件才会加载。
数据段的特权级检查规则与我们的直观感受相符。当前特权级越高,访问数据段就越不会受到限制。
也就是说,当前特权级高于或等于要访问的数据段 DPL 时,才能通过特权级检查。
数值上可表示为:RPL、CPL <= DPL
注意:如果是通过段寄存器 SS 访问数据段,则要求 CPL、RPL = DPL
代码段
如上面所介绍,CPU 只允许低特权级调用高特权级的代码,所以只有在当前特权级低于或等于目标代码段的 DPL 时,才允许进行控制转移。
数值上可表示为:RPL、CPL >= DPL
CPL 只有在一种情况下会改变:CALL 指令通过调用门转移到特权级更高的代码段。
调用门
使用调用门时涉及到两个 DPL:一个是调用门描述符自身的 DPL,另一个是目标代码段(调用门中段选择符对应的代码段)的 DPL。
调用门自身的 DPL 检查规则与数据段的规则一样,就是当前特权级必须足够高,才能够对调用门进行调用。但由于只能低特权级代码调用高特权级代码,所以又要求当前特权级要低于或等于目标代码段的特权级。
因此,检查特权级时,数值上应该满足:
RPL、CPL <= 调用门 DPL
RPL、CPL >= 目标代码段 DPL
任务门
当要使用 JMP 或 CALL 通过任务门来切换任务时,特权级的检查规则与访问数据段的规则一致。
即数值上:RPL、CPL <= DPL
1. 直接调用或跳转到另一个代码段
JMP,CALL,RET指令的远转移形式会将控制转移到另外一个代码段。
当不通过调用门转移代码段时,处理器会验证4种特权级和类型信息:
- 当前CPL
- 目的代码段的DPl
- 段选择符的RPL
- 目的代码段描述符中的一致性标志C。它确定了一个代码段是一致性代码段还是非一致性代码段
访问非一致代码段时
CPL=DPL,RPL<=CPL,(非一致性代码段的段选择符被加载至CS寄存器时)特权级不变
访问一致性代码段时
CPL>=DPL,不检查RPL,CPL不改变,[由于CPL没有改变,因此堆栈也不会切换?]
2. 门描述符
- 调用门
- 陷阱门
- 中断门
- 任务门
任务门用于任务切换,将在“任务管理”一节介绍。陷阱门和中断门是调用门的特殊类,专门用于调用异常和中断的处理程序,将在中断一节说明。本节只介绍调用门的用法。
调用门用于在不同特权级之间实现受控的程序控制转移。
调用门描述符可以存放在GDT或LDT中,但不能在IDT中。
主要有以下功能:
-
段选择字段:指定要访问的代码段
-
偏移值字段:指定段中入口点,通常是指定过程的第一条指令
-
DPL字段:调用门的特权级,指定通过调用门访问特定过程所要求的特权级
-
标志P:指明调用门描述符是否有效
-
参数个数字段:指明堆栈切换时从调用者堆栈复制到新堆栈中的参数个数
注:Linux内核中并未用到调用门,这里介绍调用门是为了中断和异常门做准备
3. 通过调用门访问代码段
过程:
通过CALL或JMP指令,目标是一个远指针。指针的段选择符指向门描述符,偏移值可设置为任意值,CPU不会用它。之后CPU会使用调用门的段选择符来定位目标代码段的段描述符,将段基地址与调用门的偏移值组合,形成程序入口点的线性地址。
这个过程中,CPU会进行以下特权级检查:
-
CPL
-
调用门选择符中的请求特权级RPL
-
调用门描述符中的描述符特权级DPL
-
目的代码段描述符中的DPL
另外,也会检查目的代码段描述符的一致性标志C
关于特权级的检查
CALL和JMP分别有不同的规则
注:“1”中,调用者CPL,调用门段选择符RPL
,“2”中,调用者CPL,目标代码段DPL
如果转移到了更高特权级的非一致性代码中,CPL会被设置为目标代码的DPL,并引起堆栈变换[仅有这一种情况CPL会改变]
如果转移到了更高特权级的一致性代码中,CPL不会改变,不会堆栈变换。
4.堆栈切换
当控制权转移到更高级别的非一致性代码时,CPU会自动切换到目的代码段特权级的堆栈去。
目的是,防止高特权级程序栈空间不足,同时防止低特权级程序通过共享的堆栈无意中干扰高特权级的程序。
最多4个栈,每个栈位于不同的段中,使用段选择符和段中偏移值指定。当特权级3的程序运行时,会被放入SS和ESP,且发生堆栈切换时被保存在被调用过程的堆栈上。
如何实现切换?
通过TSS,特权级0、1、2的堆栈初始指针值存放在当前运行任务的TSS段中。
这个指针值是只读的,任务运行时CPU不会修改它们,调用时建立新栈,返回时栈就不存在了,再调用时再创建...
每个栈必须可读可写,存放以下信息:
-
调用过程的SS、ESP、CS和EIP寄存器的内容
-
被调用过程的参数和临时变量所需使用的空间
-
当隐含调用一个异常或中断过程时标志寄存器EFLAGS和出错码使用的空间。
具体过程:
5.从被调用过程返回
RET指令用于:
- 近返回
- 同特权级远返回
- 不同特权级的远返回
该指令用于从使用CALL指令调用到的过程中返回
近返回:在当前代码段中转移控制权,不涉及切换段,只进行界限检查
相同特权级的远返回:CPU从栈中弹出返回代码段的选择符(SS)和返回指令指针(ESP),进行特权级检查
发生特权级改变的远返回:仅允许返回到低特权级程序中(DPl>CPL)。过程如下
- 检查保存的CS寄存器中RPL字段的值,以确定返回时特权级是否需要改变
- 弹出被调用过程堆栈上的值加载CS和EIP寄存器。同时对代码段描述符和代码段选择符进行特权级与类型检查[选择符和描述符即对应CS和EIP]
- 如果RET指令包含一个参数个数操作数并且返回操作会改变特权级,那么就在弹出栈中CS和EIP值之后把参数个数值加到ESP寄存器值中,以跳过(丢弃)被调用者栈上的参数。此时ESP寄存器指向原来保存的调用者堆栈的指针SS和EIP
- 把保存的SS和ESP加载到SS和ESP寄存器中,从而切换回调用者的堆栈。此时被调者栈的SS和ESP被丢弃
- 如果RET指令包含一个参数个数操作数,则把参数个数值加到ESP寄存器值中,以跳过(丢弃)调用者栈的参数
- 检查段寄存器DS、ES、FS、GS的内容,如果其中有指向DPL小于新CPL的段(一致代码除外),那么CPU将该寄存器值设置为NULL
调用门的作用是,让一个代码段中的过程被不同特权级的程序访问。通常用于低特权级代码来访问高特权级的代码段。