RISC-V特权模式及切换

1 RISC-V特权模式基本概念

1.1 RISC-V特权模式介绍

RISC-V 指令集架构(ISA)采用多特权级别设计作为其核心安全机制,通过层次化的权限管理实现系统资源的隔离与保护。该架构明确定义了四个层次化的特权模式,按照权限等级由高至低依次为:

  • 机器模式(Machine Mode,M-mode) - 最高特权级别,负责硬件初始化和底层安全控制
  • 虚拟机监控模式(Hypervisor Mode,H-mode) - 可选模式,用于虚拟化扩展
  • 监管者模式(Supervisor Mode,S-mode) - 操作系统内核运行级别,管理系统资源
  • 用户模式(User Mode,U-mode) - 最低权限级别,仅允许用户程序运行

和ARM的EL0-EL3分级类似,RISC-V这种分级特权架构为系统设计提供了灵活的权限控制方案,各模式间通过严格的权限边界实现安全隔离,满足从嵌入式系统到云计算平台的不同安全需求。每个特权级别都配备专属的寄存器组和控制机制,确保执行环境的完整性和安全性。

  1. Machine Mode (M-mode)

    机器模式是RISC-V最高特权级别,可以访问所有内存区域(包括内存、外设和CSR寄存器),执行所有的特权指令和处理所有的中断和异常;一般会在系统启动BootLoader阶段使用。

  2. Hypervisor Mode (H-mode)

    虚拟机模式是RISC-V次高特权级别模式,依赖与硬件虚拟化扩展(H扩展),一般应用在服务器级别,日常实际接触并不多(后面基本不会涉及);该模式支持管理虚拟机的内存和资源和处理虚拟机的异常和中断。

  3. Supervisor Mode (S-mode)

    监督者模式是 中等特权级别的模式,该模式依赖与M-mode的支持,可以访问内核及硬件资源,处理系统调用的部分异常和中断,一般在运行操作系统内核(如Linux)、管理任务调度、管理内存映射(如页表)和控制用户程序等场景下使用。

  4. User Mode (U-mode)

    用户模式是最低特权级别的模式,正常情况下仅访问用户内存区域,允许运行用户程序,无法直接访问硬件或修改系统状态的,也是无法执行CSR特权指令;在触发异常时通过ecall切换到特权模式(如S-mode 或 M-mode)请求内核服务

1.2 常见的设计

常用的RISC-V芯片架构中,M 模式是权限最高的特权等级,也是 RISC-V Spec 中明确规定必须要实现的特权等级,其他三个特权等级是可选的,处理器设计厂商可根据产品的功能定位和成本效益进行选择性实现,在典型的嵌入式应用场景中,对于不需要运行Linux等复杂操作系统的微控制器,为优化功耗控制和芯片面积,往往仅实现Machine模式,或者选择性地实现Machine/User两种模式组,一般情况下:

  • 单片机(运行RTOS):通常支持M模式和U模式,或者只有M模式
  • 通用SOC(运行linux):通常支持M、S、U模式

在一个典型 Linux 系统中,用户态应用程序跑在 U 模式,内核跑在 S 模式,而 M 模式一般是 OpenSBI/U-Boot、Bootloader启动阶段在用。

另外:

RISC-V 手册中有提到过一个 Debug Mode,可以理解为比M模式权限更高的特权等级,用于支持芯片调试。关于这个模式不再这里介绍,想要了解,资料可参考riscv-debug-release.pdf

2 特权模式切换

对于嵌入式工程师来说,对Linux的操作系统更熟悉些,所以为了更好的理解,这里从Linux用户态、内核态角度,对比理解硬件的特权模式。

2.1 Linux操作系统和特权关系

一般来说用户程序都运行在用户态,当它们需要切换到内核态以获得更高权限时,需要向操作系统申请;而操作系统内核和设备驱动程序则默认就运行在内核态。

Linux操作系统的用户态和内核态就分别对应到RISC-V处理器的特权等级就是:用户态对应U-Mode,内核态对应S-Mode。

2.2 Linux用户态、内核态的切换

通常,用户程序只运行在用户态(User Mode),仅在以下三种情况下会切换到内核态(Kernel Mode)进行处理:

  • 系统调用:当应用程序需要访问操作系统服务(如文件读写、进程创建)时,通过 int 指令或 syscall 指令主动请求内核执行操作。
  • 异常:运行时若发生异常(如缺页异常、非法指令、除零错误等),CPU 会自动切换到内核态,由操作系统处理异常。
  • 外部中断:外设(如磁盘、网卡)完成任务后向 CPU 发送中断信号,CPU 根据中断向量表跳转到内核态处理事件。

