Qemu 加载你指定的 initrd、dtb 到哪里?

起源

由于需要在一个没有磁盘的系统上启动一个 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

  1. initrd 加载地址:
    如果 mem_size >= 1GB, 那么加载到 kernel_entry + 512 MB 的位置
    如果 mem_size < 1GB, 那么加载到 kernel_entry + mem_size/2 的位置
  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);
}
相关推荐
jiuri_12157 天前
QEMU 搭建arm linux开发环境
linux·arm开发·qemu
skywalk816323 天前
尝试qemu仿真VisionFive2 OpenKylin系统
qemu·kylin
公西雒3 个月前
关于在GitLab的CI/CD中用docker buildx本地化多架构打包dotnet应用的问题
ci/cd·docker·gitlab·qemu·dotnet
ywang_wnlo4 个月前
【Kenel】基于 QEMU 的 Linux 内核编译和安装
linux·qemu·kernel
ywang_wnlo4 个月前
【Kernel】基于 QEMU 的 Linux 内核编译和安装
linux·qemu·kernel
plmm烟酒僧4 个月前
qemu模拟arm64环境-构建6.1内核以及debian12
linux·debian·qemu·虚拟机·香橙派·aarch64
思禾5 个月前
Qemu开发ARM篇-3、qemu运行uboot演示
linux·arm开发·qemu·uboot
张世争5 个月前
rtems 5.3 qemu realview_pbx_a9 环境搭建:生成 rtems arm 工具链
qemu·rtems·realview_pbx_a9
EastWood20136 个月前
qemu:gpio使用
qemu