[置顶] 泰晓 RISC-V 实验箱,配套 30+ 讲嵌入式 Linux 系统开发公开课
QEMU 启动方式分析(3): QEMU 代码与 RISCV virt 平台 ZSBL 分析
Corrector: TinyCorrect v0.1-rc3 - [spaces images] Author: YJMSTR jay1273062855@outlook.com Date: 2022/09/11 Revisor: Bin Meng, Falcon Project: RISC-V Linux 内核剖析 Environment: Ubuntu22.04 LTS Sponsor: PLCT Lab, ISCAS
前言
在本系列 上一篇文章 中,我们介绍了在 QEMU RISC-V ‘virt’ 平台下使用 OpenSBI + U-Boot 引导 Linux 内核的流程。本文将根据 QEMU 启动 RISC-V ‘virt’ 设备的流程,简要介绍 QEMU 的参数解析过程与 QOM 模型,并进一步结合 QEMU 代码分析 QEMU 对 RISC-V ‘virt’ 设备的模拟以及其 Zeroth Stage Boot Loader(ZSBL)阶段的行为。
系统模式下 QEMU 程序的入口是 softmmu/main.c
。该函数如下所示:
int main(int argc, char **argv, char **envp)
{
qemu_init(argc, argv, envp);
qemu_main_loop();
qemu_cleanup();
return 0;
}
其中 qemu_init
函数负责 QEMU 的初始化,包括参数解析、设备初始化并根据解析的参数进行配置。qemu_main_loop
是 QEMU 的主循环,它将循环监听事件并进行处理。qemu_cleanup
进行退出时的相关清理,释放占用的资源。
本文分析的目标 QEMU RISC-V ‘virt’ 机器的 ZSBL 阶段同样属于 qemu_init
函数处理的范围,因此,我们重点关注其中的 qemu_init
函数。本文使用的 QEMU 版本为 v7.0.0。
参数解析过程
参数解析过程的流程图如下所示:
涉及的数据结构
在 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
类型定义于 include/qemu/option.h
:
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
类型定义于 include/qemu/option_int.h
:
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
类型同样定义于 include/qemu/option_int.h
中:
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
类型定义于 include/qemu/option.h
:
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
数组中,这个数组是根据 qemu-options.hx
文件进行填充的。在参数解析的第一阶段会通过 lookup_opt()
函数在该数组中进行查找,以确定是否是合法参数,并将其后跟随的参数(如果有)存入 poptarg
。第二阶段同样使用 lookup_opt()
函数进行查找,判断模拟的目标是否支持该参数,并根据选项的类型执行不同的分支,具体实现在 softmmu/vl.c
中。
其中,-kernel
参数和 -bios
参数对应的分支代码如下:
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
参数及其组合是否合法。可以在其中看见如下代码:
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
参数的情况下使用。
RISC-V ‘virt’ 初始化
QOM 简介
QEMU 中通过 QOM(QEMU Object Model)实现了面向对象机制。它提供了一个框架用于注册用户可创建的类,并实例化这些类的对象。它提供了以下特性:
- 一个支持动态地注册“类”的系统
- “类”的单继承
- “无状态接口”的多重继承
QEMU 中的机器组件(Machine)就是通过 QOM 进行抽象的,有关 QEMU QOM 以及使用 QOM 对机器进行抽象的更详细介绍,可以参考 QEMU 源码,QEMU 官方文档 和 QEMU wiki,此处仅为了方便后续分析进行简要介绍。
QOM 中注册类的流程如下图所示:
QOM 中定义一个类 MyType
一般需要 TypeInfo
,MyTypeClass
,MyTypeState
,TypeImpl
等结构,其中 TypeInfo
是用户定义类时提供的该类的信息,其会在注册到系统时被转换成 TypeImpl
,MyTypeClass
与 MyTypeState
两个结构体分别是类与该类的对象的结构体,前者中提供该类的虚函数列表供子类实现,后者记录了该类的对象的相关信息。
QOM 中注册的类在 main 函数执行之前就统一添加到了链表中。以 virt 设备为例,在 hw/riscv/virt.c
中,代码的最后通过 type_init(virt_machine_init_register_types)
将 virt 设备注册到了系统中。type_init
宏定义于 include/qemu/module.h
中:
#define type_init(function) module_init(function, MODULE_INIT_QOM)
其通过 module_init
宏进行操作:
#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
的实现如下:
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
的实现如下:
static ModuleTypeList *find_type(module_init_type type)
{
init_lists();
return &init_type_list[type];
}
其中 init_lists()
的实现如下:
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
:
static void machine_register_types(void)
{
type_register_static(&machine_info);
}
其中 type_register_static
于 qom/object.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
进行注册,相关函数如下:
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 类的信息位于 hw/core/machine.c
的 static const TypeInfo machine_info
中:
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 通过 include/hw/boards.h
中的 OBJECT_DECLARE_TYPE(MachineState, MachineClass, MACHINE)
宏完成机器类定义的大部分工作,但机器类的虚方法需要再通过定义一个 struct MachineClass
来给出。对象结构体是同一个文件中的 struct MachineState
。在 MachineClass
中定义了若干虚方法对 MachineState
进行操作:
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 机器
创建 virt 机器涉及到的函数调用关系如下图所示:
在解析完选项与参数并进行一些其它设置后,由于我们指定了 -M virt
,QEMU 会在随后调用的 qemu_create_machine(machine_opts_dict)
函数中进行 virt 机器的创建。
其首先调用 select_machine
根据支持模拟的机器列表进行判断,若支持,则从中选出给定参数对应的机器类并提供相应的属性信息,其中的参数 MachineClass
即为 QEMU QOM 中定义的 Machine 类,select_machine
选出的是其各种具体的机器类型对应的派生类,MachineClass
是它们共同的基类。随后执行了如下语句:
GSList *machines = object_class_get_list(TYPE_MACHINE, false);
得到一个机器类型的链表,随后将通过 find_machine
从中找出我们指定的机器。
object_class_get_list
的定义如下:
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
定义如下:
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
定义如下:
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 对应的机器类型的初始化函数是位于 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
:
current_machine = MACHINE(object_new_with_class(OBJECT_CLASS(machine_class)));
其中 current_machine
是一个指向 MachineState 的指针,声明于 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
进行配置。
随后有如下代码:
if (!preconfig_requested) {
qmp_x_exit_preconfig(&error_fatal);
}
函数 qmp_x_exit_preconfig()
中进行了如下操作:
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,是一种基于 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 对应的机器类:
MachineClass *machine_class = MACHINE_GET_CLASS(machine);
在完成一些初始化操作后,又调用了 machine_class->init(machine)
,来调用该机器类型的初始化方法对实例 machine 进行初始化。借助 gdb 等工具可知此处实际调用了 hw/riscv/virt.c
中的 virt_machine_init(MachineState *machine)
,来对 virt 设备进行初始化。
virt 代码与 ZSBL 分析
QEMU 模拟 RISC-V ‘virt’ 机器相关的代码位于 hw/riscv
目录下的 virt.c
与 virt.h
中。ZSBL 指 Zero Stage Bootloader,在这一阶段 virt 机器从 ROM 中取指,进行初始化并跳转到 FSBL 或是 Runtime,接下来我们将对这一过程进行分析。
在 virt.c
中,给出了 virt 平台的地址分布如下:
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
如下:
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 的初始化:
/* 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
参数指定的二进制文件的加载:
/*
* 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
参数指定的固件。该函数定义于 hw/riscv/boot.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
参数,接下来的代码将进行对应的加载:
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
:
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 中加载地址的计算:
/*
* 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 GiB 中的较小值减去 fdt 的大小后按 16 MiB 对齐向下取整:
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 处:
rom_add_blob_fixed_as("fdt", fdt, fdtsize, fdt_addr,
&address_space_memory);
接下来的代码加载复位向量(reset vector)进 ROM:
/* 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
中复位向量如下所示:
/* 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
的值读入 t0 和 a1 中,最后跳转到 t0 指向的位置,即 start_addr
指向的位置。在之前分析过的代码中:
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
函数:
riscv_rom_copy_firmware_info(machine, rom_base, rom_size, sizeof(reset_vec),
kernel_entry);
从函数的注释中我们可以得知,存放在复位向量的汇编代码中存放进 a2 的是 dynamic 类型固件信息的地址。其中,结构体 fw_dynamic_info
的定义如下,其位于 include/hw/riscv/boot_opensbi.h
中:
/** 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 中:
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 启动路径上的实现。
参考资料
猜你喜欢:
- 我要投稿:发表原创技术文章,收获福利、挚友与行业影响力
- 知识星球:独家 Linux 实战经验与技巧,订阅「Linux知识星球」
- 视频频道:泰晓学院,B 站,发布各类 Linux 视频课
- 开源小店:欢迎光临泰晓科技自营店,购物支持泰晓原创
- 技术交流:Linux 用户技术交流微信群,联系微信号:tinylab
支付宝打赏 ¥9.68元 | 微信打赏 ¥9.68元 | |
请作者喝杯咖啡吧 |
Read Album:
- TinyBPT 和面向 buildroot 的二进制包管理服务(1):设计简介与框架
- RISC-V Linux 内核及周边技术动态第 118 期
- RISC-V Linux 内核及周边技术动态第 117 期
- 实时分析工具 rtla timerlat 介绍(二):延迟测试原理
- 实时分析工具 rtla timerlat 介绍(一):交叉编译及使用