Linux 0.11-从内核态到用户态-23

  • 从内核态到用户态
  • 转载

从内核态到用户态

书接上回,上回书咱们从整体上鸟瞰了一下第三部分要讲的内容,代码上就是还差四句话就走到了 main 函数的尽头。

void main(void) {...    move_to_user_mode();if (!fork()) {init();}for(;;) pause();
}

今天我们就重点讲这第一句代码,move_to_user_mode

让进程无法逃出用户态

这行代码的意思直接说非常简单,就是从内核态转变为了用户态,但要解释清楚这个意思,还需要听我慢慢道来。

我相信你肯定听说过操作系统的内核态与用户态,用户进程都在用户态这个特权级下运行,而有时程序想要做一些内核态才允许做的事情,比如读取硬盘的数据,就需要通过系统调用,来请求操作系统在内核态特权级下执行一些指令。

我们现在的代码,还是在内核态下运行,之后操作系统达到怠速状态时,是以用户态的 shell 进程运行,随时等待着来自用户输入的命令。

所以,就在这一步,也就是 move_to_user_mode 这行代码,作用就是将当前代码的特权级,从内核态变为用户态。

一旦转变为了用户态,那么之后的代码将一直处于用户态的模式,除非发生了中断,比如用户发出了系统调用的中断指令,那么此时将会从用户态陷入内核态,不过当中断处理程序执行完之后,又会通过中断返回指令从内核态回到用户态。

整个过程被操作系统的机制拿捏的死死的,始终让用户进程处于用户态运行,必要的时候陷入一下内核态,但很快就会被返回而再次回到用户态,是不是非常无奈?这样操作系统就掌控了控制权,而用户进程再怎么折腾也无法逃出这个模式。

内核态与用户态的本质-特权级

首先从一个最大的视角来看,这一切都源于 CPU 的保护机制。CPU 为了配合操作系统完成保护机制这一特性,分别设计了分段保护机制分页保护机制

当我们在 第七回 | 六行代码就进入了保护模式 将 cr0 寄存器的 PE 位开启时,就开启了保护模式,也即开启了分段保护机制

当我们在 第九回 | Intel 内存管理两板斧:分段与分页 将 cr0 寄存器的 PG 位开启时,就开启了分页模式,也即开启了分页保护机制

有关特权级的保护,实际上属于分段保护机制的一种。具体怎么保护的呢?由于这里的细节比较繁琐,所以我举个例子简单理解下即可,实际上的特权级检查规则要比我说的多好多内容。

我们目前正在执行的代码地址,是通过 CPU 中的两个寄存器 cs : eip 指向的对吧?cs 寄存器是代码段寄存器,里面存着的是段选择子,还记得它的结构么?

这里面的低端两位,此时表示 CPL,也就是当前所处的特权级,假如我们现在这个时刻,CS 寄存器的后两位为 3,二进制就是 11,就表示是当前处理器处于用户态这个特权级。

假如我们此时要跳转到另一处内存地址执行,在最终的汇编指令层面无非就是 jmp、call 和中断。我们拿 jmp 跳转来举例。

如果是短跳转,也就是直接 jmp xxx,那不涉及到段的变换,也就没有特权级检查这回事。

如果是长跳转,也就是 jmp yyy : xxx,这里的 yyy 就是另一个要跳转到的段的段选择子结构。

这个结构仍然是一样的段选择子结构,只不过这里的低端两位,表示 RPL,也就是请求特权级,表示我想请求的特权级是什么。同时,CPU 会拿这个段选择子去全局描述符表中寻找段描述符,从中找到段基址。

那还记得段描述符的样子么?

你看,这里面又有个 DPL,这表示目标代码段特权级,也就是即将要跳转过去的那个段的特权级。

好了,我们总结一下简图,就是这三个玩意的比较。

这里的检查规则比较多,简单说,绝大多数情况下,要求 CPL 必须等于 DPL,才会跳转成功,否则就会报错。

