【Linux 0.12】80x86保护模式——保护机制

  • [ ] 什么是门?起到什么作用?

80x86保护模式及其编程

保护

保护机制是可靠的多任务运行环境必须的。它用于避免各个任务相互干扰。

保护机制被用于分段和分页机制。处理器寄存器的2bit定义了当前执行程序的特权级,称为当前特权级(CPL)。在分段和分页地址转换的过程中处理器将对CPL验证。

通过设置寄存器CR0的PE标志位,可以让处理器工作在保护模式下,同时也开启了分段机制。

通过设置寄存器CR0的PG标志位,可以开启分页机制,同时页开启了分页保护。

段级保护

提供了对某些段和页面的访问限制能力。

使用保护机制时,每个内存引用都会受到检查以验证内存引用符合各种保护要求。由于检查操作和地址变换同步金星,所以处理器的性能没有受到影响。所进行的保护检查分为以下几类:段界限检查,段类型检查,特权级检查,可寻址范围限制,过程入口点限制,指令集限制。

所有违反保护的操作都会导致一个异常。

1. 段限长检查

段描述符的段限长字段用于防止程序或过程寻址到段外内存位置。段限长的有效值依赖于颗粒度G的设置。对于数据段,还涉及到(扩展方向)标志E和(默认栈指针大小和/或上限)标志B有关。

除了对段限长检查,处理器也会检查描述符的长度。GDTR,IDTR,LDTR寄存器中包含16bit的限长值,处理器用它来防止程序在描述符表以外寻找描述符。

2. 段类型检查

除了应用程序代码段和数据段有描述符以外,处理器还有系统段和门两种描述符类型。这些数据结构用于管理任务以及异常和中断。并非所有的描述符都定义一个段,门描述符中存放有指向一个过程入口点的指针。段描述符在两个地方含有类型信息,即描述符的S标志和类型字段TYPE。

当一个描述符的选择符加载进一个段寄存器中,此时某些段寄存器只能存放特定类型的描述符。例如:

  • CS寄存器中只能被加载进一个可执行段的选择符。
  • 不可读可执行段的选择符不能被加载到数据段寄存器
  • 只有科协数据段的选择符才能加载到SS寄存器

当指令访问一个段,而该段的描述符已经加载到了段寄存器,指令只能使用某些预定义的方法来访问某些段。

  • 任何指令不能写一个可执行段
  • 任何指令不能写一个不可写数据段
  • 任何指令不能读一个可执行段,除非其可读

3. 特权级

0~3 4个特权级。如果只使用2个特权级,则使用0和3.

  • 0级:操作系统核心
  • 1级/2级:操作系统服务
  • 3级:应用层

处理器利用特权级防止运行在较低权限的程序或任务访问具有高特权级的一个段,除非在受控的条件下。如果处理器检测到一个违反特权级的操作,就会产生一个一般保护性异常。

为了在各个代码段和数据段之间进行特权级检测处理,处理器可以识别以下三种类型的特权级:

  1. 当前特权级CPL:指当前正在执行的程序或任务的特权级。存放于CS和SS段寄存器的位0和位1。通常CPL等于当前代码段的特权级。当程序把控制转移到另一个具有不同特权级的代码段中时,处理器就会改变CPL。当访问一个一致性代码时,处理器的CPL设置略有不同。特权级值高于(即低特权级)或等于一致代码段DPL的任何段都能访问一致代码段。并且当处理器访问一个特权级不同于CPL的一致代码段时,CPL并不会被修改为一致代码段的DPL。
  2. 描述特权级DPL(Descriptor Privilege Level):DPL是一个段或门的特权级。它存放在段或门描述符的DPL字段中。在当前执行代码段试图访问一个段或门时,段或门的DPL会用来和CPL比较。根据被访问的段或门的类型不同,DPL也有不同的含义:
    • 数据段:其DPL指出允许访问本数据段的程序或任务应该具有的最大特权值。比如如果DPL=1,则只有CPL=0,1的程序才能访问这个段
    • 非一致代码段:其DPL指出程序或任务访问该段必须具有的特权级。例如,某个非一致性代码段的DPL是0,那么只有运行在CPL为0的程序能访问这个段。
    • 调用门:其DPL指出访问调用门的当前执行程序或任务可以处于的最大特权级数值。
    • 任务状态段TSS:其DPL指出访问TSS的当前执行程序或任务可以处于的最大特权级数值。
  3. 请求特权级RPL:是赋予段选择符的超越特权级,存放在选择符的位0和位1,。