这种机制通过限制用户程序的直接硬件访问权限,确保系统的安全性和稳定性,仅在必要时通过上述三种方式切换到内核态。

2.3 RISC-V特权模式切换

特权模式切换示意:

  1. 从高特权级别到低特权级别:

    • 通过异常返回指令(如MRET、SRET)返回到低特权级别
    • 处理器恢复之前保存的状态并切换到低特权级别
  2. 从低特权级别到高特权级别:

    • 通过异常(如系统调用、中断、页错误)或陷阱(trap)进入高特权级别
    • 处理器保存当前状态(如PC、寄存器)并切换到高特权级别

2.3.1 opensbi启动过程特权模式切换

以Linux启动过程中,opensbi阶段特权模式为例。

这里仅保留和切换相关的代码,一般opensbi阶段工作在M模式,完成初始化之后,将切到S模式。

c 复制代码
sbi_hart_switch_mode(unsigned long arg0, unsigned long arg1,
             unsigned long next_addr, unsigned long next_mode,
             bool next_virt)
{
    // 1. 配置mstatus寄存器:设置MSTATUS.MPP=S模式,关闭M模式中断
    unsigned long mstatus = csr_read(CSR_MSTATUS);
    mstatus = INSERT_FIELD(mstatus, MSTATUS_MPP, next_mode); // MSTATUS.MPP = S模式
    mstatus = INSERT_FIELD(mstatus, MSTATUS_MPIE, 0);       // 禁用M模式中断
    csr_write(CSR_MSTATUS, mstatus);

    // 2. 设置mepc为S模式代码入口地址
    csr_write(CSR_MEPC, next_addr);

    // 3. 设置S模式相关寄存器
    csr_write(CSR_STVEC, next_addr);  // 设置S模式异常入口
    csr_write(CSR_SSCRATCH, 0);       // 清零S模式临时寄存器
    csr_write(CSR_SIE, 0);                             //关闭S模式中断使能
    csr_write(CSR_SATP, 0);                          //禁用S模式的分页机制

    // 4. 向a0/a1传递参数(例如设备树地址或启动参数)
    register unsigned long a0 asm("a0") = arg0;
    register unsigned long a1 asm("a1") = arg1;

    // 5. 执行mret切换到S模式,跳转到next_addr
    __asm__ __volatile__("mret" : : "r"(a0), "r"(a1));
    __builtin_unreachable();
}

这里再来简单看下流程

  1. 配置 mstatus:
    • 将 mstatus.MPP 设置为 PRV_S,表示 mret 后切换到S模式。
    • 关闭M模式中断(mstatus.MPIE = 0),确保切换时中断处于禁用状态。
  2. 设置 mepc:
    • 将目标S模式代码的入口地址写入 mepc,mret 后CPU跳转至此地址。
  3. 设置S模式相关寄存器
    • 设置S模式异常入口CSR_STVEC
    • 清空S模式临时寄存器
    • 关闭S模式中断使能(防止内核启动时被打断)
    • 禁用S模式分页机制
  4. 执行 mret:
    • CPU从M模式退出,特权级降为S模式。
    • 跳转到 mepc 指定的地址执行代码。
    • 通过 a0 和 a1 寄存器传递启动参数。

另外,Linux内核特权模式切换,在内核起来之后,就会拉起用户态程序,这是就需要把特权模式从S模式切换到U模式。

Linux内核支持多种架构,代码相对晦涩,riscv特权模式切换代码位于arch/riscv/kernel/entry.S,这里不再粘贴,(和M切S模式类似 ,感兴趣自行学习)

2.3.2 中断、异常、系统调用情况特权模式切换

前面2.2介绍了linux用户态(U-mode)如何进入内核态(M-mode),一般来说常见的就是陷入内核态,此时用户态程序在被异常、中断、系统调用等,将暂停当前的任务,切换到高特权级别。

  1. 中断/异常:
    中断本省就可以看做异常的一种,在触发后行为也比较类似,特权模式切换是被动的,这里放到一起。

