1、特权级
2、一致代码段和非一致代码段
3、DPL、RPL、CPL分别代表的含义,存储在什么位置,以及它们之间的关系
4、不同特权级数据段之间的访问规则
5、不同特权级代码段之间的转移
6、代码段之间的转移对堆栈的影响
7、结合pmtest5.asm来见证不同特权级代码段之间的跳转
一、特权级
在IA32的分段机制下,特权级总共有4个特权级别,从高到低分别是0、1、2、3。数字越小表示的特权级越大。特权级如下图所示:
较为核心的代码和数据,将被存放在特权级较高的层级中。处理器将用这样的机制来避免低特权级的任务在不被允许的情况下访问位于高特权级的段。
二、一致代码段 和 非一致代码段
系统要安全,必须保证内核与用户程序分离开,内核要安全,必须不能被用户来打扰。但是有的时候,用户程序也是需要访问内核中的部分数据,那怎么办?
于是操作系统就将内核中的段分为共享的代码段和非共享的代码段两部分。
其中一致代码段就是操作系统拿出来被共享的代码段,可以被低特权级的用户直接访问的代码。
一致代码段的限制作用:
(1)特权级高的代码段不允许访问特权级低的代码段:即内核态不允许调用用户态下的代码。
(2)特权级低的代码段可以访问特权级高的代码段,但是当前的特权级不发生变化。即:用户态可以访问内核态的代码,但是用户态仍然是用户态。
非一致代码段:为了避免低特权级的访问而被操作系统保护起来的系统代码,也就是非共享代码。
非一致代码段的限制作用:
(1)只允许同特权级间访问
(2)绝对禁止不同级间访问,即:用户态不能访问内核态,内核态也不访问用户态。
下图为一致码段与非一致码段的访问规则:
三、CPL、DPL、RPL分别代表的含义,存储在什么位置,以及它们之间的关系
1、CPL(Current Privilege Level)是当前执行的程序或任务的特权级。它被存储在CS和SS的第0位和第1位上。通常情况下,CPL等于代码的段的特权级。在遇到一致代码段时,一致代码段可以被相同或者更低特权级的代码访问。当处理器访问一个与CPL特权级不同的一致代码段时,CPL不会被改变。
2、DPL(Descriptor Privilege Level):DPL表示段或者门的特权级,它被存储在段描述符或者门描述符的DPL字段中。当当前代码段试图访问一个段或者门时,DPL将会和CPL以及段或门选择子的RPL相比较,根据段或者门类型的不同,DPL将会被区别对待,下面介绍一下各种类型的段或者门的情况。
(1)数据段:DPL规定了可以访问此段的最低特权级。比如,一个数据段的DPL是1,那么只有运行在CPL为0或者1的程序才有权访问它。
(2)非一致代码段(不使用调用门的情况下):DPL规定访问此段的特权级。比如一个非一致代码段的特权级为0,那么只有CPL为0的程序才可以访问它。
(3)调用门:DPL规定了当前执行的程序或任务可以访问此调用门的最低特权级(这与数据段的规则是一致的)。
(4)一致代码段和通过调用门访问的非一致代码段:DPL规定了访问此段的最高特权级。比如,一个一致代码段的DPL是2,那么CPL为0和1的程序将无法访问此段。
3、RPL(Requested Privilege Level):RPL是通过选择子的第0位和第1位表现出来的。处理器通过检查RPL和CPL来确认一个访问请求是否合法。
四、不同特权级数据段之间的访问规则
数据段中DPL规定了可以访问此段的最低特权级,因此,对数据的访问,只要CPL和RPL都小于被访问的数据段的DPL就可以了,即CPL<=DPL和RPL<=DPL。
五、不同特权级代码段之间的转移
使用jmp或call指令可以实现下列4种转移
(1)目标操作数包含目标代码段的段选择子。
(2)目标操作数指向一个包含目标代码段选择子的调用门描述符。
(3)目标操作数指向一个包含目标代码段选择子的TSS。
(4)目标操作数指向一个任务门,这个任务门指向一个包含目标代码段选择子的TSS。
这4种方式可以看做是两大类,一类是通过jmp和call的直接转移(上述第一种),另一类是通过某个描述
符的间接转移(上述第2,3,4种)。
1、通过jmp或call进行直接转移
2、通过调用门进行转移
(1)门描述符的结构
调用门描述符里面保存着目标代码段的段选择子,偏移量,以及属性。
(2)调用门的使用方式
假设我们想由代码A转移到代码B,运用一个调用门G,即调用门G中的目标选择子指向代码B的段。实际上,这个问题主要涉及这几个元素:CPL、RPL、代码B的DPL(记做DPL_B),调用门G的DPL(记做DPL_G)。
调用门使用时特权级检验的规则如下:
也就是说,通过调用门和call指令,可以实现从低特权级到高特权级的转移,无论目标代码段时一致的还是非一致的。
通过调用门和jmp指令,如果目标代码段是一致的,则可以实现从低特权级到高特权级的转移。如果目标代码段是非一致的,则只能实现相同特权级的转移。
六、代码段之间的转移对堆栈的影响
1、“长”跳转/调用 和 “短”跳转/调用
如果一个调用或跳转指令时段间而不是段内进行的,那么我们称之为“长”的(Far jmp/call),反之,如果在段内则是“短”的(Near jmp/call)。
那么长的和短的jmp或call有什么分别呢?
对于jmp而言,仅仅是结果不同罢了,短跳转对应段内,长跳转对应段间。
对于call来说,就比较复杂一些,因为call指令是会影响堆栈的,长调用和短调用对堆栈的影响是不同的。
下面我们讨论短调用对堆栈的影响,call指令执行时下一条指令的eip压栈,到ret指令执行时,这个eip会被从堆栈中弹出,
如下图所示:
这是短调用的情况。
下面我们讨论长调用对堆栈的影响,call指令执行时会将调用者的cs和eip压栈,到ret指令执行时,这个eip和cs会被从堆栈中弹出,如下图所示:
2、有特权级变换的转移对堆栈的影响
在不同特权级下的堆栈段不同,所以每一个任务最多可能在4个特权级间转移,所以,每个任务实际上需要4个堆栈。可是我们只有一个ss和一个esp,那么当发生堆栈切换,我们该从哪里获得其余堆栈的ss和esp呢?
解决这个问题,需要一个数据结构TSS(Task-State Stack),如图:
当堆栈发生切换时,内层的ss和esp就是从这里取得的,比如,我们当前所在的是ring3,当转移至ring1时,堆栈将被自动切换到由ss1和esp1指定的位置。由于只是在由外层转移到内层(低特权级到高特权级)切换时新堆栈才会从TSS中取得,所以TSS中没有位于最外层的ring3的堆栈信息。
下面让我们来看看整个的转移过程是怎么样的?
执行call前后堆栈段的变化:
(1)根据目标代码段的DPL(新的CPL)从TSS中选择应该切换至哪个ss和esp
(2)从TSS中读取新的ss和esp。在这过程中如果发现ss、esp或者TSS界限错误都会导致无效TSS异常
(3)对ss描述符进行检验,如果发生错误,同样产生#TS异常
(4)暂时性地保存当前ss和esp的值
(5)加载新的ss和esp
(6)将刚刚保存起来的ss和esp的值压入新栈
(7)从调用者堆栈中将参数复制到被调用者堆栈(新堆栈)中,复制参数的数目由调用门中Param Count一项来决定。
(8)如果Param Count是零的话,将不会复制参数。
(9)将当前的cs和eip压栈
(10)加载调用门中指定的新的cs和eip,开始执行被调用者过程。
执行ret前后堆栈段的变化:
(1)检查保存的cs中的RPL以判断返回时是否要变换特权级
(2)加载被调用者堆栈上的cs和eip(此时会进行代码段描述符和选择子类型和特权级检验)
(3)如果ret指令含有参数,则增加esp的值以跳过参数,然后esp将指向被保存过的调用者ss和esp。注意,ret的参数必须对应调用门中的Param Count的值
(4)加载ss和esp,切换到调用者堆栈,被调用者的ss和esp被丢弃。在这里将会进行ss描述符、esp、以及ss段描述符的检验
(5)如果ret指令含有参数,增加esp的值以跳过参数(此时已经在调用者堆栈中)
(6)检查ds、es、fs、gs的值,如果其中哪一个寄存器指向的段的DPL小于CPL(此规则不适合于一致代码段),那么一个空描述符会被加载到该寄存器中。
综上所述,使用调用门的过程实际上分为两部分,一部分是从低特权级到高特权级,通过调用门和call指令来实现;另一部分则是从高特权级到低特权级,通过ret指令来实现.