QEMU 安装与第一个 RISC-V 虚拟平台:零工具链跑通 OpenSBI
这是 SoC 与 QEMU 入门路线的第 02 篇,也是整个系列的第一次动手实验。零基础的读者建议先读第 01 篇 认识 QEMU:从零看懂虚拟机、镜像与快照,把宿主机、客户机、固件、机器模型这几个词建立起直觉,再回来安装。后面的文章会沿着「SoC 由什么组成 → QEMU 怎么模拟它 → 怎么在虚拟平台上 bring-up 固件与 Linux」展开;本篇只解决入门的第一道坎:把 QEMU 装起来,亲眼看到一块 RISC-V 虚拟开发板从上电到打印固件横幅的全过程。如果你对 QEMU 为什么能做功能模拟、它和 gem5 这类周期精确模拟器的差别感兴趣,可以先读 QEMU/TCG 与功能平台,本篇专注动手。
本文要回答的问题
- 不买开发板、不装 RISC-V 交叉工具链,能不能先看到一块 RISC-V 板子启动?
qemu-system和qemu-user两种模式有什么区别,入门该用哪个?virt机器模型到底模拟了什么,怎么查看它的硬件组成?-bios default的固件从哪来,启动横幅里每一行在说什么?- 安装过程中 brew 拉取 formula 被网络重置、tap 不受信任,分别怎么处理?
一、为什么第一个实验选 RISC-V virt
学 SoC 软件栈最大的门槛是硬件:一块能跑 Linux 的 RISC-V 或 ARM 开发板价格几百到上千元,还需要 JTAG 调试器、串口转接和一整套交叉编译环境,环境问题往往比知识点本身更耗时间。QEMU 的全系统模拟把这块门槛整个抹平了——virt 是 QEMU 官方维护的通用 RISC-V 虚拟机器,CPU、中断控制器、定时器、串口、virtio 磁盘全部用软件模型实现,跑在普通的 x86 Mac 或 PC 上。
选它做第一个实验还有两个现实原因。其一,virt 的默认固件 OpenSBI 直接打包在 QEMU 安装包里,-bios default 一条参数就能启动,不需要自己编译任何 RISC-V 代码,这正是本篇标题里「零工具链」的含义。其二,virt 是后续所有进阶实验的公共底座:裸机程序、U-Boot、完整 Linux 发行版都加载在同一块虚拟板上,本篇先把这块板的上电流程跑顺,后面的文章只是往上面放更复杂的软件。
下图是整个实验的五步路径,每一步的产物都会在后面的小节逐一展开。
二、安装 QEMU:先分清两种模拟模式
QEMU 实际上提供两类能力差别很大的模拟器,安装前先把概念分清,后面选命令才不会混。
| 模式 | 命令形态 | 模拟对象 | 典型用途 |
|---|---|---|---|
| 全系统模拟 | qemu-system-riscv64、qemu-system-aarch64 | 一整台机器:CPU、内存、中断控制器、外设 | 固件/内核 bring-up、驱动开发、跑完整发行版 |
| 用户态模拟 | qemu-riscv64、qemu-aarch64 | 单个跨架构进程,转发系统调用给宿主内核 | 在 x86 上直接跑 RISC-V 编译出的单个程序 |
本系列的主角是全系统模拟,因为 SoC 的中断、MMU、设备枚举这些话题只有在一整台机器里才存在。需要注意一个平台差异:macOS 的 Homebrew 只提供 qemu-system-* 全系统模拟器,用户态模拟依赖 Linux 内核的系统调用转发机制,只能在 Linux 主机上安装(包名通常是 qemu-user)。在 Mac 上做跨架构单进程实验的替代方案是起一个 Linux 虚拟机,这也是后续工程化实验包会采用的形态。
macOS 上的安装命令只有一条。
brew install qemuLinux 上按发行版选择,Ubuntu/Debian 一条命令可以把全系统模拟器和工具集装齐。
sudo apt install qemu-system-misc qemu-utils device-tree-compiler装完后先确认版本,本篇所有实验基于 QEMU 11.1.2 实测,8.x 之后的大版本参数基本兼容。
qemu-system-riscv64 --version三、virt 机器模型:一块软件定义的开发板
QEMU 里「机器(machine)」是对一整块板子的建模:CPU 核数、内存起始地址、中断控制器型号、内存映射外设的地址布局都由机器模型固定。RISC-V 架构下最常用的就是 virt,它不对应任何真实芯片,而是一份干净、标准化、专门为虚拟化设计的虚拟板——这反而让它成为学 SoC 软件栈的最佳教材,没有真实板子那些历史遗留的地址坑。
先用 -machine help 看看本机 QEMU 支持哪些 RISC-V 机器。
qemu-system-riscv64 -machine help | grep -E '^virt'virt 这一行就是后面所有实验的目标。下面这张图把它的内部结构摊开:整个 QEMU 是宿主机上的一个普通进程,进程内部分成三层——负责执行 Guest 指令的 TCG 翻译引擎、模拟物理地址空间的虚拟内存,以及一组设备模型。
读这张图抓住两条数据流。蓝色的 Guest 软件栈是被模拟的一方:OpenSBI 运行在 RISC-V 最高特权级 M 态,后续的内核或裸机程序通过 SBI 标准接口向它请求控制台打印、时钟设置等服务。橙色的 TCG 与虚拟内存是执行引擎:Guest 每取一条 RISC-V 指令,TCG 就把它翻译成一小段宿主机原生指令执行,访存请求则走软 MMU 落到 MemoryRegion 管理的虚拟地址空间。绿色的四个设备模型对应真实 SoC 里的 IP 核:UART 负责把字符送到你的终端,CLINT 提供定时器和软件中断,PLIC 汇聚外部中断,virtio-mmio 承载磁盘和网络。理解了这张图,后面讲启动链路、设备树、中断路由时都有了落点。
四、第一次启动:读懂 OpenSBI 横幅
现在执行本篇的核心命令。-nographic 把虚拟机的串口直接接到当前终端,-serial mon:stdio 让同一个终端兼顾串口输出与 QEMU 监视器,-no-reboot 保证固件退出后虚拟机不反复重启。
qemu-system-riscv64 -machine virt -bios default -nographic -serial mon:stdio -no-reboot因为没有给虚拟机加载内核,OpenSBI 完成初始化后会停在那里等待下一阶段软件,这是正常现象。在真实终端里按 Ctrl+A 再按 X 可以主动退出 QEMU;在自动化脚本里则用后台运行加定时结束的方式控制运行时长。下面是本机真实启动输出的终端实录。

