特权级机制¶
本节导读¶
为了保护我们的批处理操作系统不受到出错应用程序的影响并全程稳定工作,单凭软件实现是很难做到的,而是需要 CPU 提供一种特权级隔离机制,使CPU在执行应用程序和操作系统内核的指令时处于不同的特权级。本节主要介绍了特权级机制的软硬件设计思路,以及RISC-V的特权级架构,包括特权指令的描述。
特权级的软硬件协同设计¶
实现特权级机制的根本原因是应用程序运行的安全性不可充分信任。在上一节里,操作系统和应用紧密连接在一起,形成一个应用程序来执行。随着应用需求的增加,操作系统也越来越大,会以库的形式存在;同时应用自身也会越来越复杂。由于操作系统会给多个应用提供服务,所以它可能的错误会比较快地被发现,但应用自身的错误可能就不会很快发现。由于二者通过编译器形成一个应用程序来执行,即使是应用本身的问题,也会导致操作系统受到连累,从而可能导致整个计算机系统都不可用了。 所以,计算机专家就想到一个方法,能否让相对安全可靠的操作系统不受到应用程序的破坏,运行在一个安全的执行环境中,而让应用程序运行在一个无法破坏操作系统的执行环境中?
为确保操作系统的安全,对应用程序而言,需要限制的主要有两个方面: - 应用程序不能访问任意的地址空间(这个在第四章会进一步讲解,本章不会讲解) - 应用程序不能执行某些可能破会计算机系统的指令(本章的重点)
假设有了这样的限制,我们还需要确保应用程序能够得到操作系统的服务,即应用程序和操作系统还需要有交互的手段。使得低特权级软件都只能做高特权级软件允许它做的,且低特权级软件的超出其能力的要求必须寻求高特权级软件的帮助。在这里的高特权级软件就是低特权级软件的软件执行环境。
为了完成这样的特权级需求,需要进行软硬件协同设计。一个比较简洁的方法就是,处理器设置两个不同安全等级的执行环境:用户态特权级的执行环境和内核态特权级的执行环境。且明确指出可能破会计算机系统的内核态特权级指令子集,规定内核态特权级指令子集中的指令只能在内核态特权级的执行环境中执行,如果在用户态特权级的执行环境中执行这些指令,会产生异常。处理器在执行不同特权级的执行环境下的指令前进行特权级安全检查。
为了让应用程序获得操作系统的函数服务,采用传统的函数调用方式(即通常的 call
和 ret
指令或指令组合)将会直接绕过硬件的特权级保护检查。所以要设计新的指令:执行环境调用(Execution Environment Call,简称 ecall
)和执行环境返回(Execution Environment Return,简称 eret
)):
ecall
:具有用户态到内核态的执行环境切换能力的函数调用指令(RISC-V中就有这条指令)eret
:具有内核态到用户态的执行环境切换能力的函数返回指令(RISC-V中有类似的sret
指令)
但硬件具有了这样的机制后,还需要操作系统的配合才能最终完成对操作系统自己的保护。首先,操作系统需要提供相应的控制流,能在执行 eret
前准备和恢复用户态执行应用程序的上下文。其次,在应用程序调用 ecall
指令后,能够保存用户态执行应用程序的上下文,便于后续的恢复;且还要坚持应用程序发出的服务请求是安全的。
注解
在实际的CPU,如x86、RISC-V等,设计了多达4种特权级。对于一般的操作系统而言,其实只要两种特权级就够了。
RISC-V 特权级架构¶
RISC-V 架构中一共定义了 4 种特权级:
级别 |
编码 |
名称 |
---|---|---|
0 |
00 |
用户/应用模式 (U, User/Application) |
1 |
01 |
监督模式 (S, Supervisor) |
2 |
10 |
H, Hypervisor |
3 |
11 |
机器模式 (M, Machine) |
其中,级别的数值越大,特权级越高,掌控硬件的能力越强。从表中可以看出, M 模式处在最高的特权级,而 U 模式处于最低的特权级。
之前我们给出过支持应用程序运行的一套 执行环境栈 ,现在我们站在特权级架构的角度去重新看待它:
和之前一样,白色块表示一层执行环境,黑色块表示相邻两层执行环境之间的接口。这张图片给出了能够支持运行 Unix 这类复杂系统的软件栈。其中 内核代码运行在 S 模式上;应用程序运行在 U 模式上。运行在 M 模式上的软件被称为 监督模式执行环境 (SEE, Supervisor Execution Environment) ,这是站在运行在 S 模式上的软件的视角来看,它的下面也需要一层执行环境支撑,因此被命名为 SEE,它需要在相比 S 模式更高的特权级下运行, 一般情况下在 M 模式上运行。
注解
按需实现 RISC-V 特权级
RISC-V 架构中,只有 M 模式是必须实现的,剩下的特权级则可以根据跑在 CPU 上应用的实际需求进行调整:
简单的嵌入式应用只需要实现 M 模式;
带有一定保护能力的嵌入式系统需要实现 M/U 模式;
复杂的多任务系统则需要实现 M/S/U 模式。
到目前为止,(Hypervisor, H)模式的特权规范还没完全制定好。所以本书不会涉及。
之前我们提到过,执行环境的其中一种功能是在执行它支持的上层软件之前进行一些初始化工作。我们之前提到的引导加载程序会在加电后对整个系统进行 初始化,它实际上是 SEE 功能的一部分,也就是说在 RISC-V 架构上引导加载程序一般运行在 M 模式上。此外,编程语言的标准库也会在执行程序员 编写的逻辑之前进行一些初始化工作,但是在这张图中我们并没有将其展开,而是统一归类到 U 模式软件,也就是应用程序中。
回顾第一章,当时只是实现了简单的支持单个裸机应用的库级别的“三叶虫”操作系统,它和应用程序全程运行在 S 模式下,应用程序很容易破坏没有任何保护的执行环境–操作系统。而在后续的章节中,我们会涉及到RISC-V的 M/S/U 三种特权级:其中应用程序和用户态支持库运行在 U 模式的最低特权级;操作系统内核运行在 S 模式特权级(在本章表现为一个简单的批处理系统),形成支撑应用程序和用户态支持库的执行环境;而第一章提到的预编译的 bootloader – RustSBI
实际上是运行在更底层的 M 模式特权级下的软件,是操作系统内核的执行环境。整个软件系统就由这三层运行在不同特权级下的不同软件组成。
在特权级相关机制方面,本书正文中我们重点关心RISC-V的 S/U 特权级, M 特权级的机制细节则是作为可选内容在 附录 C:深入机器模式:RustSBI 中讲解,有兴趣的读者可以参考。
执行环境的另一种功能是对上层软件的执行进行监控管理。监控管理可以理解为,当上层软件执行的时候出现了一些情况导致需要用到执行环境中提供的功能, 因此需要暂停上层软件的执行,转而运行执行环境的代码。由于上层软件和执行环境被设计为运行在不同的特权级,这个过程也往往(而 不一定 ) 伴随着 CPU 的 特权级切换 。当执行环境的代码运行结束后,我们需要回到上层软件暂停的位置继续执行。在 RISC-V 架构中,这种与常规控制流 (顺序、循环、分支、函数调用)不同的 异常控制流 (ECF, Exception Control Flow) 被称为 异常(Exception) 。
用户态应用直接触发从用户态到内核态的 异常控制流 的原因总体上可以分为两种:执行 Trap类异常
指令和执行了会产生 Fault类异常
的指令 。Trap类异常
指令
就是指用户态软件为获得内核态操作系统的服务功能而发出的特殊指令。 Fault类
的指令是指用户态软件执行了在内核态操作系统看来是非法操作的指令。下表中我们给出了 RISC-V 特权级定义的会导致从低特权级到高特权级的各种 异常:
Interrupt |
Exception Code |
Description |
---|---|---|
0 |
0 |
Instruction address misaligned |
0 |
1 |
Instruction access fault |
0 |
2 |
Illegal instruction |
0 |
3 |
Breakpoint |
0 |
4 |
Load address misaligned |
0 |
5 |
Load access fault |
0 |
6 |
Store/AMO address misaligned |
0 |
7 |
Store/AMO access fault |
0 |
8 |
Environment call from U-mode |
0 |
9 |
Environment call from S-mode |
0 |
11 |
Environment call from M-mode |
0 |
12 |
Instruction page fault |
0 |
13 |
Load page fault |
0 |
15 |
Store/AMO page fault |
其中断点(Breakpoint) 和 执行环境调用 (Environment call) 两个异常(为了与其他非有意为之的异常区分,会把这种有意为之的指令称为 陷入
或
trap
类指令)是通过在上层软件中执行一条特定的指令触发的:当执行 ebreak
这条指令的之后就会触发断点陷入异常;而执行 ecall
这条指令的时候则会随着 CPU 当前所处特权级而触发不同的 陷入
情况。从表中可以看出,当 CPU 分别
处于 M/S/U 三种特权级时执行 ecall
这条指令会触发三种陷入。
在这里我们需要说明一下执行环境调用 ecall
,这是一种很特殊的会产生 陷入
的指令, 上图 中相邻两特权级软件之间的接口正是基于这种陷入
机制实现的。M 模式软件 SEE 和 S 模式的内核之间的接口被称为 监督模式二进制接口 (Supervisor Binary Interface, SBI),而内核和
U 模式的应用程序之间的接口被称为 应用程序二进制接口 (Application Binary Interface, ABI),当然它有一个更加通俗的名字—— 系统调用
(syscall, System Call) 。而之所以叫做二进制接口,是因为它和在同一种编程语言内部调用接口不同,是汇编指令级的一种接口。事实上 M/S/U
三个特权级的软件可能分别由不同的编程语言实现,即使是用同一种编程语言实现的,其调用也并不是普通的函数调用执行流,而是**陷入异常控制流** ,在该过程中会
切换 CPU 特权级。因此只有将接口下降到汇编指令级才能够满足其通用性和灵活性。
可以看到,在这样的架构之下,每层特权级的软件都只能做高特权级软件允许它做的、且不会产生什么撼动高特权级软件的事情,一旦低特权级软件的要求超出了其能力范围, 就必须寻求高特权级软件的帮助。因此,在一条执行流中我们经常能够看到特权级切换。如下图所示:
其他的异常则一般是在执行某一条指令的时候发生了某种错误(如除零、无效地址访问、无效指令等),或处理器认为处于当前特权级下执行当前指令是高特权级指令或会访问不应该访问的高特权级的资源(可能危害系统)。碰到这些情况,就需要需要将控制转交给高特权级的软件(如操作系统)来处理。当处理错误恢复后,则可重新回到低优先级软件去执行;如果不能回复错误,那高特权级软件可以杀死和清除低特权级软件,免破坏整个执行环境。
RISC-V的特权指令¶
与特权级无关的一般的指令和通用寄存器 x0~x31
在任何特权级都可以任意执行。而每个特权级都对应一些特殊指令和 控制状态寄存器 (CSR, Control and Status Register) ,来控制该特权级的某些行为并描述其状态。当然特权指令不只是具有有读写 CSR 的指令,还有其他功能的特权指令。
如果低优先级下的处理器执行了高优先级的指令,会产生非法指令错误的异常,于是位于高特权级的执行环境能够得知低优先级的软件出现了该错误,这个错误一般是不可恢复的,此时一般它会将上层的低特权级软件终止。这在某种程度上体现了特权级保护机制的作用。
在RISC-V中,会有两类低优先级U模式下运行高优先级S模式的指令:
指令本身属于高特权级的指令,如
sret
指令(表示从S模式返回到U模式)。指令访问了 S模式特权级下才能访问的寄存器 或内存,如表示S模式系统状态的 控制状态寄存器
sstatus
等。
指令 |
含义 |
---|---|
sret |
从S模式返回U模式。在U模式下执行会产生非法指令异常 |
wfi |
处理器在空闲时进入低功耗状态等待中断。在U模式下执行会尝试非法指令异常 |
sfence.vma |
刷新TLB缓存。在U模式下执行会尝试非法指令异常 |
访问S模式CSR的指令 |
通过访问 sepc/stvec/scause/sscartch/stval/sstatus/satp等CSR 来改变系统状态。在U模式下执行会尝试非法指令异常 |
在下一节中,我们将看到 在U模式下的用户态应用程序 ,如果执行上述S模式特权指令指令,将会产生非法指令异常,从而看出RISC-V的特权模式设计在一定程度上提供了对操作系统的保护。