1. RISC-V启动流程概述
在RISC-V架构的虚拟化环境中,QEMU+OpenSBI+edk2的组合提供了一个完整的固件启动链。这个启动流程模拟了真实硬件从上电到操作系统加载的全过程,对于理解RISC-V平台的启动机制具有重要意义。
启动流程的核心在于三个组件的协同工作:
- QEMU:作为硬件模拟器,负责构建虚拟硬件环境并提供初始启动代码
- OpenSBI:作为RISC-V的M模式固件,提供基础的硬件抽象层
- edk2/UEFI:作为S模式的系统固件,提供标准的启动接口
这个启动链的设计充分体现了RISC-V特权级架构的特点,从M模式到S模式的权限转移过程清晰可见。下面我们将深入分析每个组件的具体职责和它们之间的交互方式。
2. QEMU的启动流程解析
2.1 QEMU的硬件模拟与初始化
QEMU在启动RISC-V虚拟机时,首先会进行硬件环境的模拟和初始化。这个过程主要发生在virt_machine_init函数中:
c复制static void virt_machine_init(MachineState *machine)
{
RISCVVirtState *s = RISCV_VIRT_MACHINE(machine);
MemoryRegion *system_memory = get_system_memory();
MemoryRegion *mask_rom = g_new(MemoryRegion, 1);
// 初始化CPU和中断控制器
for (int i = 0; i < riscv_socket_count(machine); i++) {
create_fdt_socket_cpus(s, i);
create_fdt_socket_plic(s, system_memory, i);
}
// 初始化Flash设备
if (pflash0) {
virt_flash_create(s);
virt_flash_map(s, system_memory);
}
// 初始化内存
memory_region_init_ram(main_mem, NULL, "riscv_virt_board.ram",
machine->ram_size, &error_fatal);
// 初始化外设
create_fdt_virtio(s, system_memory);
create_fdt_pcie(s, system_memory);
// 注册完成回调
qemu_register_machine_init_done_notifier(&s->machine_done);
}
这个初始化过程构建了一个完整的虚拟硬件环境,包括CPU、内存、中断控制器和各种外设。
2.2 固件加载与启动决策
在硬件初始化完成后,QEMU进入virt_machine_done阶段,开始加载固件并决定启动流程:
- 加载OpenSBI固件:
c复制firmware_end_addr = riscv_find_and_load_firmware(machine, firmware_name,
&start_addr, NULL);
QEMU将-bios参数指定的OpenSBI固件加载到RAM中(通常是0x80000000处),并记录入口地址。
- 检测Flash设备:
c复制pflash_blk0 = pflash_cfi01_get_blk(s->flash[0]);
kernel_entry = s->memmap[VIRT_FLASH].base; // 0x20000000
这个步骤决定了OpenSBI执行完成后将跳转到Flash中的edk2固件。
-
加载设备树:
QEMU将动态生成的FDT(Flattened Device Tree)放入DRAM的高地址区域,供后续阶段使用。 -
设置复位向量:
c复制riscv_setup_rom_reset_vec(machine, &s->soc[0], start_addr,
virt_memmap[VIRT_MROM].base,
virt_memmap[VIRT_MROM].size,
virt_memmap[VIRT_DRAM].base,
virt_memmap[VIRT_DRAM].size);
这个函数在0x1000(MROM)处写入几条汇编指令,使CPU在复位时跳转到OpenSBI的入口地址。
2.3 复位向量生成细节
riscv_setup_rom_reset_vec函数生成的复位向量包含以下关键内容:
- 跳转指令:
assembly复制auipc t0, 0x0 # 计算OpenSBI入口地址
addi t0, t0, 12 # 调整偏移量
jr t0 # 跳转到OpenSBI
- OpenSBI引导信息:
c复制struct fw_dynamic_info {
uint64_t magic;
uint64_t version;
uint64_t next_addr;
uint64_t next_mode;
uint64_t options;
};
这个结构体通过寄存器a2传递给OpenSBI,包含了下一阶段的入口地址和特权级信息。
3. OpenSBI的工作流程
3.1 OpenSBI的启动入口
OpenSBI的启动从fw_base.S中的_start标签开始,主要完成以下工作:
- 确定启动HART:
assembly复制call fw_boot_hart
这个调用决定当前HART是否作为启动核,如果不是则进入等待状态。
- 环境初始化:
assembly复制# 重定位代码
la a0, _link_start
la a1, _link_end
la a2, _load_start
# 清空BSS段
la a0, _bss_start
la a1, _bss_end
- 保存启动信息:
assembly复制# 保存FDT地址
mv s1, a1
# 解析fw_dynamic_info
ld a0, 0(a2) # magic
ld a1, 8(a2) # version
ld a2, 16(a2) # next_addr
3.2 sbi_init流程
OpenSBI的核心初始化流程在sbi_init函数中完成:
- 条件检查:
c复制if (!sbi_hart_expected_privilege(&hartid, &privilege))
sbi_hart_hang();
- 冷启动选择:
c复制if (atomic_cmpxchg(&coldboot_done, 0, 1) == 0) {
init_coldboot(scratch);
} else {
init_warmboot(scratch);
}
- 平台初始化:
c复制sbi_platform_nascent_init(plat);
- 跳转到下一阶段:
c复制sbi_hart_switch_mode(hartid, scratch->next_addr,
scratch->next_mode, scratch->next_arg1);
3.3 OpenSBI的参数传递机制
OpenSBI通过scratch数据结构向下一阶段传递参数:
- scratch结构体:
c复制struct sbi_scratch {
unsigned long next_addr;
unsigned long next_mode;
unsigned long next_arg1;
// 其他字段...
};
- 跳转实现:
c复制void sbi_hart_switch_mode(unsigned long arg0, unsigned long arg1,
unsigned long next_addr, unsigned long next_mode)
{
// 设置MEPC和MSTATUS
csr_write(CSR_MEPC, next_addr);
csr_write(CSR_MSTATUS, mstatus);
// 设置寄存器
register unsigned long a0 asm("a0") = arg0;
register unsigned long a1 asm("a1") = arg1;
// 执行跳转
asm volatile("mret" : : "r"(a0), "r"(a1));
}
4. edk2/UEFI的启动流程
4.1 edk2的入口点
edk2的入口在SecEntry.S中,主要完成以下工作:
- 保存参数:
assembly复制# 保存hartid和FDT地址
mv s0, a0 # hartid
mv s1, a1 # FDT address
- 初始化临时RAM:
assembly复制# 从PCD获取临时RAM信息
la a2, PcdGet32(PcdSecPeiTempRamBase)
la a3, PcdGet32(PcdSecPeiTempRamSize)
- 调用平台初始化:
assembly复制call SecStartupPlatform
4.2 PEI-less启动路径
edk2在RISC-V平台上采用了PEI-less的启动路径,主要流程如下:
- 初始化执行环境:
c复制InitializeFloatingPointUnits();
InitializeCpuExceptionHandlers();
- 创建HOB列表:
c复制HobConstructor(HobList, TempRamBase + TempRamSize - SIZE_16KB, SIZE_16KB);
PrePeiSetHobList(HobList);
- 解析设备树:
c复制Status = FdtParseMemory (FdtBase, &MemoryBase, &MemorySize);
BuildResourceDescriptorHob (EFI_RESOURCE_SYSTEM_MEMORY,
MemoryBase, MemorySize);
- 加载DXE核心:
c复制Status = LoadDxeCoreFromFv (&DxeCoreEntryPoint);
if (!EFI_ERROR(Status)) {
DxeCoreEntryPoint (HobList.Raw, NULL, NULL);
}
5. 设备树的流转过程
5.1 QEMU阶段的设备树
QEMU在启动时动态生成设备树,主要包含以下信息:
- CPU信息:
dts复制cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
reg = <0>;
status = "okay";
};
};
- 内存布局:
dts复制memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>;
};
- 外设信息:
dts复制plic: interrupt-controller@c000000 {
#interrupt-cells = <1>;
interrupt-controller;
reg = <0xc000000 0x4000000>;
};
5.2 OpenSBI对设备树的处理
OpenSBI主要使用设备树获取以下信息:
- CPU配置:
c复制int fdt_parse_cpu(const void *fdt, struct fdt_cpu *cpu)
{
// 解析CPU节点
node = fdt_get_node_by_path(fdt, "/cpus");
// 获取CPU数量和特性
}
- 中断控制器:
c复制int fdt_parse_plic(const void *fdt, struct fdt_plic *plic)
{
// 查找PLIC节点
node = fdt_node_offset_by_compatible(fdt, -1, "riscv,plic0");
}
5.3 UEFI对设备树的处理
UEFI将设备树信息转换为HOB(Hand-Off Block)数据结构:
- 内存资源:
c复制BuildResourceDescriptorHob(EFI_RESOURCE_SYSTEM_MEMORY,
MemoryBase, MemorySize);
- ACPI表:
c复制BuildGuidDataHob(&gEfiAcpiTableGuid, AcpiTable, AcpiTableSize);
6. 关键问题解答
6.1 各组件职责划分
- QEMU:
- 模拟硬件环境
- 生成初始设备树
- 加载第一级固件(OpenSBI)
- 设置初始执行环境
- OpenSBI:
- 提供M模式运行时环境
- 实现SBI调用接口
- 管理多HART启动
- 完成M模式到S模式的切换
- UEFI:
- 提供标准化的固件接口
- 设备枚举和初始化
- 操作系统加载服务
- 提供运行时服务
6.2 设备树流转路径
- QEMU:
- 动态生成设备树
- 将DTB放入内存
- 通过a1寄存器传递DTB地址
- OpenSBI:
- 读取设备树获取硬件信息
- 保留DTB在内存中的位置
- 通过a1寄存器传递DTB地址
- UEFI:
- 解析设备树获取资源信息
- 将关键信息转换为HOB
- 可选择将DTB或ACPI传递给操作系统
6.3 固件间跳转与参数传递
- QEMU → OpenSBI:
- 跳转点:OpenSBI的入口地址
- 传递参数:
- a0:hartid
- a1:DTB地址
- a2:fw_dynamic_info结构体
- OpenSBI → UEFI:
- 跳转点:edk2的入口地址(0x20000000)
- 传递参数:
- a0:hartid
- a1:DTB地址
- UEFI → 操作系统:
- 跳转点:操作系统内核入口
- 传递参数:
- a0:hartid
- a1:DTB或ACPI表地址
7. 实际调试技巧
7.1 QEMU调试方法
- 查看设备树:
bash复制qemu-system-riscv64 -machine virt,dumpdtb=virt.dtb
dtc -I dtb -O dts virt.dtb > virt.dts
- 调试OpenSBI:
bash复制qemu-system-riscv64 -s -S
# 在另一个终端
riscv64-unknown-elf-gdb
(gdb) target remote :1234
7.2 OpenSBI调试技巧
- 打印调试信息:
c复制sbi_printf("Hart %d: Starting cold boot\n", hartid);
- 查看scratch内容:
c复制struct sbi_scratch *scratch = sbi_hart_id_to_scratch(hartid);
sbi_printf("next_addr=0x%lx, next_mode=0x%lx\n",
scratch->next_addr, scratch->next_mode);
7.3 UEFI调试方法
- 串口输出调试:
c复制DEBUG((EFI_D_INFO, "Memory Base: 0x%lx, Size: 0x%lx\n",
MemoryBase, MemorySize));
- 查看HOB列表:
c复制EFI_PEI_HOB_POINTERS Hob;
Hob.Raw = GetHobList();
while (!END_OF_HOB_LIST(Hob)) {
// 处理HOB
Hob.Raw = GET_NEXT_HOB(Hob);
}
8. 常见问题与解决方案
8.1 启动卡在OpenSBI阶段
可能原因:
- fw_dynamic_info结构体格式不匹配
- 下一阶段入口地址不正确
- 设备树解析失败
解决方案:
- 检查QEMU和OpenSBI版本兼容性
- 确认fw_dynamic.bin是否正确生成
- 添加OpenSBI调试输出,查看卡住的位置
8.2 UEFI无法找到DXE核心
可能原因:
- Flash镜像构建不正确
- FV(Firmware Volume)布局错误
- 内存映射不完整
解决方案:
- 检查edk2构建日志
- 使用UEFI Shell查看FV内容
- 确认设备树中的内存信息正确
8.3 设备树传递失败
可能原因:
- 寄存器传递错误
- 设备树地址无效
- 内存区域冲突
解决方案:
- 检查各阶段寄存器设置
- 确认设备树在内存中的位置
- 使用调试器查看内存内容
9. 性能优化建议
9.1 启动时间优化
- 精简设备树:
- 只保留必要的设备节点
- 移除未使用的兼容性字符串
- 预生成设备树:
- 避免QEMU运行时生成
- 使用静态DTB文件
9.2 内存使用优化
- 合理布局固件:
- 避免内存区域重叠
- 优化固件加载顺序
- 共享内存区域:
- 复用设备树内存
- 合理设置对齐
9.3 多HART启动优化
- 并行初始化:
- 非启动HART尽早参与
- 减少同步等待
- 按需唤醒:
- 延迟非关键HART启动
- 动态负载均衡
10. 扩展与定制
10.1 自定义OpenSBI功能
- 添加平台特定驱动:
c复制struct sbi_platform_operations platform_ops = {
.early_init = my_early_init,
.final_init = my_final_init,
};
- 扩展SBI调用:
c复制int my_sbi_handler(unsigned long fid, unsigned long arg0,
unsigned long arg1, unsigned long arg2,
unsigned long *out_val)
{
// 实现自定义功能
return 0;
}
10.2 定制UEFI固件
- 添加新驱动:
c复制EFI_STATUS
EFIAPI
MyDriverEntryPoint(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable)
{
// 驱动初始化
return EFI_SUCCESS;
}
- 修改启动流程:
c复制VOID
EFIAPI
MyPlatformInit(IN VOID *Context)
{
// 自定义初始化
}
10.3 集成其他组件
- 添加安全启动:
- 集成TEE(如OP-TEE)
- 实现镜像验证
- 支持其他操作系统:
- 调整启动参数
- 修改设备树格式
通过深入理解QEMU+OpenSBI+edk2的启动流程,开发者可以更好地定制RISC-V平台的固件解决方案,满足各种特定的应用需求。这个启动链的模块化设计也使得各个组件的替换和升级变得相对容易,为RISC-V生态的发展提供了坚实的基础。
