• 《操作系统真象还原》特权级


    下面是看第五章特权级的收获

    特权

    简介

      什么是特权?顾名思义,如果特权级别高的,就能拥有更强大的能力,能访问低特权不能访问的数据。操作系统将特权分为四层,分别为0、1、2、3,0是最高特权级别,3是最低特权级别。特权级别为0一般时由操作系统占有,特权级别1、2是由驱动程序和虚拟机占有,而用户程序一般都在特权级别3的级别下,也就是最低特权级别,能拥有的能力是最低的。

    官方文档

    https://software.intel.com/sites/default/files/managed/a4/60/325384-sdm-vol-3abcd.pdf

      关于特权级别很多细节都能在官方文档找到,毕竟以官方为准嘛,特权的在这文档的第五章有详细描述,有疑惑的可以在这找找答案。

    TSS

      TSS是一种描述任务状态的数据结构,每一个任务都会有这样的一个结构。为什么要说这个结构,是因为在特权级别转移的时候会用到这个结构,但TSS不仅仅用于特权级别转移,既然它是描述任务状态的,那它必然也包含任务的其它信息。

      那什么是任务呢?在没有操作系统内核的情况下,任务就是进程,一段独立的程序运行起来就是一个任务;但在有操作系统的情况下,任务就不仅仅包括用户的进程了,它还包含内核程序,用户进程处于特权级别3,内核程序处于特权级别0,即一个任务是有可能从特权级别3的用户程序跳到特权级别0的内核程序的,反之亦然,这时候是需要借助TSS完成这个过程的。

      先来看看TSS的整体结构:

      我们关注一下TSS结构偏移4到偏移27的三个栈指针,包括ss和esp。一个任务最多有4个栈,每个特权级别会用到一个独立的栈,但并不是每个任务都会有4个栈,因为不是每个特权级别都会用到。为什么每个特权级别的栈是独立的,而不是共用一个栈呢?这是因为如果共用一个栈,容易导致交叉引用引起混乱,并且一个栈也比较容易导致溢出。

      那既然至多有4个栈,那为啥TSS只有三个栈指针信息呢?esp0和ss0、esp1和ss1、esp2和ss2分别代表特权级别0、1、2的栈指针信息,唯独没有特权级别3的栈指针。

      要理解这个问题,我们首先得知道为什么要用到TSS的这三个栈指针信息。

      分两种情况,一种是从低特权级别转移到高特权级别,一种是由高特权级别转移到低特权级别。

      ①低特权级别转移到高特权级别时,栈也要由低转到高(省略的是特权级别这几个字,下面也是),所以处理器就会在TSS结构里面找到高特权级别的栈信息,并转移到高特权级别的栈上。这种情况下,只需要知道高特权级别的栈信息,所以作为最低特权级别的3的栈信息是不需要在TSS记录的。

      ②高特权级别转移到低特权级别时,高转低之前一般都有低转高的过程。这种情况下是不会用到TSS的,这是因为低转高时已经将低的栈指针压进高的栈里面了,所以只需要从高的栈里面获取低的栈指针即可返回到低的栈上。

      另一个值得注意的点是,低转高时,由于高特权级别下执行指令时不免会对高特权级别的栈进行操作,此时高的esp就会变更,但是TSS里面的esp并不会跟着变更,如果要变更,也只有操作系统直接对TSS操作才会变更。

    当前特权级别

      接下来的内容会比较复杂,做好心理准备,不会有太多难点,只不过理清好关系需要一点时间。

      首先,需要明确一点,当前处于什么特权是相对于处理器而言的,例如,我们可以说如果处理器在执行内核代码,处理器就处于特权级别0。

      其次,需要知道特权下的访问者是谁,访问者是指令,对于处理器来说,只有指令是被执行的。

      接着,再想想,我们怎样才知道当前的特权级别等级(CPL)是什么,在哪里查看,或者说是多少。既然处理器无时无刻不在执行指令,而且指令为访问者,我们用当前执行的指令的特权等级作为当前特权级别等级,但一条指令并不会指定它自己的特权级别,指令肯定是在某个代码段里面,而代码段是有自己的特权级别等级的,所以CPU用当前执行的代码段特权级别作为当前特权级别等级。

      还记不记得段描述符的结构,不记得的话,看一下下面这图:

      段描述符的DPL就是描述某个段的特权级别,每个段都有自己的特权级别。结合上面所说的,当前执行代码段的DPL=CPL

      那么CPL需要有个地方保存,这样子处理器才能知道自己是处在什么特权级别。而这个地方就是选择子,准确来说的选择子中的RPL部分。

      选择子在哪里存储的?段寄存器。代码段寄存器是CS,所以CS中的RPL=CPL,我们写作CS.RPL=CPL,综上,当前代码段DPL=CPL=CS.RPL

    一般原则

      一般而言,低特权级别是不能访问高特权级别的资源,但高特权级别能访问低特权级别的资源。现在看来只有段描述符是有特权级别的,当然后面也有像门结构描述符也有DPL,下面只谈论“段”。

      所以我们分别讨论一下代码段和数据段(这里除代码段以外都称为数据段):

      这里以三个例子来讨论一般规则。

      ①如果受访者是数据段,且特权级别为2,处于哪些特权等级的访问者可以访问?

      遵循一般原则,只有高于或等于受访者的特权等级才能访问。所以只有处于特权级别0、1、2的访问才能对数据段进行访问。即数值上访问者的特权级别小于等于受访者的特权级别

      ②如果受访者是非一致性代码段,且特权级别为2,处于哪些特权级别的访问者可以访问?

      首先比受访者特权级别低的访问者肯定是不能直接访问了,这得遵循一般原则。所以我们不能通过jmp或call等指令跳到或调用特权等级较高的代码。

      然后比受访者特权级别高的访问者可不可以访问呢?就是访问者能访问比自己特权级别低的代码段吗?不能,书上的解释是:低特权等级能做的事,高特权等级也能做,所以高特权等级没有必要跳到低特权等级里执行,访问者没有必要自降等级去做它本来能做的事情。凡是都有例外,唯一一种从高特权等级跳到低特权等级的情况是:从中断服务程序返回到用户态。

      综上只有平级的访问者才能访问非一致性代码段,即特权级别为2才能访问。

      ③如果受访者是一致性代码段,且特权级别为2,处于哪些特权级别的访问者可以访问?

      一致性代码段能让比自己特权级别低的访问者访问,所以特权级别3,2的访问者可以访问。注意访问者转移到一致性代码段后,转移后的特权级别还是和转移前一样,这是一致性代码段得到特点,一致性代码段不以自己的特权级别等级为主。

      特权级别检查发生在访问受访者的一瞬间,之后访问这个受访者段里的内容都不需要再检查了。

    门结构

      上面说到低特权级别不能访问高特权级别,但总有一些情况是需要访问到比自己高的特权级别等级,比如需要请求操作系统内核从外设里获取数据。处理器提供了一种"门结构"的机制,只有通过门结构才能让处理器转移到高特权级别等级。门结构是在内存里面存储的,所以也是由操作系统设置的,之所以说是处理器的机制,是因为处理器就是这样设计的,操作系统需要遵循处理器的这种机制,来实现门结构的效果,说白了,对于处理器来说,操作系统只是它的应用。

      门结构在操作系统有四种:任务门、调用门、中断门、陷阱门。

      它们像描述段的段描述符那样,门结构都有自己的描述符。

      那么它们存储在那里呢?

      任务门描述符存储在GDT,LDT,IDT,IDT是中断描述符表,之后会说。GDT,LDT是全局描述符表和局部描述符表。

      调用门描述符存储在GDT,LDT。

      中断门描述符和陷阱们描述符都存储在IDT里面。

      任务门可以通过call和jmp指令调用,用任务门选择子作为参数。

      调用门可以通过call和jmp指令调用,用调用门选择子作为参数。

      中断门通过int指令发出中断。

      陷阱门通过int3指令发出中断,一般是在编译器调试时用,不做过多关注。

      为了下面更好的讲述RPL,这里讲一下调用门,调用门是指定一段程序入口地址的门结构,调用门的结构如下图所示:

      

      调用门的结构和段描述符的结构类似,都有P、DPL、S、TYPE,因为调用门需要描述一段程序,所以需要用到调用例程的选择子和偏移量,还有参数个数。

    调用门的执行流程

      我们说到调用门是描述一段例程的结构,可以让低特权级别转移到高特权级别等级,那么具体的使用流程是怎么样的呢?

      具体流程如下:

      ①通过call 调用门选择子开始调用。

      ②根据选择子的索引在GDT或者LDT里面找到调用门描述符,从调用门描述符里得到了例程所在的段描述符选择子段内偏移地址

      ③再根据段描述符选择子在GDT或LDT找到该段的基址。

      ④将段基址和段内偏移地址相加得到最终的例程 入口地址。

      ⑤跳到该入口地址执行。

    调用门的栈的变化

      从低特权级别等级转移到高特权级别等级,还涉及栈的转移,上面说了,每个特权级别都有独立的栈,所以栈也要跟着转移,那么这个过程是怎样的呢?

      流程:

      ①call调用门选择子之前先压入参数。

      ②判断例程所在的段的DPL是否和CPL相同,不同则发生特权级别转移。

      ③若发生特权级别转移,在TSS查找转移后的特权级别的栈指针(包括ss和esp)。处理器临时找个地方保存当前特权级别的栈指针,再转移到新的特权级别的栈。

      ④在新栈中压入旧的栈指针(③所保存的),再根据调用门中的参数个数,复制低特权级别栈中的参数到新栈。

      ⑤再压入cs和eip。

      若不发生特权级别转移,直接由②跳到⑤。

      那么执行完之后,如何返回到低特权级别时的状态呢?通过retf指令将ss、esp、cs、ip弹到寄存器里面,从而恢复现场。另外retf的用法是 retf+参数个数,这样子retf指令才知道中间应该跳过多少个参数。

    调用门的特权检查

      这里结合使用调用门的流程来讲讲其中的特权级别检查:

      ①通过call 调用门选择子开始调用。

      ②根据选择子的索引在GDT或者LDT里面找到调用门描述符,判断CPL数值上是否小于等于调用门描述符的DPL,设为DPL_GATE,即数值上CPL<=DPL_GATE。可以看出调用门描述符的DPL是第一道门槛,起码访问者特权级别要高于受访者“门描述符”的特权级别,才能通过特权级别检查,这遵循一般原则。

      ③从调用门描述符里得到了例程所在的段描述符选择子和段内偏移地址。

      ④再根据段描述符选择子在GDT或LDT找到该段描述符,通过这个段描述符得到该段的特权级别等级DPL_CODE,由于门结构的特点,第二个约束就是数值上CPL>=DPL_CODE,就是说转移后的特权等级要高于转移前的特权等级。

      ④将段描述符的段基址和调用门的段内偏移地址相加得到最终的例程 入口地址。

      ⑤跳到该入口地址执行。

      综上数值上DPL_CODE<=CPL<=DPL_GATE。

    RPL

      根据上面谈及的一般原则,当访问者访问受访者时,访问者的特权级别数值上要小于等于受访者的特权级别DPL,所以CPL<=DPL。

      这里将访问者看作当前执行的指令,所以访问者的特权级别等级时CPL。

      如果我们单纯靠CPL和DPL来进行特权级别检查的话会有什么问题呢?

      考虑这样一种情况,用户程序想获取外设里的数据,比如通过网卡获取的网络数据,用户程序不能直接对外设进行访问,所以只能通过操作系统内核来获取。寻求内核的帮助需要用到门结构,因为涉及到特权级别的转移,这里假设用户是通过调用门转移到内核。用户程序向内核提交了一个缓冲区的选择子,缓冲区的段内偏移地址,写入数据大小三个参数。

      用户提交参数后,并且通过了DPL_CODE<=CPL<=DPL_GATE的校验,转移到内核去执行调用门指向的例程,这时候CPL为0,处理器拥有最高的权限,可以对内存里的任何地方进行读写,用户提交缓冲区的选择子指向用户态下的数据段,此数据段DPL为3,由于CPL<=DPL,所以处理器能够通过特权校验并对缓冲区写入。

      那么再想想,如果用户通过某种途径知道了内核环境下的数据段,并且提交了一个指向内核缓冲区的选择子,试图破坏内核环境,结果会怎么样?

      由于此时处理器处于最高特权级CPL=0,内核缓冲区的DPL=0,即CPL<=DPL校验通过,所以处理器也是能对内核缓冲区进行读写操作,所以可能造成了破环内核环境的后果,这就很可怕了。

      

       出现这问题的原因是显然易见的。内核并不知道请求资源者的真正的能力是多少,当通过调用门转移到内核的时候,特权级别已经是最高了,内核成为了请求资源者的代理,能够对任何缓冲区进行读写,对于缓冲区来说,它认为访问者是内核,特权检查必定能通过,所以缓冲区肯定会放任内核去对自己进行操作。但实际上缓冲区的真正请求者是用户程序而不是内核。所以我们有必要让受访者知道真正请求资源的是谁。

      RPL出现了。由于需要通过选择子去索引段描述符,RPL放在选择子再合适不过。

      RPL代表选择子请求资源的能力,还记得选择子的结构吗,RPL就是在这里用到的。

    一般原则补充

      其实特权级别校验不单单有DPL和CPL的参与,还有RPL的参与,这里对上面说到的一般原则进行补充。

      ①如果受访者是数据段,特权级别需要满足:数值上数据段的DPL大于等于CPL和数据段选择子的RPL,即CPL<=DPL且RPL<=DPL。

      ②如果受访者是非一致性代码段,特权级别需要满足:数值上代码段的DPL等于CPL等于代码段选择子的RPL,即CPL=DPL=RPL。

      ③如果受访者是一致性代码段你,特权级别需要满足:数值上代码段的DPL小于等于CPL和RPL,即CPL>=DPL且RPL>=DPL。

    调用门特权检查补充

      RPL同样需要在使用调用门时参与校验。

      在访问调用门描述符时,需要指定调用门选择子,选择子的RPL要数值上小于等于门描述符的DPL_GATE,即RPL<=DPL_GATE,同样需要CPL<=DPL_GATE,之前讨论的。

      在调用调用门指向的例程之前,门描述符选择子的RPL不需要参与校验,因为它只是用来索引门描述符,所以只需CPL>=DPL_CODE。

    RPL防止越权读写资源

      再回到刚才那个问题,操作系统是怎么利用RPL防止用户破坏内核环境的或者偷偷获取到内核数据的。用户通过调用门转移到内核服务程序执行,内核服务程序会将用户提交的选择子里的RPL变更为用户进程的CPL,这就防止了这个问题的出现。

      假设用户伪造了一个RPL为0的选择子,并且这个选择子指向的时内核数据段。在执行内核服务程序时,这个选择子RPL被改成了3,此时CPL=0,RPL指向的数据段DPL=0,通过上面的讨论,得出虽然CPL<=DPL,但是RPL<=DPL并不满足,所以特权校验并不通过,自然就不会写进内核缓冲区了。

    几种情况

      在这里再讨论三种情况。

      ①不通过调用门,描述一下处于特权级别为3的用户程序向当前特权级别的缓冲区里写数据时,DPL、RPL、CPL的校验过程:

      用户程序执行时CPL=3,缓冲区选择子RPL=3,缓冲区数据段DPL=3,CPL=RPL=DPL,校验通过。

      ②不通过调用门,描述一下处于特权级别3的用户程序尝试写入特权级别0的缓冲区中,DPL、RPL、CPL的校验过程:

      用户程序执行时CPL=3,缓冲去选择子RPL=3,缓冲区数据段DPL=0,CPL>DPL、RPL>DPL,都不通过校验,特权检查失败。

      ③通过调用门,用户程序想获取外设里的数据,用户程序向内核提交了缓冲区的选择子,缓冲区的段内偏移地址,写入数据大小三个参数,DPL、RPL、CPL的校验过程:

      这情况是对上面讨论的总结。

      首先用户程序当前CPL=3,提交的缓冲区选择子RPL=3,指向的缓冲区所在数据段的DPL=3。

      用户程序指定了一个调用门选择子,其RPL=3,调用门描述符的DPL_GATE=3(如果这里为2或2以下就通过不了校验了,所以一定为3),RPL<=DPL_GATE且CPL<=DPL_GATE,通过调用门描述符的特权检查。

      再检查是否能转移到内核服务程序中,内核服务程序的DPL=0,所以CPL=3>=DPL,通过校验,跳到内核服务程序执行。

      内核服务程序获取外设数据后需要对缓冲区进行写入,此时CPL=0,缓冲区选择子RPL=3,缓冲区所在数据段DPL=3,所以CPL<=DPL且RPL<=DPL,通过校验,能对缓冲区进行写入。

      如果用户伪造了一个缓冲区选择子RPL=0且指向的缓冲区所在的数据段DPL=0,会怎么样。

      内核服务程序会将这个选择子的RPL改为3,虽然CPL<=DPL但RPL>DPL,所以校验不通过,不能对缓冲区进行写入,入侵失败。

    访问外设

      以下为补充内容,体现特权在IO和指令的限制。

    特权指令

      特权级不仅仅体现在对数据和代码的访问,而且体现在对指令的限制。

      有一些指令只能在特权级别为0时才能执行,这类指令叫特权指令,如lgdt、lidt、ltr、popf等,这些指令涉及到对内存的管理、中断等等,不应该由用户程序操作,处理器也只信任操作系统,所以放在特权级别为0下运行也十分合理。

      还有一些指令是需要受到IOPL限制,这些指令是IO读写指令,如in、out、cli、sti,也被称为IO敏感指令。只有在当前特权级别大于等于IOPL才能执行IO指令。

    IOPL

      还记得eflags的结构吗:

       IOPL是IO特权级,是用来限制访问IO指令的最低特权级别。每个任务都有eflags寄存器,所以每个任务都会有自己的IOPL。

      除此之外IOPL还可以用来决定任务是否能够访问所有外设端口。当数值上CPL<=IOPL时,任务能够访问所有外设端口,那么当CPL>IOPL时是不是就不能访问外设端口呢?不是,处理器允许部分IO端口被任务访问,哪些端口允许访问是IO位图决定的。所以IOPL有点像防火墙,首先禁止所有访问,再打开想开放的端口。

      像处于特权级别0的操作系统和处于特权级别1的驱动程序就能对所有端口进行访问。驱动程序就是通过in、out对硬件直接访问的程序。

      那么像任务处于特权级别2和3的话,虽然操作系统不允许开放所有端口,但还是允许部分端口对任务开放,只要通过IO位图做好限制即可。

      为什么处理器允许特权级别1、2、3能够对IO进行直接操作呢,原因是如果所有IO操作都通过内核的话,上下文切换的开销会有点大,这样做主要是为了提速。

    IO位图

      什么是位图,位图就是用位映射到某些资源上,IO位图就是由一个位映射到一个端口,如果这个位是1,则端口关闭,如果是0,代表端口打开。

      IO位图存储在TSS里面:

      TSS的前104个字节的结构是固定的,不固定的是IO位图的起始位置。怎么找到IO位图的起始位置呢?TSS中偏移102字节的地方就是保存着IO位图在TSS的偏移地址,占2个字节。IO位图的偏移地址取值范围是104~TSS段界限limit之间,如果偏移地址不再这个范围,即大于等于TSS段界限limit,则代表没有IO位图。IO位图起始一个位代表第0个端口,以此类推。Intel处理器最大支持65536个端口,所以位图大小65536/8=8192个字节。IO位图如果存在且映射所有端口,TSS的大小为IO位图偏移地址+8192+1,1代表TSS最后的0xff,下面会解释,如果IO位图不存在,TSS大小为104。

      那么为什么最后有个0xFF呢?

      每个端口只能读写一个字节的数据,但IO指令是可以都多个端口进行读写,如果对一个端口进行连续读写,那相当于以该端口号为起始的多个端口一并读写进来,如in ax, 0x234,in可以读取16位端口数据,即两个字节,假设0x234是16位端口,in ax,0x234就相当于in al,0x234和in ah, 0x235两个指令。

      所以一个指令是有可能会读取多个端口的,处理器会在IO位图检查这些端口是否都打开了,连续的bit会有可能跨字节,比如0x234端口在一个字节的最后一位,0x235端口在一个字节的首位,处理器需要将两个字节都都进来。大部分情况跨字节检查不会有问题,但是如果这发生在IO位图的最后一个字节的话,读取两个字节,第二个字节就会越界。

      因此0xff会有两个作用:

      ①处理器允许IO位图不映射所有端口,但要保证最后一个字节位0xff。假设映射到0-23的端口,我读取23端口,且读取两个字节的数据,即会读取两个端口23、24,那么0xff就表明24端口关闭,这样既不会越界,也不会不合理,因为没有映射到的端口就是关闭了;0xff即代表24-31号端口关闭,也防止越界,当然防止越界才是真正的目的,前者只是合理化的结果。

      ②如果IO位图映射所有端口,那么0xff就不代表任何端口,它就是作为位图的边界标记,防止越界。

  • 相关阅读:
    ArcEngine 一些实现代码(转载)
    关于GIS支持的地理数据源的命名空间
    SpringBoot-Web配置
    RedisGeo
    JedisCluster
    Java并发编程:Lock
    java并发编程:线程变量-ThreadLocal类
    java并发编程:线程池-Executors
    解决Mybatis配置ORM映射 时分秒都为0
    Kafka的存储机制以及可靠性
  • 原文地址:https://www.cnblogs.com/thougr/p/12210220.html
Copyright © 2020-2023  润新知