2. RISC-V基础

这一章我们来聊聊 RISC-V 的启动流程。说白了,就是板子通电后,CPU 怎么一步步把 Linux 跑起来,最后让你看到那个熟悉的命令行。

我刚开始接触 RISC-V 时,总觉得启动流程很神秘。后来在 QEMU 里反复调试,才把这条链路彻底摸透。嗯,其实没那么复杂,就是几个固件接力跑。

2.1 整体启动链路概览

RISC-V 的启动,通常分四个阶段:

  1. ROM 阶段:芯片内部固化代码,加载第一级 Bootloader
  2. OpenSBI 阶段:提供 SBI 服务,切换到 S 模式
  3. Linux 内核阶段:解压、初始化、挂载根文件系统
  4. 用户空间阶段:init 进程启动,shell 就绪

在 QEMU 里,我们通常从 OpenSBI 开始分析。因为 QEMU 模拟了 ROM 行为,直接加载 OpenSBI 到内存。

核心要点:QEMU 启动 RISC-V 时,-bios 参数指定 OpenSBI 固件,-kernel 参数指定 Linux 内核。设备树由 QEMU 自动生成,或通过 -dtb 手动指定。

RISC-V 启动流程(QEMU 环境) QEMU ROM OpenSBI Linux Kernel 根文件系统 设备树(DTB)由 QEMU 生成,通过 a0 寄存器传递给 OpenSBI OpenSBI 解析 DTB,跳转到内核入口,a1 寄存器携带 DTB 地址 M 模式 M → S 模式 S 模式 S → U 模式 特权级切换:M → S → U,逐级降权,提升安全性

2.2 OpenSBI 固件加载

OpenSBI 是 RISC-V 的 Supervisor Binary Interface 实现。它运行在 M 模式(机器模式),为 S 模式提供底层服务。

在 QEMU 中启动时,我习惯这样指定:

qemu-system-riscv64 \
  -machine virt \
  -bios opensbi-riscv64-generic-fw_dynamic.bin \
  -kernel Image \
  -append "root=/dev/vda ro" \
  -drive file=rootfs.ext4,format=raw,id=hd0 \
  -device virtio-blk-device,drive=hd0

OpenSBI 加载后,会做这几件事:

  • 初始化硬件(定时器、中断控制器)
  • 解析设备树(DTB),获取内存布局
  • 切换到 S 模式,跳转到 Linux 内核入口

个人经验:我在调试一个自定义板卡时,发现 OpenSBI 启动后内核不跑。后来加 -d cpu_reset 日志,才发现是 DTB 中内存起始地址写错了。OpenSBI 打印的日志非常关键,建议启动时加上 -d sbi 参数。

2.3 设备树传递机制

设备树(Device Tree)是描述硬件信息的结构。RISC-V 启动时,通过寄存器传递 DTB 地址:

  • a0:当前 Hart ID(CPU 核心编号)
  • a1:DTB 在内存中的物理地址

OpenSBI 收到 DTB 后,会解析并可能修改它(比如添加 chosen 节点)。然后,OpenSBI 跳转到内核时,a1 仍然指向 DTB。

内核启动早期,会调用 early_init_dt_scan() 解析 DTB。你想想看,如果没有设备树,内核根本不知道内存有多大、外设在哪儿。

避坑指南:我曾经在 QEMU 里手动指定 -dtb my.dtb,结果内核启动到一半就挂了。原因是 QEMU 生成的 DTB 和手动编译的版本不一致,导致中断控制器节点不匹配。建议优先用 QEMU 自动生成的 DTB,除非你非常确定自己在做什么。

2.4 Linux 内核启动

内核启动分两个阶段:

阶段 说明 关键函数
早期汇编 设置页表、初始化栈、跳转到 C 代码 _start, start_kernel
C 语言初始化 中断、调度、内存管理、驱动加载 setup_arch, rest_init

在 QEMU 里,我经常用 -d in_asm 观察内核第一条指令的执行。你会发现,内核入口地址就是 OpenSBI 跳转的目标。

# 查看内核入口地址
readelf -h Image | grep Entry
# 输出示例:Entry point address: 0x80200000

内核启动日志里,有几个关键节点:

  • [ 0.000000] Linux version ... — 内核版本
  • [ 0.000000] OF: fdt: ... — 设备树解析
  • [ 0.000000] Memory: ... — 内存布局
  • [ 0.000000] CPU: ... — CPU 信息

调试技巧:如果内核启动卡住,先看最后一条日志。我遇到过 clocksource 初始化失败导致死循环,加 clocksource=riscv_clocksource 参数绕过的。

2.5 根文件系统挂载

内核初始化完成后,最后一步是挂载根文件系统。挂载方式由 root= 参数决定:

参数示例 说明 适用场景
root=/dev/vda VirtIO 块设备 QEMU 虚拟机
root=/dev/mmcblk0 SD 卡设备 硬件板卡
root=/dev/nfs 网络文件系统 无盘启动
root=initramfs 内存文件系统 早期调试

挂载成功后,内核执行 /sbin/init,启动用户空间。你会在日志里看到:

[    1.234567] VFS: Mounted root (ext4 filesystem) on device 254:0.
[    1.345678] devtmpfs: mounted
[    1.456789] Freeing unused kernel memory: 1024K
[    1.567890] Run /sbin/init as init process

看到 Run /sbin/init,说明启动流程基本走通了。接下来就是 busybox 或者 systemd 的事了。

我的习惯:调试初期,我常用 initramfs 做根文件系统。它打包在内核里,不需要额外磁盘镜像,启动快、调试方便。等内核稳定了,再切到 ext4 根文件系统。

2.6 启动日志分析实战

最后,我们来看一个完整的启动日志片段。我会标注关键点:

# OpenSBI 阶段
OpenSBI v1.2
  Platform Name: riscv-virtio,qemu
  Platform Features: medeleg

# 内核阶段
[    0.000000] Linux version 6.1.0 (user@host) #1 SMP ...
[    0.000000] OF: fdt: Machine model: riscv-virtio,qemu
[    0.000000] Memory: 1024MB available
[    0.000000] CPU: rv64imafdc

# 驱动加载
[    0.500000] virtio_blk virtio0: [vda] 2097152 512-byte logical blocks

# 根文件系统
[    1.200000] EXT4-fs (vda): mounted filesystem with ordered data mode
[    1.300000] Run /sbin/init as init process

# 用户空间
Welcome to Buildroot
buildroot login:

看到 buildroot login:,恭喜你,启动成功了。

注意:如果日志停在 Run /sbin/init 之后没反应,通常是根文件系统里缺少 init 程序,或者 busybox 编译时没选对架构。我踩过这个坑,折腾了一天才发现是交叉编译工具链版本不对。

这一章的内容就到这里。启动流程是调试的基础,搞懂了它,后面分析内核崩溃、驱动问题就顺手多了。