保护模式下的特权级检查

    CPU通过GDT保护了内存区域。通过GDT,不同的内存区域被规定了大小,赋予了访问权限,比如是否可写,是否可运行。但是对于支撑多任务来说,这还远远不够。譬如我们无法做到让某个数据段尽可被操作系统访问,不可被其它应用程序访问。特权级引入了更强力的任务保护。
    特权级的想法是将不同的特权给予不同的任务。所谓特权,指访问资源的能力。比如操作系统需要访问所有可以访问的资源,而某个应用程序只需要有权利访问某些资源,比如1MB的内存块和声卡。当应用程序试图突破它的访问权限时,我们希望特权级机制可以阻止这种行为。
    IA-32给出四种特权级,0级权利最大而3级权利最小,用于细化资源访问能力。同时,IA-32给出三个概念,DPL(Descriptor's Privilege Level)、RPL(Requestor's Privilege Level)和CRL(Current Privilege Level)用于判断任务是否试图突破特权级。如果突破,CPU产生异常中断。
    一个任务可以访问的资源包括内存和外设,而内存又可分为数据和代码,外设则统一为端口。因此,资源可以分为数据,代码和端口。下面以数据、代码、端口的顺序简述CPU如何防止一个任务突破自己的访问权限。

一、数据
    1. 普通数据段
    内存中的数据大都存储在数据段,然后也有少数情况存储在代码段。这里只讲述数据存储在数据段中的情况。至于如何访问代码段中的数据,可以参考《64 IA-32 architectures software developer manual》中的5.6.1 Accessing Data in Code Segments。
    根据先注册再使用原则,保护模式下一切对数据的访问都必须开始于对段寄存器的修改。任何对段寄存器的修改都会触发CPU进行特权级检测,以防任务越权访问。检测规则是:
           如果CRL、RPL在数值上都比目标数据段的DPL不高(在特权不低),则允许访问
简单说来:高特权级的任务有权访问低特权级的数据段。CRL取决于当前的代码段。CRL等于当前代码段的优先级。RPL是段描述符的最后两位。比如在如下操作中:
           mov ds,ax
RPL就是ax的最低2位。《64 IA-32 architectures software developer manual》中的5.6 Privilege Level Checking When Accessing Data Segment给出了一个很好的例子。
    2. 栈段
    栈段因其在程序运行过程中的重要作用而总是被特别关注。
    对栈段的访问同样必须开始于对SS的更改,因此任何对SS的更改都会触发特权级检测。对栈段的特权级检测规则较之于普通数据段更加严格:
           只有当CRL、RPL、DPL三者相等时,检测才能通过
    简单的说:代码只能使用和自己同优先级的栈段。
    总结一下特权级机制对数据段访问的控制:“大体趋势”是允许高特权级的代码访问低特权级的数据,不允许低特权级的代码访问高特权级的数据。栈段要求则更严格。

二、代码
    特权级机制会在代码段之间进行更替时发挥作用。所有让CS更改的动作都会触发特权级检测。IA-32指令集中有很多指令可以更改CS,包括jmp,call,int,sysenter,sysexit等等。这里只讨论jmp和call。而jmp和call指令的操作数也有很多情况:段选择子,调用门选择子,指向TSS的指针,指向任务门的指针。这里只讲述操作数是段选择子和调用门选择子的情况。
    1. jmp/call 段选择子
    事实上,这种情况指:
              jmp/call far 段选择子:偏移
相对来说,near转移并不会改变CS,因而不触发特权级检测,而far转移会修改CS,故而触发特权级检测。特权检测的结果不仅和CPL,DPL,RPL有关,还和目标代码段的C(Conforming)位有关。C位决定了目标代码段的依从性质,进而决定特权检测规则。
    如果C=0,即目标代码段是非依从的,那么检测规则是:
              CPL = DPL
              RPL <= CPL
转移之后,CPL保持不变。
    如果C=1,即目标代码段是依从的,那么特权级检测规则是:
              DPL <= CPL
