From 579d3c5a5a814c07f8dab8fad42c8a92b3c97de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ysyx=5F22040406-=E5=BC=A0=E7=82=80=E6=9D=B0?= Date: Mon, 26 Sep 2022 22:45:24 +0800 Subject: [PATCH] add 20220911-qemu-zsbl.md --- articles/20220911-qemu-zsbl.md | 829 +++++++++++++++++++++++++++++++++ 1 file changed, 829 insertions(+) create mode 100644 articles/20220911-qemu-zsbl.md diff --git a/articles/20220911-qemu-zsbl.md b/articles/20220911-qemu-zsbl.md new file mode 100644 index 0000000..51529fe --- /dev/null +++ b/articles/20220911-qemu-zsbl.md @@ -0,0 +1,829 @@ +> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.1-rc2 - [header]
+> Author: YJMSTR
+> Date: 2022/09/11
+> Revisor: Bin Meng, Falcon
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> Environment: Ubuntu22.04 LTS
+> Sponsor: PLCT Lab, ISCAS + +# QEMU 启动方式分析(3): QEMU 代码与 ZSBL 分析 + +使用软件版本如下: + +> QEMU: v7.0.0 + +## 前言 + +在本系列 [上一篇文章][2] 中,我们介绍了在 QEMU RISC-V 'virt' 平台下使用 OpenSBI + U-Boot 引导 Linux 内核的流程。本文将简要介绍 QEMU 的参数解析过程与 QOM 模型,并进一步结合 QEMU 代码分析 QEMU 对 'virt' 设备的模拟以及其 ZSBL 阶段的行为。 + +系统模式下 QEMU 程序的入口是 `/path/to/qemu/softmmu/main.c`。该函数如下所示: + +```c +int main(int argc, char **argv, char **envp) +{ + qemu_init(argc, argv, envp); + qemu_main_loop(); + qemu_cleanup(); + + return 0; +} +``` + +我们重点分析其中的 `qemu_init` 函数。 + +## 涉及的数据结构 + +在 `/path/to/qemu/include/qemu/queue.h` 中定义了四种数据结构:单链表,双向链表,简单队列,尾队列。其中: + +- 单链表:相关的宏定义以 `QSLIST` 开头。以指向表头的指针表示。只能正向遍历,移除元素时需要遍历链表,只能在表头之后或是某个已存在的元素之后插入元素。 +- 双向链表:相关的宏定义以 `QLIST` 开头。以指向表头的指针表示。只能正向遍历,移除元素时不需要遍历链表,可以在表头和已存在元素的前面或后面插入元素。 +- 简单队列:相关的宏定义以 `QSIMPLEQ` 开头。简单队列以一对分别指向队首元素与队尾元素的指针表示。元素之间单向连接,只能在队首,队尾或已存在元素之后插入新元素,只支持删除队首元素,只能正向遍历。 +- 尾队列:相关的宏定义以 `QTAILQ` 开头。其同样以一对分别指向队首元素与队尾元素的指针表示。元素之间双向连接,因此可以在不遍历队列的情况下删除队列中的任意元素。可以在队首/队尾/已存在元素的前面或后面插入元素。支持双向遍历。 + +各种宏的详细定义可以参考代码。 + +在进入 `qemu_init` 函数后,QEMU 接着调用了 `qemu_add_opts` 与 `qemu_add_drive_opts` 等函数。它们分别将作为参数传入的 `QemuOptsList` 数据结构加入 `vm_config_groups[]` 中与 `drive_config_groups[]` 中。 + + `QemuOptsList` 类型定义于 `/path/to/qemu/include/qemu/option.h`: + +```c +struct QemuOptsList { + const char *name; + const char *implied_opt_name; + bool merge_lists; /* Merge multiple uses of option into a single list? */ + QTAILQ_HEAD(, QemuOpts) head; + QemuOptDesc desc[]; +}; +``` + +其中 `QTAILQ_HEAD(, QemuOpts) head` 表示其拥有的 `QemuOpts` 尾队列的队头,`QemuOpts` 类型定义于 `/path/to/qemu/include/qemu/option_int.h`: + +```c +struct QemuOpts { + char *id; + QemuOptsList *list; + Location loc; + QTAILQ_HEAD(, QemuOpt) head; + QTAILQ_ENTRY(QemuOpts) next; +}; +``` + +其中 `QTAILQ_HEAD(, QemuOpt) head` 表示其拥有的 `QemuOpt` 类型的尾队列队头,`QTAILQ_ENTRY(QemuOpts) next` 中包含这一 `QemuOpts` 在所属尾队列中指向下一个元素的指针,以及指向前一个元素的该指针的二级指针。 + +`QemuOpt` 类型同样定义于 `/path/to/qemu/include/qemu/option_int.h` 中: + +```c +struct QemuOpt { + char *name; + char *str; + + const QemuOptDesc *desc; + union { + bool boolean; + uint64_t uint; + } value; + + QemuOpts *opts; + QTAILQ_ENTRY(QemuOpt) next; +}; +``` + +其中的 `QemuOpts *opts` 指向其所属的 `QemuOpts` 实例,`QTAILQ_ENTRY(QemuOpt) next` 中包含这一 `QemuOpt` 在所属尾队列中指向下一个元素的指针,以及指向前一个元素的该指针的二级指针。 + +其中 `QemuOptDesc` 类型定义于 `/path/to/qemu/include/qemu/option.h`: + +```c +typedef struct QemuOptDesc { + const char *name; + enum QemuOptType type; + const char *help; + const char *def_value_str; +} QemuOptDesc; +``` + +其包含了用于描述选项的若干信息。 + +由上述分析可知,每个 `QemuOptsList` 其实包含了“尾队列套尾队列”的结构,表示一类选项。这些选项中的每一个用 `QemuOpts` 表示,又包含若干的子选项,用 `QemuOpt` 表示。 + +在将这些 `QemuOptsList` 加入对应的数组之后,`QEMU` 会调用 `module_call_init(MODULE_INIT_OPTS)` 进行参数模块类型链表 `ModuleTypeList` 的初始化。 + +## 参数解析过程 + +解析分为两个阶段,所有可用的参数以 `QEMUOption` 类型存储在 `qemu_options` 数组中,并在第一阶段通过 `lookup_opt()` 函数进行查找,以确定是否是合法参数,并将其后跟随的参数(如果有)存入 `poptarg`。第二阶段同样使用 `lookup_opt()` 函数进行查找,判断模拟的目标是否支持该参数,并根据选项的类型执行不同的分支,具体实现在 `/path/to/qemu/softmmu/vl.c` 中。 + +其中,`-kernel` 参数和 `-bios` 参数对应的分支代码如下: + +```c + case QEMU_OPTION_kernel: + qdict_put_str(machine_opts_dict, "kernel", optarg); + break; +... + case QEMU_OPTION_bios: + qdict_put_str(machine_opts_dict, "firmware", optarg); + break; +``` + +此处并没有执行更进一步的操作,而是将解析出的选项与参数存至 `machine_opts_dict` 中。 + +随后在函数 `qemu_validate_options()` 中,QEMU 检查解析出的 `-kernel`, `-initrd` 和 `-append` 参数及其组合是否合法。可以在其中看见如下代码: + +```c +static void qemu_validate_options(const QDict *machine_opts) +{ + ... + if (kernel_filename == NULL) { + if (kernel_cmdline != NULL) { + error_report("-append only allowed with -kernel option"); + exit(1); + } + + if (initrd_filename != NULL) { + error_report("-initrd only allowed with -kernel option"); + exit(1); + } + } + ... +} +``` + +可以看出,QEMU 的 `-initrd` 和 `-append` 参数仅可在使用了 `-kernel` 参数的情况下使用。 + +## QOM 简介 + +QEMU 中通过 QOM (QEMU Object Model) 实现了面向对象机制。它提供了一个框架用于注册用户可创建的类,并实例化这些类的对象。它提供了以下特性: + +- 一个支持动态地注册“类”的系统 +- “类”的单继承 +- “无状态接口”的多重继承 + +QEMU 中的机器组件 (Machine) 就是通过 QOM 进行抽象的,有关 QEMU QOM 以及 使用 QOM 对机器进行抽象的更详细介绍,可以参考 QEMU 源码, [QEMU 官方文档][5] 和 [QEMU wiki][6] ,此处仅为了方便后续分析进行简要介绍。 + +QOM 中定义一个类 `MyType` 一般需要 `TypeInfo`,`MyTypeClass`,`MyTypeState`, `TypeImpl` 等结构,其中 `TypeInfo` 是用户定义类时提供的该类的信息,其会在注册到系统时被转换成 `TypeImpl`,`MyTypeClass` 与 `MyTypeState` 两个结构体分别是类与该类的对象的结构体,前者中提供该类的虚函数列表供子类实现,后者记录了该类的对象的相关信息。 + +QOM 中注册的类在 main 函数执行之前就统一添加到了链表中。以 virt 设备为例,在 `/path/to/qemu/hw/riscv/virt.c` 中,代码的最后通过 `type_init(virt_machine_init_register_types)` 将 virt 设备注册到了系统中。`type_init` 宏定义于 `include/qemu/module.h` 中: + +```c +#define type_init(function) module_init(function, MODULE_INIT_QOM) +``` + +其通过 `module_init` 宏进行操作: + +```c +#ifdef BUILD_DSO +void DSO_STAMP_FUN(void); +/* This is a dummy symbol to identify a loaded DSO as a QEMU module, so we can + * distinguish "version mismatch" from "not a QEMU module", when the stamp + * check fails during module loading */ +void qemu_module_dummy(void); + +#define module_init(function, type) \ +static void __attribute__((constructor)) do_qemu_init_ ## function(void) \ +{ \ + register_dso_module_init(function, type); \ +} +#else +/* This should not be used directly. Use block_init etc. instead. */ +#define module_init(function, type) \ +static void __attribute__((constructor)) do_qemu_init_ ## function(void) \ +{ \ + register_module_init(function, type); \ +} +#endif +``` + +根据是否定义了 `BUILD_DSO` 宏,`module_init` 将有不同的实现。未定义 `BUILD_DSO` 宏的情况下,其将通过 `register_module_init` 注册到系统中。其中加上了 `__attribute__((constructor))` 属性的函数 `do_qemu_init ## function` 将会早于 `main` 函数执行,即 QOM 类型注册是在 `main` 函数之前进行的。 + +`register_module_init` 的实现如下: + +```c +void register_module_init(void (*fn)(void), module_init_type type) +{ + ModuleEntry *e; + ModuleTypeList *l; + + e = g_malloc0(sizeof(*e)); + e->init = fn; + e->type = type; + + l = find_type(type); + + QTAILQ_INSERT_TAIL(l, e, node); +} +``` + +其通过 `find_type` 返回了一个链表(其实是”尾队列“),`find_type` 的实现如下: + +```c +static ModuleTypeList *find_type(module_init_type type) +{ + init_lists(); + + return &init_type_list[type]; +} +``` + +其中 `init_lists()` 的实现如下: + +```c +static void init_lists(void) +{ + static int inited; + int i; + + if (inited) { + return; + } + + for (i = 0; i < MODULE_INIT_MAX; i++) { + QTAILQ_INIT(&init_type_list[i]); + } + + QTAILQ_INIT(&dso_init_list); + + inited = 1; +} +``` + +它会判断数组 `init_type_list` 中的尾队列是否进行过初始化,若没有,就进行初始化。随后 `find_type` 函数会返回参数的类型所对应的尾队列,`register_module_init` 会将新的类添加到该尾队列中,并设置对应的初始化函数与类型以完成注册。 + +Machine 类型传入 `type_init` 的初始化函数是 `machine_register_types`: + +```c +static void machine_register_types(void) +{ + type_register_static(&machine_info); +} +``` + +其中 `type_register_static` 于 `/path/to/qemu/qom/object.c` 中定义: + +```c +TypeImpl *type_register(const TypeInfo *info) +{ + assert(info->parent); + return type_register_internal(info); +} + +TypeImpl *type_register_static(const TypeInfo *info) +{ + return type_register(info); +} +``` + +其最终调用了 `type_register_internal` 进行注册,相关函数如下: + +```c +static void type_table_add(TypeImpl *ti) +{ + assert(!enumerating_types); + g_hash_table_insert(type_table_get(), (void *)ti->name, ti); +} + +static TypeImpl *type_table_lookup(const char *name) +{ + return g_hash_table_lookup(type_table_get(), name); +} + +static TypeImpl *type_new(const TypeInfo *info) +{ + TypeImpl *ti = g_malloc0(sizeof(*ti)); + int i; + + g_assert(info->name != NULL); + + if (type_table_lookup(info->name) != NULL) { + fprintf(stderr, "Registering `%s' which already exists\n", info->name); + abort(); + } + + ti->name = g_strdup(info->name); + ti->parent = g_strdup(info->parent); + + ti->class_size = info->class_size; + ti->instance_size = info->instance_size; + ti->instance_align = info->instance_align; + + ti->class_init = info->class_init; + ti->class_base_init = info->class_base_init; + ti->class_data = info->class_data; + + ti->instance_init = info->instance_init; + ti->instance_post_init = info->instance_post_init; + ti->instance_finalize = info->instance_finalize; + + ti->abstract = info->abstract; + + for (i = 0; info->interfaces && info->interfaces[i].type; i++) { + ti->interfaces[i].typename = g_strdup(info->interfaces[i].type); + } + ti->num_interfaces = i; + + return ti; +} + +static TypeImpl *type_register_internal(const TypeInfo *info) +{ + TypeImpl *ti; + ti = type_new(info); + + type_table_add(ti); + return ti; +} +``` + +`type_register_internal` 将传入的 `TypeInfo` 类型参数转换为 `TypeImpl` 类型,插入哈希表中并返回转换后的结果,即完成了类的注册。 + +Machine 类的信息位于 `/path/to/qemu/hw/core/machine.c` 的 `static const TypeInfo machine_info` 中: + +```c +static const TypeInfo machine_info = { + .name = TYPE_MACHINE, + .parent = TYPE_OBJECT, + .abstract = true, + .class_size = sizeof(MachineClass), + .class_init = machine_class_init, + .class_base_init = machine_class_base_init, + .instance_size = sizeof(MachineState), + .instance_init = machine_initfn, + .instance_finalize = machine_finalize, +}; +``` + +QOM 通过 `/path/to/qemu/include/hw/boards.h` 中的 `OBJECT_DECLARE_TYPE(MachineState, MachineClass, MACHINE)` 宏完成机器类定义的大部分工作,但机器类的虚方法需要再通过定义一个 `struct MachineClass` 来给出。对象结构体是同一个文件中的 `struct MachineState`。在 `MachineClass` 中定义了若干虚方法对 `MachineState` 进行操作: + +```c +struct MachineClass { + ... + void (*init)(MachineState *state); + void (*reset)(MachineState *state); + void (*wakeup)(MachineState *state); + int (*kvm_type)(MachineState *machine, const char *arg); + ... +}; +``` + +在知道具体的机器类型后,QEMU 将会创建新的继承自 `MachineClass` 的类并给出这些虚方法的具体实现。 + +## 创建 virt 机器 + +在解析完选项与参数并进行一些其它设置后,由于我们指定了 `-M virt`,QEMU 会在随后调用的 `qemu_create_machine(machine_opts_dict)` 函数中进行 virt 机器的创建。 + +其首先调用 `select_machine` 根据支持模拟的机器列表进行判断,若支持,则从中选出给定参数对应的机器类并提供相应的属性信息,其中的参数 `MachineClass` 即为 QEMU QOM 中定义的 Machine 类,`select_machine` 选出的是其各种具体的机器类型对应的派生类,`MachineClass` 是它们共同的基类。随后执行了如下语句: + +```c +GSList *machines = object_class_get_list(TYPE_MACHINE, false); +``` + +得到一个机器类型的链表,随后将通过 `find_machine` 从中找出我们指定的机器。 + +`object_class_get_list` 的定义如下: + +```c +GSList *object_class_get_list(const char *implements_type, + bool include_abstract) +{ + GSList *list = NULL; + + object_class_foreach(object_class_get_list_tramp, + implements_type, include_abstract, &list); + return list; +} +``` + +`object_class_foreach` 定义如下: + +```c +void object_class_foreach(void (*fn)(ObjectClass *klass, void *opaque), + const char *implements_type, bool include_abstract, + void *opaque) +{ + OCFData data = { fn, implements_type, include_abstract, opaque }; + + enumerating_types = true; + g_hash_table_foreach(type_table_get(), object_class_foreach_tramp, &data); + enumerating_types = false; +} +``` + +`object_class_foreach_tramp` 定义如下: + +```c +static void object_class_foreach_tramp(gpointer key, gpointer value, + gpointer opaque) +{ + OCFData *data = opaque; + TypeImpl *type = value; + ObjectClass *k; + + type_initialize(type); + k = type->class; + + if (!data->include_abstract && type->abstract) { + return; + } + + if (data->implements_type && + !object_class_dynamic_cast(k, data->implements_type)) { + return; + } + + data->fn(k, data->opaque); +} +``` + +`object_class_get_list` 会调用 `object_class_foreach`,而该函数会对哈希表 `type_table` 中的每个元素调用 `object_class_foreach_tramp`,`object_class_foreach_tramp` 中又调用了 `type_initialize` 进行类型的初始化,`type_initialize` 中会判断 `TypeImpl` 参数是否有类的初始化函数,若有则会进行调用。 + +因此,在调用 `select_machine` 时 QEMU 就已经完成了 virt 机器对应类的初始化工作。virt 对应的机器类型的初始化函数是位于 `/path/to/qemu/hw/riscv/virt.c` 中的 `virt_machine_class_init((ObjectClass *oc, void *data))`,在后文中将会对其进行具体分析。 + +接下来的 `object_set_machine_compat_props()` 设置 virt 机器的全局属性,随后通过调用 `set_memory_options(machine_class)` 设置内存大小,若在 QEMU 启动参数中没有指定内存大小,则该函数会根据 `machine_class` 中的信息将其设置为 virt 平台的默认大小。 + +随后,QEMU 根据选择出的机器类型创建一个该类的实例 `current_machine`: + +```c +current_machine = MACHINE(object_new_with_class(OBJECT_CLASS(machine_class))); +``` + +其中 `current_machine` 是一个指向 MachineState 的指针,声明于 `/path/to/qemu/hw/core/machine.c` 中。此时的 `current_machine` 即代表了 virt 机器,对 virt 的各类初始化操作即对 `current_machine` 进行操作。 + +QEMU 在进行一些其它初始化操作后退出 `qemu_create_machine()` 函数,并接着调用了 `qemu_apply_legacy_machine_options(machine_opts_dict)`, `qemu_apply_machine_options(machine_opts_dict)` 等函数,按照之前解析出的设备相关参数 `machine_opts_dict` 进行配置。 + +随后有如下代码: + +```c +if (!preconfig_requested) { + qmp_x_exit_preconfig(&error_fatal); +} +``` + +函数 `qmp_x_exit_preconfig()` 中进行了如下操作: + +```c +void qmp_x_exit_preconfig(Error **errp) +{ + if (phase_check(PHASE_MACHINE_INITIALIZED)) { + error_setg(errp, "The command is permitted only before machine initialization"); + return; + } + + qemu_init_board(); + qemu_create_cli_devices(); + qemu_machine_creation_done(); + ... +} +``` + +此处的“qmp”指 [QEMU Machine Protocol][7],是一种基于 JSON 的协议,它允许应用程序控制 QEMU 实例。当启用了 `--preconfig` 参数后,QEMU 将会在完成创建初始虚拟机之前暂停,进入“preconfig”状态并允许通过 QMP 进行一些配置。`if (!preconfig_requested)` 判断是否要求进入“preconfig”状态,若没有,则会直接调用 `qmp_x_exit_preconfig()` 函数退出“preconfig”态并完成初始虚拟机的创建。 + +其中 `qemu_init_board()` 函数中调用了 `machine_run_board_init()` 进行机器(主板)的一些初始化操作。 + +在 `machine_run_board_init()` 中,首先通过 `MACHINE_GET_CLASS` 宏得到传入的 MachineState 对象对应的派生类,此处即 virt 对应的机器类: + +```c +MachineClass *machine_class = MACHINE_GET_CLASS(machine); +``` + +在完成一些初始化操作后,又调用了 `machine_class->init(machine)`,来调用该机器类型的初始化方法对实例 machine 进行初始化。借助 gdb 等工具可知此处实际调用了 `/path/to/qemu/hw/riscv/virt.c` 中的 `virt_machine_init(MachineState *machine)`,来对 virt 设备进行初始化。 + +## virt 代码与 ZSBL 分析 + +QEMU 模拟 RISC-V“virt”机器相关的代码位于 `/path/to/qemu/hw/riscv` 目录下的 `virt.c` 与 `virt.h` 中。ZSBL 指 Zero Stage Bootloader,在这一阶段 virt 机器从 ROM 中取指,进行初始化并跳转到 FSBL 或是 Runtime,接下来我们将对这一过程进行分析。 + +在 `virt.c` 中,给出了 virt 平台的地址分布如下: + +```c +static const MemMapEntry virt_memmap[] = { + [VIRT_DEBUG] = { 0x0, 0x100 }, + [VIRT_MROM] = { 0x1000, 0xf000 }, + [VIRT_TEST] = { 0x100000, 0x1000 }, + [VIRT_RTC] = { 0x101000, 0x1000 }, + [VIRT_CLINT] = { 0x2000000, 0x10000 }, + [VIRT_ACLINT_SSWI] = { 0x2F00000, 0x4000 }, + [VIRT_PCIE_PIO] = { 0x3000000, 0x10000 }, + [VIRT_PLIC] = { 0xc000000, VIRT_PLIC_SIZE(VIRT_CPUS_MAX * 2) }, + [VIRT_APLIC_M] = { 0xc000000, APLIC_SIZE(VIRT_CPUS_MAX) }, + [VIRT_APLIC_S] = { 0xd000000, APLIC_SIZE(VIRT_CPUS_MAX) }, + [VIRT_UART0] = { 0x10000000, 0x100 }, + [VIRT_VIRTIO] = { 0x10001000, 0x1000 }, + [VIRT_FW_CFG] = { 0x10100000, 0x18 }, + [VIRT_FLASH] = { 0x20000000, 0x4000000 }, + [VIRT_IMSIC_M] = { 0x24000000, VIRT_IMSIC_MAX_SIZE }, + [VIRT_IMSIC_S] = { 0x28000000, VIRT_IMSIC_MAX_SIZE }, + [VIRT_PCIE_ECAM] = { 0x30000000, 0x10000000 }, + [VIRT_PCIE_MMIO] = { 0x40000000, 0x40000000 }, + [VIRT_DRAM] = { 0x80000000, 0x0 }, +}; +``` + +可以观察到内存 `VIRT_DRAM` 是从 0x80000000 开始的,大小没有在此处指定,而是根据用户指定的参数进行设置。若用户未指定该参数,则会将其设置为默认值。 + +`virt_machine` 类的 `TypeInfo` 如下: + +```c +static const TypeInfo virt_machine_typeinfo = { + .name = MACHINE_TYPE_NAME("virt"), + .parent = TYPE_MACHINE, + .class_init = virt_machine_class_init, + .instance_init = virt_machine_instance_init, + .instance_size = sizeof(RISCVVirtState), +}; +``` + +其中指定了父类类型 TYPE_MACHINE,类初始化函数 `virt_machine_class_init`,并在之后通过 `type_init(virt_machine_init_register_types)` 注册了该类。`type_init` 宏已在前文进行分析,由于指定了 `.class_init` 为 `virt_machine_class_init`,因此在 `type_initialize` 时将会调用该函数进行 virt 对应的机器类的初始化。 + +在 `virt_machine_class_init` 中,为 virt 类的对象指定了初始化函数 `virt_machine_init`,它首先进行处理器等模拟硬件的相关初始化,以及设备树的创建。 + +首先是对 sockets 数量进行判断,判断其是否不超过支持的最大 socket 数。随后进行各 socket 及中断控制器的初始化,并设置支持的最大内存大小。 + +接下来的代码首先进行了 ROM 的初始化: + +```c + /* 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); +``` + +随后进行的是 `-bios` 参数指定的二进制文件的加载: + +```c + /* + * Only direct boot kernel is currently supported for KVM VM, + * so the "-bios" parameter is ignored and treated like "-bios none" + * when KVM is enabled. + */ + if (kvm_enabled()) { + g_free(machine->firmware); + machine->firmware = g_strdup("none"); + } + + if (riscv_is_32bit(&s->soc[0])) { + firmware_end_addr = riscv_find_and_load_firmware(machine, + RISCV32_BIOS_BIN, start_addr, NULL); + } else { + firmware_end_addr = riscv_find_and_load_firmware(machine, + RISCV64_BIOS_BIN, start_addr, NULL); + } +``` + +在注释中提到,由于 KVM 目前仅支持直接引导内核,因此开启 KVM 时,`-bios` 参数一律视为 `-bios none`。 + +接下来判断机器的位数并通过 `riscv_find_and_load_firmware()` 函数加载 `-bios` 参数指定的固件。该函数定义于 `/path/to/qemu/hw/riscv/boot.c` 中: + +```c +target_ulong riscv_find_and_load_firmware(MachineState *machine, + const char *default_machine_firmware, + hwaddr firmware_load_addr, + symbol_fn_t sym_cb) +{ + char *firmware_filename = NULL; + target_ulong firmware_end_addr = firmware_load_addr; + + 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); + } + + if (firmware_filename) { + /* If not "none" load the firmware */ + firmware_end_addr = riscv_load_firmware(firmware_filename, + firmware_load_addr, sym_cb); + g_free(firmware_filename); + } + + return firmware_end_addr; +} + +``` + +其中第一个 if 对用户未指定 `-bios` 参数及用户指定了 `-bios default` 参数的情况进行处理,该情况下 QEMU 将会加载自带的 OpenSBI 二进制文件,并通过 `riscv_find_firmware` 函数直接查找用户输入的文件名。32 位和 64 位的默认文件分别是 `opensbi-riscv32-generic-fw_dynamic.bin` 与 `opensbi-riscv64-generic-fw_dynamic.bin`, + +第二个 if 中处理了用户通过 `-bios` 参数指定文件的情况。其通过 `riscv_find_firmware` 函数直接查找用户输入的文件名。 + +最后一个 if 根据之前解析出的文件名通过 `riscv_load_firmware` 进行固件的加载,该函数支持加载 ELF 格式文件和普通二进制文件。 + +随后回到 `virt.c` 中,若用户指定了 `-kernel` 参数,接下来的代码将进行对应的加载: + +```c + if (machine->kernel_filename) { + kernel_start_addr = riscv_calc_kernel_start_addr(&s->soc[0], + firmware_end_addr); + + kernel_entry = riscv_load_kernel(machine->kernel_filename, + kernel_start_addr, NULL); + + if (machine->initrd_filename) { + hwaddr start; + hwaddr end = riscv_load_initrd(machine->initrd_filename, + machine->ram_size, kernel_entry, + &start); + qemu_fdt_setprop_cell(machine->fdt, "/chosen", + "linux,initrd-start", start); + qemu_fdt_setprop_cell(machine->fdt, "/chosen", "linux,initrd-end", + end); + } + } else { + /* + * If dynamic firmware is used, it doesn't know where is the next mode + * if kernel argument is not set. + */ + kernel_entry = 0; + } +``` + +首先根据之前计算出的固件在内存中的结束地址 `firmware_end_addr` 与机器的位数对 kernel 的起始地址 `kernel_start_addr` 进行计算,根据机器位数的不同,内存的对齐方式也不同。32 位和 64 位机器分别按 4 MiB 和 2 MiB 进行对齐。 + +随后通过 `riscv_load_kernel` 函数加载内核并计算内核入口。该函数中除了用到了 `riscv_load_firmware` 加载 ELF 格式与普通二进制格式的两个函数外,还有 `load_uimage_as` 函数进行 uImage 格式文件的加载。 + +接下来的 if 判断用户是否通过 `-initrd` 参数指定了初始 RAM 磁盘文件,并进行加载及相关设置。 + +若用户没有指定 `-kernel` 参数,或是使用了 dynamic 类型的固件,则此处将不会设置内核入口。 + +接下来这段代码检测是否存在 PFLASH 设备,如果存在,就将起始地址从默认值 DRAM 起始位置 `memmap[VIRT_DRAM].base` 改成 FLASH 的起始地址 `virt_memmap[VIRT_FLASH].base`: + +```c + if (drive_get(IF_PFLASH, 0, 0)) { + /* + * Pflash was supplied, let's overwrite the address we jump to after + * reset to the base of the flash. + */ + start_addr = virt_memmap[VIRT_FLASH].base; + } +``` + +随后进行 fw_cfg 的初始化与 fdt 在 dram 中加载地址的计算: + +```c + /* + * Init fw_cfg. Must be done before riscv_load_fdt, otherwise the device + * tree cannot be altered and we get FDT_ERR_NOSPACE. + */ + s->fw_cfg = create_fw_cfg(machine); + rom_set_fw(s->fw_cfg); + + /* Compute the fdt load address in dram */ + fdt_load_addr = riscv_load_fdt(memmap[VIRT_DRAM].base, + machine->ram_size, machine->fdt); +``` + +fw_cfg 是指 firmware configuration。这是虚拟机获取 QEMU 提供数据的一种接口,此处配置了 virt 对象的该接口。 + +函数 `riscv_load_fdt` 中,将 fdt 地址设置为了内存结束地址与 3 GB 中的较小值减去 fdt 的大小后按 16 MiB 对齐向下取整: + +```c + temp = MIN(dram_end, 3072 * MiB); + fdt_addr = QEMU_ALIGN_DOWN(temp - fdtsize, 16 * MiB); +``` + +随后会通过 `rom_add_blob_fixed_as` 宏调用 `rom_add_blob` 函数,拷贝 fdt 到 ROM 的 fdt_addr 处: + +```c + rom_add_blob_fixed_as("fdt", fdt, fdtsize, fdt_addr, + &address_space_memory); +``` + +接下来的代码加载复位向量(reset vector)进 ROM: + +```c + /* load the reset vector */ + riscv_setup_rom_reset_vec(machine, &s->soc[0], start_addr, + virt_memmap[VIRT_MROM].base, + virt_memmap[VIRT_MROM].size, kernel_entry, + fdt_load_addr, machine->fdt); + +``` + +`riscv_setup_rom_reset_vec` 中复位向量如下所示: + +```c + /* reset vector */ + uint32_t reset_vec[10] = { + 0x00000297, /* 1: auipc t0, %pcrel_hi(fw_dyn) */ + 0x02828613, /* addi a2, t0, %pcrel_lo(1b) */ + 0xf1402573, /* csrr a0, mhartid */ + 0, + 0, + 0x00028067, /* jr t0 */ + start_addr, /* start: .dword */ + start_addr_hi32, + fdt_load_addr, /* fdt_laddr: .dword */ + 0x00000000, + /* fw_dyn: */ + }; + if (riscv_is_32bit(harts)) { + reset_vec[3] = 0x0202a583; /* lw a1, 32(t0) */ + reset_vec[4] = 0x0182a283; /* lw t0, 24(t0) */ + } else { + reset_vec[3] = 0x0202b583; /* ld a1, 32(t0) */ + reset_vec[4] = 0x0182b283; /* ld t0, 24(t0) */ + } +``` + +其随后也通过 `rom_add_blob_fixed_as` 将复位向量拷贝至 ROM 中,复位向量中的代码被拷贝进了 ROM 的起始位置。virt 机器启动后最先执行的指令就是 ROM 起始位置处的这些指令。 + +复位向量中的指令将 fw_dyn 的地址存入 a2,fw_dyn 具体是什么将在后文进行分析。 + +运行当前代码的硬件线程的 id (mhartid) 被存入 a0。随后,分别将 `start_addr` 和 `fdt_load_addr` 的值读入 a1 和 t0 中,最后跳转到 t0 指向的位置,即 `start_addr` 指向的位置。在之前分析过的代码中: + +```c + if (riscv_is_32bit(&s->soc[0])) { + firmware_end_addr = riscv_find_and_load_firmware(machine, + RISCV32_BIOS_BIN, start_addr, NULL); + } else { + firmware_end_addr = riscv_find_and_load_firmware(machine, + RISCV64_BIOS_BIN, start_addr, NULL); + } +``` + +已经将固件加载到了 virt 机器的 DRAM 的基地址。若没有添加 PFLASH,virt 机器此时将会跳转到 firmware 所处位置。 + +随后调用了 `riscv_rom_copy_firmware_info` 函数: + +```c + riscv_rom_copy_firmware_info(machine, rom_base, rom_size, sizeof(reset_vec), + kernel_entry); +``` + +从函数的注释中我们可以得知,存放在复位向量的汇编代码中存放进 a2 的是 dynamic 类型固件信息的地址。其中,结构体 `fw_dynamic_info` 的定义如下,其位于 `/path/to/qemu/include/hw/riscv/boot_opensbi.h` 中: + +```c +/** Representation dynamic info passed by previous booting stage */ +struct fw_dynamic_info { + /** Info magic */ + target_long magic; + /** Info version */ + target_long version; + /** Next booting stage address */ + target_long next_addr; + /** Next booting stage mode */ + target_long next_mode; + /** Options for OpenSBI library */ + target_long options; + /** + * Preferred boot HART id + * + * It is possible that the previous booting stage uses same link + * address as the FW_DYNAMIC firmware. In this case, the relocation + * lottery mechanism can potentially overwrite the previous booting + * stage while other HARTs are still running in the previous booting + * stage leading to boot-time crash. To avoid this boot-time crash, + * the previous booting stage can specify last HART that will jump + * to the FW_DYNAMIC firmware as the preferred boot HART. + * + * To avoid specifying a preferred boot HART, the previous booting + * stage can set it to -1UL which will force the FW_DYNAMIC firmware + * to use the relocation lottery mechanism. + */ + target_long boot_hart; +}; + +``` + +根据注释可知,这个结构体的作用是存储上一个引导阶段传入的信息,准备将其传递给 OpenSBI。要传递的信息包括魔数、版本信息、下一阶段的地址、下一阶段的模式、OpenSBI 库的选项和指定引导过程优先使用硬件线程的 id。 + +回到 `riscv_rom_copy_firmware_info`,它在完成 `struct fw_dynamic_info` 的初始化后,将会判断 ROM 剩余空间的大小,并将这个结构体拷贝进 ROM 中: + + ```c + if (dinfo_len > (rom_size - reset_vec_size)) + { + error_report("not enough space to store dynamic firmware info"); + exit(1); + } + + rom_add_blob_fixed_as("mrom.finfo", &dinfo, dinfo_len, + rom_base + reset_vec_size, + &address_space_memory); + ``` + +## 小结 + +本文简要介绍了 QEMU 中常用的数据结构、参数的解析过程、QOM,以及 RISC-V virt 设备的初始化,并对 virt 设备 ZSBL 阶段的行为进行了分析。在后续篇章中我们将进一步介绍 OpenSBI 引入 fw_dynamic 这类 firmware 并取代 fw_jump 与 fw_payload 成为 QEMU 推荐的默认选择的原因,并分析 SBI 规范的 HSM 扩展在 RISC-V 启动路径上的实现。 + +## 参考资料 + +1. [gdb 文档][1] +2. [QEMU 启动方式分析(1):QEMU 及 RISC-V 启动流程简介][3] +3. [QEMU 启动方式分析(2): QEMU 'virt' 平台下通过 OpenSBI + U-Boot 引导 RISCV64 Linux Kernel][2] +4. [QEMU 文档][4] +5. [QEMU QOM 文档][5] +6. [QEMU wiki][6] +7. [QEMU QMP][7] +8. [QEMU 源码分析-QOM][8] +9. [QEMU virt 分析][9] +10. 《QEMU/KVM 源码解析与应用》,李强 + +[1]: https://sourceware.org/gdb/documentation/ +[2]: https://gitee.com/tinylab/riscv-linux/blob/master/articles/20220823-boot-riscv-linux-kernel-with-uboot-on-qemu-virt-machine.md +[3]: https://gitee.com/tinylab/riscv-linux/blob/master/articles/20220816-introduction-to-qemu-and-riscv-upstream-boot-flow.md +[4]: https://qemu.readthedocs.io/en/latest/index.html +[5]: https://qemu.readthedocs.io/en/latest/devel/qom.html +[6]: https://wiki.qemu.org/Features/QOM/Machine +[7]: https://qemu.readthedocs.io/en/latest/system/managed-startup.html +[8]: http://lifeislife.cn/2022/03/09/QEMU源码分析-QOM/ +[9]: https://juejin.cn/post/6891922292075397127 -- Gitee