目标:了解riscv平台,基于QEMU+OpenSBI+edk2的启动流程
主要回答以下几个关键问题:
- QEMU启动流程中QEMU 、 OpenSBI 、 UEFI的职责分别是什么?
- DT(Device Tree)如何在QEMU 、 OpenSBI 、 UEFI 、 Linux间流转?
- QEMU 、 OpenSBI 、 UEFI固件之间的跳转点及参数传递是什么?
以下面的qemu-system-riscv64启动参数为例,结合源码进行分析:
bash
qemu-system-riscv64 \
-M virt,pflash0=pflash0,pflash1=pflash1,acpi=off,accel=tcg \
-m 8192 \
-smp 2 \
-serial mon:stdio \
-bios opensbi/build/platform/generic/firmware/fw_dynamic.bin \
-blockdev node-name=pflash0,driver=file,read-only=on,filename=RISCV_VIRT_CODE.fd \
-blockdev node-name=pflash1,driver=file,filename=RISCV_VIRT_VARS.fd
主要流程如下:

名词解释
设备树相关名词解释:
-
DT (Device Tree):DT 是一种描述硬件拓扑与资源的抽象数据结构规范,是一棵逻辑树

-
DTS (Device Tree Source):DTS 是 DT 的一种"文本表示形式"内核工程师、固件工程师真正"写"的东西

-
DTB (Device Tree Blob):DTB 是 DTS 编译后的二进制文件,由 dtc 生成,一般存在于SPI Flash、UEFI firmware volume、/boot 目录等。是"可以被传递、加载、校验的实体"。

-
FDT(Flattened Device Tree):FDT 是 DTB 在内存中的"线性布局表示"在代码里看到的其实都是 FDT

1. 工程介绍与代码分析
- Qemu
- https://github.com/qemu/qemu
- QEMU (Quick Emulator) 是一个通用且开源的机器与用户空间模拟器及虚拟化器,为完整计算机系统提供了全面的模拟功能。
- Opensbi
- https://github.com/riscv-software-src/opensbi
- RISC-V Supervisor Binary Interface (SBI) 是运行在 M-mode 的平台特定固件与更高特权级软件之间的推荐接口。OpenSBI 提供了一个在 M-mode 执行的开源参考实现,RISC-V 平台和 SoC 供应商可以轻松扩展它以适应其特定的硬件配置。
- EDK2
- https://github.com/tianocore/edk2
- EDK II(EFI 开发工具包 II)是一个现代、功能丰富、跨平台的固件开发环境,用于实现符合 UEFI(统一可扩展固件接口)标准的固件。
QEMU相关源码解析
hw/riscv/virt.c
参数解析-->硬件初始化 --> 固件加载与启动决策
-
硬件初始化阶段(virt_machine_init)
- 创建 CPU/HART 与中断控制器:遍历 socket,创建 TYPE_RISCV_HART_ARRAY,根据 aia/aclint/tcg 选择 PLIC/ACLINT/CLINT 等中断相关设备。
- 创建 Flash 设备: 由于传入了 pflash0=pflash0,代码通过 virt_flash_create 和 virt_flash_map 将 Flash 设备映射到地址 0x20000000。
- 内存初始化: 根据 -m 8192,将 8GB RAM 映射到 0x80000000 开始的区域。
- 外设:创建 virtio-mmio、PCIe、platform-bus、UART、RTC 等设备并连接 IRQ。
- 设备树:如果没有用户提供 dtb,动态生成 FDT
- 注册完成回调:注册 virt_machine_done,用于在初始化结束后加载固件/FDT/reset_vec。
-
固件加载与启动决策阶段 (virt_machine_done)
① 加载 OpenSBI 到 RAM
- firmware_end_addr = riscv_find_and_load_firmware(machine, firmware_name, &start_addr, NULL);
- QEMU 将 -bios 指定的fw_dynamic.bin加载到 RAM 中(通常是 0x80000000处)。
- 变量start_addr被更新为 OpenSBI 在 RAM 中的入口地址。
② 检测 Flash 并决定下一步跳转edk2地址pflash
- pflash_blk0 = pflash_cfi01_get_blk(s->flash[0]);
kernel_entry = s->memmap[VIRT_FLASH].base; // 设置下一步入口为 0x20000000 (Flash)
关键结果: kernel_entry被强制修改为 Flash 的基地址 (0x20000000)。这意味着 QEMU 决定:OpenSBI 运行完后,应该去执行 Flash 里的代码 (EDK2)。
③加载FDT到RAM
- 将初始化动态生成的FDT放入 DRAM 高地址处(靠近 DRAM 末尾)。
④ 设置复位向量 (Reset Vector), riscv_setup_rom_reset_vec (在 boot.c)
- 把 reset_vec 写到 rom_base(MROM 起始地址)后,客户机 CPU 在复位时会从 rom_base 取指执行,按顺序执行这些指令。
⑤ 构造smbios、ACPI(若有)
-
复位向量生成细节 (riscv_setup_rom_reset_vec)
-
生成 MROM 启动指令:
- 在 0x1000 (MROM) 处写入几条汇编指令。
- 指令的作用是跳转到 start_addr (即 RAM 中的 OpenSBI)。

