jjl9807's blog - QEMU
type: channel
link: https://blog.jjl9807.com/tag/QEMU/
items: QEMU 机器创建及初始化机制简析
items: QEMU 设备模型简析（三）：QOM 设计实现
items: QEMU 设备模型简析（二）：面向对象的设备管理
items: QEMU 设备模型简析（一）：生命周期
items: QEMU 事件循环机制简析（三）：下半部机制
items: QEMU 事件循环机制简析（二）：基本组成
items: QEMU 事件循环机制简析（一）：glib 事件循环
items: QEMU 参数解析机制简析
items: QEMU 系统模式下指令解码模块简析：以 RISC-V 为例

QEMU 机器创建及初始化机制简析
type: item
date: 2023-09-24T16:00:00+08:00
description: 前言QEMU 是一个通用的、开源的模拟器，能够通过纯软件方式实现硬件的虚拟化，模拟外部硬件，为用户提供抽象、虚拟的硬件环境，可以模拟各种硬件架构并运行多种操作系统。QEMU 的初始化是一个复杂的过程，由 qemu_init 函数负责完成，该过程主要包括参数解析、主循环初始化、机器创建、设备初始化等流程，本文将以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例，分析 virt 机器创建及初始化机制。本文使用以下命令在 QEMU 中创建一台 virt 机器并启动 5.18 版本的 RISC-V Linux 内核：qemu-system-riscv64 -M virt -m 256M -nographic \ -kernel linux-kernel/arch/riscv/boot/Image \ -drive file=rootfs.img,format=raw,id=hd0 \ -device virtio-blk-device,drive=hd0 \ -append "root=/dev/vda rw console=ttyS0"机器创建的工作由函数 qemu_create_machine 完成，而机器初始化则由 qemu_apply_legacy_machine_options 和 qemu_apply_machine_options 完成，如果命令行参数没有配置 --preconfig 选项，则 qmp_x_exit_preconfig 也会承担一部分的机器初始化工作。待机器初始化完成后，QEMU 会调用机器类型对应的初始化方法对机器实例进行初始化。QEMU 机器创建及初始化的基本流程如下图所示：机器创建QEMU 在完成命令行参数的解析以及主循环的初始化后，会根据命令行指定的机器类型调用函数 qemu_create_machine 进行机器创建：/* softmmu/vl.c: 2011 */ static void qemu_create_machine(QDict *qdict) { MachineClass *machine_class = select_machine(qdict, &error_fatal); object_set_machine_compat_props(machine_class->compat_props); current_machine = MACHINE(object_new_with_class(OBJECT_CLASS(machine_class))); object_property_add_child(object_get_root(), "machine", OBJECT(current_machine)); object_property_add_child(container_get(OBJECT(current_machine), "/unattached"), "sysbus", OBJECT(sysbus_get_default())); if (machine_class->minimum_page_bits) { if (!set_preferred_target_page_bits(machine_class->minimum_page_bits)) { /* This would be a board error: specifying a minimum smaller than * a target's compile-time fixed setting. */ g_assert_not_reached(); } } cpu_exec_init_all(); page_size_init(); if (machine_class->hw_version) { qemu_set_hw_version(machine_class->hw_version); } /* * Get the default machine options from the machine if it is not already * specified either by the configuration file or by the command line. */ if (machine_class->default_machine_opts) { QDict *default_opts = keyval_parse(machine_class->default_machine_opts, NULL, NULL, &error_abort); qemu_apply_legacy_machine_options(default_…

QEMU 设备模型简析（三）：QOM 设计实现
type: item
date: 2023-09-07T16:00:00+08:00
description: 前言在上一篇文章中我们聚焦 QEMU 中面向对象的设备管理机制，阐述了引入对象模型的必要性，介绍了 QOM 基本功能和顶层设计，本文将在此基础上进一步深入，详细分析 QOM 对象系统的设计思路以及实现方式。关键结构通过之前的分析，我们已经知道 QOM 有如下几大关键数据结构：Object、ObjectClass、TypeInfo 和 TypeImpl，他们的关系如下图所示：通过上图不难看出，QOM 对象模型的核心结构是 Object，这个结构体十分简洁，只保存了有关类型和父类的信息。ObjectClass 结构体就比较复杂，保存指向类型信息 TypeImpl 结构体的指针，保证了能够获取有关类型的信息。TypeImpl 存储着一个类型的信息，包括类型名称、类型大小、是否抽象类、父类名称、父类类型指针、ObjectClass 等。TypeInfo 结构体没有直接与其他任何数据结构产生直接关联，通过前面的分析我们已经知道，这是面向开发者的一个工具结构，主要用于在注册类型的时候提供类型的基本信息，在类型注册伊始，QEMU 会自动生成对应的 TypeImpl 结构体，保存类型的全部信息。面向对象特性首先，我们需要回顾一下面向对象的基本特征：封装接口继承析构静态成员多态动态类型装换下面我们将详细分析上述特性在 QOM 对象系统中的具体实现。封装在分析 QOM 如何实现封装之前，我们需要再次回顾 Object 结构体的定义：/* include/qom/object.h: 153 */ struct Object { /* private: */ ObjectClass *class; ObjectFree *free; GHashTable *properties; uint32_t ref; Object *parent; };这里需要关注的是第一行的注释，它表示结构体中的所有属性都是私有的，只能被类的内部成员访问和修改。但是，仅靠 C 语言中的结构体是无法实现对私有变量的访问控制的，因此 QEMU 在 Object 中引入了属性表，即 properties 指针，它指向一张哈希表，该表含了 Object 中的所有可以访问、修改的数据和函数其中每一个键值对表示 property 的名称以及指向相应 ObjectProperty 结构体的指针。下面给出 ObjectProperty 结构体的实现代码：/* include/qom/object.h: 88 */ struct ObjectProperty { char *name; char *type; char *description; ObjectPropertyAccessor *get; ObjectPropertyAccessor *set; ObjectPropertyResolve *resolve; ObjectPropertyRelease *release; ObjectPropertyInit *init; void *opaque; QObject *defval; };可以看到，ObjectProperty 结构体包含了这个属性的名称、类型、描述、读写方法以及解析和释放函数，还包括这个 property 特有的属性，使用 opaque 指针来表示。QOM 通过 ObjectProperty 结构体将对象的每个数据都保存在这样一个单元之中，再利用一个哈希表实现对对象所有数据的统一管理，进而实现了数据封装。当用户需要向 Object 中增加属性时，需要调用 object_property_add 函数：/* qom/object.c: 1257 */ ObjectProperty * object_property_add(Object *obj, const char *name, const char *type, ObjectPropertyAccessor *get, ObjectPropertyAccessor *set, ObjectPropertyRelease *release, void *opaque) { return object_property_try_add(obj, name, type, get, set, release, opaque, &error_abort); }该函数通过进一步调用 object_property_try_add 函数向 properties 所指向的哈希表中插入了一个新的属性：/* qom/object.c: 1206 */ ObjectProperty * object_property_try_add(Object *obj, const char *name, const char *type…

QEMU 设备模型简析（二）：面向对象的设备管理
type: item
date: 2023-08-30T16:00:00+08:00
description: 前言上一篇文章介绍了 QEMU 设备模型的生命周期，分析了 QEMU 中设备类型注册、设备类型初始化、设备实例化等环节的基本原理与执行流程，在这个过程中我们不难发现 QEMU 设备模型采用了面向对象的设计思想，本文将以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例，阐述 QEMU 中面向对象的设备管理机制。在 Linux 中一切皆文件，而 QEMU 中模拟的一切实体皆对象。以 CPU 模拟为例，QEMU 中要实现对各种 CPU 架构的模拟，而且对于同一种架构的 CPU，比如 RISC-V 架构，由于功能特性的不同，也会有不同的 CPU 型号。任何型号的 CPU 中都有 CPU 的通用属性，同时也包含各自特有的属性，使用面向对象的设计思想可以非常高效地实现各种型号 CPU 的模拟。此外，在主板上一个设备会通过总线与其他的设备相连接，而其他的设备也可以进一步通过总线与更多的设备连接，同时一个总线上会连接多个设备。这种总线型关系的模拟也可以非常便捷地使用面向对象的模型实现。QEMU 对象模型（QEMU Object Model, QOM）提供了一个通用的面向对象框架，用于注册用户创建的设备类型和实例化这些类型的对象。QOM 提供以下功能：动态的设备类型注册系统支持设备类型的单一继承无状态接口的多重继承将内部成员映射到公开的属性QOM 中最基础的对象是 TYPE_OBJECT，它提供所有对象共有的基本方法。基类ObjectClass 是 QOM 中所有类的基础，每一类对象会实例化一个 ObjectClass，类的成员是这类对象通用的内容：/* include/qom/object.h: 127 */ struct ObjectClass { /* private: */ Type type; GSList *interfaces; const char *object_cast_cache[OBJECT_CLASS_CAST_CACHE]; const char *class_cast_cache[OBJECT_CLASS_CAST_CACHE]; ObjectUnparent *unparent; GHashTable *properties; };Object 是 QOM 中所有对象的基础，QOM 的每一个设备会实例化一个 Object，Object 的成员是每个设备独有的内容：/* include/qom/object.h: 153 */ struct Object { /* private: */ ObjectClass *class; ObjectFree *free; GHashTable *properties; uint32_t ref; Object *parent; };Object 的第一个成员是指向 ObjectClass 的指针。由于 C 语言保证结构体的第一个成员始终从该结构体首地址开始，只要任何子对象将其父对象作为第一个成员，就可以直接将其转换为 Object。QOM 中 ObjectClass 和 Object 的关系，可以像上一篇文章中一样理解为设备类型与具体设备的关系，一个 ObjectClass 可以对应多个 Object，而 一个 Object 只会指向一个 ObjectClass。也可以将 ObjectClass 理解为设备驱动，Object 理解为设备。ObjectClass 所描述的主要是某一类设备的通用操作接口，而且 ObjectClass 也会实例化一个实体，这个实体是这一类设备所共用的；而每一个实际的设备都对应一个 Object，每个 Object 又会有一个指针指向 ObjectClass。下面以 virtio-net 设备为例，分析 QEMU 对象模型的继承和派生关系：QOM 最底层的基类是 ObjectClass，对应的对象是 Object设备的类 DeviceClass，对应的对象是 DeviceStatePCI 设备在设备类的基础上派生出了 PCIDeviceClass，对应的对象是 PCIDevicevirtio-pci 设备又在 PCI 设备的基础上派生出 VirtioPCIClass，对应的对象是 VirtIOPCIProxyvirtio-net 设备在 VIRTIO-PCI 设备的基础上进一步派生出新的类，这里需要注意的是 virtio-net 设备相比 virtio-pci 设备并不需要增加新的内容，所以这一层派生出的依旧是 VirtioPCIClass，但是对应的对象依然需要增加网络设备特有的内容，因此会派生出新结构体 VirtIONetPCI本质上 C 语言中派生的实现形式就是结构体的包含嵌套，派生类层层包含父类…

QEMU 设备模型简析（一）：生命周期
type: item
date: 2023-08-22T16:00:00+08:00
description: 前言QEMU 系统模拟模式能够提供运行虚拟机所需的完整环境，包括 CPU，内存和外部设备等。其中，设备模拟是系统模拟模式的重要组成部分，也是系统模拟模式和用户模拟模式的本质区别之一。设备模型是 QEMU 中用于描述和实现设备模拟的软件结构和框架，它为设备提供了标准化的接口和方法，使得开发者可以轻松地为 QEMU 添加新的设备。本文将以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例，分析介绍 QEMU 设备模型的生命周期。设备类型注册定义QEMU 模拟的每一种设备都必须定义对应的设备类型，这个设备类型在代码中由结构体 TypeInfo 描述：/* include/qom/object.h: 412 */ struct TypeInfo { const char *name; const char *parent; size_t instance_size; size_t instance_align; void (*instance_init)(Object *obj); void (*instance_post_init)(Object *obj); void (*instance_finalize)(Object *obj); bool abstract; size_t class_size; void (*class_init)(ObjectClass *klass, void *data); void (*class_base_init)(ObjectClass *klass, void *data); void *class_data; InterfaceInfo *interfaces; };该结构体的成员变量比较繁多，目前只需要关注以下两个成员：name： 设备类型名称parent： 父设备类型名称通过这两个变量我们可以发现，QEMU 设备模型中使用名称作为设备类型的唯一标识，并且设备类型之间还存在继承关系。注册完成设备类型的定义后，必须注册这一设备类型，以通知 QEMU 系统可以支持这一类型的设备，为后续添加这一类型的设备准备条件。注册工作由 type_register 函数负责：/* qom/object.c: 140 */ static TypeImpl *type_register_internal(const TypeInfo *info) { TypeImpl *ti; ti = type_new(info); type_table_add(ti); return ti; } TypeImpl *type_register(const TypeInfo *info) { assert(info->parent); return type_register_internal(info); }该函数通过调用 type_new 生成一个 TypeInfo 对应的 TypeImpl 类型，并以 name 为关键字添加到名为 type_table 的一个哈希表中。type_table 是 QEMU 维护的一个全局的类型哈希表，使用类型名字符串索引到具体的 TypeImpl 对象。下面给出 TypeImpl 结构体的定义：/* qom/object.c: 48 */ struct TypeImpl { const char *name; size_t class_size; size_t instance_size; size_t instance_align; void (*class_init)(ObjectClass *klass, void *data); void (*class_base_init)(ObjectClass *klass, void *data); void *class_data; void (*instance_init)(Object *obj); void (*instance_post_init)(Object *obj); void (*instance_finalize)(Object *obj); bool abstract; const char *parent; TypeImpl *parent_type; ObjectClass *class; int num_interfaces; InterfaceImpl interfaces[MAX_INTERFACES]; };通过观察不难发现 TypeInfo 和 TypeImpl 两个结构体的结构相似，绝大多数成员变量都相同，那么为什么注册过程中不直接使用，而要重新复制一遍呢？这与 QEMU 对象系统的设计理念有关，TypeInfo 结构体是面向 API 使用者的一个工具…

QEMU 事件循环机制简析（三）：下半部机制
type: item
date: 2023-08-15T16:00:00+08:00
description: 前言在上一篇文章中，我们详细分析了 QEMU 事件循环机制的基本组成，包括事件源、状态机回调函数以及 poll 机制，介绍了 QEMU 主事件循环的运行流程，本文将在此基础上进一步深入，以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例，分析 QEMU 下半部（Bottom Halves，BH）机制。在操作系统内核中，下半部是一个延迟执行的函数，在中断上下文之外运行。在 QEMU 中，下半部用于异步事件处理，QEMU 下半部机制利用事件循环机制，向其他模块提供异步调用的接口。因为其它模块的运行可能不在主线程中，但它想把自己注册的函数加入到 QEMU 主线程中运行，这时就可以使用下半部机制。QEMU 下半部功能在一个事件循环中实现，当下半部函数注册到一个事件循环中，这个事件循环在下一次 poll 的时候，就会调用该函数，从而实现异步延迟调用的效果。数据结构QEMU 将下半部机制所有必要信息（关联事件源、名称、回调函数和状态）进行抽象，封装为 QEMUBH 结构体：/* util/async.c: 61 */ struct QEMUBH { AioContext *ctx; const char *name; QEMUBHFunc *cb; void *opaque; QSLIST_ENTRY(QEMUBH) next; unsigned flags; };下面逐一分析该结构体各成员变量的含义与作用：AioContext *ctx： 指向 AioContext 的指针，代表与下半部关联的异步 I/O 上下文。这允许 QEMU 在不同的上下文中处理不同的事件const char *name： 下半部名称QEMUBHFunc *cb： 指向回调函数的指针，当下半部被触发时，这个函数会被调用，这个回调函数通常会处理与下半部关联的事件void *opaque： 指向数据的指针，它可以在回调函数中使用，一般用于为回调函数提供参数或者额外的上下文数据。QSLIST_ENTRY(QEMUBH) next： 一个用于将 QEMUBH 结构体插入到单向链表的宏unsigned flags： 用于存储底部半部的状态和属性的标志位，表示底部半部是否已经被调度或是否已经被删除其中，flags 标志一共有五种：/* util/async.c: 44 */ enum { /* Already enqueued and waiting for aio_bh_poll() */ BH_PENDING = (1 << 0), /* Invoke the callback */ BH_SCHEDULED = (1 << 1), /* Delete without invoking callback */ BH_DELETED = (1 << 2), /* Delete after invoking callback */ BH_ONESHOT = (1 << 3), /* Schedule periodically when the event loop is idle */ BH_IDLE = (1 << 4), };上述五种标志分别对应如下含义：BH_PENDING： 表示下半部已经入队并等待 aio_bh_poll() 的处理，当一个下半部被调度但尚未执行时，设置为该状态BH_SCHEDULED： 表示下半部的回调函数应该被调用，当下半部被调度并准备执行其回调函数时，设置为该状态BH_DELETED： 表示下半部应该被删除，但在删除之前不应该调用其回调函数，这通常在下半部不再需要时使用以确保其回调函数不被执行BH_ONESHOT： 表示下半部应该在调用其回调函数后被删除，一般用于标示只需要执行一次的下半部BH_IDLE： 表示下半部应该在事件循环空闲时被周期性地调度，一般用于标示需要在没有其他活动时执行的任务很有用为什么需要设置 flag 标志？因为 QEMU 下半部的注册和执行是异步的，因此需要有一种方法用来通知执行者下半部应该怎样执行。flag 标志的设置提供了一种方式来控制和管理下半部的行为。例如，可以组合使用这五种标志来确定是否应该执行下半部的回调函数，或者下半部是否应该在执行后被删除。操作挂载QEMU 下半部在一个事件循环中实现，因此新建下半部需要指出基于哪个 AioContext，同时要指定下半部中需要执行什么函数以及函数参数，下半部的创建由 aio_bh_new_full 函数具体执行：/* util/async.c: 139 */ QEMUBH *aio_bh_new_full(AioContext *ctx, QEMUBHFunc *cb, void *opaque, const char *na…

QEMU 事件循环机制简析（二）：基本组成
type: item
date: 2023-08-07T16:00:00+08:00
description: 前言QEMU 采用了基于事件驱动的架构，其事件循环机制基于 glib 实现，上一篇文章介绍了 glib 事件循环机制，本文将在此基础上以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例深入分析 QEMU 事件循环机制。概述QEMU 的事件循环机制是其核心功能之一，用于协调虚拟机的各种异步操作。通过监听和调度各种事件源，它允许 QEMU 有效地响应外部输入、定时器事件等，从而实现高性能的虚拟化。QEMU 的默认事件循环是位于文件 utils/main-loop.c 文件中的主循环 main-loop，我们也可以使用选项 –object iothread, id=<my_iothread_name> 自己创建事件循环。QEMU 事件源尽管 QEMU 的事件循环机制基于 glib 实现，但是 QEMU 没有直接使用 glib 提供的事件源，而是自定义了一个新的事件源 AioContext，主要有以下两种类型：监听各种事件，例如 iohandler_ctx处理块设备层的异步 I/O 请求，例如默认的 qemu_aio_context 或者模块自己创建的 AioContext/* include/block/aio.h: 124 */ struct AioContext { GSource source; /* Used by AioContext users to protect from multi-threaded access. */ QemuRecMutex lock; /* * Keep track of readers and writers of the block layer graph. * This is essential to avoid performing additions and removal * of nodes and edges from block graph while some * other thread is traversing it. */ BdrvGraphRWlock *bdrv_graph; /* The list of registered AIO handlers. Protected by ctx->list_lock. */ AioHandlerList aio_handlers; /* The list of AIO handlers to be deleted. Protected by ctx->list_lock. */ AioHandlerList deleted_aio_handlers; /* Used to avoid unnecessary event_notifier_set calls in aio_notify; * only written from the AioContext home thread, or under the BQL in * the case of the main AioContext. However, it is read from any * thread so it is still accessed with atomic primitives. * * If this field is 0, everything (file descriptors, bottom halves, * timers) will be re-evaluated before the next blocking poll() or * io_uring wait; therefore, the event_notifier_set call can be * skipped. If it is non-zero, you may need to wake up a concurrent * aio_poll or the glib main event loop, making event_notifier_set * necessary. * * Bit 0 is reserved for GSource usage of the AioContext, and is 1 * between a call to aio_ctx_prepare and the next call to aio_ctx_check. * Bits 1-31 simply count the number of active calls to aio_poll * that are in the prepare or poll phase. * * The GSource and aio_poll m…

QEMU 事件循环机制简析（一）：glib 事件循环
type: item
date: 2023-07-25T16:00:00+08:00
description: 前言QEMU 采用了基于事件驱动的架构，其事件循环机制基于 glib 实现，因此 QEMU 中有大量的概念来源于 glib，在深入分析 QEMU 事件循环机制之前先了解 glib 事件循环的基本原理有助于增进我们对 QEMU 事件循环机制的理解，本文将基于 glib v2.25.7 源码介绍 glib 事件循环机制。概述glib 是一个跨平台的、使用 C 语言编写的若干底层库的集合，它实现了完整的事件循环分发机制，在这个机制中有一个主事件循环负责管理所有可用事件源。这些事件可以来自多种不同类型的源，如文件描述符（文件、管道或套接字）和超时，我们还可以通过调用 g_source_attach() 函数添加新类型的事件源。glib 使用 GMainLoop 结构体来表示一个主事件循环，一个 GMainLoop 由 g_main_loop_new() 函数创建，添加初始事件源后，调用 g_main_loop_run() 函数运行，该函数将持续检查每个事件源的新事件，并将其派发出去，直到处理完其中一个事件源的事件后，调用 g_main_loop_quit() 函数退出主循环，g_main_loop_run() 函数才会返回。GMainContext 用于表示主事件循环的上下文，每一个 GMainLoop 都有一个对应的上下文 GMainContext，它封装了事件循环的状态和行为。事件源使用 GSource 表示，每个 GSource 可以关联多个文件描述符，为了能够在不同线程中处理多组独立的事件源，每个 GSource 会关联到一个 GMainContext，而一个 GMainContext 可以关联多个 GSource，具体关系如下图所示。需要注意的是，GMainContext 只能在单线程中运行，但是事件源可以由其他线程添加或删除。因此，所有对 GMainContext 以及内置的 GSource 进行操作的函数都是线程安全的。glib 中每个事件源都有一个优先级，默认优先级 G_PRIORITY_DEFAULT 为 0，大于 0 表示优先级较低。来自高优先级事件源的事件总是先于来自低优先级事件源的事件得到处理。此外，还可以添加空闲函数（idle functions），并为其分配优先级，当没有来自更高优先级事件源的事件需要处理时，空闲函数就会运行。状态机glib 的核心是 poll 机制，通过 poll 检查用户注册的事件源，并执行对应的回调，用户不需要关注其具体实现，只需要按照要求注册对应的事件源和回调。glib 事件循环机制管理所有注册的事件源，不同事件源可以在一个线程中处理，也可以在不同线程中处理，这取决于事件源所在的上下文，一个上下文只能运行在一个线程中，如果想要事件源在不同线程中并发被处理，可以将其放在不同的上下文中。glib 中一次主循环分为 4 个阶段：prepare、query、check 和 dispatch，其状态转移如下图所示：上述状态机中的四个阶段分别对应以下四个函数：prepare：gboolean g_main_context_prepare(GMainContext *context, gint *priority)该函数接受两个参数：context： 主循环的上下文，若为 NULL，则使用默认上下文priority： 用于返回准备好的事件源的最高优先级函数执行过程中会循环遍历所有事件源，调用相应事件源的 prepare 函数检查事件源是否准备好被分派。如果准备好了，设置相应事件源的 G_SOURCE_READY 标志。此外，该函数还会计算下一个超时事件的时间。如果存在准备好的事件源，超时将被设置为 0，函数返回 TRUE，否则待超时后直接返回 FALSE。query：gint g_main_context_query(GMainContext *context, gint max_priority, gint *timeout_, GPollFD *fds, gint n_fds)该函数接受五个参数：context： 主循环的上下文max_priority： 要查询的最大优先级timeout： 返回下一个超时的时间fds： 用于存储查询结果的文件描述符数组n_fds： 文件描述符数组的大小函数执行过程中会遍历上下文中的轮询记录，将每个记录转换为 GPollFD 结构，并存储在 fds 数组中，其中数组的大小由 n_fds 指定，如果轮询记录的数量超过 n_fds，则多余的记录将被忽略。如果 timeout 参数不为 NULL，则函数会将上下文的超时值复制到 timeout 指向的位置。如果超时不为 0，则设置 context->time_is_current 为 FALSE。最后，函数将返回填充到…

QEMU 参数解析机制简析
type: item
date: 2023-07-15T16:00:00+08:00
description: 前言QEMU 使用命令行来配置模拟器的启动参数，本文将以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例，简要梳理 QEMU 常用参数的含义与用法，重点分析 QEMU 的参数解析机制。常用参数QEMU 各选项的具体用法可以通过指令 qemu-system-riscv64 [options] help 查看。下面以在 QEMU 中启动 RISC-V Linux 内核的命令为例，简要介绍 QEMU 常用参数的含义：qemu-system-riscv64 -M virt -m 256M -nographic \ -kernel linux-kernel/arch/riscv/boot/Image \ -drive file=rootfs.img,format=raw,id=hd0 \ -device virtio-blk-device,drive=hd0 \ -append "root=/dev/vda rw console=ttyS0"-M： 指定模拟器运行时的设备类型，在上面的例子中指定了 qemu-system-riscv64 的模拟设备为 virt，即 RISC-V 架构下的“通用虚拟平台”-m： 指定虚拟机内部的内存大小-kernel： 指定内核镜像-append： 指定内核命令行代码-drive： 定义磁盘，file 表示磁盘镜像的文件路径，format 表示磁盘格式，id 表示磁盘号-device： 添加设备到虚拟机中参数解析流程概述QEMU 的全部可用参数定义于项目目录下的 qemu-options.hx 文件中，而参数解析的主体逻辑位于 softmmu/vl.c 文件的 qemu_init 函数中，主要分为两个阶段，第一阶段负责检查传入参数的合法性，并不具体解析，第二阶段执行具体的解析操作，并通过 switch 语句跳转不同为分支完成对应选项的设置。QEMU 在不同的抽象层次上定义了多种数据结构来对不同的参数进行描述，这些数据结构都在参数解析之前完成初始化。数据结构及初始化QEMU 在 softmmu/vl.c 文件中定义了 QEMUOption 结构体来描述不同的命令行参数，其代码如下：typedef struct QEMUOption { const char *name; int flags; int index; uint32_t arch_mask; } QEMUOption;其中 name 表示参数名称，flags 表示参数属性，为 1 表示拥有子参数，为 0 则表示无子参数，index 表示命令索引 (QEMU_OPTION_cmd)，arch_mask 表示参数支持的架构。在 softmmu/vl.c 文件中还定义了一个全局 QEMUOption 数组 qemu_options 来描述 QEMU 的全部可用参数，具体如下：static const QEMUOption qemu_options[] = { { "h", 0, QEMU_OPTION_h, QEMU_ARCH_ALL }, #define DEF(option, opt_arg, opt_enum, opt_help, arch_mask) \ { option, opt_arg, opt_enum, arch_mask }, #define DEFHEADING(text) #define ARCHHEADING(text, arch_mask) #include "qemu-options.def" { /* end of list */ } };可以看到，qemu_options 数组中首先定义了一个参数 h，其使用方法为 qemu-system-riscv64 -h，作用是打印帮助信息。其余所有的可用参数都都通过 DEF 宏定义在 <qemu_build_dir>/qemu-options.def 文件中。需要注意的是，qemu-options.def 文件是由 scripts/hxtool 脚本在编译时根据 qemu-options.hx 文件生成的，因此不在 QEMU 源代码目录中。对于含有多个子选项的参数，QEMU 在文件 include/qemu/option_int.h 中定义了两个结构体 QemuOpt 对子参数进行描述：struct QemuOpt { char *name; char *str; const QemuOptDesc *desc; union { bool boolean; uint64_t uint; } value; QemuOpts *opts; QTAILQ_ENTRY(QemuOpt) next; };其中 QemuOpt 用于存储子选项，每…

QEMU 系统模式下指令解码模块简析：以 RISC-V 为例
type: item
date: 2023-07-08T16:00:00+08:00
description: 前言QEMU 是一个通用的、开源的模拟器，通过纯软件方式实现硬件的虚拟化，模拟外部硬件，为用户提供抽象、虚拟的硬件环境。QEMU 亦可藉由硬件虚拟化变身为虚拟机。QEMU 支持以下两种方式进行模拟：用户模式（User Mode Emulation）：在一种架构的 CPU 上运行为另一种架构的 CPU 编译的程序。在该模式下，QEMU 作为进程级虚拟机，只模拟系统调用前的用户态代码，系统调用进入内核后，由宿主机操作系统原生执行，QEMU 提供不同架构系统调用映射的转换。系统模拟（System Emulation）：在该模式下，QEMU 作为系统级虚拟机提供运行客户机所需的完整环境，包括 CPU，内存和外围设备等。概述QEMU 模拟的基本逻辑QEMU 提供两种 CPU 模拟实现方式，一种基于架构无关的中间码实现，另一种基于硬件虚拟化技术实现。第一种方式，QEMU 使用 Tiny Code Generator（下文简称 TCG）将客户机指令动态翻译为宿主机指令执行。这种方式的主要思想是使用纯软件的方法将客户机 CPU 指令先解码为架构无关的中间码（即 Intermediate Representation），然后再把中间码翻译为宿主机 CPU 指令执行。其中，由客户机 CPU 指令解码为中间码的过程被称为前端，由中间码翻译为宿主机 CPU 指令的过程被称为后端。以 RISC-V 为例，指令的前端解码逻辑位于 target/riscv 中，后端翻译逻辑位于 tcg/ 中，外设及其他硬件模拟代码位于 hw/riscv 中。第二种方式，基于硬件虚拟化技术实现，直接使用宿主机 CPU 执行客户机指令，可以达到接近真实机器的运行性能。本文只关注系统模式下 TCG 方式的分析，不讨论基础硬件虚拟化技术的运行逻辑。QEMU 翻译执行过程TCG 定义了一系列中间码，将已经翻译的代码以代码块的形式储存在 Translation Block 中，通过跳转指令将宿主机 CPU 的指令集和客户机 CPU 的指令集链接。下图以 RISC-V 为例，给出了 QEMU v8.0.0 系统模拟的翻译执行流程。我们可以发现 TCG 前端解码和后端翻译都按照指令块的粒度进行，将一个客户机指令块翻译成中间码，然后把中间码翻译成宿主机 CPU 指令，整个过程动态执行。为了提高翻译效率，QEMU 将翻译成的宿主机 CPU 指令块做了缓存，即上文提到的 Translation Block，CPU 执行的时候，先在缓存中查找对应的 TB，如果查找成功就直接执行，否则进入翻译流程。从更加抽象的视角来看，TCG 模式下所谓客户机 CPU 的运行，实际上就是根据指令不断改变客户机 CPU 的状态，即改变描述客户机 CPU 的状态的数据结构中的有关变量。因为实际的代码执行过程是在宿主机 CPU 上完成的，因此客户机 CPU 的指令必须被翻译为宿主机 CPU 指令才能被执行，才能改变客户机 CPU 的数据状态。QEMU 为了解耦把客户机 CPU 指令先解码为中间码，中间码其实就是一组描述如何改变客户机 CPU 数据状态且架构无关的语句，所以目标 CPU 状态参数会被传入中间码描述语句。中间码实际上是改变客户机 CPU 状态的抽象的描述，对于部分难以抽象成一般描述的指令就用 helper 函数进行补充。例如 RV32FD 指令集中的 fadd.d rd, rs1, rs2 指令，表示双精度浮点加，将 rs1 和 rs2 寄存器中的双精度浮点数相加并将舍入后的结果送到 rd 寄存器。对于该指令的行为，TCG 中间码并没有合适的描述，因此 QEMU 使用 helper_fmadd_d 函数根据情况直接调用模拟 FPU 解决问题。最后，将中间码翻译为宿主机 CPU 代码时，TCG 后端使用 tcg_gen_xxx 函数描述具体某条客户机 CPU 指令对客户机 CPU 数据状态的改变。翻译过程中 gen_intermediate_code 函数负责前端解码，把客户机的指令翻译成中间码。而 tcg_gen_code 负责后端翻译，将中间码翻译成宿主机 CPU 上的指令，其中 tcg_out_xxx 函数执行具体的翻译工作。TCG 细节前端解码QEMU 定义了 instruction pattern 来描述客户机 CPU 指令，一个 instruction pattern 是指一组相同或相近的指令，RISC-V 架构的指令描述位于 target/riscv 目录下的 insn16.decode、insn32.decode 文件中。QEMU 编译的时候会解析 .decode 文件，使用脚本 scripts/decodetree.py 生成对应指令描述函数的声明并存放于 <qemu_build_dir>/…