高端建站和普通建站有哪些不同网络链接推广
🚀 前言
操作系统区分了内核态和用户态确保安全,其实这是两个问题,一个是如何从内核态切换到用户态,另一个问题是如何确保当前进程不会逃出用户态。本文内容对应于书中的第22回。希望各位给个三连,拜托啦,这对我真的很重要!!!
目录
- 🚀 前言
- 🏆特权级
- 🏆特权级转换
- 📃特权级变化
- 📃特权级以外的改变
- 📃让进程无法跳出用户态
- 🎯总结
- 📖参考资料
🏆特权级
特权级说明白点就是给不同的人不同的权限。你是内核态,是公司老板,那就有权利过问任何人;你是用户态,是部门经理,就只能管自己部门的人。那么在操作系统中是如何体现的呢?
首先回顾一下段选择子的结构,不记得段选择子可以看这篇博客:linux0.11内核源码修仙传第二章——setup.s。简单来讲,段选择子就三个东西,特权级,描述符索引,TI(描述索引符从GDT取还是LDT取)
先来看CPL/RPL,为什么是两个呢,这是因为一个是标记自己,另一个是寻找别人。什么意思?举个例子:还是公司里的例子,我是xx部门经理,那我的工牌上就是xx经理,这叫做CPL,标榜自己当前所处特权级;现在其他部门的员工需要我开放某个文档的权限,那他就找xx经理,这叫做RPL。看吧,自己并没有变,只是使用主体变了,CPL是告诉自己我是什么特权级,RPL是别人来找我是什么特权级,也就是请求特权级。CPL = 0 是内核态,CPL = 3 是用户态。
假设要跳转到另一处执行,那汇编的话就是jmp
,call
和中断。以jmp举例:
短跳转:也就是段内跳转,不涉及段的变化,也就没有特权级检查
长跳转:也就是段间跳转,jmp yyy:xxx,这里的 yyy 是另一个要跳转的段选择子结构
直接看长跳转,这里面yyy是一个段选择子结构,那既然是段选择子,那就有和上面一样的结构,注意!此处是别人找xx经理,所以此时段选择子最后两位是RPL,请求特权级!同时,CPU 会拿这个段选择子去全局描述符表中寻找段描述符,从中找到段基址。
还记得段描述符吗,详细依旧可以查看这篇博客:linux0.11内核源码修仙传第二章——setup.s。简单来说这个描述符里面存放了这块地址属于什么段(代码段还是数据段)同时还有基地址等等信息,结构如下:
注意看,这里面在上方的13~14位有个DPL
,这表示目标代码段特权级,也就是 yyy 所对应的特权级,用下面的图更好理解:
搞了这么半天,其实就是CPL
与DPL
比较,CPL
必须等于DPL
,才会跳转成功。也就是说,当前代码所处段的特权级,必须要等于要跳转过去的代码所处的段的特权级,那就只能用户态往用户态跳,内核态往内核态跳,这样就防止了处于用户态的程序,跳转到内核态的代码段中做坏事。
在访问内存数据时也会有数据段的特权级检查,其本质还是上面这三个进行比较。
CPL = 0(内核态):此时所有特权级是的数据段都可被访问;
CPL = 1:只有在特权级1到3的数据段可被访问;
CPL = 3(用户态):只有处于特权级3的数据段可被访问
最终的效果就是,处于内核态的代码可以访问任何特权级的数据段,处于用户态的代码则只可以访问用户态的数据段,这也就实现了内存数据读写的保护。
好的,来总结一下这一小节,就是代码跳转只能同特权级,数据访问只能高特权级访问低特权级。
🏆特权级转换
📃特权级变化
这里先讲结论,底层是采用中断返回实现的。很神奇吧?具体怎么做的,首先从main函数里面开始:
void main(void)
{ ···move_to_user_mode();···
}
main函数里面就这一句话,不清不楚的,我们接着往下挖:
#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \"pushl $0x17\n\t" \ // 给ss赋值"pushl %%eax\n\t" \"pushfl\n\t" \"pushl $0x0f\n\t" \ // 给cs赋值"pushl $1f\n\t" \"iret\n" \ // 中断返回"1:\tmovl $0x17,%%eax\n\t" \"movw %%ax,%%ds\n\t" \"movw %%ax,%%es\n\t" \"movw %%ax,%%fs\n\t" \"movw %%ax,%%gs" \:::"ax")
从上面代码和注释可以看到,这个宏函数是压了五个值进栈,然后中断返回了。具体啥子意思喃,我们先来看下面这幅图:
这幅图是中断发生时,CPU自动帮我们做的压栈操作,注意这里,是自动!!!也就是不需要我们写出来的。由于是从内核态到用户态,发生了特权级变化,因此除了最后的错误码,刚好有五个需要压栈。当然了,这里是正常发生中断。但事实是,我们上面代码并没有任何中断发生,就是从main函数丝滑的调到这个汇编来了,我们之前开的中断里面也没有对应的中断。
这就得说说linus本人天才的地方了。Intel设计的CPU中,中断和中断返回可以不成套出现。what???这是不是很反直觉?但事实正是如此,linus祖师爷也正是利用这一特性,在最开始往栈里面压入了对应的五个值,模拟已经发生了中断,这样在中断返回后,CPU 又会帮我们把压栈的这些值返序赋值给响应的寄存器,即:SS、ESP、EFLAGS、CS、EIP 这几个寄存器,这就感觉像是正确返回了一样,让其误以为这是通过中断进来的。
解释一下这五个寄存器:
CS 和 EIP 就表示中断发生前代码所处的位置,这样中断返回后好继续去那里执行。
SS 和 ESP 表示中断发生前的栈的位置,这样中断返回后才好恢复原来的栈。
其中,特权级的转换,就体现在 CS 和 SS 寄存器的值里。最后这里可以抽象思维一下,这里其实就是假设我们中断前本来就是在用户态,中断进来用户态,中断后又返回原来的状态,就和下面这个图一样:
来详细看一下CS段和SS段的值:
push 00000017h
push 0000000fh
这里看个cs段举例,先把0x0f
化为二进制,再配合段选择子来看:
0000000000001111
最后两位是 11
表示特权级3(CPL=3),这正是代表了用户态。所以在经过iretd之后,CS寄存器的值就变成了这个,也就是成了用户态特权级。
📃特权级以外的改变
还是看上面CS寄存器的值,这次看除了最后两位的其他位。其实除了最后两位,就是看看TI以及描述符索引了。
倒数第3位表示TI位,这一位的含义是,前面的描述符索引是从GDT还是从LDT中取。这里是1,表示从LDT,也就是局部描述符中取。关于局部描述符可以参看这篇博客:linux0.11内核源码修仙传第十章——进程调度始化
这里回顾一下GDT与LDT中的分布情况,如下所示:
描述符索引是1,且从LDT中选取,则代表是代码段。如上面的红框框所示。
继续回到上面的汇编代码,也就是move_to_user_mode
这个宏函数。返回后的EIP
是标号1的位置。也就是返回后会执行标签1后面的内容,但是此时已经是用户态了。
···
"pushl $1f\n\t" \
"iret\n" \ // 中断返回
"1:\tmovl $0x17,%%eax\n\t" \
"movw %%ax,%%ds\n\t" \
"movw %%ax,%%es\n\t" \
"movw %%ax,%%fs\n\t" \
"movw %%ax,%%gs" \
:::"ax")
这后面的内容是将ds,es,fs,gs寄存器都设置为0x17
。既然是设置这些段寄存器,那还是涉及到段寄存器,0x17的最后两位也是11,表明对应的CPL也修改为了用户态。
📃让进程无法跳出用户态
具体如何让进程无法跳出来,已经在上一大节:🏆特权级 中描述过了。其实就是进行长跳转的时候要检查CPL、RPL、DPL三者的关系,代码只有同级才能发生跳转。
经过 move_to_user_mode
这行代码,当前就已经从内核态变化到用户态了。一旦转变为了用户态,那么之后的代码将一直处于用户态的模式,除非发生了中断,比如用户发出了系统调用的中断指令,那么此时将会从用户态陷入内核态,不过当中断处理程序执行完之后,又会通过中断返回指令从内核态回到用户态。整个变化过程如下所示:
🎯总结
来回顾一下本文重要的一些点:1. 首先是关于特权级,其实本质就是比较CPL,RPL与DPL之间的关系。2. 特权级的转换是通过中断返回实现的,这个中断返回不需要中断调用,只需要模拟中断发生,往栈里面压5个值即可。3. 进入用户态后就会一直在用户态,只有中断才可以进入内核态,而后又会回到用户态。
📖参考资料
[1] linux源码趣读
[2] 一个64位操作系统的设计与实现
[3] 操作系统学习(九) 、访问数据段时的特权级检查