注:本文章的内容尚未完成,已完成的部分请参见页面侧边的目录。
RISC-V ISA 规范中给不同的平台实现留下了许多可自定义的地方。作为操作系统设计者,有必要了解目标平台的实现细节,并对自己的操作系统源代码进行针对性的调整。但 QEMU 的 RISC-V 平台的相关文档较少,如果不借助 QEMU 的源代码,操作系统设计者将很难获知 QEMU RISC-V 平台的设计细节。
本文将借助 QEMU 的源代码,分析 QEMU 的 RISC-V virt 平台的实现细节,以帮助想要理解 virt 实现细节的操作系统设计者。
下文中的所有源代码均以 QEMU v5.1.0 为准。
QEMU virt 平台介绍
virt 平台是 QEMU 上的 RISC-V 虚拟硬件平台之一。其源代码文件 hw/riscv/virt.c 开头的版权声明如下:
/*
* QEMU RISC-V VirtIO Board
*
* Copyright (c) 2017 SiFive, Inc.
*
* RISC-V machine with 16550a UART and VirtIO MMIO
*
...
*/
从声明中可以看出,virt 平台的全称是 QEMU RISC-V VirtIO Board,由 SiFive 公司设计,并且包含 16550a UART 和 VirtIO MMIO 作为外设和 I/O 接口。
使用 -machine virt
参数启动 QEMU RISC-V 即可使用 virt 平台。
与 virt 平台类似,QEMU 中还有 spike 平台,由伯克利大学博士生 Sagar Karandikar 和 SiFive 联合设计。源代码文件为 hw/riscv/spike.c。spike 的设计相比 virt 要简单很多,但本文不会对 spike 平台进行详细介绍。
virt 平台的内存空间布局
RISC-V 规范中声明,每个平台的内存空间布局可以由不同平台自己定义。virt 平台的 M-mode 内存布局在 hw/riscv/virt.c 文件中定义,名为 virt_memmap
,代码如下:
static const struct MemmapEntry {
hwaddr base;
hwaddr size;
} virt_memmap[] = {
[VIRT_DEBUG] = { 0x0, 0x100 },
[VIRT_MROM] = { 0x1000, 0xf000 },
[VIRT_TEST] = { 0x100000, 0x1000 },
[VIRT_RTC] = { 0x101000, 0x1000 },
[VIRT_CLINT] = { 0x2000000, 0x10000 },
[VIRT_PCIE_PIO] = { 0x3000000, 0x10000 },
[VIRT_PLIC] = { 0xc000000, 0x4000000 },
[VIRT_UART0] = { 0x10000000, 0x100 },
[VIRT_VIRTIO] = { 0x10001000, 0x1000 },
[VIRT_FLASH] = { 0x20000000, 0x4000000 },
[VIRT_PCIE_ECAM] = { 0x30000000, 0x10000000 },
[VIRT_PCIE_MMIO] = { 0x40000000, 0x40000000 },
[VIRT_DRAM] = { 0x80000000, 0x0 },
};
其中,对于操作系统设计者来说,比较重要的内存块有:
- 启动 ROM:
VIRT_MROM
; - 设备内存:
VIRT_DRAM
; - UART 串口:
VIRT_UART0
。
virt 平台启动流程
通过调试模式启动 QEMU 可以发现,virt 平台在 reset 时,会将 PC 置为 0x1000
。0x1000
处的内存块名为 VIRT_MROM
。配置这块内存块的 代码 如下:
/* boot rom */
memory_region_init_rom(mask_rom, NULL, "riscv_virt_board.mrom",
memmap[VIRT_MROM].size, &error_fatal);
memory_region_add_subregion(system_memory, memmap[VIRT_MROM].base,
mask_rom);
可以看出,MROM 是 mask ROM 的简写(mask 是 ROM 的一种制造工艺)。注意,源代码中的 riscv_virt_board.mrom
实际上并不对应 host OS 上的文件名,而只是这个内存区域的一个名称。
真正 向这个内存块中载入数据 的代码如下:
/* load the reset vector */
riscv_setup_rom_reset_vec(start_addr, virt_memmap[VIRT_MROM].base,
virt_memmap[VIRT_MROM].size, kernel_entry,
fdt_load_addr, s->fdt);
可以看出,这块内存的内容名为 reset vector。riscv_setup_rom_reset_vec
的代码位于 hw/riscv/boot.c 文件中,如下:
uint32_t reset_vec[10] = {
0x00000297, /* 1: auipc t0, %pcrel_hi(fw_dyn) */
0x02828613, /* addi a2, t0, %pcrel_lo(1b) */
0xf1402573, /* csrr a0, mhartid */
#if defined(TARGET_RISCV32)
0x0202a583, /* lw a1, 32(t0) */
0x0182a283, /* lw t0, 24(t0) */
#elif defined(TARGET_RISCV64)
0x0202b583, /* ld a1, 32(t0) */
0x0182b283, /* ld t0, 24(t0) */
#endif
0x00028067, /* jr t0 */
start_addr, /* start: .dword */
start_addr_hi32,
fdt_load_addr, /* fdt_laddr: .dword */
0x00000000,
/* fw_dyn: */
};
QEMU 会将这一段汇编代码写入内存地址 0x1000
处,并在虚拟 CPU 启动时执行。这段代码做的事情主要有:
- 将这段代码的基地址(即
0x1000
)载入a2
寄存器中; - 将当前的
mhartid
载入a0
寄存器中; - 将一个名为
fdt_load_addr
的值载入a1
寄存器中(FDT 可能是某种 device tree 的名称); - 跳转到
start_addr
,将控制权交给 SBI 或用户的操作系统。
在 hw/riscv/virt.c 中,start_addr
的决定规则如下:
- 如果虚拟机器上有安装 Pflash,则
start_addr
将设为VIRT_FLASH
内存块的基址(对应代码); - 否则,
start_addr
将设为VIRT_DRAM
内存块的基址。
通常,QEMU 会将 固件 或 操作系统内核 加载到 VIRT_DRAM
的偏移 0 位置。因此跳转到 start_addr
相当于将控制权交给 SBI 或用户的操作系统。
固件与内核的加载
在 hw/riscv/virt.c 中,触发固件加载的 代码 如下:
riscv_find_and_load_firmware(machine, BIOS_FILENAME,
memmap[VIRT_DRAM].base, NULL);
riscv_find_and_load_firmware
函数的实现在 hw/riscv/boot.c 文件中。其中,决定固件文件名的代码如下:
if ((!machine->firmware) || (!strcmp(machine->firmware, "default"))) {
/*
* The user didn't specify -bios, or has specified "-bios default".
* That means we are going to load the OpenSBI binary included in
* the QEMU source.
*/
firmware_filename = riscv_find_firmware(default_machine_firmware);
} else if (strcmp(machine->firmware, "none")) {
firmware_filename = riscv_find_firmware(machine->firmware);
}
总共分为 3 种情况:
- 若在 QEMU 命令行参数中没有指定
-bios
,或指定了-bios default
,则会加载 QEMU 自带的 OpenSBI 作为固件。 - 若指定了
-bios none
,则不加载固件,即firmware_filename = NULL
。 - 否则,用户可以通过
-bios
指定一个固件文件名。
默认的 OpenSBI 固件的文件名在 hw/riscv/virt.c 中定义,名为 BIOS_FILENAME
。对于 RISC-V 64 平台,该文件名为 opensbi-riscv64-virt-fw_jump.bin
。
加载固件时的 逻辑 为:
- 首先尝试将文件作为 ELF 格式加载;
- 若不成功,则再作为普通二进制格式加载,若仍不成功,则启动失败。
加载操作系统内核时的 逻辑 稍有不同,为:
- 首先尝试将文件作为 ELF 格式加载;
- 若不成功,则再尝试作为 uImage 格式加载;
- 若不成功,则再作为普通二进制格式加载,若仍不成功,则启动失败。
固件加载的基地址:从本节开始处的 hw/riscv/virt.c 的 代码 中可以看出,固件加载的基地址就是 VIRT_DRAM
内存块的基地址。但在实际加载过程中,若固件是 ELF 格式,则基地址由 ELF 的节表信息决定,不一定是 VIRT_DRAM
内存块的基地址。(当加载 ELF 格式遇到内存布局冲突时,QEMU 将输出错误信息并退出。)
内核加载的基地址:加载内核时,若内核是 ELF 格式,则基地址由 ELF 的节表信息决定。若内核是二进制格式,则基地址由 KERNEL_BOOT_ADDRESS
定义;对于 RISC-V 64 平台,该值 为 0x80200000
。