CPL是当前进程的权限级别(Current Privilege Level),是当前正在执行的代码所在的段的特权级,存在于CS寄存器的低两位。

RPL说明的是进程对段访问的请求权限(Request Privilege Level),是对于段选择子而言的,每个段选择子有自己的RPL,它说明的是进程对段访问的请求权限,有点像函数参数。而且RPL对每个段来说不是固定的,两次访问同一段时的RPL可以不同。RPL可能会削弱CPL的作用,例如当前CPL=0的进程要访问一个数据段,它把段选择符中的RPL设为3,这样虽然它对该段仍然只有特权为3的访问权限。

 **DPL**存储在段描述符中,规定访问该段的权限级别(**Descriptor Privilege Level**),每个段的DPL固定。当进程访问一个段时,需要进程特权级检查,一般要求**DPL >= max {CPL, RPL}**

下面打一个比方,中国官员分为6级国家主席1、总理2、省长3、市长4、县长5、乡长6,假设我是当前进程,级别总理(CPL=2),我去聊城市 (DPL=4)考察 (呵呵 ),我用省长的级别 ( RPL=3 这样也能吓死他们 :-))去访问,可以吧,如果我用县长的级别,人家就不理咱了 ,明白了吧!为什么采用 RPL,是考虑到安全的问题,就好像你明明对一个文件用有写权限,为什么用只读打开它呢,还不是为了安全!

段描述符的段选择符被加载到一个段寄存器时,就会进行特权级检查。但是对数据访问的检查方式和用于代码段之间进行程序控制转移的检查方式不同。下面分两种情况考虑该问题(访问数据段的特权级检查,代码段之间转移控制时的特权级检查)。

访问数据段时的特权级检查

为了访问数据段的操作数,数据段的段选择符必须加载到数据段寄存器(DS,ES,FS,GS)或者堆栈段寄存器(SS)中。

在把一个段选择符加载到段寄存器之前,处理器会进行特权级检查。它会将当前运行程序或任务的CPL、段选择符的RPL、段描述符的DPL进行比较。只有当 DPL >= max{CPL, RPL}时,才会将段选择符加载到段寄存器,否则将产生一个一般保护异常。

已知一个程序或任务的可循之区域随着其CPL的改变而变化。当CPL=0时,所有特权级的数据段都可以访问。CPL=1时,只能访问特权级1~3的数据。

代码段之间转移控制时的特权级检查

对于将程序的控制权从一个代码段转移到另一个代码段,目标代码段的短选择符必须加载进代码段寄存器CS中。作为加载过程的一部分,处理器会检查特权级、限长等。如果检查通过,则能成功加载。于是控制权转移到新代码段,程序从EIP指向的指令集训执行。

程序的控制转移使用指令 JMP,RET,INT,和IRET以及异常和中断机制来实现。

JMP和CALL指令可以利用下面四种方法之一来引用另一个代码段:

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

先介绍前两种。

1. 直接调用或者跳转到代码段

JMP、CALL、RET的近转移形式只在当前代码段中执行控制程序转移,所以不会执行特权级检查。

远转移形式会把控制转移到另外一个代码段中。此时一定会执行特权级检查。

不通过调用门把程序控制权转移到另一个代码段时,处理器会验证4种特权级:

  • 当前特权级CPL(执行调用的代码段的特权级)
  • 含有被调用过程的墓地代码段段描述符的描述符特权级DPL
  • 目的代码段的段选择符的请求特权级
  • 目的代码段描述符中的一致性标志C。它确定了一个代码段是一致还是非一致代码段。

2. 门描述符

为了对具有不同特权级的代码段提供受控的访问,处理器提供了称为门描述符的特殊描述符集。共有四种门描述符:

  • 调用门:TYPE=12
  • 陷阱门:TYPE=15
  • 中断门:TYPE=14
  • 任务门:TYPE=5

调用门用于在不同的特权级之间实现受控的程序控制转移。它们通常仅用于使用特权级保护机制的操作系统中。下图给出了调用门描述符的格式。调用门描述符可以存放在GDT或者LDT。一个调用门具有以下功能:

  • 指定要访问的功能
  • 在指定代码段中定义程序的入口点
  • 指定访问过程的调用者具备的特权级
  • 如果发生堆栈切换,它会指定在堆栈之间需要复制的可选参数个数。
  • 指明调用门描述符是否有效。