也就是说,当前代码所处段的特权级,必须要等于要跳转过去的代码所处的段的特权级,那就只能用户态往用户态跳内核态往内核态跳,这样就防止了处于用户态的程序,跳转到内核态的代码段中做坏事。

这只是代码段跳转时所做的特权级检查,还有访问内存数据时也会有数据段的特权级检查,这里就不展开了。最终的效果是,处于内核态的代码可以访问任何特权级的数据段,处于用户态的代码则只可以访问用户态的数据段,这也就实现了内存数据读写的保护。

说了这么多,其实就是,代码跳转只能同特权级,数据访问只能高特权级访问低特权级

特权级转换的方式

诶不对呀,那我们今天要讲的是,从内核态转变为用户态,那如果代码跳转只能同特权级跳,我们现在处于内核态,要怎么样才能跳转到用户态呢?

Intel 设计了好多种特权级转换的方式,中断中断返回就是其中的一种。

处于用户态的程序,通过触发中断,可以进入内核态,之后再通过中断返回,又可以恢复为用户态

就是刚刚的图所表示的。

系统调用就是这么玩的,用户通过 int 0x80 中断指令触发了中断,CPU 切换至内核态,执行中断处理程序,之后中断程序返回,又从内核态切换回用户态。

但有个问题是,我们当前的代码,此时就是处于内核态,并不是由一个用户态程序通过中断而切换到的内核态,那怎么回到原来的用户态呢?答案还是,通过中断返回。

没有中断也能中断返回?可以的,Intel 设计的 CPU 就是这样不符合人们的直觉,中断和中断返回的确是应该配套使用的,但也可以单独使用,我们看代码。

void main(void) {...    move_to_user_mode();...
}#define move_to_user_mode() \
_asm { \_asm mov eax,esp \_asm push 00000017h \_asm push eax \_asm pushfd \_asm push 0000000fh \_asm push offset l1 \_asm iretd /* 执行中断返回指令*/ \
_asm l1: mov eax,17h \_asm mov ds,ax \_asm mov es,ax \_asm mov fs,ax \_asm mov gs,ax \
}

你看,这个方法里直接就执行了中断返回指令 iretd。

那么为什么之前进行了一共五次的压栈操作呢?因为中断返回理论上就是应该和中断配合使用的,而此时并不是真的发生了中断到这里,所以我们得假装发生了中断才行。

怎么假装呢?其实就把栈做做工作就好了,中断发生时,CPU 会自动帮我们做如下的压栈操作。而中断返回时,CPU 又会帮我们把压栈的这些值返序赋值给响应的寄存器。

去掉错误码,刚好是五个参数,所以我们在代码中模仿 CPU 进行了五次压栈操作,这样在执行 iretd 指令时,硬件会按顺序将刚刚压入栈中的数据,分别赋值给 SS、ESP、EFLAGS、CS、EIP 这几个寄存器,这就感觉像是正确返回了一样,让其误以为这是通过中断进来的

压入栈的 CS 和 EIP 就表示中断发生前代码所处的位置,这样中断返回后好继续去那里执行。

压入栈的 SS 和 ESP 表示中断发生前的栈的位置,这样中断返回后才好恢复原来的栈。

其中,特权级的转换,就体现在 CS 和 SS 寄存器的值里,都是细节!

CS 和 SS 寄存器是段寄存器的一种,段寄存器里的值是段选择子,其结构上面已经提过两遍了,在 第六回 | 先解决段寄存器的历史包袱问题 中也专门讲了这个结构的作用。

对着这个结构,我们看代码。

#define move_to_user_mode() \
_asm { \_asm mov eax,esp \_asm push 00000017h \ ; 给 SS 赋值_asm push eax \_asm pushfd \_asm push 0000000fh \ ; 给 CS 赋值_asm push offset l1 \_asm iretd /* 执行中断返回指令*/ \
_asm l1: mov eax,17h \_asm mov ds,ax \_asm mov es,ax \_asm mov fs,ax \_asm mov gs,ax \
}

拿 CS 举例,给它赋的值是,0000000fh,用二进制表示为:

0000000000001111

