2. RISC-V基础
这一章我们来聊聊 RISC-V 的启动流程。说白了,就是板子通电后,CPU 怎么一步步把 Linux 跑起来,最后让你看到那个熟悉的命令行。
我刚开始接触 RISC-V 时,总觉得启动流程很神秘。后来在 QEMU 里反复调试,才把这条链路彻底摸透。嗯,其实没那么复杂,就是几个固件接力跑。
2.1 整体启动链路概览
RISC-V 的启动,通常分四个阶段:
- ROM 阶段:芯片内部固化代码,加载第一级 Bootloader
- OpenSBI 阶段:提供 SBI 服务,切换到 S 模式
- Linux 内核阶段:解压、初始化、挂载根文件系统
- 用户空间阶段:init 进程启动,shell 就绪
在 QEMU 里,我们通常从 OpenSBI 开始分析。因为 QEMU 模拟了 ROM 行为,直接加载 OpenSBI 到内存。
核心要点:QEMU 启动 RISC-V 时,-bios 参数指定 OpenSBI 固件,-kernel 参数指定 Linux 内核。设备树由 QEMU 自动生成,或通过 -dtb 手动指定。
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 编译时没选对架构。我踩过这个坑,折腾了一天才发现是交叉编译工具链版本不对。
这一章的内容就到这里。启动流程是调试的基础,搞懂了它,后面分析内核崩溃、驱动问题就顺手多了。