在IA32的分段机制中,总共有4个特权级别,从高到低分别是0、1、2、3。处理器通过识别下面3中特权级进行特权级检验
1.CPL(Current Privilege Level)CPL是当前执行的程序或者任务的特权级,它被存储在CS和SS的第0位和第1位上,通常情况下,CPL等于代码所在的段的特权级,但是有一个例外是如果在遇到一致代码段时,当处理器访问一个与CPL特权级不同的代码段时,CPL不会发生改变。
什么是一致码段呢?它其实是由段描述符数据结构中的属性字段决定的,“一致”的意思是当转移的目标是一个特权级更高的代码段时,当前的特权级会被延续下去,而向特权级更高的非一致代码段转移会引起常规保护错误,除非使用调用们或者任务门。而当目标代码段的特权级低的话,无论它是不是一致代码段,都不能通过call或者jmp转移进去。另外所以的数据段都是非一致的,这意味着不可能被低特权级的代码访问到,然而,与代码段不同的是,数据段可以被更高特权级的代码段访问,而不需要特定的门。
2.DPL(Descriptor Privilege Level) DPL表示段或者门的特权级,它被存储在段描述符或者门描述符的DPL字段中,当当前代码段试图访问一个段或者门时,DPL将会和CPL以及段或者门选择自的RPL相比较。DPL的比较分下面5种情况:
- 数据段:DPL规定了可以访问此段的最低特权级
- 非一致代码段(不使用调用门情况下):DPL规定了访问此段的特权级
- 调用门:DPL规定了当前执行的程序或者任务可以访问此调用门的最低特权级(这个和数据段的规则是一致的)
- 一致代码段和通过调用门访问的非一致代码段:DPL规定了可以访问此段的最高特权级
- TSS:DPL规定了可以访问此TSS的最低特权级
3.RPL(Requested Privilege Level) RPL是通过段选择子的第0位和第1位表示出来的。处理器通过检查RPL和CPL来确认一个访问请求是否合法。
所以对数据段的访问比较简单,就是检验特权级,只要CPL和RPL都小于被访问的数据段的DPL就可以了
对程序段的访问比较复杂,程序从一个代码转移到另外一个代码之前,目标代码段的选择子会被加载到cs中,作为加载的一部分,处理器将检查描述符的界限、类型、特权级的内容。如果检验成功,cs将会被加载。
使用jmp或者call指令可以实现下列4种转移:
- 目标操作数包含目标代码段的段选择子(直接转移)
- 目标操作数指向一个包含目标代码段选择子的调用门描述符(间接转移)
- 目标操作数指向一个包含目标代码段选择子的TSS(间接转移)
- 目标操作数指向一个任务门,这个任务门指向一个包含目标代码段选择子的TSS(间接转移)
调用门:什么是调用门,它也是一种描述符,它的结构定义如下:
; 门
; usage: Gate Selector, Offset, DCount, Attr
; Selector: dw
; Offset: dd
; DCount: db
; Attr: db
%macro Gate 4
dw (%2 & 0FFFFh); 偏移 1(2 字节)
dw %1; 选择子(2 字节)
dw (%3 & 1Fh) | ((%4 << 8) & 0FF00h) ; 属性(2 字节)
dw ((%2 >> 16) & 0FFFFh); 偏移 2(2 字节)
%endmacro ; 共 8 字节
和段描述符的6个字节结构不一样,门是由8字节构成,这个描述符也是放置在GDT,LDT或者IDT中以供调用。门描述符分为4种:
- 调用门
- 中断门
- 陷阱门
- 任务门
门调用其实就是提供了一个入口地址,它可以实现的是不同特权级的代码之间的转移。下面介绍一下具体的过程:
假设我们想由代码A转移到代码B,运用一个调用门G,即调用门G中的目标选择子指向代码B的段,我们涉及这么几个要素:CPL、RPL、代码B的DPL(DPL_B)和调用门的DPL(DPL_G)。根据门调用的规则,A要调用G,则A的CPL和RPL要小于DPL_G,另外还要比较CPL和DPL_B,如果是一致代码段的话,要求DPL_B小于等于CPL,如果是非一致代码段的话,call指令要求DPL_B小于等于CPL,jmp指令要求DPL_B=CPL。
这里我不是特别清楚,可能是如果没有通过门调用,会直接比较RPL和DPL_B,因此门调用能够实现从低特权级到高特权级的转移。
TSS介绍:在进行call和jmp时候,必然要使用堆栈,然而由于不同的特权级会使用不同的堆栈,因此一个任务最多可能有4个堆栈,那么当转移指令在不同特权级之间变化的时候,压入堆栈的内容在另外一个特权级里面已经不是原来的堆栈了,对于这个问题,就引出了TSS(Task-State Stack)这个数据结构。这个数据结构中存在着ss2、esp2;ss1、esp1;ss0、esp0,转移过程如下:
- 根据目标代码的DPL从TSS中选择应该切换至哪个ss和esp
- 从TSS中读取新的ss和esp
- 暂时性地保存当前的ss和esp的值
- 加载新的ss和esp
- 将刚刚保存起来的ss和esp的值压入新栈
- 将当前的cs和eip压栈
- 加载调用门中指定的新的cs和eip。
所以,从上面的过程中我们可以知道,在进行特权级转移时,首先根据目标代码的DPL从TSS中取得对应的ss和esp,然后加载新的ss和esp,也就是使用新的堆栈,并把老的堆栈的信息压入新栈,以便返回时使用,然后再进行转移
下面说明代码段和数据段的访问:
一、代码段间跳转
1、普通(直接)跳转:
JMP Selector:0 或 CALL Selector:0
1)一致代码段(JMP&CALL)
要求:CPL>=DPL,RPL不作检查
特权变化:跳转后程序CPL=跳转前程序CPL
2)非一致代码段(JMP&CALL)
要求:CPL=DPL & RPL<=DPL
特权变化:跳转后程序CPL=目标代码段DPL
2、通过调用门跳转:
JMP 调用门Selector:0 或 CALL 调用门Selector:0 (注意:此时如果选择子后面跟着32位偏移量也不会被CPU使用,因为调用门描述符已经记录了目标代码的偏移)
step1: 要求:指示调用门的选择子的RPL<=门描述符DPL & 当前代码段的CPL<=门描述符的DPL。
只有满足以上条件时,CPU才会进一步从调用门描述符中读取代码段的选择子或地址偏移。而从调用门中读取代码选择子和地址偏移后,跟普通跳转又站在同一起跑线上了。
唯一不同的是CPU会将目标代码段RPL清0。此后需要分类讨论,如下:
step2:
1)一致代码段(JMP&CALL) <------------------------------------------------------------------------------------------------------
要求:CPL>=DPL,RPL不作检查(因为RPL总被清0) |
特权变化:跳转后程序CPL=跳转前程序CPL |
比较
2)非一致代码段(JMP) |
要求:CPL=DPL,RPL不作检查(因为RPL总被清0) |
特权变化:跳转后程序CPL=目标代码段DPL |
3)非一致代码段(CALL) <------------------------------------------------------------------------------------------------------------
要求:CPL>=DPL,RPL不作检查(因为RPL总被清0)
特权变化:跳转后程序CPL=目标代码段DPL(CPL>DPL的情况下,特权级发生跃迁)
二、访问数据段
数据段:特权级低->高:NO | 特权级高->低:YES | 特权级同级之间:YES
注意:
1、一致代码段:无论那种方式跳转到一致代码段,CPL都不会改变(不变化为目标代码段的DPL),也即加载目标代码段选择子时,只加载高14位,表示CPL的低2位保持不变。
因此,“一致”的意思就是——代码段被调用执行时,不使用自己描述符的DPL,而采用调用这特权级,CS的低2位保持不变(与“调用者保持一致”)
2、非一致代码段:无论采用哪种方式跳转到非一致代码段,CPL都发生变化,也即在加载目标代码段选择子时,将整个选择子放入到CS中。
3、为了访问调用门,调用者程序的特权级CPL必须小于或等于调用门的DPL。调用门段选择符的RPL也要同调用CPL一样遵守相同的规则,即RPL也必须小于或等于调用门的DPL