Linux从头学12:读完这篇【特权级】文章,你就比别人更“精通”操作系统!
作 者:道哥,10+年嵌入式开发老兵,专注于:C/C++、嵌入式、Linux。
关注下方公众号,回复【书籍】,获取 Linux、嵌入式领域经典书籍;回复【PDF】,获取所有原创文章( PDF 格式)。
【IOT物联网小镇】
目录
x86
处理器中,提供了4
个特权级别:0,1,2,3。数字越小,特权级别越高!
一般来说,操作系统是的重要性、可靠性是最高的,需要运行在 0 特权级
;
应用程序工作在最上层,来源广泛、可靠性最低,工作在 3 特权级别
。
中间的1 和 2
两个特权级别,一般很少使用。
理论上来讲,可以把那些可靠性介于操作系统和应用程序之间的程序安排在这两个特权级上。
在处理器中,有3
个相关的术语与特权级密切相关:
CPL: Current Privilege Level 当前特权级;
DPL: Descriptor Privilege Level 描述符特权级;
RPL: Requestor Privilege Level 请求特权级;
理解了这3
个特权级的保护规则,就理解了操作系统保护系统的终极密码!
CPL:当前特权级
当前特权级,是指当前正在执行的代码的特权级别,它由当前正在执行的代码段寄存器cs
中的bit[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,表示到 LDT 中查找段描述符;
索引号:bit[15 ~ 3] 的索引值为 0,表示到 LDT 中偏移量为 0 (0 = 0 * 4, 每个描述符占据 4 个字节) 的位置获取段描述符;
当处理器进行一系列权限检查之后,允许进入这段代码中去执行,那么就设置 cs = 0x0007
。
此时cs
寄存器中的最低2
位就等于选择子中的 RPL
,也就是 3
。
在一般情况下,CPL
都是等于RPL
的。
DPL:描述符特权级
DPL
指的是一个段描述符中,用来指定这个描述符所代表的段,具有什么样的特权级别。
关于描述符的结构,如下图所示:
bit[14 ~ 13]
就表示这个段描述符的特权级别。
当请求访问一个段时(不论是数据段,还是代码段),处理器在GDT
或者LDT
中找到段描述符之后,就会把CPL、RPL
与描述符中的 DPL
进行比较。只有满足一定的规则,才允许访问这个描述符所指向的那个段。
具体的比较规则,下文有描述。
RPL:请求者特权级
刚才的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 的第 2 个表项);
TI: 使用 GDT;
RPL: 0;
此时,如果操作系统很无脑的就原样接收了用户程序的调用请求,就会通过GDT
找到属于操作系统的数据段进行破坏性操作。
操作系统不会这么傻的,它在接收用户程序请求的时候,会严格检查用户程序传入的参数。
如果它发现运行在 3 特权级
的用户程序,传入一个 0 特权级
的 RPL,就会警觉:请求特权级竟然比你自己的运行特权级还高,你想干什么?
于是,操作系统就会把选择子中的RPL
修改为用户程序的当前特权级 CPL
。
就好比:一个村长去找市长办事,诉求是:想在自己村的集体土地上盖一座厂房。市长认为:这是你们村自己的土地,你可以随便折腾,准许。
但是,如果村长的诉求是:想在市民广场的旁边盖一座厂房。此时市长就会呵斥:这个地方不是你们村的一亩三分地,想干啥就干啥,滚开!
特权级检查规则
代码段的特权级检查
一般情况下,只允许两个特权级相同的代码段进行转移。
例如:
从用户程序的一个代码段(CPL = 3),跳转到另一个 DPL = 3 的代码段;
从操作系统的一个代码段(CPL = 0),跳转到另一个 DPL = 0 的代码段;
但是处理器也提供了一些特殊途径,让低特权级的代码可以转移到高特权级的代码中去执行:
如果在高特权级代码段描述中的 TYPE 字段中,C = 1,就允许低特权级的代码转移进来;
通过调用门,低特权级代码也可以转移到高特权级的代码段;
这里主要描述第一种情况,也就是当目标代码段描述符的TYPE
字段中 C = 1
,也就是所谓的依从代码,或者一致性代码。
也即是说:如果 TYPE.C = 1
,那么处理器就允许:比这个描述符的 DPL 更低特权级的代码,转移到这个代码中来执行。
在数值上就是:(特权级越低,数值越大)
CPL >= DPL
RPL >= DPL
例如:操作系统中有2
个代码段,它们的描述符中的C
标志位不同:
此刻正在执行一个用户程序: CPL = 3
。
那么用户程序就可以转移到代码段 2中去执行,不可以转移到代码 1中。
并且,转移到操作系统的代码段2 之后,当前特权级CPL
保持不变,仍然为 3
。
有两个类比:
1. 类似于 Linux 中的 sudo 指令
如果一条指令需要root
权限,我们可以使用su -
指令,把身份转换到 root
,然后再去执行。
此时所有的身份、环境变量等信息,都是root
用户的。
我们还可以直接使用sudo
指令,这时就相当于是临时提升了用户的权限,但是那些环境变量等信息,依然是当前用户的,而不是 root 用户的。
2. 村长找市长办贷款
村长去市里的银行申请贷款,但是自己的权力不够,银行不鸟他,于是村长就去找市长帮忙。
于是,市长就给村长一个亲笔介绍信,村长带着这封信到银行之后,银行一看:有市长大人的背书,于是就给村长办理贷款手续了。
但是,在办理手续的过程中,所有需要签字的地方,只能写村长自己(特权级不变),而不能写市长的名字。
另外,对于上图中的代码段1,由于其C
标志位是 0
,只能允许相同特权级的程序转移进来,从数值上表示就是:
CPL == DPL
RPL == DPL
最后还有一点需要记住:高特权级的代码,永远都不能转移到低特权级的代码。就好比:市长永远都不会以村长的身份去办事。
数据段的特权级检查
数据段的特权级检查规则比较简单:高特权级的程序,可以访问低特权级的数据,反之不可以。
从数值上表示就是:
CPL <= DPL
RPL <= DPL
栈段的特权级检查
栈段的特权级检查规则,也比较简单,x86
处理器要求当前特权级 CPL 必须与目标栈段的 DPL 相同。
从数值上表示就是:
CPL == DPL
RPL == DPL
为了满足这个要求,当从用户程序(CPL = 3
)转移到操作系统(DPL = 0
)时,如果是通过依存(一致性)代码段转移进去,当前特权级是不变的,此时使用的栈仍然是用户程序的栈空间。
如果是通过其他途径转移进去(eg: 调用门),当前特权级发生了变化(CPL = 0),此时使用的栈就必须是 0 特权级下的栈空间了。
因此,操作系统在加载这个用户程序的时候,就需要提前申请一块栈空间,以准备在以上这样的场景中使用。
在Linux
系统中,只用了0 和 3
这两个特权级,因此每一个用户程序只需要提前准备好0
特权级下使用的栈就可以了。
如果一个操作系统使用了0 ~ 3
所有的四个特权级,那么操作系统就必须为:运行在3
特权级下的用户程序准备3
个栈空间,用于该用户程序转移到特权级 0、1、2
下作为栈空间来使用。
------ End ------
这篇文章主要从特权级的角度,来理解操作系统对系统的保护。
在这样的机制下,操作系统很好的保护了系统不被恶意程序破坏,同时也为用户程序的执行提供了一些通道,来调用更底层的功能。
推荐阅读
【1】C语言指针-从底层原理到花式技巧,用图文和代码帮你讲解透彻
【2】一步步分析-如何用C实现面向对象编程
【3】原来gdb的底层调试原理这么简单
【4】内联汇编很可怕吗?看完这篇文章,终结它!
其他系列专辑:精选文章、C语言、Linux操作系统、应用程序设计、物联网
星标公众号,能更快找到我!
Linux从头学12:读完这篇【特权级】文章,你就比别人更“精通”操作系统!的更多相关文章
- Linux从头学13:想彻底搞懂“系统调用”的底层原理?建议您别错过这篇【调用门】
作 者:道哥,10+年嵌入式开发老兵,专注于:C/C++.嵌入式.Linux. 关注下方公众号,回复[书籍],获取 Linux.嵌入式领域经典书籍:回复[PDF],获取所有原创文章( PDF 格式). ...
- Linux从头学08:Linux 是如何保护内核代码的?【从实模式到保护模式】
作 者:道哥,10+年的嵌入式开发老兵. 公众号:[IOT物联网小镇],专注于:C/C++.Linux操作系统.应用程序设计.物联网.单片机和嵌入式开发等领域. 公众号回复[书籍],获取 Linux. ...
- Linux 从头学 01:CPU 是如何执行一条指令的?
作 者:道哥,10+年的嵌入式开发老兵. 公众号:[IOT物联网小镇],专注于:C/C++.Linux操作系统.应用程序设计.物联网.单片机和嵌入式开发等领域. 公众号回复[书籍],获取 Linux. ...
- Linux从头学02:x86中内存【段寻址】方式的来龙去脉
作 者:道哥,10+年的嵌入式开发老兵. 公众号:[IOT物联网小镇],专注于:C/C++.Linux操作系统.应用程序设计.物联网.单片机和嵌入式开发等领域. 公众号回复[书籍],获取 Linux. ...
- Linux从头学03:如何告诉 CPU,代码段、数据段、栈段在内存中什么位置?
作 者:道哥,10+年的嵌入式开发老兵. 公众号:[IOT物联网小镇],专注于:C/C++.Linux操作系统.应用程序设计.物联网.单片机和嵌入式开发等领域. 公众号回复[书籍],获取 Linux. ...
- Linux从头学06:16张结构图,彻底理解【代码重定位】的底层原理
作 者:道哥,10+年的嵌入式开发老兵. 公众号:[IOT物联网小镇],专注于:C/C++.Linux操作系统.应用程序设计.物联网.单片机和嵌入式开发等领域. 公众号回复[书籍],获取 Linux. ...
- Linux从头学07:中断那么重要,它的本质到底是什么?
作 者:道哥,10+年的嵌入式开发老兵. 公众号:[IOT物联网小镇],专注于:C/C++.Linux操作系统.应用程序设计.物联网.单片机和嵌入式开发等领域. 公众号回复[书籍],获取 Linux. ...
- Linux从头学09:x86 处理器如何进行-层层的内存保护?
作 者:道哥,10+年的嵌入式开发老兵. 公众号:[IOT物联网小镇],专注于:C/C++.Linux操作系统.应用程序设计.物联网.单片机和嵌入式开发等领域. 公众号回复[书籍],获取 Linux. ...
- Linux从头学10:三级跳过程详解-从 bootloader 到 操作系统,再到应用程序
作 者:道哥,10+年的嵌入式开发老兵. 公众号:[IOT物联网小镇],专注于:C/C++.Linux操作系统.应用程序设计.物联网.单片机和嵌入式开发等领域. 公众号回复[书籍],获取 Linux. ...
随机推荐
- promise详解 : 实现promise(附实现代码)
promise then 的特点 : then 函数的返回值是一个 promise, 可以继续调用 then 函数 回调函数 resolve 和 reject 的参数 value /reason, 可 ...
- i春秋CTF-web-upload
------------恢复内容开始------------ 记一道文件上传的题目. 题目告诉我们随意上传,第一想到的当然给他上传一个木马文件然后蚁剑拿shell,上传之后可以在源码里查看我们上传的文 ...
- 100的累加和 while 循环
//100的累加和 while 循环 #include <stdio.h> int main() { int sum = 0; //5050 int i = 0; while(i < ...
- 内置函数 字符串的复制 strcpy
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<string.h> 4 5 6 void main() 7 ...
- 2020年最新阿里、字节、腾讯、京东等一线大厂高频面试(Android岗)真题合集,面试轻松无压力
本文涵盖了阿里巴巴.腾讯.字节跳动.京东.华为等大厂的Android面试真题,不管你是要面试大厂还是普通的互联网公司,这些面试题对你肯定是有帮助的,毕竟大厂一定是行发展的标杆,很多公司的面试官同样会研 ...
- SpringMVC学习04(数据处理及跳转)
4.数据处理及跳转 4.1结果跳转方式 4.1.1 ModelAndView 设置ModelAndView对象 , 根据view的名称 , 和视图解析器跳到指定的页面 . 页面 : {视图解析器前缀} ...
- MySQL记录之间是单向链表还是双向链表?
前言 本文的观点是基于MySQL使用Innodb存储引擎的情况下进行的! 很多渠道说:MySQL数据按照主键大小依次排列,记录之间是双向链表连起来.如果说我告诉你这种说法很大程度上是错的,你肯定说我在 ...
- STP工作流程
第一步:选择一个根网桥: 第二步:在每个非根网乔上选举一个根端口: 第三步:在每个网段上选举一个指定端口: 第四步:阻塞非根,非指定端口:
- Linux、Windows 下手动生成 sha256 等类型的校验文件
目录 1 - 校验文件的作用 2 - Linux 下生成校验文件 3 - Windows 下生成校验文件 参考资料 版权声明 1 - 校验文件的作用 从网服务器下载文件,尤其是比较大的文件时,很容易由 ...
- [转]C# 互操作性入门系列(四):在C# 中调用COM组件
传送门 C#互操作系列文章: C# 互操作性入门系列(一):C#中互操作性介绍 C# 互操作性入门系列(二):使用平台调用调用Win32 函数 C# 互操作性入门系列(三):平台调用中的数据封送处理 ...