CPU 指令集、权限与用户态内核态机制
目录
一、CPU 指令集概述
二、CPU 指令集的权限分级机制
三、内核态与用户态的定义及权限差异
1、ring 0(内核态)
2、ring 3(用户态)
四、内存资源的使用限制与划分
五、用户态与内核态的切换机制
1、触发用户态到内核态切换的场景
1. 系统调用
2. 异常
3. 中断
2、切换时 CPU 的操作流程
3、切换流程详解
一、CPU 指令集概述
-
CPU 指令集是 CPU 实现软件指挥硬件执行动作的关键媒介。
-
具体而言,每一条汇编语句都精准对应一条 CPU 指令。
-
众多 CPU 指令汇聚在一起,可构成一个甚至多个集合,这些指令的集合便被称为 CPU 指令集。
二、CPU 指令集的权限分级机制
CPU 指令集具备权限分级特性。由于 CPU 指令集可直接操作硬件,一旦指令操作不规范,极有可能引发严重错误,进而影响整个计算机系统的稳定运行。例如,开发人员若因对硬件操作不熟悉,在编写程序时出现失误,可能导致操作系统内核以及所有正在运行的程序遭受不可挽回的损害,最终只能通过重启计算机来恢复系统。
对于开发人员来说,确保硬件操作的准确性和安全性是一项艰巨的任务,这不仅增加了开发负担,而且开发人员在这方面的操作能力往往不被充分信任。因此,操作系统内核直接屏蔽了开发人员对硬件操作的可能性,禁止开发人员直接接触 CPU 指令集。
为满足这一需求,硬件设备商提供了硬件级别的支持,具体做法是对 CPU 指令集设置权限。不同级别的权限所能使用的 CPU 指令集是有限的。以 Intel CPU 为例,Intel 将 CPU 指令集操作的权限由高到低划分为 4 个等级:
-
ring 0:权限最高,可自由使用所有 CPU 指令集。
-
ring 1:权限次之。
-
ring 2:权限再次之。
-
ring 3:权限最低,仅能使用常规 CPU 指令集,无法使用操作硬件资源的 CPU 指令集,例如 IO 读写、网卡访问、内存申请等操作均不可行。
需要特别指出的是,Linux 系统仅采用 ring 0 和 ring 3 这两个权限级别。在 CPU 中,存在一个标志字段,用于标识线程的运行状态,其中用户态对应数值 3,内核态对应数值 0。
三、内核态与用户态的定义及权限差异
1、ring 0(内核态)
-
完全在操作系统内核中运行。
-
执行内核空间的代码时,处于 ring 0 保护级别,拥有对硬件的所有操作权限,可执行所有 CPU 指令集,并能够访问任意地址的内存。
-
然而,在内核模式下,任何异常都可能是灾难性的,甚至会导致整台机器停机。
2、ring 3(用户态)
-
在应用程序中运行。
-
在用户模式下,处于 ring 3 保护级别,代码没有对硬件的直接控制权限,也不能直接访问特定地址的内存。
-
程序通过调用系统接口(System Call APIs)来实现对硬件和内存的访问。在这种保护模式下,即使程序发生崩溃,也是可以恢复的。
-
在电脑上,大部分程序都在用户模式下运行。
低权限的资源范围相对较小,而高权限的资源范围更大。因此,用户态与内核态的概念本质上就是 CPU 指令集权限的差异体现。
四、内存资源的使用限制与划分
我们通过指令集权限来区分用户态和内核态,同时还对内存资源的使用进行了限制。操作系统为用户态与内核态划分了两块独立的内存空间,并分配给它们对应的指令集使用。
在内存资源使用方面,操作系统对用户态与内核态做出了明确限制。每个进程创建时都会被分配虚拟空间地址。以 Linux 32 位操作系统为例,其寻址空间范围为 4G(2 的 32 次方)。操作系统会将虚拟控制地址划分为两部分:一部分为内核空间,另一部分为用户空间。高位的 1G(从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核使用,而低位的 3G(从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程使用。

-
用户态:只能操作 0 - 3G 范围的低位虚拟空间地址。
-
内核态:可操作 0 - 4G 范围的虚拟空间地址,尤其是对 3 - 4G 范围的高位虚拟空间地址,必须由内核态进行操作。其中,3G - 4G 部分是所有进程共享的(即所有进程的内核态逻辑地址共享同一块内存地址),这是内核态的地址空间,存放着整个内核的代码、所有的内核模块以及内核所维护的数据。
在内核运行过程中,会涉及内核栈的分配。内核的进程管理代码会将内核栈创建在内核空间中,同时相应的页表也会被创建。
五、用户态与内核态的切换机制
1、触发用户态到内核态切换的场景
1. 系统调用
-
这是用户态进程主动切换到内核态的方式。
-
用户态进程通过系统调用向操作系统申请资源以完成工作,例如 fork() 就是一个创建新进程的系统调用。
-
操作系统提供了中断指令 int 0x80 来主动进入内核,这是用户程序发起调用访问内核代码的唯一方式。
-
调用系统函数时,会通过内联汇编代码插入 int 0x80 的中断指令。
-
内核接收到 int 0x80 中断后,查询中断处理函数地址,随后进入系统调用。
2. 异常
-
当 CPU 在执行用户态的进程时,如果发生了未预知的异常,当前运行进程会切换到处理此异常的内核相关进程中,即切换到内核态,例如缺页异常。
3. 中断
-
当 CPU 在执行用户态的进程时,外围设备完成用户请求的操作后,会向 CPU 发出相应的中断信号。
-
这时,CPU 会暂停执行下一条即将要执行的指令,转到与中断信号对应的处理程序去执行,也就是切换到了内核态。
-
例如,硬盘读写操作完成后,系统会切换到硬盘读写的中断处理程序中执行后续操作。
2、切换时 CPU 的操作流程
-
当某个进程需要进行 IO 读写操作时,必然会用到 ring 0 级别的 CPU 指令集。
-
而此时 CPU 的指令集操作权限只有 ring 3,为了能够操作 ring 0 级别的 CPU 指令集,CPU 需要切换指令集操作权限级别为 ring 0(可称之为提权),然后再执行相应的 ring 0 级别的 CPU 指令集(内核代码)。
-
代码发生提权时,CPU 需要切换栈。前面提到过,内核有自己的内核栈。
-
CPU 切换栈需要栈段描述符(ss 寄存器)和栈顶指针(esp 寄存器),这两个值的来源如下:CPU 通过一个段寄存器(tr)确定 TSS(任务状态段,struct TSS)的位置。
-
在 TSS 结构中存在 SS0 和 ESP0。提权时,CPU 就从这个 TSS 里把 SS0 和 ESP0 取出来,放到 ss 和 esp 寄存器中。
3、切换流程详解
-
保存用户态现场信息:从用户态切换到内核态时,用户态可以直接读写寄存器。用户态操作 CPU,将寄存器的状态保存到对应的内存中,然后调用对应的系统函数,传入对应的用户栈地址和寄存器信息,以便后续内核方法调用完毕后,能够恢复用户方法执行的现场。
-
提权操作:从用户态切换到内核态需要进行提权,CPU 将指令集操作权限级别切换为 ring 0。
-
切换内核栈:提权后,切换内核栈,然后开始执行内核方法,相应的方法栈帧保存在内核栈中。
-
恢复用户态执行:当内核方法执行完毕后,CPU 将指令集操作权限级别切换为 ring 3,然后利用之前写入的信息来恢复用户栈的执行。
从上述流程可以看出,用户态切换到内核态时,会涉及用户态现场信息的保存与恢复,还需要进行一系列的安全检查,这一过程相对耗费资源。
