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 |
模式切换通过异常(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 |
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 启动流程概览
- 上电复位: CPU 从地址
0x1000(QEMU 模拟)或0x80000000(物理硬件)开始执行。 - 启动 ROM: 执行固化代码,读取存储介质(如 SPI Flash、SD 卡)中的 Bootloader 到内存。
- Bootloader(如 OpenSBI): 运行在 M-mode,完成时钟、中断控制器、内存控制器等初始化,然后跳转到 S-mode 的内核入口。
- 内核启动: xv6 或 Linux 在 S-mode 运行,初始化页表、进程管理、设备驱动等。
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 内核启动的完整流程,包括特权级别切换和关键组件。
3.6 异常处理流程 SVG 架构图
下图展示了 xv6 中从用户程序触发 ecall 到内核处理完毕返回的完整异常处理流程,包括特权级别切换和关键寄存器操作。
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 中定义 |
-d in_asm 参数跟踪指令执行,观察 _entry 和 start 函数的执行顺序。使用 info registers 查看 CSR 状态,验证特权级别切换是否正确。