切换流程:

  • 硬件行为:

    • 暂停当前指令,保存当前PC 到mepc/sepc(中断触发导致会禁用中断(mstatus.MIE/sstatus.SIE 清零))
    • 异常原因存入mcause/scause
    • 当前特权级存入mstatus 的MPP/SPP 字段
    • 根据中断或者异常类型跳转到mtvec(M 模式异常入口)或 stvec(S 模式异常入口)
  • 软件行为:

    • 保存寄存器上下文(手动)
    • 读取 mcause/scause 判断中断或者异常类型
    • 执行中断异常服务例程(如处理定时器、响应外设 / 终止进程、修复缺页)
    • 恢复上下文,执行 mret/sret 返回

具体的流程可以参考: ECLIC中断流程及实际应用 ------ RISC-V中断机制(二)

  1. 系统调用流程
    系统调用是软件通过执行ecall指令主动请求进入搞特权级别模式,
  • 硬件行为:

    • 同步触发异常,保存 pc 到 mepc/sepc(指向 ecall 的下一条指令)
    • 设置 mcause/scause 为 ECALL_FROM_U_MODE(code:8)
    • 切换特权模式(通常到 S-mode)
    • 跳转到 stvec 指定的系统调用处理程序
  • 软件处理:

    • 保存上下文。
    • 从 a7 读取系统调用号,a0-a6 读取参数
    • 执行内核服务(如文件读写)
    • 将结果写入 a0,恢复上下文
    • 执行 sret 返回用户态

我们以 Linux 系统 sys_open 系统调用为例,我们看一下用户态程序(特权等级 0, U 模式)是怎么陷入到 Linux 内核(特权等级 1, S 模式)中执行系统调用的。

c 复制代码
   22482:	eb8d               	bnez	a5,224b4 <__libc_open+0x64>
   22484:	03800893          	li	a7,56
   22488:	f9c00513          	li	a0,-100
   2248c:	8622               	mv	a2,s0
   2248e:	00000073           	ecall

陷入到内核态后,处理器从 STVEC 寄存器加载异常处理程序入口。在 Linux 内核初始化过程中 (arch/riscv/kernel/head.S),就已经通过 CSR 指令设置好了 STVEC 寄存器,指向 handle_exception 函数:

c 复制代码
setup_trap_vector:
	/* Set trap vector to exception handler */
	la a0, handle_exception
	csrw CSR_TVEC, a0
              /*
	 * Set sup0 scratch register to 0, indicating to exception vector that
	 * we are presently executing in kernel.
	 */
	csrw CSR_SCRATCH, zero
	ret

handle_exception 最终会跳转到 handle_syscall,然后从 a7 寄存器中拿到系统调用编号,从 sys_call_table 中索引到最终系统调用处理函数 (arch/riscv/kernel/entry.S):

c 复制代码
/* Check to make sure we don't jump to a bogus syscall number. */
	li t0, __NR_syscalls
	la s0, sys_ni_syscall
	/*
	 * Syscall number held in a7.
	 * If syscall number is above allowed value, redirect to ni_syscall.
	 */
	bgeu a7, t0, 1f
	/* Call syscall */
	la s0, sys_call_table
	slli t0, a7, RISCV_LGPTR
	add s0, s0, t0
	REG_L s0, 0(s0)
1:
	jalr s0

参考:
riscv-privileged-20240411.pdf
RISC-V特权模式与寄存器
RISC-V特权等级与Linux内核的启动

相关推荐
眠修24 分钟前
Kuberrnetes 服务发布
linux·运维·服务器
即将头秃的程序媛3 小时前
centos 7.9安装tomcat,并实现开机自启
linux·运维·centos
fangeqin3 小时前
ubuntu源码安装python3.13遇到Could not build the ssl module!解决方法
linux·python·ubuntu·openssl
爱奥尼欧5 小时前
【Linux 系统】基础IO——Linux中对文件的理解
linux·服务器·microsoft
超喜欢下雨天5 小时前
服务器安装 ros2时遇到底层库依赖冲突的问题
linux·运维·服务器·ros2
tan77º6 小时前
【Linux网络编程】网络基础
linux·服务器·网络
笑衬人心。7 小时前
Ubuntu 22.04 + MySQL 8 无密码登录问题与 root 密码重置指南
linux·mysql·ubuntu
chanalbert8 小时前
CentOS系统新手指导手册
linux·运维·centos
星宸追风9 小时前
Ubuntu更换Home目录所在硬盘的过程
linux·运维·ubuntu
热爱生活的猴子9 小时前
Poetry 在 Linux 和 Windows 系统中的安装步骤
linux·运维·windows