《Orange’s 一个操作系统的实现》3.保护模式5----特权级概述(转)
学习过程中遇到一个对保护模式总结很好的Blog,转来分享一下。
先说下特权级的概念,在保护模式下,系统依靠特权级来实施代码和数据的保护,相当于权限啦。特权级共有4个级别,0,1,2,3,数字越小表示权限越高。如图:
较为核心的代码和数据放在较高(靠内)的层级中,处理器用此来防止较低特权的任务在不被允许的情况下访问处于高特权级的段。为了防止概念混淆,我们不用特权级大小来说明,改为内层(高),外层(低)来讲。
特权级有3种:CPL,DPL和RPL,每个都是有4个等级。
我对他们的关系理解是这样:一般来说,CPL代表当前代码段的权限,如果它想要去访问一个段或门,首先要看看对方的权限如何,也就是检查对方的DPL,如果满足当前的权限比要访问的权限高,则有可能允许去访问,有些情况我们还要检查
选择子的权限,即RPL,因为我们通过选择子:偏移量的方式去访问一个段,这算是一个访问请求动作,因此
称为请求访问权限RPL(Requst Privilege Level)。当请求权限也满足条件,那么访问就被允许了。
CPL(Current Privilege Level)
CPL是当前执行的任务的特权等级,它存储在CS和SS的第0位和第1位上。(两位表示0~3四个等级)
通常情况下,CPL等于代码所在段的特权等级,当程序转移到不同的代码段时,处理器将改变CPL。
注意:在遇到一致代码段时,情况特殊,一致代码段的特点是:可以被等级相同或者更低特权级的代码访问,当处理器访问一个与当前代码段CPL特权级不同的一致代码段时,CPL不会改变。
DPL(Descriptor Privilege Level)
表示门或者段的特权级,存储在门或者段的描述符的DPL字段中。正如上面说的那样,当当前代码段试图访问一个段或者门时,其DPL将会和当前特权级CPL以及段或门的选择子比较,根据段或者门的类型不同,DPL的含义不同:
1.数据段的DPL:规定了访问此段的最低权限。比如一个数据段的DPL是1,那么只有运行在CPL为0或1的程序才可能访问它。为什么说可能呢?因为还有一个比较的因素是RPL。访问数据段要满足有效特权级别(上述)高于数据段的DPL.
2.非一致代码段的DPL(不使用调用门的情况):DPL规定访问此段的特权,只有CPL与之相等才有可能访问。
3.调用门的DPL,规定了程序或任务访问该门的最低权限。与数据段同。
4.一致代码段和通过调用门访问的非一致代码段,DPL规定访问此段的最高权限。
比如一个段的DPL为2,那么CPL为0或者1的程序都无法访问。
5. TSS的DPL,同数据段。
RPL(Rquest Privilege Level)
RPL是通过选择子的低两位来表现出来的(这么说来,CS和SS也是存放选择子的,同时CPL存放在CS和SS的低两位上,那么对CS和SS来说,选择子的RPL=当前段的CPL)。处理器通过检查RPL和CPL来确认一个访问是否合法。即提出访问的段除了有足够的特权级CPL,如果RPL不够也是不行的(有些情况会忽略RPL检查)。
为什么要有RPL?
操作系统往往通过设置RPL的方法来避免低特权级的应用程序访问高特权级的内层数据。
例子情景:调用者调用操作系统的某过程去访问一个段。
当操作系统(被调用过程)从应用程序(调用者)接受一个选择子时,会把选择子的RPL设置称调用者的权限等级,于是操作系统用这个选择子去访问相应的段时(这时CPL为操作系统的等级,因为正在运行操作系统的代码),处理器会使用调用者的特权级去进行特权级检查,而不是正在实施访问动作的操作系统的特权级(CPL),这样操作系统就不用以自己的身份去访问(就防止了应用去访问需要高权限的内层数据,除非应用程序本身的权限就足够高)。
那么RPL的作用就比较明显了:因为同一时刻只能有一个CPL,而当低权限的应用去调用拥有至高权限的操作系统的功能来访问一个目标段时,进入操作系统代码段时CPL变成了操作系统的CPL,如果没有RPL,那么权限检查的时候就会用CPL,而这个CPL权限比应用程序高,也就可能去访问需要高权限才能访问的数据,这就不安全了。所以引入RPL,让它去代表访问权限,因此在检查CPL的同时,也会检查RPL.一般来说如果RPL的数字比CPL大(权限比CPL的低),那么RPL会起决定性作用。
说这么多不明白都不行啦~真累
下面是引用的一个超棒的关于权限控制的总结:
引用地址
还有一篇文章在此。
一、 访问data segment时(ds、es、fs 及gs)
1、 程序指令要访问数据时,data segment selector 被加载进 data segment register(ds、es、fs 和 gs)前,处理器会进行一系列的权限检查,通过了才能被加载进入segment register。处理器分为两步进行检查:
★ CPL(当前程序运行的权限级别)与RPL(位于selector中的 RPL)作比较,并设置有效权限级别为低权限的一个。
★ 得出的有效权限级别与DPL(segment descriptor 中的DPL)作比较,有效权限级别高于DPL,那么就通过。低于就不允许访问。
如果:CPL = 3、RPL = 2、DPL = 2。那么
EPL = CPL > RPL ? CPL : RPL;
if (EPL <= DPL) { /* 允许访问 */ } else { /* 失败,#GP 异常生产,执行异常 */ }
或者:
if ((CPL <= DPL) && (RPL <= DPL)) { /* 允许访问 */ } else { /* 失败,#GP 异常生产,执行异常 */ } |
也就是要访问目标data segment,那么必须要有足够的权限,这个足够的权限就是:当前运行的权限级别及选择子的请求权限级别要高于等于目标data segment的权限级别。
二、 访问stack segment时
1、 该问stack 时的权限检查更严格,CPL、RPL及DPL三者必须相等才能通过该问请求。
2、 举个例子:
if (CPL == RPL && RPL == DPL && CPL == DPL) {
/* 允许访问 */ } else { /* 失败,#GP 异常生产,执行异常 */ } |
也就是说每个权限级别有相对应的statck segment。不能越权访问,即使高权限访问低权限也是被拒绝的
■ 控制权的转移及权限检查。
权限检查的4个要素:
★ CPL:处理器当前运行的级别,也就是:当前 CS 的级别,在 CS 的 BIT0 ~ Bit1
★ DPL:访问目标代码段所需的级别。定义在 segment descriptor 的 DPL 域中
★ RPL: 通过 selector 进行访问时,selector 内定义的级别。
★ conforming/nonconforming:目标代码属于 nonconforming 还是 conforming 定义在segment descritptor 的 C 标志位中
x86 的各方面检查依赖于目标代码段是 nonconforming(不一致) 还是 conforming(一致) 类型
一、 直接转移(far call 及 far jmp)
1、 直接转移定义为不带gate selector或 taskselector的远调用。当执行一条 call cs:eip 或 jmp cs:eip 指令时,cs 是目标代码段的selector,处理器在加载指令操作数中的cs进cs register前,要进行一系列的权限检查,控制权的转移权限分两部分,根据目标代码段descriptor定义的两种情况:
1)、nonconforming target code segment
★ 直接转移后的权限级别是不能必改变的。因此,CPL 必须要等于目标代码段的 DPL。
★ 要有足够的请求权限进行访问。因此,目标代码段选择子的RPL <= CPL
2)、conforming target code segment
★ conforming code segment 允许访问高权限级别的代码。这里只需检查 CPL >= DPL 即可,RPL 忽略不检查。
★ 转移后CPL不会改变。
2、 以上两步通过后,处理器加载目标代码段的CS 进入CS register,但权限级别不改变,继而RPL被忽略。
★ 处理器根据CS selector在相应的descriptor table 找到 code segment descriptor。CS 的Bit2(TI域) 指示在哪个descriptor table 表查找,CS.TI = 0 时在GDT查找,CS.TI = 1时在LDT 查找。
★ CS的Bit15~Bit3 是selector index 值,处理器基于GDT或LDT来查找segment descriptor。具体是:GDTR.base 或 LDTR.base + CS.SI × 8 得出code segment descritpro。
★ 处理器自动加载code segment descriptor 的 base address、segment limit及attribute 域进入 CS register的相应的隐藏域。
★ 转到CS.base + eip 处执行指令
总结:用代码形式来说明直接转移 call cs:eip 这条指令
例: call 1a:804304c (即cs = 1a, eip = 804304c)
target_cs = 1a;
target_eip = 0x0804304c; CPL = CS.RPL; /* 当前执行的代码段的权限级别就是CPL */ RPL = target_cs.RPL; /* 目标段 selector 的低3位是RPL */ target_si = target_cs.SI; /* 目标段 selector 的索引是Bit15~Bit3 */ target_ti = target_cs.TI; /* 目标段selector的描述符表索引是Bit2 */ CODESEG_DESCRIPTOR target_descriptor;
if (target_ti == 0) { /* target_cs.TI为0 就是参考到 GDT(全局描述符表) */ /* 以GDTR寄存器的base 为基地址加上selector的索引乘以8即得出目标 段描述符,目标描述符的DPL就是目标段所需的访问权限 */ target_descriptor = GDTR.base + target_si * 8 } else { /* 否则就是参考 LDT (局部描述符表)*/ /* 以 LDTR寄存器的base 为基地址得出目标段描述符 */ target_descriptor = LDTR.base + target_si * 8; } DPL = target_descriptor.DPL; /* 获取DPL */
if (target_descriptor.type & 0x06) { /* conforming */
if (CPL >= DPL) { /* 允许执行高权限代码 */ /* go ahead */ } else { /* 引发 #GP 异常 */ goto DO_GP_EXCEPTION; } } else { /* nonconforming */ if (CPL == DPL && RPL <= CPL) { /* go ahead */ } else { /* 引发 #GP 异常 */ goto DO_GP_EXCEPTION; } } /****** go ahead … …******/
CS = target_cs; /* 加载目标段CS 进入 CS 寄存器 */ EIP = target_eip; /* 加载目标指令EIP 进入 EIP 寄存器 */ /* 当前执行权限 CPL 不改变 */ goto target_descriptor.base + target_eip; /* 跳转到目标地址执行 */ DO_GP_EXCEPTION: /* 执行 #GP异常点 */ … … |
二、 使用call gate 进行控制权的转移
使用call gate进行转移控制,目的是建立一个利用gate进行向高权限代码转移的一种保护机制。gate符相当一个进入高权限代码的一个通道。
对于指令 call cs:eip 来说:
★ 目标代码的selector是一个门符的选择子。用来获取门描述符。
★ 门描述符含目标代码段的selector及目标代码的偏移量。
★ 目标代码的ip值被忽略。因为门符已经提供了目标代码的偏移量。
1、 权限的检查
★ 首先,必须要有足够的权限来访问gate符,所以:CPL <= DPLg(门符的DPL)且:RPL <= DPLg
★ 进前代码向高权限代码转移,所以,对于conforming类型的代码段来说,必须CPL >= DPLs(目标代码段的DPL)
★ 对于nonconforming类型代码段来说,必须CPL = DPLs
★ call 指令改变当前权限,而jmp指令不改变当前权限。
总结:
if ((CPL <= DPLg) && (RPL <= DPLg)) { /* 足够的权限访问门符 */
if (target.C == CONFORMING) { /* 目标代码属于 conforming类型 */ /* 向高权限级别代码转移控制 */ if (CPL >= DPLs) { /* 通过访问 */ } else { /* 失败,#Gp异常发生 */ } } else { /* 目标代码属于 nonconforming 类型 */ /* 平级转移 */ if (CPL == DPLs) { /* 通过访问 */ } else { /* 失败,#GP 异常发生 */ } } } else { /* 没有足够权限访问门符 */ /* #GP 异常发生 */ } |
2、 控制权的转移
指令:call 1a:804304c (其中1a是call gate selector)
gate_selector = 0x1a; /* call gate selector */
RPL = gate_selector.RPL; /* 门符选择子RPL */ CPL = CS.RPL; /* 当前代码段低3位是CPL*/ CALLGATE_DESCRIPTOR call_gate_descriptor; /* 门符的描述符 */
CODESEG_DESCRIPTOR target_cs_descritpor; /* 目标代码段的描述符 */ call_gate_si = gate_selector.SI; /* 门符 selector 的索引 */
call_gate_ti = gate_selector.TI; /* 门符selector的描述符表索引 */ /* 获取call gate descriptor */ if (call_gate_ti == 0) { /* TI为0 就是参考到 GDT(全局描述符表) */ /* 以GDTR寄存器的base 为基地址加上selector的索引乘以8即得出门符的描述符,门符的DPL就是门符的访问权限 */ call_gate_descriptor = GDTR.base + call_gate_si * 8; } else { /* 否则就是参考 LDT (局部描述符表)*/ /* 以 LDTR寄存器的base 为基地址得出目标段描述符 */ call_gate_descriptor = LDTR.base + call_gate_si * 8; } /* 获取 target code segment descriptor */ target_cs = call_gate_descriptor.selector; /* 获取门符的目标代码段选择子 */
target_cs_si = target_cs.SI; /* 目标代码段的索引 */ target_cs_ti = target_cs.TI; /* 目标代码描述符表索引 */ if (target_cs_ti == 0)
target_cs_descriptor = GDTR.base + target_cs_si * 8; else target_cs_descriptor = LDTR.base + target_cs_si * 8; DPLg = call_gate_descriptor.DPL; /* 获取门符的DPL */ DPLs = target_cs_descriptor.DPL; /* 获取目标代码段的DPL */ if (CPL <= DPLg && RPL <= DPLg) { /* 有足够权限访问门符 */ if (target_cs_descriptor.type & 0x06) { /* conforming */ if (CPL >= DPLs) { /* 允许访问目标代码段 */ } else { /* #GP 异常产生 */ } } else if (CPL == DPLs) { /* nonconforming */ /* 允许访问目标代码段 */ } else { /* 拒绝访问,#GP 异常发生 */ goto DO_GP_EXCEPTION; } } else { /* 无权限访问门符 */ /* 拒绝访问, #GP异常发生 */ goto DO_GP_EXCEPTION; } /* 允许访问 */
current_CS = target_cs; /* 加载目标代码段进入CS 寄存器 */ current_CS.RPL = DPLs; /* 改变当前执行段权限 */ current_EIP = call_gate_descriptor.offset; /* 加载EIP */ /* 跳转到目标代码执行 */
/* goto current_CS:current_EIP */ goto target_cs_descriptor.base + call_gate_descriptor.offset; return;
DO_GP_EXCEPTION: /* 执行异常 */
|
三、 使用中断门或陷井门进行转移
中断门符及陷井门必须存放在IDT中,IDT表也可以存放call gate。
1、 中断调用时的权限检查
用中断门符进行转移时,所作的权限检查同call gate相同,区别在于intterrupt gate 转移不需要检查RPL,因为,没有RPL需要检查。
★ 必须有足够的权限访问门符,CPL <= DPLg
★ 向同级权限代码转移时,CPL == DPLs,向高权限代码转移时,CPL > DPLs
总结
if (CPL <= DPLg) { /* 有足够权限访问门符 */
if (CPL >= DPLs) { /* 允许访问目标代码头 */ } else { /* 失败,#GP异常发生 */ } } else { /* 失败,#GP异常发生 */ } |
2、 控制权的转移
发生异常或中断调用时
★ 用中断向量在中断描述符表查找描述符:中断向量×8,然后加上IDT表基址得出描述符表。
★ 从查找到的描述符中得到目标代码段选择子,并在相应的GDT或LDT中获取目标代码段描述符。
★ 目标代码段描述符的基址加上门符中的offset,确定最终执行入口点。
中断或陷井门符转移的总结:
例: int 0x80 指令发生的情况
vector = 0x80;
INTGATE_DESCRIPTOR gate_descriptor = IDTR.base + vector * 8; CODESEG_DESCRIPTOR target_descriptor; TSS tss = TR.base; /* 得到TSS 内存块 */ DPLg = gate_descriptor.DPL; target_cs = gate_descriptor.selector; if (CPL <= DPLg) { /* 允许访问门符 */
if (target_cs.TI == 0) { /* index on GDT */ target_descriptor = GDTR.base + target_cs.SI * 8; } else { /* index on LDT */ target_descriptor = LDTR.base + target_cs.SI * 8; } DPLs = target_descriptor.DPL; if (CPL > DPLs) { /* 向高权限代码转移 */ /* 根据目标代码段的DPL值来选取相应权限的stack结构 */ switch (DPLs) { case 0 : /* 假如目标代码处理0级,则选0级的stack结构 */ SS = tss.ss0; ESP = tss.esp0; break; case 1: SS = tss.ss1; ESP = tss.esp1; break; case 2: SS = tss.ss2; ESP = tss.esp2; break; } /* 以下必须保护旧的stack结构,以便返回 */ *--esp = SS; /* 将当前SS入栈保护 */ *--esp = ESP; /* 将当前ESP入栈保护 */ } else if (CPL == DPLs) { /* 同级转移,继续向下执行 */ } else { /* 失败,#GP异常产生,转去处理异常 */ } *--esp = EFLAGS; /* eflags 寄存器入栈 */ /* 分别将 NT、NT、RF及VM标志位清0 */ EFLAGS.TF = 0; EFLAGS.NT = 0; EFLAGS.RF = 0; EFLAGS.VM = 0; if (gate_descriptor.type == I_GATE32) { /* 假如是中断门符 */ EFLAGS.IF = 0; /* 也将IF标志位清0,屏蔽响应中断 */ } *--esp = CS; /* 当前段选择子入栈 */ *--esp = EIP; /* 当前EIP 入栈 */ CS = target_selector; /* 加载目标代码段 */
CS.RPL = DPLs; /* 改变当前执行权限级别 */ EIP = gate_descriptor.offset; /* 加载进入EIP */ /* 执行中断例程 */ goto target_descritptor.base + gate_descriptor.offset; } else { /* 失败,#GP 异常产生,转去处理异常 */ } |
■ 堆栈的切换
控制权发生转移后,处理器自动进行相应的堆栈切换。
1、 当转向到同权限级别的代码时,不会进行堆栈级别的调整,也就是不进行堆栈切换。
2、 当转向高权限级别时,将发生相应级别的堆栈切换。从TSS块获取相应级别的stack结构。
例:假如当前运行级别CPL为2时,发生了向0级代码转移时:
TSS tss = TR.base; /* 从TR寄存器中获取TSS 块 */
CPL = 2; /* 当前运行级别为2 级*/ DPL = 0; /* 目标代码需要级别为 0 级 */ /* 根据目标代码需要的级别进行选取相应的权限级别的stack结构 */ switch (DPL) { case 0: SS = tss.ss0; ESP = tss.esp0; break; case 1: SS = tss.ss1; ESP = tss.esp1; break; case 2: SS = tss.ss2; ESP = tss.esp2; break; } *--esp = SS; /* 保存旧的stack结构 */
*--esp = ESP; /* 然后再作相当的保存工作,如保存参数等 */
*--esp = CS; /* 最后保存返回地址 */
*--esp = EIP; |
■ 控制权的返回
当目标代码执行完毕,需要返回控制权给原代码时,将产生返回控制权行为。返回控制权行为,比转移控制权行为简单得多。因为,一切条件已经在交出控制权之前准备完毕,返回时仅需出栈就行了。
1、 near call 的返回
近调用情况下,段不改变,即CS不改变,权限级别不改变。仅需从栈中pop返回地址就可以了。
2、 直接控制权转移的返回(far call或far jmp)
直接控制权的转移是一种不改变当前运行级别的行为。只是发生跨段的转移。这时,CS被从栈中pop出来的CS值加载进去,处理器会检查CPL与这个pop出来的选择子中的RPL进行检查,相符则返回。不相符则发生 #GP异常。
总结:假如当前运行的目标代码执行完毕后,将要返回。这时CPL为2
CPL = 2; /* 当前代码运行级别为 2 */
… … EIP = *esp++; /* pop出原EIP 值 */ CS = *esp++; /* pop出原CS值 */ if (CPL == CS.RPL) {
/* CS.RPL 代表是原来的运行级别,与CPL相符则返回 */ return ; } else { /* #GP异常产生,执行异常处理 */ } |
3、 利用各种门符进行向高权限代码转移后的返回
从高权限代码返回低权限代码,须从stack中pop出原来的stack结构。这个stack结构属于低权限代码的stack结构。然后直接pop 出原返回地址就可以了。
总结:
… …
EIP = *esp++; /* 恢复原地址 */
CS = *esp++; /* 恢复原地址及运行级别 */
ESP = *esp ++; /* 恢复原stack结构 */
SS = *esp++; /* 恢复原stack 结构,同时恢复了原stack访问级别 */
答:特权级是保护模式下一个重要的概念,CPL,RPL和DPL是其中的核心概念,查阅资料无数,总结如下:
简单解释:
--------------------------------------------------------------------------------
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,是考虑到安全的问题,就好像你明明对一个文件用有写权限,为什么用只读打开它呢,还不是为了安全!
全面解释:
--------------------------------------------------------------------------------
RPL是段选择子里面的bit 0和bit 1位组合所得的值,但这里要首先搞清楚什么是段选择子,根据Intel 的文件(IA-32 IntelR Architecture Software Developer's Manual, Volume 3System Programming Guide)它是一个16Bit identifier (原文:A segment selector is a 16-bit identifier for a segment). 但 identifier 又是什么. identifier 可以是一个变数的名字( An identifier is a name for variables), 简单的说它可以就是一般意义的变数. 这里 16-bit identifier for a segment 可以就是一个一般意义的16bit变数但同时要求对它的值解释的时候必须跟据Intel定下的规则---也就是bit 0和bit 1位的组合值就是RPL等等… 因此在程序里如果有需要的话你可以声明一个或者多个变数来代表这些段选择子,这样的话你的程序在某一时刻就可以有很多段选择子,当然有那么多段选择子就有那么多RPL.可以这样说程序有多少个是RPL是你怎样看待你自己声明的变数. |
程序的CPL(CS.RPL)是CS register 里bit 0和bit 1 位组合所得的值.在某一时刻就只有这个值唯一的代表程序的CPL.
而DPL是段描述符中的特权级, 它的本意是用来代表它所描述的段的特权级. 一个程序可以使用很多段(Data,Code,Stack)也可以只用一个code段等.在正常的情况下当程序的环境建立好后,段描述符都不需要改变-----当然DPL也不需要改变.
一、对数据段和堆栈段访问时的特权级控制:
要求访问数据段或堆栈段的程序的CPL≤待访问的数据段或堆栈段的DPL,同时选择子的RPL≤待访问的数据段或堆栈段的DPL,即程序访问数据段或堆栈段要遵循一个准则:只有相同或更高特权级的代码才能访问相应的数据段。这里,RPL可能会削弱CPL的作用,访问数据段或堆栈段时,默认用CPU和RPL中的最小特权级去访问数据段,所以max {CPL, RPL} ≤ DPL,否则访问失败。
二、对代码段访问的特权级控制(代码执行权的特权转移):
让我们先来记一些“定律”:
所有的程序转跳,CPU都不会把段选择子的RPL赋给转跳后程序的CS.RPL. .
转跳后程序的CPL(CS.RPL)只会有下面的俩种可能
转跳后程序的CPL(CS.RPL) = 转跳前程序的CPL(CS.RPL)
或
转跳后程序的CPL(CS.RPL) = 转跳后程序的CodeDescriptor.DPL
以 Call 为例(只能跳到等于当前特权级或比当前特权级更高的段):
怎样决定这两种选择,这就要首先知道转跳后程序的段是一致代码段还是非一致代码段.其实也很简单,规则如下:
如果能成功转跳到一致代码段, 转跳后程序的CPL(CS.RPL) = 转跳前程序的CPL(CS.RPL),(转跳后程序的CPL继承了转跳前程序的CPL)
如果能成功转跳到非一致代码段, 转跳后程序的CPL(CS.RPL) =转跳后程序的Descriptor.DPL。(转跳后程序的CPL变成了该代码段的特权级.我在前面提到DPL是段描述符中的特权级, 它的本意是用来代表它所描述的段的特权级)怎样才能成功转跳啦?
这里有四个重要的概念:
1).段的保护观念是高特权级不找低特权级办事,低特权级找高特权级帮忙,相同的一定没问题.(这样想逻辑是没错,事实对不对就不知道.)也就是县长不找乡长,乡长不求农民,反过来农民求乡长,乡长找县长.这个概念是最重要的。
2) 一致代码段的意义: 让客人很方便的利用主人(一致代码段)的东西为自己办事.但客人这身份没有改变NewCS.RPL=OldCS.RPL所以只能帮自己办事。比方说乡长有一头牛,农民可以借来帮自己种田,但不能种别人的田.但是如果你是乡长当然可以种乡里所有的田。
3) 非一致代码段的意义:主人(非一致代码段)可以帮客人但一定是用自己的身份NewCS.RPL= DestinationDescriptorCode.DPL这里可能有安全的问题, 搞不好很容易农民变县长。主人太顽固了一定要坚持自己的身份,有什么方法变通一下,来个妥协好不好。好的,它就是RPL的用处。
4) RPL: 它让程序有需要的时候可以表示一个特权级更低的身份Max(RPL,CPL)而不会失去本身的特权级CPL(CS.RPL),有需要的时候是指要检查身份的时候。事实上RPL跟段本身的特权级DPL和当前特权级CPL没有什么关系,因为RPL的值在成功转跳后并不赋给转跳后的CS.RPL。
还是要问怎样才能成功转跳啦?这里分两种情况:
普通转跳(没有经过Gate 这东西):即JMP或Call后跟着48位全指针(16位段选择子+32位地址偏移),且其中的段选择子指向代码段描述符,这样的跳转称为直接(普通)跳转。普通跳转不能使特权级发生跃迁,即不会引起CPL的变化,看下面的详细描述:
目标是一致代码段:
要求:CPL(CS.RPL)>=DestinationDescriptorCode.DPL ,其他RPL是不检查的。
转跳后程序的CPL(NewCS.RPL) = 转跳前程序的CPL( OldCS.RPL)
上面的安排就是概念1,2的意思,此时,CPL没有发生变化,纵使它执行了特权级(DPL)较高的代码。若访问时不满足要求,则发生异常。
目标是非一致代码段:
要求:CPL(CS.RPL)=DestinationDescriptorCode.DPL AND RPL≤CPL(CS.RPL)
转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL
上面的安排就是概念3的意思和部分1的意思----主人(一致代码段)只帮相同特权级的帮客人做事。因为前提是CPL=DPL,所以转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL不会改变CPL的值,特权级(CPL)也没有发生变化。如果访问时不满足前提CPL=DPL,则引发异常。
通过调用门的跳转:当段间转移指令JMP和段间转移指令CALL后跟着的目标段选择子指向一个调用门描述符时,该跳转就是利用调用门的跳转。这时如果选择子后跟着32位的地址偏移,也不会被cpu使用,因为调用门描述符已经记录了目标代码的偏移。使用调门进行的跳转比普通跳转多一个步骤,即在访问调用门描述符时要将描述符当作一个数据段来检查访问权限,要求指示调用门的选择子的RPL≤门描述符DPL,同时当前代码段CPL≤门描述符DPL,就如同访问数据段一样,要求访问数据段的程序的CPL≤待访问的数据段的DPL,同时选择子的RPL≤待访问的数据段或堆栈段的DPL。只有满足了以上条件,CPU才会进一步从调用门描述符中读取目标代码段的选择子和地址偏移,进行下一步的操作。
从调用门中读取到目标代码的段选择子和地址偏移后,我们当前掌握的信息又回到了先前,和普通跳转站在了同一条起跑线上(普通跳转一开始就得到了目标代码的段选择子和地址偏移),有所不同的是,此时,CPU会将读到的目标代码段选择子中的RPL清0,即忽略了调用门中代码段选择子的RPL的作用。完成这一步后,CPU开始对当前程序的CPL,目标代码段选择子的RPL(事实上它被清0后总能满足要求)以及由目标代码选择子指示的目标代码段描述符中的DPL进行特权级检查,并根据情况进行跳转,具体情况如下:
目标是一致代码段:
要求:CPL(CS.RPL)≥DestinationDescriptorCode.DPL ,RPL不检查,因为RPL被清0,所以事实上永远满足RPL≤DPL,这一点与普通跳转一致,适用于JMP和CALL。
转跳后程序的CPL(NewCS.RPL) = 转跳前程序的CPL( OldCS.RPL),因此特权级没有发生跃迁。
目标是非一致代码段:
当用JMP指令跳转时:
要求:CPL(CS.RPL)=DestinationDescriptorCode.DPL AND RPL<= CPL(CS.RPL)(事实上因为RPL被清0,所以RPL≤CPL总能满足,因此RPL与CPL的关系在此不检查)。若不满足要求则程序引起异常。
转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL
因为前提是CPL=DPL,所以转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL不会改变CPL的值,特权级也没有发生变化。如果访问时不满足前提CPL=DPL,则引发异常。
当用CALL指令跳转时:
要求:CPL(CS.RPL)≥DestinationDescriptorCode.DPL(RPL被清0,不检查),若不满足要求则程序引起异常。
转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL
当条件CPL=DPL时,程序跳转后CPL=DPL,特权级不发生跃迁;当CPL>DPL时,程序跳转后CPL=DPL,特权级发生跃迁,这是我们当目前位置唯一见到的使程序当前执行忧先级(CPL)发生变化的跳转方法,即用CALL指令+调用门方式跳转,且目标代码段是非一致代码段。
总结:以上介绍了两种情况的跳转,分别是普通跳转和使用调用门的跳转,其中又可细分为JMP跳转和CALL跳转,跳转成功已否是由CPL,RPL和DPL综合决定的。所有跳转都是从低特权级代码向同级或更高特权级(DPL)跳转,但保持当前执行特权级(CPL)不变,这里有点难于区别为什么说向高特权级跳转,又说特权级没变,这里“高特权级”是指目标代码段描述符的DPL,它规定了可以跳转到该段代码的最高特权级;而后面的CPL不变才真正说明了特权级未发生跃迁。我们可以看到,只有用CALL指令+调用门方式跳转,且目标代码段是非一致代码段时,才会引起CPL的变化,即引起代码执行特权级的跃迁,这是目前得知的改变执行特权级的唯一办法,如果各位读者还知道其他方法请留言告诉我。