亲宝软件园·资讯

展开

【自制操作系统07】深入浅出特权级

闪客sun 人气:2

一、到目前为止的程序流程图

本讲我们不继续写任何代码,而是专门拿出一讲来说说特权级的事,为后续的工作做一个知识储备。这段内容太难啃了,也可能我恰好对这块不太感冒,反正我是恶心了好久才啃下来。

为了让大家清楚目前的程序进度,画了到目前为止的程序流程图,如下

二、什么时候处理器会进行特权级检查

为什么要进行特权级检查,我就不说太多了,简单理解,操作系统不希望用户进程访问内核数据,所以需要给指令呀还有数据呀都附上一个特权级的属性,让程序受限制。

特权级分为 0 1 2 3 四种,我们常说的 用户态 就是最低等级的 3 特权级,内核态 就是最高等级的 0 特权级。

处理器在 访问数据 或 跳转到代码 时,需要进行特权级检查,特权级检查的具体细则在下面有具体描述。这里我先把大致的思想方向总结出来:

  • 不论是访问数据,还是跳转到代码,特权级检查仅发生在 重新加载选择子 时,而不是每条指令都检查一遍。
  • 对于 访问数据 来说,只能高特权级的指令访问地特权级的数据
  • 对于 跳转到代码 来说,只能平级跳转,如果想从低特权级跳到高特权级需要通过 门,如果想从高特权级跳到低特权级需要通过 返回指令

一些术语

  • CPL:处理器当前特权级
  • DPL:段或门的特权级,段描述符或者门描述符的 DPL 字段(45-46位)
  • RPL:请求特权级,选择子 RPL 字段(0-1位)

特权级检查的时机与条件

受访者为数据时

  • 检查时机:特权级检查会发生在往 数据段寄存器 中加载 段选择子 的时候,数据段寄存器包括 DS 和附加段寄存器 ES、FS、GS,如

    mov ds,ax

  • 检查条件:CPL <= 目标数据段DPL && RPL <= 目标数据段DPL (只能高特权级的指令访问地特权级的数据)

受访者为代码时

  • 检查时机:特权级检查会发生在能够 改变 代码 段寄存器 CS 和 指令指针寄存器 EIP 的指令中,即这些指令要么改变 EIP,要么改变 CS 和 EIP。例如 call、jmp、int、ret、sysexit 等能改变程序执行流的指令,如

    call 内核选择子

  • 检查条件
    • 无门结构且目标为非一致代码段:CPL = RPL = 目标代码段DPL
    • 无门结构且目标为一致代码段:CPL >= 目标数据段DPL && RPL >= 目标数据段DPL
    • 有门结构:DPL_GATE >= CPL >= DPL_CODE && RPL <= DPL_GATE(从低特权级跳到高特权级需要通过门)

总结成最精炼的一句话就是:数据只能高的访问低的,代码只能从低的跳到高的(门或一致),从高到低只有返回指令可以完成

三、门描述符

门描述符一共有四种,分别是

  • 任务门描述符
  • 中断门描述符
  • 跳转门描述符
  • 调用门描述符

这些描述符也是记录在 全局描述符表 GDT 中的,与之前说的 段描述符 一样。所以这里把之前的段描述符,以及今天要说的四种门描述符,都画在下面的图中

四、调用门跳转流程

门描述符的访问流程是类似的,这里我们用 调用门 来举例。

没有门描述符的时候,我们用 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 位,配合着这两个东西,指令和数据才有特权级的属性,单独的代码和数据讨论特权级是没有意义的。这也顺利成章地证明了处理器不会每执行一条指令就去检查特权级,只是某些条件下才进行一次特权级检查。

写在最后:开源项目和课程规划

如果你对自制一个操作系统感兴趣,不妨跟随这个系列课程看下去,甚至加入我们,一起来开发。

参考书籍

《操作系统真相还原》这本书真的赞!强烈推荐

项目开源

项目开源地址:https://gitee.com/sunym1993/flashos

当你看到该文章时,代码可能已经比文章中的又多写了一些部分了。你可以通过提交记录历史来查看历史的代码,我会慢慢梳理提交历史以及项目说明文档,争取给每一课都准备一个可执行的代码。当然文章中的代码也是全的,采用复制粘贴的方式也是完全可以的。

如果你有兴趣加入这个自制操作系统的大军,也可以在留言区留下您的联系方式,或者在 gitee 私信我您的联系方式。

课程规划

本课程打算出系列课程,我写到哪觉得可以写成一篇文章了就写出来分享给大家,最终会完成一个功能全面的操作系统,我觉得这是最好的学习操作系统的方式了。所以中间遇到的各种坎也会写进去,如果你能持续跟进,跟着我一块写,必然会有很好的收货。即使没有,交个朋友也是好的哈哈。

目前的系列包括

  • 【自制操作系统01】硬核讲解计算机的启动过程
  • 【自制操作系统02】环境准备与启动区实现
  • 【自制操作系统03】读取硬盘中的数据
  • 【自制操作系统04】从实模式到保护模式
  • 【自制操作系统05】开启内存分页机制
  • 【自制操作系统06】终于开始用 C 语言了,第一行内核代码!

加载全部内容

相关教程
猜你喜欢
用户评论