保护机制是多任务环境和系统能够运行的一种基础,它能够保护任务独立运行,免受其他任务的干扰。在 80x86 设计中,在分页机制和分段机制下使用了保护机制。例如分段过程中有虚拟内存的保护,能够保证应用程序在访问两个不同的任务下不会相互干扰;另外还有段和寄存器之间的保护,通过定义优先级来判断应用程序是否具有访问指定段和寄存器的权限,而分页里面由于有页目录和页表结构的存在,这个结构中有 R/W 和 U/S 位,也可以提供访问和写入保护。
下面我们就来详细聊一下保护机制,即分段对应段级保护,分页对应页级保护。
段级保护
在保护模式下,80x86 提供了段级保护和页级保护。这种保护机制根据特权级提供对段和页的访问能力,段保护是 Level-4 级保护,页保护是 Level-2 级保护。操作系统代码和数据存放在要比普通应用程序具有高特权级的段中。此后处理器的保护机制将会限制应用程序按照指定特权级的权限来访问。
使用保护机制时,每个内存引用都会被检查用来验证内存引用的保护要求,如果符合内存保护要求的话,就会执行地址转换操作,这种检查 - 执行的操作很像我们平常写代码的(下述为伪代码)
if(expression){ ... }else{ ... }
检查和执行操作是同时进行的,因此性能不会受到太多影响。
说到保护机制的检查,下面共有几种检查方式:
段限长检查;
段类型检查;
特权级检查;
可寻址范围限制;
过程入口点限制;
指令集限制。
如果违反上面任意一种检查操作都将导致一个异常产生,下面我们就来具体聊一下这些检查机制都是干什么的。
段限长检查
还记得段描述符吗?这段描述能够比较形象的说明段描述符的作用:
段描述符是 GDT 和 LDT 表中的一个数据项,用于向处理器提供有关一个段的位置和大小信息以及访问控制的状态信息。
段描述符能够向处理器提供段的位置和大小等相关信息,每个段都由段基址(BASE)、段界限(LIMIT)和段属性组成,这表明段是有限制的。
段限长检查也就是对段界限/段限长进行 Limit 检查,它会限制应用程序防止其寻址到段外内存位置。段限长的有效值会依赖于颗粒度标志 G 的设置状态,如果是数据段的话,段限长还与 E 标志(扩展方向)、B 标志(默认栈指针大小/上界限)有关。
颗粒度标志 --- G
这个字段用于确定段限长字段 Limit 值的单位,如果颗粒度标志为 0 ,则段限长值的单位是字节;有效范围是 20 位段描述符中段限长字段 Limit 的值,Limit 的范围从 0 - 0xFFFFF(1MB)。
如果设置了颗粒度标志,则段限长使用 4KB 单位,这时候 Limit 需要乘以颗粒度标志,有效范围是从 0 - 0xFFFFFFFF(4GB)。
这里需要注意:段偏移地址的低 12 位不会进行 Limit 检查。
扩展方向 --- E
段有两种扩展方式,一种是向上扩展,一种是向下扩展。根据扩展方向 E 的不同,处理器会以不同的方式使用数据段;对于向上扩展的数据段(简称上扩段),逻辑地址中的偏移范围从 0 - 段限长 Limit 。大于 Limit 的偏移值会产生异常;对于向下扩展的数据段,段限长 Limit 的含义相反。根据默认栈指针大小标志 B 的设置,偏移范围可从段限长 Limit 到 0xFFFFFFFF 或 0xFFF。而小于段限长 Limit 的偏移值会产生一般性保护异常。对于下扩段来说,减小段限长字段中的值会在该段地址空间底部分配新的内存。由于 80x86 是向下扩展的,因此这种方式很适合扩展堆栈。
D/B --- 默认操作大小/默认栈指针大小和/或上界限 Default operation size/default stack pointer size and/or upper bound
根据段描述符表示的是可执行代码段、下扩数据段还是堆栈段,这个标志具有不同的功能(如果是 32 位,这个标志应该设置为 1,16 位应该设置为 0 )。如果是可执行代码段时,这个标志是 D 标志;如果是栈段和下扩数据段,这个标志是 B 标志;
除了下扩数据段以外的所有类型,有效 Limit 的值是段中允许被访问的最后一个地址,它比段长度小 1 个字节。任何超出段限长字段指定的有效地址范围都将产生一个保护性异常。
对于下扩数据段来说,段限长具有同样的功能,但是其含义不同。段限长指定了段中最后一个不允许访问的地址,因此在设置了 B 标志的情况下,有效偏移范围是从(有效段偏移 + 1)到 0xFFFF FFFF;当 B 清零时,有效地址范围从(有效段偏移 + 1)到 0xFFFF 。当下扩段段限长为 0 时,段会有最大长度。
除了对段限长进行检查,处理器还会检查段描述符表的长度。GDTR、IDTR、LDTR 寄存器中包含有 16 位的段限长,处理器用它来防止程序在描述符表外面选择描述符。描述符表的限长值指明了表中最后一个有效字节。因为每个描述符是 8 字节,因此含有 N 个描述符项的表应具有的限长值为 8N - 1。
段类型 TYPE 检查
除了应用程序代码和数据段有描述符之外,处理器还有系统段和门两种描述符类型。这些数据结构用于管理任务以及异常和中断。但是并非所有的描述符都定义一个段。段描述符在结构中的 S 标志和类型字段 TYPE 含有类型信息。处理器利用这些信息对由于非法使用段或门导致的编程错误进行检测。
当操作段选择子和段描述符时,处理器会随时检查类型信息。主要在以下两种情况下检查类型信息:
当一个描述符的选择子加载进一个段寄存器中。此时某些段寄存器只能存放特定类型的描述符,比如
CS 寄存器中只能被加载进一个可执行段的选择子;
不可读可执行段的选择子不能被加载进数据段寄存器中;
只有可写数据段的选择子才能被加载进 SS 寄存器中。
当指令访问一个段,而且该段的描述符已经加载进段寄存器中。指令只能使用某些预定义的方法来访问某些段
任何指令不能写一个可执行段;
任何指令不能写一个可写位没有置位的数据段;
任何指令不能读一个可执行段,除非可执行段设置了可读标志。
特权级
处理器的保护机制有四个级别,之前也聊过了,这四个级别从 0 -> 3 依次降低,数值越大,特权级越小。下图是特权级的四个级别。
在上图中,Level-0 位于最内侧,这一层是内核层,由内核代码、数据和堆栈组成,由操作系统的内核访问;Level-0 的外环是 Level-1 和 Level-2 层,这两层就是由操作系统访问的逻辑层,最外层是 Level-3 层,由应用程序访问。
处理器会利用特权级来防止运行在较低特权级的程序或任务访问具有较高特权级的段,也就是具有特权级为 Level 1-3 的不能访问 Level - 0 特权级,而 Level-0 特权级可以访问 Level 1 - 3 ,当处理器检测到一个违反特权级的操作时,会触发一个一般保护性异常。
这个好理解,正如你领导的办公室你没有权限随便进一样,要是给你权限那不就麻烦了吗?万一领导正在教训女员工让你碰到了可咋整?相反的情况,领导可以随便进任何一个人的屋子,难道领导进你们小开发的屋子还先敲门申请?那领导不是永远发现不了你在摸鱼吗?
处理器能够识别的特权级有三种:
当前特权级 CPL(Current Priviledge Level),这个是当前正在执行程序或任务的特权级。它一般会存放在 CS 寄存器中,也有可能存放在 SS 寄存器中。通常 CPL 等于当前代码段的特权级。当程序把控制权转移到另外一个具有不同特权级的代码段中时,处理器就会改变 CPL 。
描述符的特权级 DPL(Descriptor Priviledge Level),DPL 表示段描述符/门描述符的特权级,存储在段描述符的 DPL 字段中,当前代码想要访问段描述符时,段描述符的 DPL 会和当前代码段的 CPL 以及段选择子的 RPL(下面会说明)进行比较,由于段描述符有不同的类型,所以需要分情况讨论:
如果是数据段(Data Segment),DPL 只能允许比它自己特权级大的代码段访问,也就是说如果 DPL = 1,那么只有当前代码段等于 0 和 1 才能访问。
非一致性代码段(Nonconforming code segment),我在学到这里的时候比较疑惑,什么叫做非一致代码段,难道还有一致性代码段?我查阅资料后发现果不其然,一致性代码段和非一致性代码段是在段描述符中的 S 位进行区分的,S = 0 表示系统,S = 1 表示代码或数据。
当 S = 1 时,TYPE 中有四个二进制位(位 8 - 位 11),位 8 -> 位 11 分别是 访问位、读写位、一致位、执行位 ,大家看到了吗,这个是否表示一致性代码是由这个位 10 一致位来判断的。此位置 1 表示一致性代码,为 0 表示非一致性代码。
这里解释下什么是一致性代码和非一致性代码:
一致性代码就是操作系统拿出来可以共享的代码段,可以被低特权级用户直接调用访问的代码;非一致性代码是为了避免低特权级的访问而被操作系统保护起来的系统代码。
如果某个非一致性代码的 DPL = 0 ,那么只有 CPL 为 0 的程序才能够访问这个段。
调用门(Call Gate)(下述会讨论),调用门的 DPL 指出访问调用门的当前执行程序或任务可处于的最大特权级数值。
一致性和非一致性代码(通过调用门访问),其 DPL 指出允许访问代码段的程序或任务具有的最小特权级数值。比如代码段的特权级是 2,那么 CPL = 0 就不能访问。
任务状态段 TSS,其 DPL 指出允许访问 TSS 的当前程序或任务具有的最大特权级数值。
请求特权级 RPL (Request Privilege Level),RPL 是段选择子的特权级,在段选择子的位 0 和 位 1,如下图所示
处理器会同时检查 RPL 和 CPL 来确定是否允许访问一个段。如果程序有足够的 CPL 特权级,那么 RPL 特权级不够的话也不能访问。也可以理解为 RPL 的高特权级会覆盖 CPL 的低特权级。
访问数据段时的特权级检查
访问数据段时会进行特权级检查,数据段中的段选择子会存储在数据段寄存器和堆栈段寄存器中,数据段寄存器就四个,即 DS、ES、FS 或 GS,堆栈段寄存器就是 SS。
把段选择子加载进段寄存器的指令是 MOV、POP、LDS、LES、LFS、LGS 和 LSS。常见的就是 MOV 和 POP,一般记住这两个就行。
通过加载指令把段选择子加载进段寄存器之前会进行特权级检查,特权级检查会把当前运行程序的 CPL 、段选择子 RPL 和段描述符的 DPL 进行比较,如下图所示。
只有当段的 DPL >= CPU && RPL 时,处理器才会把段选择子加载进段寄存器中。否则就会产生一个一般性异常,并且不加载段选择子。
另外,有可能会把数据保存在代码段中。我们可以通过下面三种方式来访问代码段中的数据:
把非一致可读代码段的段选择子加载进数据段寄存器中。
把一致可读代码段的选择子加载进一个数据段寄存器中。
使用代码段覆盖前缀 CS 来读取一个选择子已经在 CS 寄存器中的可读代码段。
在使用堆栈选择子加载 SS 寄存器时也会执行特权级检查。这里和堆栈相关的特权级必须与 CPL 匹配。也就是 CPL 、堆栈选择子的 RPL 和堆栈描述符的 DPL 相同,否则也会产生一般性保护异常。
在切换代码段时的特权级检查
处理器会频繁的执行在不同代码段之间的切换工作。对于将程序控制权从代码段转移到另一个代码段时,目标代码段的段选择子必须要先加载进代码段寄存器中。当然在加载进代码段寄存器之前还需要进行特权级检查工作,emm 必要的工作不能少。
此时的特权级检查包括对段限长、段类型和特权级进行检查,检查没问题后才会把目标代码段加载进 CS 寄存器,这样就会把控制转移权交给目标代码段,从 CS:EIP 处开始执行代码。
把控制转移权交给目标代码段固然是一句话就可以描述的事情,但是,,,如何才会使得控制权进行转移呢?一般有这几种指令:JMP、CALL、RET、INT 和 IRET,除此之外,异常和中断机制也是一种实现方式。
编辑:黄飞
评论
查看更多