本讲咱们不继续写任何代码,而是专门拿出一讲来讲说特权级的事,为后续的工做作一个知识储备。这段内容太难啃了,也可能我刚好对这块不太感冒,反正我是恶心了很久才啃下来。html
为了让你们清楚目前的程序进度,画了到目前为止的程序流程图,以下linux
为何要进行特权级检查,我就不说太多了,简单理解,操做系统不但愿用户进程访问内核数据,因此须要给指令呀还有数据呀都附上一个特权级的属性,让程序受限制。git
特权级分为 0 1 2 3 四种,咱们常说的 用户态 就是最低等级的 3 特权级,内核态 就是最高等级的 0 特权级。安全
处理器在 访问数据 或 跳转到代码 时,须要进行特权级检查,特权级检查的具体细则在下面有具体描述。这里我先把大体的思想方向总结出来:学习
检查时机:特权级检查会发生在往 数据段寄存器 中加载 段选择子 的时候,数据段寄存器包括 DS 和附加段寄存器 ES、FS、GS,如操作系统
mov ds,ax指针
检查条件:CPL <= 目标数据段DPL && RPL <= 目标数据段DPL (只能高特权级的指令访问地特权级的数据)htm
检查时机:特权级检查会发生在可以 改变 代码 段寄存器 CS 和 指令指针寄存器 EIP 的指令中,即这些指令要么改变 EIP,要么改变 CS 和 EIP。例如 call、jmp、int、ret、sysexit 等能改变程序执行流的指令,如blog
call 内核选择子索引
总结成最精炼的一句话就是:数据只能高的访问低的,代码只能从低的跳到高的(门或一致),从高到低只有返回指令能够完成
门描述符一共有四种,分别是
这些描述符也是记录在 描述符表 中的,与以前说的 段描述符 同样。因此这里把以前的段描述符,以及今天要说的四种门描述符,都画在下面的图中
门 | type值 | 存在位置 | 用法 |
---|---|---|---|
任务门 | 0101 | GDT、LDT、IDT | 与TSS配合实现任务切换,不过大多数操做系统都不这么玩 |
中断门 | 1110 | IDT | 进入中断后屏蔽中断(eflags的IF位置0),linux利用此实现系统调用,int 0x80 |
陷阱门 | 1111 | IDT | 进入中断后不屏蔽中断 |
调用门 | 1100 | GDT、LDT | 用户用call或jmp指令从用户进程进入0特权级 |
门描述符的访问流程是相似的,这里咱们用 调用门 来举例。
没有门描述符的时候,咱们用 jmp 指令指向一个普通的段描述符,通过一次拼接(段基址 + 偏移地址)就获得了逻辑地址。
调用门是用 jmp 或者 call 指令跳转过去的,当指向一个调用门时,无非就是多一次拼接而已,最开始的 选择子:偏移地址 中的 选择子 用来定位一个门描述符,偏移地址 则被忽略了,以下图。
直观地说就是:当前特权级必须比门特权级高,又必须比最终要跳到的代码段的特权级低
下面用一个调用门的具体例子梳理一下整个过程,因为书中的描述太精彩了,我看完以后对整个流程的理解又有了一大飞跃,因此我原封不动粘贴过来:
假设当前处理器正在 DPL 为 3 的代码段上运行,即正在运行用户程序,故处理器当前特权级 CPL 为 3。此时用户进程想获取安装的物理内存大小,该数据存储在操做系统的数据段中,该段 DPL 为 0。因为当前运行的是用户程序,CPL 为 3,因此没法访问 DPL 为 0 的数据段。因而它使用调用门向系统救助。调用门是操做系统安装在全局描述符表 GDT 中的,为了让用户进程可使用此调用门,操做系统将该调用门描述符的 DPL 设为 3。该调用门只须要一个参数,就是用户程序用于存储系统内存容量的缓冲区所在数据段的选择子和偏移地址。调用门描述符中记录的就是内核服务程序所在代码段的选择子及在代码段内的偏移量。用户进程用“call 调用门选择子”的方式使用调用门,此调用门选择子是由操做系统提供的,该选择子的 RPL 为 3,此时若是用户伪造一个调用门选择子也没用,由于此选择子是用来索引门描述符的,并不用来指向缓冲区的选择子,调用门选择子中的高 13 位索引值必需要指向门描述符在 GDT 中的位置,选择子中低 2 位的 RPL 伪造也没意义,由于此时 CPL 为 3,是短板,以它为主。此时处理器便进行特权级检查,CPL 为 3,RPL 为 3,门描述符 DPL 为 3,即数值上(CPL≤DPL && RPL≤DPL)成立,初步检查经过。接下来还要再将 CPL 与门描述符中选择子所对应的代码段描述符 DPL 比较,这是调用门对应的内核服务程序的 DPL,为叙述方便将其记做 DPL_CODE。因为 DPL_CODE 是内核程序的特权级,因此DPL_CODE 为 0,CPL 为 3,即数值上知足 CPL≥DPL_CODE,CPL 比目标特权级低,检查经过,该用户程序能够用调用门,因而处理器的当前特权级 CPL 的值用 DPL_CODE 代替,记录在 CS.RPL 中,此时CPL 变为 0。接下来,处理器便以 0 特权级的身份开始执行该内核服务程序,因为该服务程序的参数是用户提交的缓冲区所在的数据段的选择子及偏移量,为避免用户将缓冲区指向了内核的数据区,安全起见,在该内核服务程序中,操做系统将这个用户所提交的选择子的 RPL 变动为用户进程的 CPL,也就是指向缓冲区所在段的选择子的 RPL 变成了 3。前面说过,参数都是内核在 0 级栈中得到的,虽然用户进程将缓冲区的选择子及偏移量压在了 3 特权级栈中,但因为调用门的特权级变换,参数已经由处理器在固件一级上自动复制到 0 特权级栈中了。用户的代码段寄存器 CS 也在特权级发生变化时,由处理器自动压入到 0 特权级栈中,因此操做系统须要的参数均可以在本身的 0 特权级栈中找到。用户缓冲区的选择子修改事后,接下来内核服务程序将用户所须要的内存容量大小写到这个选择子和用户提交的偏移量对应的缓冲区。若是用户程序想搞破坏,所提交的这个缓冲区选择子指向的目标段不是用户进程本身的数据段,而是内核数据段或内核代码段,因为目标段的 DPL 为 0,虽然此时已在内核中执行,CPL 为 0,但选择子 RPL 已经被改成 3,数值上不知足 CPL≤DPL && RPL≤DPL,往缓冲区中的写入被拒绝,处理器引起异常。若是用户程序提交的缓冲区选择子确实指向用户程序本身的数据段,DPL 则为 3,数值上知足 CPL≤DPL && RPL≤DPL,往缓冲区中的写入则会成功。若是中断服务程序内部再有访问内核本身内存段的操做,还会按照数值上(CPL≤DPL && RPL≤DPL)的策略进行新一轮的特权检测。一般,若是不是用户程序向内核提交缓冲区地址来接收数据的话,内核不会主动访问用户的内存段,可能是访问本身的数据段或代码段,内核服务程序中若访问内核本身的内存段,因为内存段的 DPL 为 0,因此段选择子的 RPL 也必须为 0
特权级检查又是操做系统与处理器打配合的经典案例,处理器会在硬件层面作特权级检查的工做,而操做系统负责在软件层面定义特权级须要的相关数据(如选择子和门描述符)
正常状况下代码只能平级跳转,除非是用门结构实现低跳高,或者返回指令实现高跳低。而数据只能是高特权级指令访问低特权级的数据。
在内存中的数据和指令本没有特权级的概念,自己也没有访问者或受访者的概念。特权级被赋予在选择子的 RPL 位,或者描述符的 DPL 位,配合着这两个东西,指令和数据才有特权级的属性,单独的代码和数据讨论特权级是没有意义的。这也顺利成章地证实了处理器不会每执行一条指令就去检查特权级,只是某些条件下才进行一次特权级检查。
若是你对自制一个操做系统感兴趣,不妨跟随这个系列课程看下去,甚至加入咱们,一块儿来开发。
《操做系统真相还原》这本书真的赞!强烈推荐
当你看到该文章时,代码可能已经比文章中的又多写了一些部分了。你能够经过提交记录历史来查看历史的代码,我会慢慢梳理提交历史以及项目说明文档,争取给每一课都准备一个可执行的代码。固然文章中的代码也是全的,采用复制粘贴的方式也是彻底能够的。
若是你有兴趣加入这个自制操做系统的大军,也能够在留言区留下您的联系方式,或者在 gitee 私信我您的联系方式。
本课程打算出系列课程,我写到哪以为能够写成一篇文章了就写出来分享给你们,最终会完成一个功能全面的操做系统,我以为这是最好的学习操做系统的方式了。因此中间遇到的各类坎也会写进去,若是你能持续跟进,跟着我一块写,必然会有很好的收货。即便没有,交个朋友也是好的哈哈。
目前的系列包括