目录CPL:当前特权级DPL:描述符特权级RPL:请求者特权级特权级代码段检查规则数据段检查规则堆栈段检查规则在x86处理器中,提供了4个权限级别:0,1、2、3,数字越小,特权等级越高!一般来说,操作系统是最重要、最可靠的,需要运行在0特权级;该应用程序在顶层工作,来源广泛,可靠性最低,工作在3特权级别。中间的1级和2级两个权限一般很少用到。从理论上讲,可靠性介于操作系统和应用程序之间的程序可以安排在这两个特权级别。在处理器中,有3个与特权级密切相关的相关术语:CPL:CurrentPrivilegeLevel当前特权级;DPL:DescriptorPrivilegeLevel描述符特权级;RPL:RequestorPrivilegeLevel请求权限等级;了解这3条特权级的保护规则了解操作系统保护系统的终极密码!CPL:currentprivilegelevel当前特权级是指当前正在执行的代码的特权级,由当前正在执行的代码段寄存器cs[1~0]中的位定义:cs寄存器写入RPL?原因是这样的:在我们执行一段代码之前,这段代码位于内存中的一个空间中,其代码段描述符位于LDT局部描述符表中,如下图所示:假设现在即我们要进入这段代码段并执行,那么我们需要给代码段寄存器cs赋值:0x0007(0000_0000_0000_0111)。此时cs寄存器中当前内容的低两位称为:当前优先级,要分配给cs的值为0x0007,这个0x0007称为选择器。根据选择器的结构分析:RPL:bit[1~0]=11B,十进制为3,表示该选择器的请求特权级为3;TI:bit[2]=1B,表示在LDTDescriptor中查找段;indexnumber:bit[15~3]的索引值为0,表示获取LDT中偏移0处的段描述符(0=0*4,每个描述符占4个字节);处理器进行一系列权限检查后,允许输入这段代码执行,然后设置cs=0x0007。此时cs寄存器的最低2位等于selector中的RPL,为3。一般情况下,CPL等于RPL。DPL:DescriptorprivilegelevelDPL是指一个段描述符,用来指定这个描述符所代表的段具有什么样的特权级。关于描述符的结构,如下图所示:bit[14~13]表示该段描述符的特权级。当请求访问一个段(无论是数据段还是代码段)时,处理器在GDT或LDT中找到段描述符后,会比较描述符中的CPL、RPL和DPL。仅当满足某些规则时才允许访问此描述符指向的段。下面介绍具体的比较规则。RPL:Requesterprivilegelevel在刚才CPL的内容中,已经说明了什么是RPL,它们是密切相关的。但是,有时CPL与RPL不同。例如:用户程序想通过操作系统提供的系统函数访问内存中的一块属于自己的内存空间(数据段)。用户程序需要告诉操作系统:访问哪个数据段,偏移量是多少。这些信息需要通过操作系统给数据段寄存器ds分配一个选择符。假设选择器是0x000F(二进制:0000_0000_0000_1111):索引号:1;TI:使用LDT;RPL:3;也就是说:当操作系统接受了用户程序的请求,开始执行系统函数时,此时的CPL为操作系统特权级0。此时,操作系统需要分配一个选择器给数据段寄存器ds,这个选择器由用户程序作为参数传递给操作系统。在这种情况下:CPL=0,RPL=3,它们不相等。操作系统用这个选择器0x000F给用户程序的LDT,根据索引号1找到数据段描述符,将CPL(0)、RPL(3)与描述符中的DPL进行比较,判断是否有权限访问此数据段。用户程序的数据段DPL必须为3,这是由操作系统在加载程序之初决定的;根据以下权限级别检查规则,允许此类访问;其实这里有一个隐患:如果用户程序是一个恶意程序,它想破坏操作系统的数据,所以传入一个选择器,指向操作系统的段:0x0010(二进制:0000_0000_0001_0000):索引号:2(假设通过其他方式知道操作系统的某个数据段位于GDT的第二个表项);TI:使用GDT;RPL:0;这时,如果操作系统不假思索地接收到用户程序的调用请求,就会通过GDT段找到属于操作系统的数据进行破坏性操作。操作系统不会这么傻。当它收到来自用户程序的请求时,它会严格检查用户程序传入的参数。如果它发现一个运行在权限级别3的用户程序传入了一个权限级别为0的RPL,它就会提示:请求的权限级别比你自己运行的权限级别高,你想做什么?所以,操作系统会将选择器中的RPL修改为用户程序当前的特权级CPL。就好比:一个村长找村长办事,诉求是:他想在村里的集体土地上建厂。村长想:这是你们村自己的地,随便折腾,这是允许的。但是,如果村长的要求是:在市民广场旁边建一个工厂。这时候村长就会骂:这地方不是你们村的三亩地,你们想干什么就干什么,给我滚!特权级别检查规则代码段一般情况下,只允许两个特权级别的代码段进行传递。例如:从用户程序的一个代码段(CPL=3),跳转到另一个DPL=3的代码段;从操作系统的一个代码段(CPL=0),跳转到另一个DPL=0段的代码段;但是处理器也提供了一些特殊的方式让低权限的代码可以转移到高权限的代码中执行:如果在高权限代码段描述中的TYPE域中C=1,则允许低权限的代码执行调入;通过调用门,低权限代码也可以转移到高权限代码段;这里主要描述第一种情况,即目标代码段描述符的TYPE域中C=1时,也就是所谓的compliancecode,或者conformancecode。也就是说:如果TYPE.C=1,那么处理器允许:将特权级低于该描述符的DPL的代码转移到该代码中执行。从取值上来说,是:(特权级越低,取值越大)CPL>=DPLRPL>=DPL例如:操作系统中有两个代码段,它们的描述符中的C标志是不同的:此时用户正在执行程序:CPL=3,则用户程序可以转移到代码段2执行,不能转移到代码段1。并且,转移到运行的代码段2后系统,当前权限级别CPL不变,仍然是3。有两个类比:1.类似于Linux中的sudo命令。如果一个命令需要root权限,我们可以使用su命令将身份改为root再执行。此时所有的身份、环境变量等信息都属于root用户。我们也可以直接使用sudo命令,相当于暂时提升了用户的权限,但是那些环境变量等信息还是当前用户的,不是root用户的。2.村长向村长借钱。村长到城里的银行去申请贷款,但是他没有足够的权力,银行也不管他,所以村长向村长求助。于是,村长亲手给了村长一封介绍信。村长拿着信去银行后,银行看到有村长背书,就替村长办了贷款手续。不过在办理手续的过程中,所有需要签字的地方只能写村长本人(特权等级不变),不能写村长的名字。另外,对于上图中的代码段1,由于其C标志位为0,因此只能调入相同特权级的程序。从数值上看为:CPL==DPLRPL==DPL最后,Live还需要记住一件事:高权限代码永远不能转移到低权限代码。就像:村长永远不会充当村长。数据段的特权级检查数据段的特权级检查规则比较简单:高特权级的程序可以访问低特权级的数据,反之亦然。数值上是:CPL<=DPLRPL<=DPL栈段特权级检查栈段特权级检查规则比较简单。x86处理器要求当前特权级CPL必须与目标堆栈段的DPL相同。从数值上来说就是:CPL==DPLRPL==DPL为了满足这个要求,当从用户程序(CPL=3)向操作系统(DPL=0)传递时,如果通过依赖传递(consistent)代码段回车,当前特权级不变,此时使用的栈仍然是用户程序的栈空间。如果是通过其他方式(如:调用门)传入的,当前特权级发生了变化(CPL=0),此时使用的栈一定是0特权级下的栈空间。因此,操作系统在加载用户程序时,需要提前申请一个栈空间,为上述场景的使用做准备。在Linux系统中,只使用0和3这两个特权级,所以每个用户程序只需要提前准备好0特权级下使用的栈即可。如果一个操作系统使用了从0到3的所有4个特权级,那么操作系统必须为运行在3特权级下的用户程序准备3个堆栈空间,供用户程序转移到特权级0、1、2使用作为堆栈空间。本文转载自微信公众号《IOT物联网小镇》【编辑推荐】HarmonyOS官方战略合作共建-HarmonyOS技术社区苹果推送iOS15正式版更新内容通知抛顶杰牛-图解MySQL8.0优化器查询分析三种方式减轻SQL注入威胁iPhone13即将面世iOS还要忍受这些缺陷多久?为什么现在手机越来越贵了?