最后两位 11 表示特权级为 3,即用户态。而我们刚刚说了,CS 寄存器里的特权级,表示 CPL,即当前处理器特权级。

所以经过 iretd 返回之后,CS 的值就变成了它,而当前处理器特权级,也就变成了用户态特权级。

除了改变特权级之外

除了改变了特权级之外,还做了什么事情呢?

刚刚我们关注段寄存器,只关注了特权级的部分,我们再详细看看。

刚刚说了 CS 寄存器为 0000000000001111,最后两位表示用户态的含义。

那继续解读,倒数第三位 TI 表示,前面的描述符索引,是从 GDT 还是 LDT 中取,1 表示 LDT,也就是从局部描述符表中取。

前面的描述符索引为 1,表示从局部描述符表中取到代码段描述符,如果你熟悉前面我讲过的内容,你将会直接得出上述结论。不过我还是帮你回忆一下。

在 第18回 | 大名鼎鼎的进程调度就是从这里开始的 中,将 0 号 LDT 作为当前的 LDT 索引,记录在了 CPU 的 lldt 寄存器中。

#define lldt(n) __asm__("lldt %%ax"::"a" (_LDT(n)))void sched_init(void) {...lldt(0);...
}

而整个 GDT 与 LDT 表的设计,经过整个 第一部分 进入内核前的苦力活 和 第二部分 大战前期的初始化工作 的设计后,成了这个样子。

所以,一目了然。

再看这行代码,把 EIP 寄存器赋值为了那行标号的地址。

void main(void) {...    move_to_user_mode();...
}#define move_to_user_mode() \
_asm { \_asm mov eax,esp \_asm push 00000017h \_asm push eax \_asm pushfd \_asm push 0000000fh \_asm push offset l1 \_asm iretd /* 执行中断返回指令*/ \
_asm l1: mov eax,17h \_asm mov ds,ax \_asm mov es,ax \_asm mov fs,ax \_asm mov gs,ax \
}

这里刚好设置的是下面标号 l1 的位置,所以 iretd 之后 CPU 就乖乖去那里执行了。所以其实从效果上看,就是顺序往下执行,只不过利用了 iretd 做了些特权级转换等工作。

同理,这里的栈段 ss 和数据段 ds,都被赋值为了 17h,大家可以展开二进制算一下,他们又是什么特权级,对应的描述符又是谁。

总结

所以其实,最终效果上看就是按顺序执行了我们所写的指令,仿佛没有经过什么中断和中断返回的过程,但却通过中断返回实现了特权级的翻转,也就是从内核态变为了用户态,顺便设置了栈段、代码段和数据段的基地址。

好了,我们兜兜转转终于把这个 mov_to_user_mode 讲完了,特权级这块的检查细节非常繁琐,为了理解操作系统,我们只需要暂且记住如下一句话就好了:

数据访问只能高特权级访问低特权级,代码跳转只能同特权级跳转,要想实现特权级转换,可以通过中断和中断返回来实现。

OK,我们现在已经进入了用户态,也即表明了需要内核态来完成的工作已经全部安排妥当了,其实就是整个 第一部分 进入内核前的苦力活 和 第二部分 大战前期的初始化工作 的内容,对全局描述符表、中断描述符表、页表等关键内存结构进行设置,以及对 CPU 特殊寄存器如 cr0 和 cr3 的设置,还有对外设如硬盘、键盘、定时器的设置等。

看来我们又完成了一大堆苦力活呀,内核态做的工作也真是枯燥乏味呢。接下来只需要在用户态进行工作即可了!


转载

本文转载至闪客图解操作系统系列文章

