3. RISC-V 特权架构与启动流程:机器模式(M-mode)与监督模式(S-mode)

RISC-V 特权架构定义了三种特权级别(Privilege Levels),用于隔离操作系统内核与用户应用程序。xv6 和 Linux 内核主要运行在监督模式(S-mode),而启动过程则依赖机器模式(M-mode)完成硬件初始化。理解这两种模式的区别、异常处理机制以及启动流程,是深入内核的基础。

3.1 特权级别与模式切换

RISC-V 规范定义了三个特权级别,从高到低依次为:机器模式(M-mode)、监督模式(S-mode)和用户模式(U-mode)。每个模式拥有独立的控制状态寄存器(CSR)和权限集。

特权级别 编码 名称 主要用途 可访问的 CSR
M 11 机器模式 固件、Bootloader、安全监控 所有 CSR(包括 mstatus、mtvec、mepc 等)
S 01 监督模式 操作系统内核(xv6、Linux) sepc、stvec、scause、satp 等
U 00 用户模式 用户应用程序 仅限部分通用 CSR
关键概念: 机器模式是 RISC-V 的“上帝模式”,拥有最高权限,可以访问所有硬件资源。监督模式是操作系统内核运行的特权模式,负责管理虚拟内存、进程调度和中断处理。用户模式则限制应用程序的权限,防止其直接访问硬件或破坏系统。

模式切换通过异常(Exception)和中断(Interrupt)机制实现。当低特权级代码(如 U-mode)触发异常时,处理器会提升到更高特权级(如 S-mode 或 M-mode)进行处理。xv6 的 ecall 指令就是用户程序进入内核的典型入口。

3.2 RISC-V 异常与中断向量表

RISC-V 的异常和中断处理依赖于两个关键 CSR:mtvec(机器模式陷阱向量基址寄存器)和 stvec(监督模式陷阱向量基址寄存器)。它们定义了异常处理程序的入口地址和向量表模式。

3.2.1 向量表模式

RISC-V 支持两种向量表模式:

  • 直接模式(Direct Mode): 所有异常和中断都跳转到同一个入口地址(BASE 字段)。处理程序通过读取 mcause 或 scause 寄存器区分异常类型。
  • 向量模式(Vectored Mode): 根据异常类型,跳转到 BASE + 4 × cause_code 的地址。每个中断源有独立的处理入口,适合低延迟中断处理。

xv6 使用直接模式,将所有异常统一导向 stvec 指向的 trapvec 函数。Linux 内核则根据配置选择向量模式以优化中断响应。

3.2.2 异常与中断分类

异常类型 cause 编码 描述 处理模式
指令未对齐 0 取指地址未按 2/4 字节对齐 S/M
指令访问错误 1 取指时发生物理内存错误 S/M
断点 3 ebreak 指令触发 S/M
环境调用(U-mode) 8 ecall 从 U-mode 发出 S
环境调用(S-mode) 9 ecall 从 S-mode 发出 M
指令页错误 12 取指时虚拟地址未映射 S
加载页错误 13 加载指令时虚拟地址未映射 S
存储页错误 15 存储指令时虚拟地址未映射 S
注意: 在 xv6 中,所有来自 U-mode 的 ecall 都会触发 scause=8 的异常,由 trap.c 中的 usertrap() 函数处理。而来自 S-mode 的 ecall(如 wakeup 内部调用)则触发 scause=9,由 kerneltrap() 处理。

3.3 启动 ROM 与 Bootloader

RISC-V 系统的启动流程通常分为三个阶段:启动 ROM、Bootloader 和 操作系统内核。启动 ROM 固化在芯片内部,负责加载 Bootloader;Bootloader 则负责初始化硬件并加载操作系统。

3.3.1 启动流程概览

  1. 上电复位: CPU 从地址 0x1000(QEMU 模拟)或 0x80000000(物理硬件)开始执行。
  2. 启动 ROM: 执行固化代码,读取存储介质(如 SPI Flash、SD 卡)中的 Bootloader 到内存。
  3. Bootloader(如 OpenSBI): 运行在 M-mode,完成时钟、中断控制器、内存控制器等初始化,然后跳转到 S-mode 的内核入口。
  4. 内核启动: xv6 或 Linux 在 S-mode 运行,初始化页表、进程管理、设备驱动等。
QEMU 模拟环境: 在 QEMU 中,启动 ROM 位于 0x1000,它加载 0x80000000 处的内核镜像。xv6 的 kernel.ld 链接脚本将入口点 _entry 放置在 0x80000000。

