如第5节所讨论的,许多计算机系统被组织成用户软件和系统软件两层。系统软件(例如,操作系统内核和设备驱动程序)是负责保护和管理整个系统的软件,包括与外设交互以执行输入和输出操作以及加载、调度、执行用户应用程序。用户软件通常仅限于对(普通)寄存器和主存上的数据执行操作。每当用户软件需要执行需要与系统其他部分交互的操作时,例如从文件中读取数据或在计算机显示器上显示信息,它就调用系统软件来替它执行该过程。
本章讨论保护系统免受错误或恶意用户程序侵害的硬件机制,以及如何编程实现这些机制。
ISA定义了软件可以用来执行计算的资源集。例如它定义了指令、指令的行为和操作数,以及寄存器。
特权级别(privilege level )定义了软件可以访问ISA中定义哪些的资源(寄存器、指令等)。它们可以用来限制软件的执行,并保护系统免受试图执行不允许的操作的软件的攻击。例如,可以将系统配置为以受限制的特权级别执行用户应用程序,以防止它们直接与外设交互或访问特殊的寄存器。RISC-V的ISA定义了三种特权级别:
- U:User/Application (译注:后面简称为U模式)
- S:Supervisor (译注:后面简称为S模式)
- M:Machine(译注:后面简称为M模式)
Machine特权级别具有最高的特权,允许对硬件进行完全访问。Supervisor权限级别具有第二高的权限,User/Application权限级别具有最低的权限。
实现RISC-V硬件时,可以实现这些特权级别的部分或全部。例如,当为紧凑和直接的嵌入式系统实现硬件时,可能只需要Machine特权级别。而为依赖于操作系统来管理应用程序(例如,桌面计算机系统)的系统实现硬件时,通常包括所有三个特权级别,以便于操作系统的实现。
RISC-V特权模式(privilege mode)定义了当前正在执行的软件的特权级别。例如,当Machine特权模式处于活跃状态时,当前执行的软件具有Machine特权级别,因此具有对硬件的完全访问权限。
非特权模式(unprivileged mode )是特权最低的模式。在RISCV中,无特权模式是User/Application特权模式,也称为用户模式或U模式。非特权ISA是在非特权模式下运行的软件可以访问的ISA子集。
为了简化讨论,本章剩下的部分将集中讨论只有U模式和Machine模式的RISC-V处理器。
U模式限制了当前执行的软件可以访问的资源;因此,为了保护系统不受错误或恶意用户程序的影响,系统软件通常在执行(或返回控制)用户代码之前,将特权模式设置为U模式。
通常采取以下措施来保护系统:
- **初始化系统:**上电后,硬件自动将特权模式设置为Machine模式,并开始执行boot code(它的功能是引导系统的启动,即对硬件系统进行初始化,并加载OS的代码)。boot code将OS加载到内存中,并在Machine模式下调用OS中的始化代码来配置整个系统。
- **执行用户代码:**一旦设置完毕,OS就可以将用户程序加载到主存中执行。但是,在转移控制以执行用户代码之前,它将特权模式设置为U模式。
- **处理非法操作:**如果用户软件试图执行特权操作,例如与外设交互,硬件将停止执行用户代码并调用OS的来处理非法操作。将在11.3节讨论硬件如何通过异常处理机制将控制权转移到OS。
- **调用OS(的功能):**如果用户程序需要执行一个敏感的过程,例如向外设输出信息,它必须请求OS来为它执行此操作。当将控制权转移到OS时,硬件必须将特权模式改为Supervisor模式或Machine模式,这样OS才能以适当的特权执行操作。为此,ISA通常包含一种称为软中断(software interrupt)的机制,允许在非特权模式下切换到特权模式,并调用OS的代码。这种机制使得用户代码不能改变特权模式并执行自己的代码。一旦操作系统为用户程序完成相关操作,它将特权模式更改回U模式,并返回到用户程序。
- **调用操作系统(的功能):**如果用户程序需要执行一个敏感的操作,例如向外设输出,它必须调用操作系统来为它实行该操作。
- **处理外部中断:**当外部中断发生时,硬件将特权模式设置为Machine模式,因此ISR有足够的特权来处理中断。请注意,ISR属于系统程序。
异常是指CPU在执行指令时,因出现异常情况而产生的事件。例如,试图执行非法指令是导致RISC-V CPU产生异常的一个原因。
异常通常触发异常处理机制(exception handling mechanism,一种处理异常的流程,下面简称为EHM),以便在CPU继续执行程序之前处理异常情况。EHM通常会导致CPU将执行流重定向到一个系统例程,该系统例程会:
- 保存当前执行的程序的上下文
- 处理异常情况
- 恢复步骤1保存的上下文,继续执行原来的程序
请注意,异常处理流程与硬件的中断处理流程非常相似。实际上,RISC-V CPU使用相同的机制来处理中断和异常,即,它保存当前上下文的一部分(例如,pc
寄存器),设置mcause
寄存器,并将执行流重定向到ISR。正如第10.3.4节所讨论的,ISR可以通过检查mcause
寄存器来区分中断和异常。具体来说就是通过检查mcause.INTERRUPT
来发现处理的是异常还是中断。另外,mcause.EXCCODE
子字段表示中断或异常的来源。在RISC-V上异常和中断有几个可能的来源。表11.1给出了中断和异常的来源,以及它们在mcause
中对应的代码值。
EHM通常用于保护系统不受非法用户代码操作的影响。在这种情况下,硬件被OS配置,一旦满足以下条件就会产生异常:
- 硬件处于U模式
- CPU试图执行某些特权操作,例如访问映射到外设的地址,或访问只能在Machine模式下访问的CSR
在发生异常时,OS中的ISR会被调用,它可以决定如何处理用户程序。
注:异常是因为CPU执行指令而发生,因此它们是同步事件。而中断可能在任何时间发生,与CPU的执行周期无关。因此它们是异步事件。
软中断(Software interrupts)是CPU在执行特殊指令时产生的事件。例如,在RISC-V中,环境调用(ecall
)和断点(break
)指令使得CPU在执行时产生软中断。它们类似于异常,因为它们是由于执行指令而发生的同步事件。尽管如此,异常只在异常情况下产生,而软中断总是在CPU执行这些特殊指令时产生。
软中断通常会触发某个机制以改变特权模式,然后执行一个用于处理中断的例程。该机制允许用户程序调用需要运行在更高特权级别的系统程序。
大多数ISA采用相同的机制来处理中断、异常和软中断,例如它们都要保存当前执行程序的部分上下文(例如pc
寄存器),设置中断原因(例如设置mcause
寄存器),并将执行流重定向到ISR。
mcause.INTERRUPT |
mcause.EXCCODE |
原因 |
---|---|---|
1 | 0 | U模式下的软中断 |
1 | 1 | S模式下的软中断 |
1 | 2 | 保留 |
1 | 3 | M模式下的软中断 |
1 | 4 | U模式下的定时器中断 |
1 | 5 | S模式下的定时器中断 |
1 | 6 | 保留 |
1 | 7 | M模式下的定时器中断 |
1 | 8 | U模式下的外部中断 |
1 | 9 | S模式下的外部中断 |
1 | 10 | 保留 |
1 | 11 | M模式下的外部中断 |
1 | 12~15 | Reserved for future standard use |
1 | ≥16 | Reserved for platform use |
mcause.INTERRUPT |
mcause.EXCCODE |
原因 |
---|---|---|
0 | 0 | 指令地址未对齐 |
0 | 11 | 指令访问错误 |
0 | 2 | 非法指令 |
0 | 3 | 断点 |
0 | 4 | load操作访问的地址未对齐 |
0 | 5 | load访问出错 |
0 | 6 | store/AMO 访问的地址未对齐 |
0 | 7 | store/AMO 访问出错 |
0 | 8 | U模式下的ecall出错 |
0 | 9 | S模式下的ecall出错 |
0 | 10 | 保留 |
0 | 11 | M模式下的ecall出错 |
0 | 12 | 指令缺页异常 |
0 | 13 | Load操作缺页异常 |
0 | 14 | 保留 |
0 | 15 | Store/AMO操作缺页异常 |
0 | 16~23 | 保留 |
0 | 24~31 | 保留 |
0 | 32~47 | 保留 |
0 | 48~63 | 保留 |
0 | ≥64 | 保留 |
表11.1,异常和中断的源,以及它们在
mcause
寄存器中的值。第1个表全是中断,第2个表全是异常
下面将讨论如何使用RISC-V的各种特权模式,以及异常和中断处理机制,来保护系统免受错误或恶意用户程序的影响。
RISC-V CPU将当前特权模式存储在程序不能直接访问的内部存储设备上。换句话说,程序不能直接检查或修改此存储设备,以获取或改变当前特权模式。检查当前特权模式的唯一方法是生成一个程序中断或异常,它将特权模式的代码复制到mstatus.MPP
(Machine Previous Privilege)寄存器中。另外,设置当前特权模式的唯一方法是修改mstatus.mpp
的值然后执行mret
指令,该指令会使用mstatus.mpp
的值去设置当前的特权模式。
下面的代码显示了系统如何将特权模式更改U模式然后调用用户程序。
- 首先,将
mstatus.MPP
的值改为00
,即U模式。 - 然后,修改
mepc
寄存器,将U模式下的程序入口点加载其中。 - 最后,执行
mret
指令,该指令使用mstatus.MPP
的值更改特权模式,使用mpec
的值修改pc
寄存器。
# 切换到U模式
csrr t1, mstatus # 读取mstatus.MPP的值到t1
li t2, ~0x1800 # 掩码,用于修改mstatus.MPP位于第11~12位
and t1, t1, t2 # 修改位于t1中mstatus.MPP的值
csrw mstatus, t1 # 回写到mstatus中
la t0, user_main # 加载用户的程序地址到t0
csrw mepc, t0 # 将t0复制到mepc中
mret # 执行该指令,使得:PC<=MEPC; mode<=MPP;
RISC-V CPU使用类似的机制来处理中断、异常和软中断,也就是说,它保存当前上下文的一部分(例如pc
寄存器),设置mcause
等其他CSR,然后将执行流重定向到ISR。因此,配置异常/软中断的处理机制,与10.3.5节讨论中讨论过的,配置外部中断的处理机制非常类似。
系统通过注册异常/软中断的服务例程来初始化异常/软中断的处理流程。这与注册ISR处理外部中断的方式相同。
- 在direct模式下,需要注册一个例程,该例程负责检查
mcause
寄存器以识别事件源并调用适当的例程。 - 而在vector模式,外部中断、异常和软中断处理例程必须在中断向量化表上注册。10.3.5节给出了在direct模式和vector模式下配置中断处理机制的代码片段。
在RISC-V上,必须通过设置mstatus
和mie
这两个CSR来启用外部中断。而异常和软中断则总是启用的,不需要额外的配置。
当指令试图执行非法操作时,RISC-V CPU会产生异常,例如执行CPU无法识别的指令。
包含U模式和M模式的RISC-V系统通常包括一个内存保护单元。 可以配置该单元,使得CPU试图从特定地址读写数据/取指执行时产生异常。操作系统可以通过配置这个单元来保护系统,当以U模式执行的代码试图访问受保护的地址时(例如映射到外设的地址或OS专属的地址时)该单元就会产生异常。此时,如果CPU试图从受保护的地址读/写数据,内存保护单元产生 Load访问出错 或者 Store/AMO访问出错 (此时mstatus.EXCCODE
的值分别为5/7,如表11.1所示)。同样,如果CPU试图从受保护的地址取指令执行,会产生一个 指令访问错误 异常(此时 mstatus.EXCCODE
= 1)。
当一个异常产生的时候,RISC-V CPU会执行以下操作:
- 保存当前
pc
寄存器的值到mepc
寄存器 - 设置
mcause
的值,该值能够分辨出异常的类型 - 将当前特权模式的值复制到
mstatus.MPP
- 将当前特权模式切换到M模式
- 设置
pc
寄存器,将执行流重定向到异常处理例程
一些异常也会设置mtval
寄存器,以额外的信息。例如,当load或store操作异常发生时,mtval
会被设置为异常产生时访问的虚拟地址。根据异常的不同,修复导致异常的问题,并让产生异常的程序继续执行可能是有意义的。缺页异常是系统可以处理的异常,因此产生异常的程序可以继续执行。在这些情况下,处理异常通常类似于处理外部中断,即异常处理例程必须保存上下文,处理异常,最后恢复上下文,以便(之前)在CPU上运行的程序可以继续执行。如果异常是因为非常操作产生,并且无法从错误中恢复时,OS可能会终结这个试图执行非法操作的进程。
如6.7.4节所述,在RISC-V中,用户代码可以通过执行环境调用(ecall
)指令来调用OS的功能,即执行一个系统调用。该指令产生一个触发异常/中断处理流程的软中断。在U模式下执行ecall
指令时,硬件会将mcause.INTERRUPT
设置为0,将mcause.EXCCODE
设置为8。因此,如果中断/异常处理机制配置为direct模式,主ISR(如上所述,direct模式下只有一个主ISR负责处理中断)可以通过mcause
寄存器的值来判断用户程序请求系统调用。
当发生异常或软中断时,系统将pc
寄存器的值保存在mepc
中。在处理异常(例如缺页异常)之后,系统可能会返回之前导致异常的那一条指令,因此程序会尝试再次执行(相同的)操作。但在软中断时,系统不能返回到同一条指令。否则它将再次进行系统调用。在这种情况下,系统必须返回到(触发异常的指令)后续的指令。为此,软中断的ISR必须在执行mret
指令之前调整mepc
中的值,使其指向(触发软中断的指令的)下一条指令。
下面的代码展示了如何在执行mret
指令之前调整mepc
寄存器,使其指向下一条指令。
# 调整MEPC寄存器的值, 使得mret之后返回到ecall指令的下一条指令
csrr a1, mepc # 将mepc的值加载到a1
addi a1, a1, 4 # a1 = a1 + 4
csrw mepc, a1 # 将a1的值更新到mepc