RPL不参与检测。转移之后,CPL不变。
    目标代码段的依从性影响了特权检测的规则。如果目标代码段不是依从的,那么转移只能发生在同特权级的代码段之间,RPL参与检测,确保真正的调用者比目标代码拥有更高的特权级。如果代码段是依从的,转移只能从低特权级的代码到高特权级的代码。为了让事情变得更简单,我们不考虑RPL对特权级检测的影响。那么,无论目标代码是否依从,特权级检测的趋势是,允许低特权级的代码跳转到高特权级的代码,不允许高特权级的代码跳转到低特权级的代码。当所有代码段都是非依从的时,跳转只被限制在同特权级内。比如将内核置于0特权级,驱动置于1特权级,应用置于3特权级,那么跳转只能发生在内核内部,驱动之间,应用之间。这实在是很严格的检测。如果将内核中的某些代码设置为依从,那么应用和驱动就可以跳转到这些内核代码上。而且,CPL依然保持在低特权级,这意味着应用和驱动并不能借内核代码提升自己的特权级。而如果将某些驱动代码设置为依从,那么这些驱动就可以被应用调用,却不可被内核调用。总之,将目标代码有无依从性两种情况都考虑进去,代码段间跳转的规则是:允许从低跳转到高,不允许从高跳转到低。并且,从低跳转到高后,CPL不变保证了低特权级的代码无法通过跳转提高自己的特权级——这看起来正是我们想要的结果。
    和特权级对数据段的保护相比,代码段的特权级检测规则似乎相反。对于数据段来说,允许高级代码访问低级数据,不允许低级代码访问高级数据;对于代码段来说,允许低级代码跳转高级代码,不允许高级代码跳转低级代码。这种代码跳转的方向性“可能”和代码稳定性相关——内核的稳定性最高。
    事实上,绝大部分的代码都是非依从的。依从性一般只应用于OS中的数学库和异常处理程序,这些程序需要低级程序调用,同时又不需要访问特权资源。一个应用或者驱动可以从自己的低特权级运行这些代码。CPL保持不变避免了这些低级代码访问特权资源。
    但是,更多时候,我们需要高级代码代替低级代码访问特权资源,也就是说,在跳转的同时,我们需要特权等级的提升。调用门可以满足这个要求。
    2. jmp/call 指向调用门的选择子:偏移
       1) 门
       门是一种数据结构,通过门,CPU可以在不同的代码段之间进行切换。
       根据用途,门可以分为四种:调用门,陷阱门,中断门和任务门。其中,陷阱门和中断门用于异常处理和中断,任务门用于任务切换。调用门使用最为普遍,用于代码跳转。
       2) 调用门(call gate)
       调用门为调用提供了一种中间机制,CPU利用这一中间机制实施特权级检查。
       调用门,作为一种数据结构实质上是描述符(Descriptor)的一种,并且可以被安插在GDT或LDT之中,同样被描述符选择子(Descriptor Selector)索引和引用。因此调用门其实指调用门描述符,它描述了一段代码与相关的特权等级。调用门的典型的用法是:
               call/jmp  指向调用门的选择子:偏移量
和段描述符一样,调用门描述符长度为64bit。一个调用门主要包含以下几个内容:
        * 目标程序入口点,包含一个16bit的目标代码段选择自和32bit的偏移
        * 控制字段,包含P位,指示本调用门有效性,类似段描述符中的P位;TYPE,固定为1100
        * DPL,2 bits, 本描述符的特权级,参与特权级检查
        * 参数数量,5bits,辅助不同代码之间的参数传递
       调用门可以视作调用的“中间人”。与jmp/call+目标代码入口点的程序跳转方式相比,jmp/call+调用门描述符的跳转方式间接指明了目标代码的入口点,并且通过调用门这一“中间人”进行了更加可控的特权级检测。这种特权级检测规则“看似”很复杂,它牵扯到目标代码的DPL,调用门的DPL,CPL,RPL、目标代码依从性和跳转方式(JMP或CALL)。但是稍加仔细观察之后就会发现,这个规则只不过是通过引入调用门DPL将<1. jmp/call 段选择子>中的规则严格化之后的结果。<1. jmp/call 段选择子>中,特权级检测规则只允许CPU保持着相同的CPL从低级代码跳转到(同)高级代码,比如一段代码CodeA的特权级是2,这意味着要不然只有2特权级的代码可以跳转到CodeA(CodeA非依从),要不然只有2特权级和3特权级的代码可以跳转到CodeA(CodeA依从)。调用门的引入允许0特权级和1特权级的代码跳转到CodeA上。引入调用门之后,特权级检测可以实为分两步进行,第一步针对调用门的DPL进行,第二步针对目标代码段的DPL进行。这两步都通过才意味着特权级检测的通过。针对调用门DPL(GateDPL)的检测规则是:
                      CPL <= GateDPL   RPL <= GateDPL