-
生成 OpenSBI 引导信息 (riscv_rom_copy_firmware_info):
- 该函数构建一个 fw_dynamic_info 结构体, - 存放在 MROM 中并通过寄存器 a2 传递给 OpenSBI。

总结:QEMU virt启动的3个阶段:

- 该函数构建一个 fw_dynamic_info 结构体, - 存放在 MROM 中并通过寄存器 a2 传递给 OpenSBI。
2. OpenSBI源码解析------OpenSBI入口
OpenSBI入口,基于 fw_dynamic 的启动流程(fw_base.S、fw_dynamic.S):
- 进入 _start:调用 fw_boot_hart(fw_dynamic 会校验 fw_dynamic_info,若版本≥2可返回 boot_hart,否则返回 -1),决定是否进入启动核/仲裁流程。
- 完成重定位、清 BSS、设置临时 trap 和栈。
- 调用 fw_save_info:fw_dynamic 把 a1(FDT 地址)保存为 next_arg1,并从 fw_dynamic_info 读取 next_addr、next_mode、options(以及可选 boot_hart)缓存起来。
- fw_platform_init 初始化平台信息。
- 为每个 HART 初始化 scratch(含 fw_next_arg1、fw_next_addr、fw_next_mode、fw_options 的返回值)。
- 如 a1 非零,执行 FDT 迁移(把上一阶段传入的 FDT 复制到 fw_next_arg1 指定的新地址)。
- 标记启动核完成,跳转到 _start_warm。
- _start_warm:为每个 HART 设置 scratch、栈、trap handler,清 MDT,然后调用 sbi_init 正式进入 OpenSBI 运行时。
sbi_init流程
fw_base.S 中已经为每个 HART 构建并填好 scratch(含 next_addr/next_mode/next_arg1),并把当前 HART 的 scratch 指针写入 MSCRATCH;sbi_init 只是读取这些结果并开始后续工作。 sbi_init 主要流程如下:
- 入口条件检查阶段
- 基于 scratch->next_mode 判断平台硬件是否支持。
- 若不支持直接挂起。
- 冷启动选择阶段
- 通过平台许可 + 原子抽签决定当前 HART 是否为 coldboot。
- 只允许支持 next_mode 的 HART 参与。
- 平台最早期初始化阶段
- 调 sbi_platform_nascent_init 做极早期平台初始化。仅做一件事:如果检测到 M‑level IMSIC,则调用 imsic_local_irqchip_init() 初始化本地 IMSIC(M 态本地中断控制器)。不涉及 FDT 解析、串口、timer 等;这些属于 generic_early_init/final_init 阶段。更早的 fw_platform_init(在 fw_base.S 调用)负责从 FDT 统计 HART 数、构建 platform 结构等,但它不属于 nascent_init。
- 初始化主流程阶段
- coldboot:执行全局初始化(scratch/heap/domain/irq/ipc/timer/pmu/域收尾等)。
- warmboot:等待 coldboot 完成后执行每核必要初始化。
完成冷/热启动初始化后,后续流程会依据 scratch->next_addr/next_mode 切换到下一阶段(EDK2),从而把控制权交出去。
OpenSBI源码解析------"启下"
- 准备下一阶段参数(来自 scratch)
- scratch->next_addr:下一阶段入口地址(fw_dynamic 由 fw_dynamic_info 给出)。
- scratch->next_mode:下一阶段特权级(S/U/M)。
- scratch->next_arg1:作为下一阶段 a1(通常是 FDT 地址)。
- 这些在 firmware/fw_base.S 的 _scratch_init 被写入。
- OpenSBI 最终通过 sbi_hart_switch_mode() 完成跳转:
- 写 MSTATUS/MEPC,设置目标模式向量/寄存器,然后 mret 跳到 next_addr。
见 lib/sbi/sbi_hart.c。
- 写 MSTATUS/MEPC,设置目标模式向量/寄存器,然后 mret 跳到 next_addr。
- 传递的参数
- a0:当前 hartid(sbi_hart_switch_mode 的 arg0)。
- a1:scratch->next_arg1(FDT 地址,在 firmware/fw_base.S 中赋值的)。
mret 前把这两个值绑定到 a0/a1 传给下一阶段edk2。
- 整体承接关系
- 上一阶段(fw_base/fw_dynamic)只负责把 next_addr/next_mode/next_arg1 填好并进入 sbi_init。
- sbi_init 完成平台与 HART 初始化后,调用跳转逻辑,把控制权交给下一阶段入口,并按约定传参。
edk2源码解读
OvmfPkg/RiscVVirt
总体定位: OvmfPkg/RiscVVirt为 QEMU 的 RISC‑V virt 平台提供 OVMF 固件实现,作为 S‑mode 负载配合 OpenSBI,默认是"PEI-less"启动路径。
下面分2部分解读该部分代码:
- edk2入口解读
- PEI-less启动路径解读
Edk2的入口: edk2/OvmfPkg/RiscVVirt/Library/PlatformSecLib/SecEntry.S


