特权级机制#

本节导读#

为了保护我们的批处理操作系统不受到出错应用程序的影响并全程稳定工作,单凭软件实现是很难做到的,而是需要 CPU 提供一种特权级隔离机制,使 CPU 在执行应用程序和操作系统内核的指令时处于不同的特权级。本节主要介绍了特权级机制的软硬件设计思路,以及 RISC-V 的特权级架构,包括特权指令的描述。

特权级的软硬件协同设计#

实现特权级机制的根本原因是应用程序运行的安全性不可充分信任。在上一章里,操作系统以库的形式和应用紧密连接在一起,构成一个整体来执行。随着应用需求的增加,操作系统的体积也越来越大;同时应用自身也会越来越复杂。由于操作系统会被频繁访问,来给多个应用提供服务,所以它可能的错误会比较快地被发现。但应用自身的错误可能就不会很快发现。由于二者通过编译器形成一个单一执行程序来执行,导致即使是应用程序本身的问题,也会让操作系统受到连累,从而可能导致整个计算机系统都不可用了。

所以,计算机科学家和工程师就想到一个方法,让相对安全可靠的操作系统运行在一个硬件保护的安全执行环境中,不受到应用程序的破坏;而让应用程序运行在另外一个无法破坏操作系统的受限执行环境中。

为确保操作系统的安全,对应用程序而言,需要限制的主要有两个方面:

  • 应用程序不能访问任意的地址空间(这个在第四章会进一步讲解,本章不会涉及)

  • 应用程序不能执行某些可能破坏计算机系统的指令(本章的重点)

假设有了这样的限制,我们还需要确保应用程序能够得到操作系统的服务,即应用程序和操作系统还需要有交互的手段。使得低特权级软件只能做高特权级软件允许它做的,且超出低特权级软件能力的功能必须寻求高特权级软件的帮助。这样,高特权级软件(操作系统)就成为低特权级软件(一般应用)的软件执行环境的重要组成部分。

为了实现这样的特权级机制,需要进行软硬件协同设计。一个比较简洁的方法就是,处理器设置两个不同安全等级的执行环境:用户态特权级的执行环境和内核态特权级的执行环境。且明确指出可能破坏计算机系统的内核态特权级指令子集,规定内核态特权级指令子集中的指令只能在内核态特权级的执行环境中执行。处理器在执行指令前会进行特权级安全检查,如果在用户态执行环境中执行这些内核态特权级指令,会产生异常。

为了让应用程序获得操作系统的函数服务,采用传统的函数调用方式(即通常的 callret 指令或指令组合)将会直接绕过硬件的特权级保护检查。为了解决这个问题, RISC-V 提供了新的机器指令:执行环境调用指令(Execution Environment Call,简称 ecall )和一类执行环境返回(Execution Environment Return,简称 eret )指令。其中:

  • ecall 具有用户态到内核态的执行环境切换能力的函数调用指令;

  • sret :具有内核态到用户态的执行环境切换能力的函数返回指令。

注解

sret 与 eret 的联系与区别

eret 代表一类执行环境返回指令,而 sret 特指从 Supervisor 模式的执行环境(即 OS 内核)返回的那条指令,也是本书中主要用到的指令。除了 sret 之外, mret 也属于执行环境返回指令,当从 Machine 模式的执行环境返回时使用, RustSBI 会用到这条指令。

硬件具有了这样的机制后,还需要操作系统的配合才能最终完成对操作系统自身的保护。首先,操作系统需要提供相应的功能代码,能在执行 sret 前准备和恢复用户态执行应用程序的上下文。其次,在应用程序调用 ecall 指令后,能够检查应用程序的系统调用参数,确保参数不会破坏操作系统。

注解

一般来说, ecall 这条指令和 eret 这类指令分别可以用来让 CPU 从当前特权级切换到比当前高一级的特权级和切换到不高于当前的特权级,因此上面提到的两条指令的功能仅是其中一种用法。在本书中,大多数情况我们只需考虑这种用法即可。

读者可能会好奇一共有多少种不同的特权级,在不同的指令集体系结构中特权级的数量也是不同的。x86 和 RISC-V 设计了多达 4 种特权级,而对于一般的操作系统而言,其实只要两种特权级就够了。

RISC-V 特权级架构#

RISC-V 架构中一共定义了 4 种特权级:

RISC-V 特权级#

级别

编码

名称

0

00

用户/应用模式 (U, User/Application)

1

01

监督模式 (S, Supervisor)

2

10

虚拟监督模式 (H, Hypervisor)

3

11

机器模式 (M, Machine)