总之,调用者必须居于比调用门更高(相等)的优先级之上。之后,针对目标代码段DPL的检测规则是:
                      如果跳转方式是CALL,则无论目标代码段是依从与否,要求CPL >= 目标代码段DPL
                      如果跳转方式是JMP,则若目标代码段是依从的,要求 CPL >= 目标代码段DPL
                                           若目标代码段不是依从的,要求CPL = 目标代码段DPL
仔细观察一下第二步检测,可以发现第二步检测的“趋势”是确保CPU只能从低级代码跳转到高级代码——这一趋势和<1. jmp/call 段选择子>中的特权级检测趋势一致,而且依从性都起到了“放宽检测规则”的作用。
        总之,引入调用门之后的特权级检测规则可以大致描述为:只允许中级代码利用低级调用门跳转到高级代码上。
        RPL可以视为CPL的补充,它表示了“真正的调用者”的优先级,姑且称之为realRPL吧。如果realRPL比CPL特权更大,那么realRPL几乎不会影响到特权级检测(貌似不符合<1. jmp/call 段选择子>中的规则,这真是个复杂的东西!!),而如果realRPL比CPL特权小,那它就会"override"或者"weaken"掉CPL,从而影响到特权级检测。
        依从性会带来两种影响。首先,一个依从的代码段被引用时,CPL会依从于调用者的DPL。第二,依从的代码段往往可以被低级代码引用,无论是否通过调用门,无论是否通过call还是jmp,而非依从的代码段只能被同特权级的代码引用。依从性可以放宽特权级检测,这是因为依从性确保了代码不会借jmp/call提升自己的特权级。也就是说,如果要跳转到一个依从的高级代码段,既可以不通过调用门直接跳转,也可以通过调用门间接跳转,甚至可以JMP上去,一去不复返,毕竟CPL不会被拉高。反之,如果要跳转到非依从的代码段,事情就要麻烦的多:直接跳转只限制在同特权级内,间接跳转则必须有去有回(使用call),虽然允许使用jmp,但是禁止jmp到更高级的代码段上。CPU从一个代码段跳转到另一个非依从的代码段,往往意味着CPL的改变,而CPL的改变,根据CPU对栈段的特权级保护规则,要求栈段DPL的改变。总之,依从性的引入给特权级带来了灵活性(低级代码引用高级代码,但不改变CPL,没有栈切换过程,就好像在用自己的代码一样)。如果没有这种灵活性,比如将所有代码都设置为非依从的,那么低级代码利用高级代码就必须利用调用门,必须经历CPL的更替和栈更替,即使低级代码只是想利用高级代码干一些简单的、不需要那么多关键资源的事情(比如数学计算)。
        调用门使得同一个代码段包含可以被不同特权级调用的代码成为可能。OS内核往往被安排为0特权级,驱动程序安排在1特权级。内核中包含三种服务代码,一种我们希望只能由OS本身调用,第二类允许被OS和驱动调用,第三类我们希望不仅可以由所有特权级调用。这时候,我们只需要将所有服代码设置为非依从,同时为第一类代码安装特权级为0的调用门,为第二类代码安装特权级为1的调用门,为第三类代码安装特权级为3的调用门。
        调用门是跨特权级执行代码的唯一途径。依从性看似允许跨特权级执行,但事实上CPL不会改变。
         3) 栈更换
        CPU从一个代码段跳转到另一个非依从的代码段,往往意味着CPL的改变,而CPL的改变,根据CPU对栈段的特权级保护规则,要求栈段DPL的改变。这是个复杂的过程。
            i. TSS(Task State Segment)
            虽然称为segment,但是TSS其实是内存中的一块区域。每个任务都由属于自己的TSS,相关的内存申请、初始化、和任务关联等操作自然而然落在了OS手中,暂时不要管了。TSS存在的意义就在于保存一个任务的状态(State),以备任务切换时进行恢复。当要切换到任务task1时,task1的TSS会告诉CPU现在task1运行到哪儿了,各个寄存器是什么样子,下一个指令在哪儿。除了寄存器的值以外,TSS还包含3套额外的栈,再加上使用中的栈,共4套。之所以有那么多,是因为特权级为3的代码有可能用到特权级为2、1、0的代码,从而需要额外的特权级为2、1、0的栈。
            ii. Stack Switch
            栈切换竟然是由CPU固件完成的!!!
            考虑什么时候发生CPL变动。只有call+调用门才可能导致CPL变动。
            栈切换的最初动机是保证,当一个任务运行在不同特权级上时使用不同的栈段。但相对于这样一个简单的动机,栈切换的过程显得尤其繁琐:
               [1] 根据目标代码段的DPL,从本任务的TSS中选择出相应特权级的栈段选择子
               [2] 缓存当前的SS和ESP
               [3] 切换到新栈,压入缓存的SS和ESP
               [4] 压入n个参数,n由调用门描述符制定,参数事先压入旧栈
               [5] 将当前的CS和EIP压入新栈
               [6] 跳转到入口点开始执行
            上述过程看似复杂,但其实仅仅是两种常见技术的复合。一是利用栈的参数传递,二是call指令。上述过程完成之后,新栈里保存有:旧栈|参数|返回点。在实模式下利用call进行远调用,同时利用栈传递参数,会导致栈的内容变成:|参数|返回点——和栈更换过程给新栈带来的影响相差无几。额外增加的旧栈仅仅是为了调用返回时可以恢复旧栈,就好像为了返回时为了恢复CS:IP要将CS和IP入栈一样。试想如果跳转操作并没有带来CPL的变化,栈切换没有发生,那么对于被调用的程序来说,栈内依然是:|参数|返回点。(旧栈呢?)
        4) 调用返回
        所谓调用返回,其核心操作是从栈中弹出调用者的EIP或者CS和EIP,跳转到上面去,前者对应指令RET,后者对应RETF。因为调用的总体方向是从低级代码到高级代码,因此调用返回的方向是从高级代码到低级代码。调用返回时,可以向ret或者retf传递一个参数,并于表示弹出返回点后ESP要跳过的字节数。如果返回涉及到特权级变换,那么调用返回流程就要复杂的多:
        i.   栈切换。除了弹出返回点、跳过参数之外,还要弹出调用者的栈(ESP和SS),装载入ESP和SS
        ii.  在调用者的栈中再次跳过参数
        iii. 检查所有数据段,包括DS、ES、FS和GS,一旦发现数据段的特权级高于CPL,马上将选择子清0。
        跨特权级的调用返回之所以会更复杂,是因为跨特权级的调用涉及栈切换。栈切换导致返回时必须进行栈恢复,就好像返回时恢复CS和EIP一样。另外,栈切换还导致原栈和新栈中都有参数存在,进而导致返回时需要两次让ESP跳过参数。除此之外,返回后需要对数据段进行检查,从而保证低特权级的代码不会访问高级数据。
        
        当代码段变换时,CPU会执行特权级检查,这一检查的大体趋势是:只允许低级代码跳转到(同)高级代码。

