2. RISC-V基础
在进入 U-Boot 与 Linux 启动流程之前,必须先建立对 RISC-V 体系结构的整体认知。RISC-V 不是某一家公司的私有架构,而是一套开放、模块化、可扩展的指令集规范。它采用“基础指令集 + 可选扩展”的组合方式,基础指令集保持极简,扩展则按需拼装。这种设计直接影响了后续启动代码的编写方式:编译器生成的指令、汇编启动代码、异常向量表以及内核入口,都建立在同一套寄存器与特权模型之上。
一、指令集特点:模块化与规整编码
RISC-V 的核心是 RV32I 与 RV64I 基础整数指令集,分别对应 32 位和 64 位地址空间。基础指令集只包含约四十条整数指令,涵盖算术、逻辑、移位、分支、跳转、加载与存储。所有指令长度固定为 32 位(压缩扩展 C 可提供 16 位短指令),指令格式高度规整,源寄存器、目的寄存器与立即数字段位置固定,这使译码逻辑简单,也便于硬件流水线实现。
扩展指令集按字母命名,常见组合包括:
- M:整数乘除法,启动代码中的除法与取模依赖它;
- A:原子操作,用于自旋锁与无锁数据结构;
- F/D:单/双精度浮点,Linux 用户态与部分驱动会使用;
- C:压缩指令,减小代码体积,对 Boot ROM 与 SPL 很关键;
- Zicsr:控制状态寄存器访问,异常与中断处理的基础;
- Zifencei:指令流同步,修改代码后必须执行。
在启动阶段,我们通常看到类似 rv64imac 或 rv64gc 的标识。前者表示 64 位基础整数加乘除、原子与压缩扩展;后者中的 G 是 IMAFDZicsrZifencei 的缩写集合。理解这些字母,才能正确选择工具链的 -march 与 -mabi 参数。
二、寄存器组织:32 个通用寄存器与调用约定
RISC-V 定义 32 个通用整数寄存器 x0 至 x31,其中 x0 恒为零,写入被丢弃,读取永远返回 0。这种“硬连线零寄存器”简化了大量指令:比较、清零、跳转都可以借助 x0 完成。此外还有独立的浮点寄存器 f0 至 f31,以及一组控制状态寄存器(CSR),用于配置特权、异常、中断与性能计数。
汇编中更常用的是 ABI 名称,它们规定了函数调用时谁保存、谁破坏。启动代码与内核入口必须严格遵守这套约定,否则 C 函数调用会立即崩溃。
| ABI 名称 | 寄存器 | 角色与保存责任 |
|---|---|---|
| zero | x0 | 恒为零,只读 |
| ra | x1 | 返回地址,调用者保存 |
| sp | x2 | 栈指针,被调用者保存 |
| gp / tp | x3 / x4 | 全局指针 / 线程指针 |
| t0–t6 | x5–x7, x28–x31 | 临时寄存器,调用者保存 |
| s0–s11 | x8–x9, x18–x27 | 被调用者保存 |
| a0–a7 | x10–x17 | 参数与返回值,调用者保存 |
启动汇编中最常见的动作是设置 sp、清零 bss、保存 a0/a1(hart id 与设备树地址)。这些寄存器在跳入 C 语言之前必须就位。
三、特权模式与内存模型
RISC-V 定义三个特权级:M 模式(机器模式,最高权限)、S 模式(监管模式,运行 Linux 内核)、U 模式(用户模式,运行应用程序)。上电后 CPU 从 M 模式开始执行,Boot ROM 与 U-Boot 的 SPL 通常运行在 M 模式;U-Boot 主阶段与 Linux 内核运行在 S 模式;用户进程运行在 U 模式。M 模式通过 mstatus、mepc、mtvec 等 CSR 控制异常与中断,并可通过 mret 降级到 S 模式。
内存模型方面,RISC-V 采用弱内存序(RVWMO),普通加载与存储可以乱序完成,因此多核之间共享数据时必须使用 fence 指令或 A 扩展的原子指令建立顺序。启动阶段在释放从核、初始化页表、切换地址空间时,fence.i 与 fence rw,rw 是保证正确性的关键。地址空间上,RISC-V 支持 Sv39、Sv48 等分页方案,Linux 通常使用 Sv39,即三级页表、39 位虚拟地址。
四、异常与中断机制:统一入口与委托
RISC-V 把异常(同步)与中断(异步)统一为“陷阱”(trap)。发生陷阱时,硬件自动完成三件事:把当前 PC 保存到 mepc 或 sepc;把原因编码写入 mcause 或 scause;跳转到 mtvec 或 stvec 指向的入口。处理完成后,通过 mret 或 sret 返回,并恢复中断使能状态。
M 模式可以通过 medeleg 与 mideleg 把部分异常和中断“委托”给 S 模式处理。Linux 启动时,OpenSBI 作为 M 模式固件,负责配置委托、提供 SBI 调用,并把控制权交给 S 模式内核。这是 RISC-V 启动链中非常独特的一层。
mtvec,在切换到 S 模式前必须配置好 medeleg/mideleg,否则内核无法接管定时器与外部中断。
五、典型场景与常见误区
假设我们在一块 RV64 开发板上调试启动失败,串口只打印到 SPL 就停止。排查顺序通常是:确认 sp 是否指向有效内存;确认 bss 是否清零;检查 mtvec 是否已设置,否则任何异常都会跳转到未知地址;再确认 mstatus 的 MPP 字段是否准备切换到 S 模式。若内核启动后卡死,则要检查 medeleg 是否把指令缺页、访存缺页委托给 S 模式,以及 PLIC 与 CLINT 的中断是否被正确路由。
tvec 与多个 CSR 协同工作。忽略 fence.i 在修改代码后的作用,也会导致难以定位的取指错误。
# 查看当前工具链支持的架构与 ABI
riscv64-linux-gnu-gcc -march=rv64gc -mabi=lp64d --version
# 反汇编启动代码,观察 CSR 与特权指令
riscv64-linux-gnu-objdump -d u-boot-spl | grep -E "csrr|mret|sret|fence"
掌握指令集扩展、寄存器约定、特权级切换、弱内存序与陷阱委托这五条主线,后续阅读 U-Boot 的 start.S、OpenSBI 的 sbi_entry 以及 Linux 的 head.S 时,才能把每一条指令放回正确的执行上下文。