基于QEMU+OpenSBI+edk2的riscv启动流程解析

目标:了解riscv平台,基于QEMU+OpenSBI+edk2的启动流程

主要回答以下几个关键问题:

  1. QEMU启动流程中QEMU 、 OpenSBI 、 UEFI的职责分别是什么?
  2. DT(Device Tree)如何在QEMU 、 OpenSBI 、 UEFI 、 Linux间流转?
  3. 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

参数解析-->硬件初始化 --> 固件加载与启动决策

  1. 硬件初始化阶段(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。
  2. 固件加载与启动决策阶段 (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(若有)
  3. 复位向量生成细节 (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个阶段:

2. OpenSBI源码解析------OpenSBI入口

OpenSBI入口,基于 fw_dynamic 的启动流程(fw_base.S、fw_dynamic.S):

  1. 进入 _start:调用 fw_boot_hart(fw_dynamic 会校验 fw_dynamic_info,若版本≥2可返回 boot_hart,否则返回 -1),决定是否进入启动核/仲裁流程。
  2. 完成重定位、清 BSS、设置临时 trap 和栈。
  3. 调用 fw_save_info:fw_dynamic 把 a1(FDT 地址)保存为 next_arg1,并从 fw_dynamic_info 读取 next_addr、next_mode、options(以及可选 boot_hart)缓存起来。
  4. fw_platform_init 初始化平台信息。
  5. 为每个 HART 初始化 scratch(含 fw_next_arg1、fw_next_addr、fw_next_mode、fw_options 的返回值)。
  6. 如 a1 非零,执行 FDT 迁移(把上一阶段传入的 FDT 复制到 fw_next_arg1 指定的新地址)。
  7. 标记启动核完成,跳转到 _start_warm。
  8. _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 主要流程如下:

  1. 入口条件检查阶段
    • 基于 scratch->next_mode 判断平台硬件是否支持。
    • 若不支持直接挂起。
  2. 冷启动选择阶段
    • 通过平台许可 + 原子抽签决定当前 HART 是否为 coldboot。
    • 只允许支持 next_mode 的 HART 参与。
  3. 平台最早期初始化阶段
    • 调 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。
  4. 初始化主流程阶段
    • coldboot:执行全局初始化(scratch/heap/domain/irq/ipc/timer/pmu/域收尾等)。
    • warmboot:等待 coldboot 完成后执行每核必要初始化。

完成冷/热启动初始化后,后续流程会依据 scratch->next_addr/next_mode 切换到下一阶段(EDK2),从而把控制权交出去。

OpenSBI源码解析------"启下"

  1. 准备下一阶段参数(来自 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 被写入。
  2. OpenSBI 最终通过 sbi_hart_switch_mode() 完成跳转:
    • 写 MSTATUS/MEPC,设置目标模式向量/寄存器,然后 mret 跳到 next_addr。
      见 lib/sbi/sbi_hart.c。
  3. 传递的参数
    • a0:当前 hartid(sbi_hart_switch_mode 的 arg0)。
    • a1:scratch->next_arg1(FDT 地址,在 firmware/fw_base.S 中赋值的)。
      mret 前把这两个值绑定到 a0/a1 传给下一阶段edk2。
  4. 整体承接关系
    • 上一阶段(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

  1. 记录并计算临时栈:根据 TempRamBase+SizeOfRam 预留 SIZE_16KB 作为栈,得到 SecStackBase/SecStackSize。
  2. 启用浮点与异常:调用 InitializeFloatingPointUnits() 设置 sstatus.FS,并用 InitializeCpuExceptionHandlers() 安装默认异常处理。
  3. 创建 HOB 列表:HobConstructor() 用临时内存建立 HOB 列表头,PrePeiSetHobList() 将其写入 sscratch,供后续阶段获取。
  4. 构建栈 HOB:BuildStackHob() 把栈区记录进 HOB。
  5. 解析FDT并用HOB的方式重新描述:SecPlatformMain(NULL) 走 PEI-less 路径,通过解析FDT中的设备资源,执行内存/CPU/平台初始化,并用HOB描述这些资源。并使用BuildGuidDataHob的方式传递给后续的阶段。
  6. 运行库构造函数:ProcessLibraryConstructorList()。
  7. 解压 BFV:FfsFindNextFile() 找到DXE FV 文件,FfsProcessFvFile() 解压到内存。
  8. 加载并跳转 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. 问题回归

  1. QEMU启动流程中QEMU 、 OpenSBI 、 UEFI的职责分别是什么?
    • QEMU 是"假想中的完美硬件 + BootROM"
    • OpenSBI 是 唯一的 M-mode 常驻固件,提供 SBI 并完成 M→S 的受控切换
    • UEFI 运行在 S-mode ,是平台与 OS 之间的标准化系统固件,负责把"平台差异"收敛成统一接口(UEFI,向 OS 提供 ACPI 或 DT)
  2. 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。
  3. 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 物理地址)
相关推荐
x-cmd1 天前
[x-cmd] QEMU 10.2.0 发布:虚拟机实时更新与性能飞跃的技术深度解读
安全·qemu·虚拟机·x-cmd
三雷科技3 天前
qemu-img 使用手册(含详细案例)
qemu
河码匠19 天前
libvirt xml 配置文件说明
qemu·kvm·libvirt
inquisiter21 天前
openocd操作ku060板子记录
riscv
深念Y22 天前
JMS583主控硬盘盒刷新版固件,修复无法使用傲腾或者频繁休眠增加硬盘不安全关机次数BUG
ssd·固态硬盘·主控·固件·jms583·硬盘盒·傲腾
乙酸氧铍25 天前
【imx6ul 学习笔记】Docker 运行百问网 imx6ul_qemu
linux·docker·arm·qemu·imx6ul
gsls2008081 个月前
移远EC20对UAC音频设备识别分析
内核·音频·alsa·固件·uac·ec20·移远
fdtsaid1 个月前
Intel 六位专家对 Simics 助力 Shift-Left 的讨论(2018)
qemu·仿真·simulation·simics·intel simics
seasonsyy1 个月前
如何快速进入BIOS(绕过键盘按键识别难题)
bios