三、端口
        OS的功能之一就是代理应用程序访问、管理硬件。在CPU看来,常见的硬件访问是通过I/O指令,即IN和OUT。当然,也可以直接把硬件挂在地址线上,这时候把硬件当作数据管理即可。既然OS负责主管硬件,那么最好的方式就是只允许高特权级的代码访问硬件。除此之外,386还允许低特权级的程序有选择的访问某些端口,从而提高这些程序的性能。总的来说,对于端口的特权级管理满足如下要求:应用程序只能通过驱动或内核访问硬件,但也允许直接访问某些特定硬件。
        EFLAG中有一位叫做IOPL(IO Priveledge Level)。如果CPL<=IOPL,那么代码可以访问所有硬件。IOPL,作为EFLAG中的一位,备份于TSS中,每次执行任务切换时,都会load入CPU中。因此每个任务都有自己的IOPL。IOPL和任务代码的DPL共同决定该任务的IO特权。常见的配置是将内核和驱动的DPL分别设置为0和1,IOPL则设置为1。若此一来,内核和驱动都有权利访问所有硬件。
        在IOPL的限制下,如果应用程序无权访问硬件,TSS中的I/O Permission Bit Map可以“精细”的赋予该应用程序访问某些端口的能力。I/O Permission Bit Map可以视作IOPL的修正(Modification)。I/O Permission Bit Map以一个bit表示一个端口,0表示允许访问,1表示禁止访问。相关细节可以参考《64-ia-32-architectures-software-developer-manual-325462》Volume 1: Basic Architecture, Chapter 16 Input/Output, 16.5 Protected-Mode I/O。

        
        386利用特权级限制任务对资源的访问。这种限制分别体现在对数据、代码和端口的特权级检查上。这些检查“大体”上保证了:
        * 高级代码才能访问低级数据
        * 低级代码只能引用高级代码
        * 应用程序只能通过高级代码间接访问设备,除非特别指出哪些端口可以直接访问
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值