其中,级别的数值越大,特权级越高,掌控硬件的能力越强。从表中可以看出, M 模式处在最高的特权级,而 U 模式处于最低的特权级。在CPU硬件层面,除了M模式必须存在外,其它模式可以不存在。

之前我们给出过支持应用程序运行的一套 执行环境栈 ,现在我们站在特权级架构的角度去重新看待它:

../_images/PrivilegeStack.png

和之前一样,白色块表示一层执行环境,黑色块表示相邻两层执行环境之间的接口。这张图片给出了能够支持运行 Unix 这类复杂系统的软件栈。其中操作系统内核代码运行在 S 模式上;应用程序运行在 U 模式上。运行在 M 模式上的软件被称为 监督模式执行环境 (SEE, Supervisor Execution Environment),如在操作系统运行前负责加载操作系统的 Bootloader – RustSBI。站在运行在 S 模式上的软件视角来看,它的下面也需要一层执行环境支撑,因此被命名为 SEE,它需要在相比 S 模式更高的特权级下运行,一般情况下 SEE 在 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) ,是 RISC-V 语境下的 Trap 种类之一。

用户态应用直接触发从用户态到内核态的异常的原因总体上可以分为两种:其一是用户态软件为获得内核态操作系统的服务功能而执行特殊指令;其二是在执行某条指令期间产生了错误(如执行了用户态不允许执行的指令或者其他错误)并被 CPU 检测到。下表中我们给出了 RISC-V 特权级规范定义的会可能导致从低特权级到高特权级的各种 异常

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 这条指令会触发三种异常(分别参考上表 Exception Code 为 11/9/8 对应的行)。

在这里我们需要说明一下执行环境调用 ecall ,这是一种很特殊的 陷入 类的指令, 上图 中相邻两特权级软件之间的接口正是基于这种陷入机制实现的。M 模式软件 SEE 和 S 模式的内核之间的接口被称为 监督模式二进制接口 (Supervisor Binary Interface, SBI),而内核和 U 模式的应用程序之间的接口被称为 应用程序二进制接口 (Application Binary Interface, ABI),当然它有一个更加通俗的名字—— 系统调用 (syscall, System Call) 。而之所以叫做二进制接口,是因为它与高级编程语言的内部调用接口不同,是机器/汇编指令级的一种接口。事实上 M/S/U 三个特权级的软件可分别由不同的编程语言实现,即使是用同一种编程语言实现的,其调用也并不是普通的函数调用控制流,而是 陷入异常控制流 ,在该过程中会切换 CPU 特权级。因此只有将接口下降到机器/汇编指令级才能够满足其跨高级语言的通用性和灵活性。

可以看到,在这样的架构之下,每层特权级的软件都只能做高特权级软件允许它做的、且不会产生什么撼动高特权级软件的事情,一旦低特权级软件的要求超出了其能力范围,就必须寻求高特权级软件的帮助,否则就是一种异常行为了。因此,在软件(应用、操作系统等)执行过程中我们经常能够看到特权级切换。如下图所示:

../_images/EnvironmentCallFlow.png

其他的异常则一般是在执行某一条指令的时候发生了某种错误(如除零、无效地址访问、无效指令等),或处理器认为处于当前特权级下执行的当前指令是高特权级指令或会访问不应该访问的高特权级的资源(可能危害系统)。碰到这些情况,就需要将控制转交给高特权级的软件(如操作系统)来处理。当错误/异常恢复后,则可重新回到低优先级软件去执行;如果不能恢复错误/异常,那高特权级软件可以杀死和清除低特权级软件,避免破坏整个执行环境。

RISC-V的特权指令#

与特权级无关的一般的指令和通用寄存器 x0 ~ x31 在任何特权级都可以执行。而每个特权级都对应一些特殊指令和 控制状态寄存器 (CSR, Control and Status Register) ,来控制该特权级的某些行为并描述其状态。当然特权指令不仅具有读写 CSR 的指令,还有其他功能的特权指令。

如果处于低特权级状态的处理器执行了高特权级的指令,会产生非法指令错误的异常。这样,位于高特权级的执行环境能够得知低特权级的软件出现了错误,这个错误一般是不可恢复的,此时执行环境会将低特权级的软件终止。这在某种程度上体现了特权级保护机制的作用。

在 RISC-V 中,会有两类属于高特权级 S 模式的特权指令:

  • 指令本身属于高特权级的指令,如 sret 指令(表示从 S 模式返回到 U 模式)。

  • 指令访问了 S模式特权级下才能访问的寄存器 或内存,如表示S模式系统状态的 控制状态寄存器 sstatus 等。

RISC-V S模式特权指令#

指令

含义

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 的特权模式提供了对操作系统一定程度的保护。