调用门描述符格式

调用门中的段选择符(段选择子)指定要访问的代码段。偏移值字段指定段中的入口点。这个入口点通常是指定过程的第一条指令。DPL字段指定调用门的特权级,从而指定通过调用门访问特定过程所要求的特权级。(Linux内核没有用到调用门。介绍这里是为了中断和异常门做准备…)

3. 通过调用门访问代码段

为了访问调用门,首先需要CALL或JMP指令的操作数提供一个远指针。

1
2
char near *p;//近指针
char far *p; //远指针

对于8086处理器(16位机),逻辑地址=段地址(16bit)+偏移(16bit)

短指针指的是只包含偏移的16bit,所以只能在段内寻址。

长指针则是 段地址+偏移的32bit,可以跨段寻址。

然而在80386处理器(32位机),所有的指针都是32bit。长指针则指的是 16bit的段+32bit的偏移 = 48bit

门调用操作过程

长指针的偏移并不被使用。(因为门描述符里保存了入口点)

通过调用门进行程序控制转移时,CPU会对当前特权级CPL和调用门选择符中的请求特权级RPL、调用门描述符中的描述符特权级DPL和目的代码段描述符的DPL四种不同的特权级进行检查,以确定转移的有效性。(具体检查细节不表。)

调用门可以让一个代码段中的过程被不同特权级的程序访问。例如,位于一个代码段中的操作系统代码可能含有操作系统自身和应用软件都允许访问的代码(比如处理字符IO的代码)。因此可以为这些过程设置一个所有特权级代码都能访问的调用门。另外可以专门为仅用于操作系统的代码设置一些更高特权级的调用门。

4. 堆栈切换

每当调用门把程序控制转移到一个更高级别的非一致性代码段时,CPU会自动切换到目的代码段特权级的堆栈中。目的是防止高特权级程序由于栈空间不足而引起崩溃,也防止低特权级程序通过共享的堆栈干扰高特权级的程序。

每个任务最多为每个特权级定义一个栈。只使用0,3特权级的系统只定义两个栈。这两个栈位于不同的段中。

特权级3的程序在执行时,特权级3的堆栈的段选择符和栈指针会分别存放在SS和ESP,并且在发生堆栈切换时保存在被调用过程的堆栈上。

特权级0,1,2的堆栈初始指针值都存放在当前运行任务的TSS段中。TSS段中的这些指针都是只读的。在任务运行时CPU并不会修改他们。当调用更高特权级程序时,CPU才会用他们建立新的栈,而返回时栈直接销毁。下一次调用该过程时再使用TSS的初始指针建立一个新的栈。

操作系统需要负责为所有用到的特权级建立堆栈和堆栈段描述符,并且在任务的TSS中设置初始指针值。每个栈必须可写,并且需要足够的空间存放以下信息:

  • 调用过程的SS、ESP、CS和EIP寄存器空间
  • 被调用过程的参数和临时变量所需要使用的空间
  • 隐含调用一个异常或中断过程时标志寄存器EFLAGS和出错码使用的空间

5. 从被调用过程返回

指令RET用于执行近返回、同特权级远返回、和不同特权级的远返回。该指令用于从使用CALL指令调用的过程中返回。近返回仅在当前代码段中转移程序控制权,因此CPU仅进行界限检查。对于相同特权级的远返回,CPU同时从堆栈弹出返回代码段的选择符和返回指令指针。由于通常情况下这两个指针时CALL指令压入栈的,因此他们理论上应该有效,但是CPU还是会执行特权级检查以应付当前过程可能修改指针值或者堆栈出现问题时的情况。(栈溢出啊栈溢出)

(远返且特权级不同则需要切换堆栈。细节不表)

页级目录保护

页目录和页表表项中的读写标志R/W 以及超级用户标志 U/S提供了分段机制保护属性的一个子集。分页机制只识别两级权限:超级用户级0,1,2,以及普通用户级3。普通用户级的页面可以标志为 只读/可执行,可读/可写/可执行。超级用户级的页面对于超级用户来将总是可读/可写/可执行的,但是普通用户不能访问。超级用户级执行的程序不仅可以访问超级用户级页面,也可以访问普通用户级的页面。