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 分钟前
Linux_T(Sticky Bit)粘滞位详解
linux·运维·服务器
HHBon28 分钟前
判断用户输入昵称是否存在(Python)
linux·开发语言·python
Paper_Love31 分钟前
Linux-pcie ranges介绍
linux
DjangoJason1 小时前
计算机网络 : 应用层自定义协议与序列化
linux·服务器·计算机网络
小杜-coding3 小时前
天机学堂(初始项目)
java·linux·运维·服务器·spring boot·spring·spring cloud
陈苏同学3 小时前
在 Linux 服务器上无需 sudo 权限解压/打包 .7z 的方法(实用命令)
linux·运维·服务器
我不是帅戈4 小时前
QT入门学习(二)---继承关系、访问控制和变量定义
linux·qt·ui
奉系坤阀5 小时前
Ubuntu终端性能监视工具
linux·运维·服务器·python·ubuntu
alive9035 小时前
FFmpeg移植教程(linux平台)
linux·ubuntu·ffmpeg·ffmpeg移植
一张假钞6 小时前
Linux 系统 Docker Compose 安装
linux·运维·docker