起源
由于需要在一个没有磁盘的系统上启动一个 linux 并做一些测试,这就需要一个大的 ramfs,来存放需要测试的程序。
在上硬件之前需要先在qemu中测试 ramfs 是不是正确的,eg:使用一个 800MB 的文件系统,会出现 initrd 与 fdt overlap 的报错,故研究一下 qemu 是如何加载这几部分到虚拟机的内存中的。
这里主要说一下 qemu-system-riscv64 virt 相关的加载
关于 opensbi 的 fw_payload 如何布局 payload 与 fdt 则在其他 blog 中进行说明
结论
开篇先给出结论,然后再结合 qemu 的代码分析
主要以 riscv64 为例,其他架构可以采用类似的方法分析
对于 qemu-system-riscv64,mem 的起始地址为 0x80000000 来说:
一旦内存的大小确认,那么 initrd 与 fdt 加载的地址即确定,qemu 提供了一个计算公式,并预留了一个比较大的空间来避免 overlap
- initrd 加载地址:
如果 mem_size >= 1GB, 那么加载到 kernel_entry + 512 MB 的位置
如果 mem_size < 1GB, 那么加载到 kernel_entry + mem_size/2 的位置 - 设备树加载地址:
MIN(ram_end, 3GB) 进行 2MB 向下对齐
eg:
mem_size = 1GB, 设备树加载到 0x9fe00000
mem_size >= 3GB, 设备树加载到 0xbfe00000
代码证据
口说无凭,下面给出 qemu (riscv64)代码中相关的部分,从中可以找到结论的来源
hw/risv/boot.c
中包含了一些初始化工作,包含了加载的相关内容
riscv_load_initrd、riscv_compute_fdt_addr 中提供了两者的计算公式
qemu-system-riscv64 默认 firmware(opensbi)加载到 0x80000000、 kernel 加载到 0x80200000,这两部分一般不容易出现问题
c
// hw/risv/boot.c
// 加载 Firmware
target_ulong riscv_load_firmware(const char *firmware_filename, // 固件文件路径
hwaddr *firmware_load_addr, // 实际加载地址
symbol_fn_t sym_cb)
{
uint64_t firmware_entry, firmware_end;
ssize_t firmware_size;
g_assert(firmware_filename != NULL);
// 尝试通过 ELF 加载固件
if (load_elf_ram_sym(firmware_filename, NULL, NULL, NULL, // ELF 文件路径
&firmware_entry, NULL, &firmware_end, NULL, // ELF 文件入口地址和结束地址
0, EM_RISCV, 1, 0, NULL, true, sym_cb) > 0) { // 目标架构是 RISCV
*firmware_load_addr = firmware_entry; // 修改 firmware_load_addr
return firmware_end; // 将 firmware_end 作为末尾地址
}
// 尝试按照普通二进制文件加载
firmware_size = load_image_targphys_as(firmware_filename, // 二进制文件路径
*firmware_load_addr, // 固件加载的起始地址
current_machine->ram_size, NULL); // 当前机器的 RAM 大小
if (firmware_size > 0) {
return *firmware_load_addr + firmware_size; // 返回 firmware_end
}
error_report("could not load firmware '%s'", firmware_filename);
exit(1);
}
// 加载 Initrd
static void riscv_load_initrd(MachineState *machine, uint64_t kernel_entry)
{
...
// initrd 加载地址 [这里决定了 initrd 被加载的位置]
// 如果内存 >= 1GB, 那么加载到 kernel_entry + 512 MB 的位置
// 如果内存 < 1GB, 那么记载到 kernel_entry + mem_size/2 的位置
start = kernel_entry + MIN(mem_size / 2, 512 * MiB);
// 优先使用 load_ramdisk 加载, 加载到 start, 最大可用空间为 mem_size - start
size = load_ramdisk(filename, start, mem_size - start);
if (size == -1) {
size = load_image_targphys(filename, start, mem_size - start);
if (size == -1) {
error_report("could not load ramdisk '%s'", filename);
exit(1);
}
}
/* Some RISC-V machines (e.g. opentitan) don't have a fdt. */
if (fdt) { // 如果 fdt 存在, 则在设备树的 /chosen 节点添加以下两个属性
end = start + size;
qemu_fdt_setprop_u64(fdt, "/chosen", "linux,initrd-start", start);
qemu_fdt_setprop_u64(fdt, "/chosen", "linux,initrd-end", end);
}
}
target_ulong riscv_calc_kernel_start_addr(RISCVHartArrayState *harts,
target_ulong firmware_end_addr) {
if (riscv_is_32bit(harts)) {
return QEMU_ALIGN_UP(firmware_end_addr, 4 * MiB);
} else {
return QEMU_ALIGN_UP(firmware_end_addr, 2 * MiB); // 默认 firmware_end_addr 向上 2MB 对齐
}
}
// 加载 kernel
target_ulong riscv_load_kernel(MachineState *machine,
RISCVHartArrayState *harts,
target_ulong kernel_start_addr,
bool load_initrd,
symbol_fn_t sym_cb)
{
...
// 尝试使用 ELF 格式加载
if (load_elf_ram_sym(kernel_filename, NULL, NULL, NULL, // kernel 文件名
NULL, &kernel_load_base, NULL, NULL, 0, // 保存加载地址
EM_RISCV, 1, 0, NULL, true, sym_cb) > 0) { // 架构为 RISCV
kernel_entry = kernel_load_base; // 修改 kernel_entry = kernel_load_base
goto out;
}
...
// 尝试加载原始二进制文件 Image
if (load_image_targphys_as(kernel_filename, kernel_start_addr, // 默认加载到 kernel_start_addr
current_machine->ram_size, NULL) > 0) {
kernel_entry = kernel_start_addr;
goto out;
}
...
return kernel_entry;
}
uint64_t riscv_compute_fdt_addr(hwaddr dram_base, hwaddr dram_size,
MachineState *ms)
{
int ret = fdt_pack(ms->fdt);
hwaddr dram_end, temp;
int fdtsize;
/* Should only fail if we've built a corrupted tree */
g_assert(ret == 0);
fdtsize = fdt_totalsize(ms->fdt); // 获取设备树大小
if (fdtsize <= 0) {
error_report("invalid device-tree");
exit(1);
}
/*
* A dram_size == 0, usually from a MemMapEntry[].size element,
* means that the DRAM block goes all the way to ms->ram_size.
*/
dram_end = dram_base;
dram_end += dram_size ? MIN(ms->ram_size, dram_size) : ms->ram_size;
/*
* We should put fdt as far as possible to avoid kernel/initrd overwriting
* its content. But it should be addressable by 32 bit system as well.
* Thus, put it at an 2MB aligned address that less than fdt size from the
* end of dram or 3GB whichever is lesser.
*/
// 当 dram_base < 3GB,那么 temp = dram_end 与 3GB 的小值 [这种情况]
// 当 dram_base > 3GB, 那么 temp = dram_end
temp = (dram_base < 3072 * MiB) ? MIN(dram_end, 3072 * MiB) : dram_end;
// MIN(dram_end, 3GB) 进行 2MB 向下对齐
// 对于 virt, drambase=0x8000000 (2GB)
// eg: mem=1GB, 0x9fe00000
// mem>=3GB, 0xbfe00000
return QEMU_ALIGN_DOWN(temp - fdtsize, 2 * MiB);
}