Linux 0.11-从内核态到用户态-23相关推荐

  1. linux 用户态 内核态 通信,procfs(从0开始,内核态和用户态通信charpter2)

    这篇博文将针对linux内核态与用户态通信方式中的procfs进行详细的学习. /proc主要存放内核的一些控制信息,所以这些信息大部分的逻辑位置位于内核控制的内存,在/proc下使用ls -l你会发 ...

  2. Linux 0.11内核分析04:多进程视图

    目录 1 进程概念的引入 1.1 使用CPU的直观想法 1.2 直观用法的缺点 1.3 直观用法的改进 1.4 进程的概念 1.4.1 保存程序执行状态 1.4.2 进程与PCB 1.5 Linux ...

  3. Linux 0.11内核分析03:系统调用

    目录 1 概述 1.1 什么是系统调用 1.2 为什么需要系统调用 2 系统调用基础设施 2.1 安装系统门 2.1.1 中断描述符 2.1.2 中断描述符安装函数 2.1.3 安装0x80系统门 2 ...

  4. Linux 0.11内核分析02:系统启动

    目录 1. 内核镜像的构建 1.1 内核源码结构 1.1.1 boot 1.1.2 fs 1.1.3 include 1.1.4 init 1.1.5 kernel 1.1.6 lib 1.1.7 m ...

  5. Linux 操作系统原理 — 内核态与用户态

    目录 文章目录 目录 Linux 的内核态与用户态 系统调用(System Call) Shell 用户态和内核态的切换 进程的用户空间和内核空间的内存布局 内核空间 用户空间 Linux 的内核态与 ...

  6. Linux 内核态与用户态通信 netlink

    参考资料: https://blog.csdn.net/zqixiao_09/article/details/77131283 https://www.cnblogs.com/lopnor/p/615 ...

  7. Linux内核态之间进程通信,内核态和用户态通信(二)--实现

    本文主要使用netlink套接字实现中断环境与用户态进程通信. 系统环境:基于linux 2.6.32.27 和 linux 3.16.36 Linux内核态和用户态进程通信方法的提出和实现 用户上下 ...

  8. linux c程序中内核态与用户态内存存储问题

    Unix/Linux的体系架构 如上图所示,从宏观上来看,Linux操作系统的体系架构分为用户态和内核态(或者用户空间和内核).内核从本质上看是一种软件--控制计算机的硬件资源,并提供上层应用程序运行 ...

  9. 【转】linux内核态和用户态的区别

    原文网址:http://www.mike.org.cn/articles/linux-kernel-mode-and-user-mode-distinction/ 内核态与用户态是操作系统的两种运行级 ...

最新文章

  1. d3.js 简介和安装
  2. 把ASP应用中的Session传递给asp.net应用
  3. Harris角点检测原理详解(转载)
  4. 工业富联灯塔工厂白皮书:智能制造里程碑.pdf(附下载链接)
  5. java web 中的乱码
  6. 杨建:网站加速--系统架构篇
  7. 解决ubuntu不能远程连接
  8. java三大特性之—封装
  9. Matlab 环境下用正弦波模拟方波和锯齿波
  10. linux rm 文件找回_Linux下用rm删除的文件的恢复方法
  11. 银行大数据风控管理针对哪些应用场景?
  12. util是什么意思计算机英语,util是什么意思_util怎么读_util翻译_用法_发音_词组_同反义词_跑龙套-新东方在线英语词典...
  13. 挑选代表( 招商银行信用卡中心)
  14. .net core 使用 Hangfire 实现定时、延时任务
  15. 数值积分:龙贝格求积
  16. item_get - 根据ID获取拼多多商品详情
  17. Excel设置下拉选项的方法
  18. 计算机二级考试是考什么?
  19. 使用虚拟主机安装cyberpanel面板并创建wordpress站点
  20. 默然日记20151123

热门文章

  1. 基于注入式木马病毒(浏览器绑架)实现及防御方法的研究
  2. (冒泡排序) Problem: 并列排名
  3. python并列排名_Oracle并列排名显示
  4. flutter阿里云OSS图片上传
  5. Matlab 图像傅里叶变换
  6. 2022下半年软考合格标准是多少?你可知?
  7. 计算机公式大小写,excel大写金额公式
  8. 【web前端开发】什么是前端?
  9. delphi源码转换为C++ Builder源码
  10. 变革中的微软:裁员7800人,对诺基亚业务减记76亿美元