在 RISC‑V 调用约定下,参数通过 a0--a3 传入:
- BootHartId(a0)和 DeviceTreeAddress(a1)由上一阶段( OpenSBI)在跳转到 _ModuleEntryPoint 前设置。
- TempRamBase(a2)和 TempRamSize(a3)在汇编入口里由 PCD 读取并写入,然后调用 SecStartupPlatform()。
PEI-less启动路径解读------SecStartupPlatform
- 记录并计算临时栈:根据 TempRamBase+SizeOfRam 预留 SIZE_16KB 作为栈,得到 SecStackBase/SecStackSize。
- 启用浮点与异常:调用 InitializeFloatingPointUnits() 设置 sstatus.FS,并用 InitializeCpuExceptionHandlers() 安装默认异常处理。
- 创建 HOB 列表:HobConstructor() 用临时内存建立 HOB 列表头,PrePeiSetHobList() 将其写入 sscratch,供后续阶段获取。
- 构建栈 HOB:BuildStackHob() 把栈区记录进 HOB。
- 解析FDT并用HOB的方式重新描述:SecPlatformMain(NULL) 走 PEI-less 路径,通过解析FDT中的设备资源,执行内存/CPU/平台初始化,并用HOB描述这些资源。并使用BuildGuidDataHob的方式传递给后续的阶段。
- 运行库构造函数:ProcessLibraryConstructorList()。
- 解压 BFV:FfsFindNextFile() 找到DXE FV 文件,FfsProcessFvFile() 解压到内存。
- 加载并跳转 DXE:LoadDxeCoreFromFv() 加载 DXE Core 并转移控制权。
4. 总体流程总结
Step 0:QEMU 上电后的"假 BootROM 行为",模拟"芯片上电后世界已经准备好"的状态
QEMU 做了 4 件关键的事
- 构造 Device Tree(DTB)
- 把 DTB 放到内存某个地址
- 为OpenSBI运行准备好参数
- 把 PC 设置为 OpenSBI 的入口
Step 1:OpenSBI 的第一条指令(M-mode)
此时世界状态是:
- 特权级:M-mode
- PC:_fw_start(OpenSBI)
- a1:DTB 的物理地址(QEMU传的)
- a2: fw_dynamic_info,指示OpenSBI下一阶段的信息(QEMU传的)
DT流转:
DT 在这一刻已经"存在于内存",OpenSBI 不是创建者,只是第一个消费者。
Step 2:OpenSBI 初始化(仍然是 M-mode),目标是为 S-mode 准备"可生存环境"
OpenSBI 在 M-mode 里做这些事:
- 初始化 hart
- 初始化 timer / IPI
- 建立 SBI 调用入口
- 解析 DT(只读)
- 识别 CPU
- 找 PLIC / CLINT
- 找内存布局
关键点:OpenSBI 不重新创建 DT,但可以轻微修改DT
Step 3:OpenSBI → UEFI 的关键跳转
OpenSBI 做的事情可以抽象成:
jump_to_supervisor(
next_addr = UEFI_SEC_ENTRY,
a0 = hartid,
a1 = dtb_addr
)
此时:
- 特权级:M → S
- DTB 地址:原封不动
- PC:UEFI SEC 入口
Step 4:UEFI SEC 阶段(S-mode)
UEFI 看到的"世界"是:
- S-mode
- 内存可用
- DTB 已存在
- 有 SBI 可以调用
SEC 只做三件事:
- 建立最小执行环境
- 保存 DTB 地址
- 跳到 PEI
Step 5:UEFI PEI 阶段(DT → HOB)
PEI 做的事:
- 从 DT 中解析:
- 内存
- CPU
- 外设
- 把这些信息转成 HOB(UEFI 内部数据结构)
- 加载DXE
Step 6:UEFI DXE → BDS → Linux
DXE 阶段做的事:
- 加载驱动
- 枚举 PCIe
- 构建 ACPI(如果有)
最后:
- UEFI 把 DT(或 ACPI)再次作为启动参数交给 Linux
5. 问题回归
- QEMU启动流程中QEMU 、 OpenSBI 、 UEFI的职责分别是什么?
- QEMU 是"假想中的完美硬件 + BootROM"
- OpenSBI 是 唯一的 M-mode 常驻固件,提供 SBI 并完成 M→S 的受控切换
- UEFI 运行在 S-mode ,是平台与 OS 之间的标准化系统固件,负责把"平台差异"收敛成统一接口(UEFI,向 OS 提供 ACPI 或 DT)
- DT如何在QEMU 、 OpenSBI 、 UEFI 、 Linux间流转?
- QEMU 构建 Device Tree,并在 reset vector 中按照 OpenSBI fw_dynamic ABI 设置启动寄存器:a0 = mhartid,a1 = DTB 物理地址,a2 = Firmware Dynamic Information 结构体指针。
- OpenSBI 读取 DT 中与 M-mode 相关的最小信息 ,完成 M-mode 运行环境和 SBI 初始化,并将 DT 地址通过 a1寄存器传递给 UEFI。
- UEFI消费DT,用于自身平台初始化,PEI阶段将DT描述成HOB,后面会选择性地将 DT 或 ACPI 表传递给 Linux。
- QEMU 、 OpenSBI 、 UEFI固件之间的跳转点及传参是什么?
- QEMU在riscv_setup_rom_reset_vec中写reset vector ,汇编最后jr t0:跳转到 - OpenSBI 入口执行。 fw_dynamic 模式下,传递的参数为:a0( mhartid ),a1(FDT物理地址),a2(fw_dynamic_info 结构体)
- OpenSBI 通过 sbi_hart_switch_mode() 切换到 fw_dynamic_info 指定的下一特权级(这里下一特权等级在qemu里设置为S-mode),并跳转到 UEFI 。传递的参数为:a0(当前 hartid ),a1:(FDT 物理地址)