上图是同一次启动在终端窗口里的完整样貌:上半屏的 OpenSBI 字符画下面依次列出平台名、HART 数量、定时器频率、串口型号和固件基址,阅读时重点对照 Platform Name 确认机器模型、对照 Firmware Base 记住固件在物理地址空间的起点。
横幅里有几行信息值得逐行读懂,它们对应着 SoC 启动时固件必须确认的硬件事实。Platform Name 标出当前机器模型是 riscv-virtio,qemu,说明这份固件确实跑在 virt 板上;Platform HART Count 是虚拟 CPU 核数,HART 是 RISC-V 对硬件执行线程的称呼;Firmware Base 和 Firmware Size 给出 OpenSBI 自身在物理地址空间中的驻留位置;PMP Regions 是物理内存保护单元的区域数,M 态固件靠它隔离下层软件能访问的内存范围。最后一行 Boot HART 指明由哪个核完成上电初始化,其余核此时停在等待中断的状态,这和真实多核 SoC 的启动语义完全一致——主核引路、从核靠软件间中断唤醒。
这里顺带建立一个后面会反复用到的认知:全系统模拟的启动链路是分层的。virt 板上电后先执行 QEMU 内置的 OpenSBI(M 态固件),OpenSBI 初始化完硬件抽象后再去加载下一阶段负载——可以是 U-Boot、Linux 内核,也可以是你自己写的裸机程序。本篇只有第一层,所以它停住了;后续文章会逐层把负载补全。
五、设备树导出与 qcow2 磁盘镜像
命令行参数能告诉 QEMU 要什么,但驱动开发者更常需要反向回答:「这块板到底给我准备了哪些设备、各自在哪个地址?」答案在设备树(Device Tree Blob,DTB)里。virt 机器支持一条机器属性把当前配置的设备树原样导出。
qemu-system-riscv64 -machine virt,dumpdtb=virt.dtb -display none导出的 virt.dtb 是二进制 Flattened Device Tree,用设备树编译器反编译成文本形式后就能读到串口、中断控制器、virtio 设备的兼容字符串与内存映射地址。
dtc -I dtb -O dts virt.dtb | less设备树是连接「QEMU 设备模型」与「Guest 内核驱动」的契约:QEMU 按设备树描述布置虚拟硬件,Linux 的驱动则靠 compatible 字符串匹配并按寄存器地址发起访问。后面讲自定义外设建模时,修改设备树会是核心动作之一。
第二件准备工作是磁盘。QEMU 的自有镜像格式 qcow2 支持稀疏分配、快照和压缩,建一个 64MB 的镜像实际只占约 200KB 磁盘。
qemu-img create -f qcow2 disk.qcow2 64M
qemu-img info disk.qcow2qemu-img info 输出里要区分两个数字:virtual size 是 Guest 看到的盘容量,disk size 是宿主机上的真实占用。后续安装完整 Linux 发行版时,把这个镜像通过 -drive 参数挂到 virtio 总线上即可,本篇先把工具链走顺。
六、安装期两个真实报错与处理
实验环境的坑本身就是知识。本篇在 macOS 上安装 QEMU 时真实遇到两个问题,处理过程一并记录,后面的报错类文章会以同样的方式逐个复现、逐个给解法。
报错一:Failed to download resource ... raw.githubusercontent.com ... Connection reset by peer。 Homebrew 4.x 默认走 API 模式,安装时仍会按 formula 里的固定地址回源到 raw.githubusercontent.com 拉取配方源文件,该域名在国内网络下经常被连接重置。处理思路是让 Homebrew 改用本地全量 tap 与国内镜像:把 homebrew/core 的 git 远端切到清华镜像,设置 HOMEBREW_NO_INSTALL_FROM_API=1 强制从本地 tap 读配方,bottle 下载域指向镜像站。一份可复用到所有 brew 安装的环境变量配置如下。
export HOMEBREW_API_DOMAIN="https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles/api"
export HOMEBREW_BOTTLE_DOMAIN="https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles"
export HOMEBREW_BREW_GIT_REMOTE="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git"
export HOMEBREW_CORE_GIT_REMOTE="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git"
export HOMEBREW_NO_INSTALL_FROM_API=1报错二:Refusing to load formula homebrew/core/qemu from untrusted tap。 这是 Homebrew 7.x 的信任机制:tap 的 git 远端不再是官方 GitHub 地址时,配方不会被自动信任,而 brew trust homebrew/core 对官方同名 tap 会直接短路、不写信任库。正解是按 formula 逐个显式信任,信任状态持久化在 ~/.homebrew/trust.json 里。
brew trust --formula homebrew/core/qemu如果依赖较多、不想逐个确认,可以对本地 tap 里的全部配方一次性批量信任,之后所有安装都不再被拦截。
find /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula -name '*.rb' \
-exec basename {} .rb \; | sed 's#^#homebrew/core/#' | xargs brew trust --formula这一篇在知识体系里的位置
本篇是 SoC 与 QEMU 系列的环境地基:跑通了 virt 板的第一层固件,拿到了设备树和磁盘镜像两个后续实验的必备产物。接下来可以沿两条线深入:往原理方向,读 QEMU/TCG 与功能平台理解指令翻译为什么「功能对但没有周期概念」,再配合缓存一致性系列理解 Guest 看到的内存层级;往实战方向,后续文章会在同一块 virt 板上加载裸机 UART 程序、接上 gdbstub 调试,再逐层补齐 OpenSBI → U-Boot → Linux 的完整启动链。
代码获取:本篇五步实验的可复现脚本(
run.sh,含无timeout环境下的有界运行封装)与说明见公开仓库 derekzhuo/geek-doc.cn 的experiments/qemu-riscv-virt-first-boot/。克隆后./run.sh | tee run1.log即可复现全部输出。
实验环境
- 机器/系统:MacBook Pro(Intel x86_64)/ macOS 26
- 工具版本:QEMU 11.1.2(Homebrew 安装,全系统模拟)、dtc 设备树编译器
- 日期:2026-11-07
- 完整命令:见公开仓库
experiments/qemu-riscv-virt-first-boot/run.sh,原始输出记录于本文终端实录
总结
QEMU 的 virt 机器把一整块 RISC-V 开发板变成了普通进程里的软件模型:brew install qemu 装好模拟器,-bios default 直接用内置 OpenSBI 上电,不装交叉工具链就能看到 HART、PMP、固件基址这些真实 SoC 启动时必须确认的硬件事实;dumpdtb 导出的设备树是虚拟硬件与 Guest 驱动之间的契约,qemu-img 建出的 qcow2 则是后续装系统的磁盘载体。安装期遇到的 GitHub 回源重置用镜像加本地 tap 解决,tap 不信任问题用 brew trust --formula 显式授权——环境通了,后面的启动链路实验才有稳定底座。