diff --git a/articles/20220911-qemu-zsbl.md b/articles/20220911-qemu-zsbl.md
new file mode 100644
index 0000000000000000000000000000000000000000..51529fe9e01e9a42327d954ce2586d547bd3f147
--- /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