3.3.2 OpenSBI 的作用

OpenSBI(Open Supervisor Binary Interface)是 RISC-V 平台的标准 Bootloader,运行在 M-mode。它提供以下服务:

  • 初始化硬件(时钟、中断控制器 PLIC、UART 等)。
  • 提供 SBI 调用接口(如 sbi_console_putchar、sbi_shutdown)。
  • 将 CPU 切换到 S-mode,并跳转到内核入口。

xv6 通过 Makefile 将 OpenSBI 与内核镜像链接在一起,生成最终的 kernel.img。启动时,OpenSBI 首先运行,然后跳转到 _entry。

3.4 xv6 的入口点 _entry 与 start 函数

xv6 的启动代码位于 kernel/entry.S 和 kernel/start.c 中。入口点 _entry 是汇编代码,负责最基础的 CPU 初始化;start 函数是 C 语言入口,负责更高级的初始化。

3.4.1 _entry 汇编入口

_entry 是 xv6 内核的第一个执行点,运行在 S-mode(由 OpenSBI 切换过来)。它的主要任务:

  • 设置每个 CPU 的内核栈指针(sp)。
  • 跳转到 start 函数。
# kernel/entry.S
.section .text
.globl _entry
_entry:
    # 设置栈指针,每个 CPU 有独立的栈
    la sp, stack0
    li a0, 1024*4
    csrr a1, mhartid
    addi a1, a1, 1
    mul a0, a0, a1
    add sp, sp, a0
    # 跳转到 start 函数
    call start
栈分配逻辑: stack0 是内核栈的基地址,每个 CPU 分配 4KB 空间。通过 mhartid(硬件线程 ID)计算偏移,确保多核环境下栈不冲突。

3.4.2 start 函数

start 函数在 kernel/start.c 中定义,负责初始化内核的关键子系统:

// kernel/start.c
void start(void) {
    // 设置 stvec 指向 trapvec(异常向量表)
    w_stvec((uint64)trapvec);

    // 设置时钟中断
    timerinit();

    // 初始化页表(仅用于身份映射)
    kvminit();

    // 切换到第一个进程
    scheduler();
}

start 函数执行完毕后,调用 scheduler() 进入调度循环,开始执行第一个用户进程(init 进程)。

3.5 启动流程 SVG 架构图

下图展示了从 CPU 上电到 xv6 内核启动的完整流程,包括特权级别切换和关键组件。

RISC-V 启动流程与特权级别切换 RISC-V 启动流程与特权级别切换 上电复位 PC = 0x1000 启动 ROM 加载 Bootloader OpenSBI (M-mode) 硬件初始化 + SBI 服务 _entry (S-mode) 设置栈指针 start 函数 初始化页表 + 调度器 M-mode 最高权限 S-mode 内核模式 S-mode 继续运行

3.6 异常处理流程 SVG 架构图

下图展示了 xv6 中从用户程序触发 ecall 到内核处理完毕返回的完整异常处理流程,包括特权级别切换和关键寄存器操作。

xv6 异常处理流程(ecall 示例) xv6 异常处理流程(ecall 示例) 用户程序 (U-mode) 执行 ecall 指令 硬件自动操作 sepc = PC, scause = 8 trapvec (S-mode) 保存上下文 usertrap() 处理系统调用 usertrapret() 恢复上下文 + sret sepc = 用户 PC scause = 8 (ecall) stvec = trapvec sstatus.SPP = 0 系统调用号 a7 = syscall number sret 指令 返回 U-mode

3.7 关键数据结构与寄存器

下表总结了 xv6 启动和异常处理中涉及的关键寄存器和数据结构:

名称 类型 作用 初始化位置
mhartid CSR 硬件线程 ID,用于多核区分 硬件自动设置
stvec CSR 监督模式陷阱向量基址 start.c 中 w_stvec()
sepc CSR 异常返回地址 硬件自动保存
scause CSR 异常原因编码 硬件自动设置
satp CSR 监督模式地址翻译和保护 kvminit() 中设置
stack0 内存区域 内核栈基地址 entry.S 中定义
trapvec 函数指针 异常处理入口函数 trap.c 中定义
实践要点: 在调试 xv6 启动过程时,可以通过 QEMU 的 -d in_asm 参数跟踪指令执行,观察 _entry 和 start 函数的执行顺序。使用 info registers 查看 CSR 状态,验证特权级别切换是否正确。