QEMU 模块系统深度解析:module_init、动态加载与 modinfo 元数据机制

原创2026-09-21 09:02:301,401 阅读
文章标签:虚拟化硬件仿真

QEMU 模块系统深度解析:module_init、动态加载与 modinfo 元数据机制

导读

本文以 QEMU 源码树中的 docs/devel/modules.rst 及其核心引用文档 include/qemu/module.h 为主体,系统讲解 QEMU 的模块(module)基础设施。你将掌握:QEMU 如何用 module_init 系列宏完成初始化注册、module_load 如何按目录搜索并动态加载共享库、module_obj/module_dep 等注解宏如何驱动 modinfo 元数据数据库,以及这一切在 util/module.c、scripts/modinfo-collect.py、scripts/modinfo-generate.py 中的底层实现。阅读后可独立理解 QEMU 设备、块驱动等子系统的模块化组织方式,并能向自己的 QEMU 源码树中新增带模块元数据的驱动代码。

QEMU 模块机制的定位与总体结构

QEMU 是一个体量庞大的模拟器项目,包含上千个设备模型、块驱动、字符设备等组件。如果全部静态链接进单一可执行文件,会显著增大二进制体积,也限制发行版按需裁剪。模块机制正是为此设计:它允许把部分组件编译为独立的动态共享对象(DSO),在运行时按需加载,同时仍保留"全部静态编译"的选项,两种模式由构建时的 CONFIG_MODULES 与 BUILD_DSO 等开关控制。

从源码结构看,模块基础设施由三个层次构成:

  1. 公开 API 与宏定义:include/qemu/module.h 定义了全部注册宏、加载函数原型与 modinfo 注解宏,是整个机制的"契约";
  2. 运行时实现:util/module.c 负责初始化列表的管理、DSO 的动态加载、按 QOM 类型或 QemuOpts 反查模块;
  3. 构建期元数据流水线:scripts/modinfo-collect.py 与 scripts/modinfo-generate.py 负责从源码中提取模块信息并生成 qemu_modinfo[] 数据库,供运行时"按需定位模块"。

docs/devel/modules.rst 本身只有一条核心指令——通过 Sphinx 的 kernel-doc 扩展直接引入 include/qemu/module.h 中的注释作为正文,因此头文件内完整的 API 注释即是本文档的技术主体,以下各节均围绕这些注释展开。

模块注册机制:module_init 与初始化类型

DSO 版本戳(DSO_STAMP)

模块加载的第一道防线是"版本匹配"。头文件通过两个宏生成一个构建期符号:

#define DSO_STAMP_FUN         glue(qemu_stamp, CONFIG_STAMP)
#define DSO_STAMP_FUN_STR     stringify(DSO_STAMP_FUN)

CONFIG_STAMP 由构建系统注入,每个构建都会得到一个不同的戳值,因此 qemu_stamp<CONFIG_STAMP> 是一个与本次构建强绑定的函数名。在 BUILD_DSO(即正在编译一个可加载模块)时,模块必须导出 DSO_STAMP_FUN 以及一个哑符号 qemu_module_dummy:

#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);

如注释所述,qemu_module_dummy 的作用是:当主程序在加载模块后查找 DSO_STAMP_FUN 失败时,如果能在该 DSO 中找到 qemu_module_dummy,说明这是一个 QEMU 模块但来自不同构建,据此向用户给出"只能加载同一次构建产出的模块"的明确提示,而不是含糊的"不是 QEMU 模块"(见下文 module_load_dso 的实现)。

module_init 宏的两种形态

module_init(function, type) 是全部注册宏的底层原语,根据是否定义了 BUILD_DSO 有两种展开方式:

#ifdef BUILD_DSO
#define module_init(function, type)                                         \
static void __attribute__((constructor)) do_qemu_init_ ## function(void)    \
{                                                                           \
    register_dso_module_init(function, type);                               \
}
#else
#define module_init(function, type)                                         \
static void __attribute__((constructor)) do_qemu_init_ ## function(void)    \
{                                                                           \
    register_module_init(function, type);                                   \
}
#endif

两种形态都借助 GCC 的 constructor 属性,让 do_qemu_init_##function 在 DSO 加载(或程序启动)时自动执行;区别仅在于把 (function, type) 记入哪张列表:

  • 非 DSO 形态调用 register_module_init,进入按类型分组的静态初始化列表 init_type_list[type];
  • DSO 形态调用 register_dso_module_init,先进入临时的 dso_init_list,等待模块被 module_load_dso 打开后统一"过户"到静态列表(详见加载流程一节)。

module_init_type:八类初始化阶段

注册时携带的类型决定了初始化回调在哪个阶段被触发。头文件中定义的枚举完整如下:

typedef enum {
    MODULE_INIT_MIGRATION,
    MODULE_INIT_BLOCK,
    MODULE_INIT_OPTS,
    MODULE_INIT_TARGET_INFO,
    MODULE_INIT_QOM,
    MODULE_INIT_TRACE,
    MODULE_INIT_XEN_BACKEND,
    MODULE_INIT_LIBQOS,
    MODULE_INIT_FUZZ_TARGET,
    MODULE_INIT_MAX
} module_init_type;

MODULE_INIT_MAX 同时用作 util/module.c 中 init_type_list[MODULE_INIT_MAX] 与 modules_init_done[MODULE_INIT_MAX] 两个数组的长度,即每种类型一张独立链表、一个"是否已执行初始化"的标记位。

便捷注册宏一览

头文件为每个阶段提供了语义清晰的便捷宏,开发者不应直接使用 module_init:

#define block_init(function) module_init(function, MODULE_INIT_BLOCK)
#define opts_init(function) module_init(function, MODULE_INIT_OPTS)
#define type_init(function) module_init(function, MODULE_INIT_QOM)
#define trace_init(function) module_init(function, MODULE_INIT_TRACE)
#define xen_backend_init(function) module_init(function, \
                                               MODULE_INIT_XEN_BACKEND)
#define libqos_init(function) module_init(function, MODULE_INIT_LIBQOS)
#define fuzz_target_init(function) module_init(function, \
                                               MODULE_INIT_FUZZ_TARGET)
#define migration_init(function) module_init(function, MODULE_INIT_MIGRATION)

典型用法在仓库中随处可见,例如块驱动在文件末尾注册初始化函数:

另外两个宏用于"按子系统前缀加载模块",它直接封装了 module_load:

#define block_module_load(lib, errp) module_load("block-", lib, errp)
#define ui_module_load(lib, errp) module_load("ui-", lib, errp)

即请求加载 block-xxx、ui-xxx 这类命名规范的模块(比如 block-curl、ui-gtk)。

初始化列表的运行时实现

util/module.c 中,模块初始化条目用如下结构组织:

typedef struct ModuleEntry
{
    void (*init)(void);
    QTAILQ_ENTRY(ModuleEntry) node;
    module_init_type type;
} ModuleEntry;

typedef QTAILQ_HEAD(, ModuleEntry) ModuleTypeList;

static ModuleTypeList init_type_list[MODULE_INIT_MAX];
static bool modules_init_done[MODULE_INIT_MAX];
static ModuleTypeList dso_init_list;

关键实现如下:

  • init_lists() 通过静态布尔量保证链表只初始化一次,为每种类型初始化一条 QTAILQ 头,并初始化 dso_init_list;
  • register_module_init(fn, type) 分配一个 ModuleEntry,按类型插入 init_type_list[type] 的尾部;
  • register_dso_module_init(fn, type) 则插入独立的 dso_init_list;
  • module_call_init(type) 遍历对应类型的链表依次调用 e->init(),并置位 modules_init_done[type],保证同一阶段只执行一次:
void module_call_init(module_init_type type)
{
    ModuleTypeList *l;
    ModuleEntry *e;

    if (modules_init_done[type]) {
        return;
    }
    l = find_type(type);
    QTAILQ_FOREACH(e, l, node) {
        e->init();
    }
    modules_init_done[type] = true;
}

主程序(system 与 user 模式)在启动早期会按依赖顺序调用若干次 module_call_init,例如在初始化 QOM 类型系统时触发 MODULE_INIT_QOM 类型的回调,从而完成设备类型注册。

模块加载:module_load 的目录搜索、返回值与依赖处理

搜索目录与返回值语义

头文件中为 module_load 给出了完整的契约注释,这是本文档的核心 API 说明,原文如下:

/*
 * module_load: attempt to load a module from a set of directories
 *
 * directories searched are:
 * - getenv("QEMU_MODULE_DIR")
 * - get_relocated_path(CONFIG_QEMU_MODDIR);
 * - /var/run/qemu/${version_dir}
 *
 * prefix:         a subsystem prefix, or the empty string ("ui-", ..., "")
 * name:           name of the module
 * errp:           error to set in case the module is found, but load failed.
 *
 * Return value:   -1 on error (errp set if not NULL).
 *                 0 if module or one of its dependencies are not installed,
 *                 1 if the module is found and loaded,
 *                 2 if the module is already loaded, or module is built-in.
 */
int module_load(const char *prefix, const char *name, Error **errp);

即搜索顺序为:环境变量 QEMU_MODULE_DIR 指定的目录 → 构建期配置的 CONFIG_QEMU_MODDIR(经 get_relocated_path 做可重定位路径解析)→ 模块升级目录 /var/run/qemu/${version_dir}(仅当编译了 CONFIG_MODULE_UPGRADES 时存在,version_dir 由 QEMU_PKGVERSION 清洗非法字符得到)。返回值四态:-1 加载失败(errp 有值)、0 模块或其依赖未安装(不算错误)、1 成功加载、2 已加载或该模块是内置(built-in)的。

module_load 的完整执行流

util/module.c 中的 module_load 完整实现了上述契约,其内部流程可概括为:

  1. 检查 g_module_supported()(GLib 的模块支持),不支持则报错返回 -1;
  2. 以 prefix + name 拼出 module_name,查询全局哈希表 loaded_modules:已存在则直接返回 2,否则先加入哈希表(防止递归/重复加载);
  3. 依次把 QEMU_MODULE_DIR、CONFIG_QEMU_MODDIR、升级目录填入 dirs[];
  4. 遍历 qemu_modinfo[] 元数据:若本模块声明了 arch 则用 module_check_arch 校验架构匹配(不匹配直接报错);若本模块声明了 deps 依赖,则先递归 module_load("", *sl, errp) 加载依赖,依赖失败(返回值 ≤ 0)则本次加载失败;反之若发现"别的模块依赖本模块",则置 export_symbols = true,使本模块符号导出到全局命名空间;
  5. 在 dirs[] 中逐个拼接 %s/%s%s(目录 + 模块名 + CONFIG_HOST_DSOSUF,如 .so)并用 access(fname, F_OK) 探测:ENOENT/ENOTDIR 表示该目录没有此模块,继续下一个目录;其他错误(最常见 EACCES)用 error_setg_errno 报错;找到则调用 module_load_dso;
  6. 全部目录都没有 → 返回 0(未安装,不视为错误);
  7. out: 标签处统一回收资源:加载失败(rv <= 0)时把模块名从 loaded_modules 中移除,释放 dirs[]。

module_load_dso:真正的动态加载

module_load_dso 是加载单个 DSO 的核心:

static bool module_load_dso(const char *fname, bool export_symbols,
                            Error **errp)
{
    ...
    flags = 0;
    if (!export_symbols) {
        flags |= G_MODULE_BIND_LOCAL;
    }
    g_module = g_module_open(fname, flags);
    ...
    if (!g_module_symbol(g_module, DSO_STAMP_FUN_STR, (gpointer *)&sym)) {
        error_setg(errp, "failed to initialize module: %s", fname);
        if (g_module_symbol(g_module, "qemu_module_dummy", (gpointer *)&sym)) {
            error_append_hint(errp,
                "Only modules from the same build can be loaded.\n");
        }
        ...
    }
    QTAILQ_FOREACH(e, &dso_init_list, node) {
        e->init();
        register_module_init(e->init, e->type);
    }
    ...
}

要点如下:

  • 符号可见性:默认以 G_MODULE_BIND_LOCAL 打开(符号不泄漏到全局命名空间);仅当有其他模块依赖本模块(export_symbols = true)时才以 0 标志打开,保证依赖方能解析到本模块符号;
  • 版本戳校验:用 g_module_symbol 查找 DSO_STAMP_FUN_STR 对应的函数。找不到时,再探测 qemu_module_dummy,区分"版本不匹配"与"不是 QEMU 模块"两种失败原因并给出 hint("Only modules from the same build can be loaded.");
  • 初始化过户:DSO 的构造函数(do_qemu_init_##function)在 g_module_open 期间已把条目填入 dso_init_list,随后遍历该链表,逐个调用 e->init() 再 register_module_init(e->init, e->type),把回调正式登记到按类型分组的静态列表,最后清空并释放 dso_init_list。

注意 module_load_dso 入口处有 assert(QTAILQ_EMPTY(&dso_init_list)),这保证"当前没有其他待处理的 DSO 初始化",从而让 DSO 与静态注册的初始化回调不会交错混入错误列表。

modinfo 元数据:注解宏与数据库生成

为什么需要 modinfo

纯靠文件名前缀(block-、ui-)做模块发现无法回答两个问题:这个模块实现了哪些 QOM 类型?它依赖哪些其他模块? modinfo 机制通过在源码中嵌入注解,构建期由脚本收集、汇总成 qemu_modinfo[] 数据库,运行时即可按类型、按选项反查模块。

头文件中的 DOC 注释完整说明了这套流水线:

/**
 * DOC: module info annotation macros
 *
 * ``scripts/modinfo-collect.py`` will collect module info,
 * using the preprocessor and -DQEMU_MODINFO.
 *
 * ``scripts/modinfo-generate.py`` will create a module meta-data database
 * from the collected information so qemu knows about module
 * dependencies and QOM objects implemented by modules.
 *
 * See ``*.modinfo`` and ``modinfo.c`` in the build directory to check the
 * script results.
 */

注解宏族

注解宏通过 QEMU_MODINFO 条件编译开关开启(未定义时全部展开为空,零开销):

#ifdef QEMU_MODINFO
# define modinfo(kind, value) \
    MODINFO_START kind value MODINFO_END
#else
# define modinfo(kind, value)
#endif

在此基础上定义的五个语义宏(每个都有完整的 kernel-doc 注释):

  • module_obj(name)——本模块实现了名为 name 的 QOM 类型:
    #define module_obj(name) modinfo(obj, name)
    
  • module_dep(name)——本模块依赖于名为 name 的模块:
    #define module_dep(name) modinfo(dep, name)
    
  • module_arch(name)——本模块专用于目标架构 arch。注释特别说明:目标相关的模块会被自动打上架构标签,因此该宏仅用于把"目标无关"的模块限制到特定架构,示例场景是"ccw 总线仅由 s390x 实现":
    #define module_arch(name) modinfo(arch, name)
    
  • module_opts(name)——本模块注册了名为 name 的 QemuOpts 配置组:
    #define module_opts(name) modinfo(opts, name)
    
  • module_kconfig(name)——本模块要求核心模块 name 已在 Kconfig 中启用:
    #define module_kconfig(name) modinfo(kconfig, name)
    

QemuModinfo 数据结构

收集到的信息最终汇入运行时只读数据库,结构定义位于头文件末尾:

typedef struct QemuModinfo QemuModinfo;
struct QemuModinfo {
    const char *name;
    const char *arch;
    const char **objs;
    const char **deps;
    const char **opts;
};
extern const QemuModinfo qemu_modinfo[];
void module_init_info(const QemuModinfo *info);

qemu_modinfo[] 由构建期生成(详见下一节),module_init_info 在运行时把指向该数组的指针交给模块层;在未启用 CONFIG_MODULES 的构建中,util/module.c 会退化为 const QemuModinfo qemu_modinfo[] = {}; 空数组及一系列空操作 stub(module_load 恒返回 2,即"已内置")。

收集与生成脚本

modinfo-collect.py 的工作方式是:读取构建目录中的 compile_commands.json,对每个目标 .c 文件取出原始编译命令,剥离 -MF/-MQ/-o/-c 等与预处理无关的选项,追加 -DQEMU_MODINFO 和 -E(仅预处理),然后对预处理输出中所有包含 MODINFO 的行原样打印;若传入 --target,还会额外输出该目标架构的 MODINFO_START arch ... MODINFO_END 标记(脚本取 -softmmu 后缀前的目标名作为架构)。输出即每个模块的 *.modinfo 文件。

modinfo-generate.py 则把这些 *.modinfo 解析为结构化数据并生成 C 代码:

  • 读取 --devices 指定的 *-config-device.mak,把其中值 y 的配置项(去掉 CONFIG_ 前缀)收集为 enabled 集合;
  • 逐行解析 MODINFO_START/END 之间的 kind 与 data,分类收集 obj/dep/opts/arch/kconfig;对 kconfig 类注解,若其要求未出现在 enabled 集合中,则整模块被判定为"未在 Kconfig 启用"而被跳过;
  • 汇总所有模块依赖,检测无法满足的依赖(默认直接报错退出;--skip-missing-deps 时迭代剔除依赖不可满足的模块并继续);
  • 最终 print_pre() 输出 #include "qemu/module.h" 与 const QemuModinfo qemu_modinfo[] = {,为每个有效模块输出 .name、.arch、.objs、.deps、.opts 字段(数组以 NULL 结尾),print_post() 以 /* end of list */ }; 收尾。生成结果即构建目录下的 modinfo.c。

按需加载:QOM 类型、QemuOpts 与架构过滤

module_load_qom:按类型反查模块

运行时"用户指定了某个设备类型,但该类型对应的模块尚未加载"这一经典场景,由 module_load_qom 解决:

int module_load_qom(const char *type, Error **errp)

其行为在头文件注释中与 module_load 的返回值约定一致(-1 错误、0 未安装、1 已加载、2 已加载或内置)。实现要点:

  • 遍历 qemu_modinfo[],跳过无 objs 的模块与架构不匹配(module_check_arch)的模块;
  • 若 type 命中某模块的 objs 列表,则调用 module_load("", modinfo->name, errp) 加载该模块;若发现多个模块都声明提供同一类型,直接报错 "multiple modules providing '%s'" 并返回 -1;
  • 加载失败(rv < 0)立即返回。

配套函数还有:

  • module_load_qom_all()——一次性尝试加载所有声明了 objs 且架构匹配的模块,module_loaded_qom_all 静态布尔量保证只做一次;常用于需要完整设备列表(如 -device help 枚举)的场景;
  • qemu_load_module_for_opts(group)——遍历 qemu_modinfo[] 的 opts 列表,为给定的 QemuOpts 配置组加载对应模块,实现"命令行出现某配置组时才加载其模块"的惰性加载。

module_allow_arch 与架构过滤

主程序在早期通过 module_allow_arch(arch) 设定当前目标架构,此后 module_check_arch 便依据全局 module_arch 过滤:

static bool module_check_arch(const QemuModinfo *modinfo)
{
    if (modinfo->arch) {
        if (!module_arch) {
            /* no arch set -> ignore all */
            return false;
        }
        if (strcmp(module_arch, modinfo->arch) != 0) {
            /* mismatch */
            return false;
        }
    }
    return true;
}

若从未设置架构(module_arch 为空),则所有带 arch 标注的模块都会被忽略;只有标注的架构与当前目标一致时才放行。module_load 中若明确加载的模块架构不匹配,还会给出可读的错误信息 "module arch does not match: expected '%s', got '%s'"。

模块注解宏的仓库实战示例

在真实代码中,注解宏与注册宏通常成对出现。以 hw/display/qxl.c 为例,该文件在末尾同时声明模块提供的 QOM 类型并注册初始化:

module_obj("qxl-vga");
module_obj("qxl");

这表示 qxl 模块实现了 qxl 与 qxl-vga 两个 QOM 类型,运行时当用户请求 -device qxl 且该类型尚未注册时,module_load_qom("qxl") 即可定位并加载 qxl.so。virtio-gpu 系列同样如此,例如 hw/display/virtio-gpu.c 中的 module_obj(TYPE_VIRTIO_GPU),以及 hw/display/vhost-user-gpu.c 中的 module_obj(TYPE_VHOST_USER_GPU)。

块驱动则更常只使用 block_init(...)(如 block/blkdebug.c、block/blkio.c),配合 block_module_load(lib, errp) 宏在需要时以 block- 前缀按名加载,例如 block-curl、block-ssh 等第三方库依赖较强的驱动,可被编译为独立模块,主程序按需 module_load("block-", "curl", errp)。

依赖标注 module_dep 的典型语义是"模块 A 依赖模块 B",module_load 在加载 A 前会先递归加载 B;反向地,一旦检测到"某模块依赖本模块",本模块即以导出符号的方式(export_symbols = true)加载,从而保证依赖方可以解析到所需符号。

模块机制的构建期开关与兼容性说明

模块能力受构建配置约束,相关开关集中体现在 meson.build 中:

  • enable_modules = get_option('modules') ...(meson.build)——顶层 -Dmodules=... 选项,决定是否启用模块构建;
  • 启用模块时链接 gmodule-export-2.0(未启用则用 gmodule-no-export-2.0),meson.build 中通过 GLib 的 gmodule 依赖提供动态加载能力;
  • CONFIG_MODULES 控制 util/module.c 中是否编译动态加载逻辑(否则所有 module_load 系函数退化为返回 2 的 stub);
  • CONFIG_MODULE_UPGRADES 启用 /var/run/qemu/${version_dir} 升级目录搜索;
  • 控制流完整性(CFI)与模块不兼容:若启用 CFI 同时开启模块,meson.build 会直接报错 'Selected Control-Flow Integrity is not compatible with modules';
  • SystemTap 探针与模块冲突的处理也在 meson.build 中有说明:--enable-modules 依赖该项,因为 SystemTap 信号量被链接进主二进制而非模块的共享库。

需要说明的是:模块机制是可选的性能/体积优化手段,QEMU 官方发布包通常同时支持"全静态"与"模块化"两种构建;若发行版未启用模块,上述按需加载路径不会触发,所有组件均以内置(built-in)方式工作,module_load 恒返回 2。

结语

QEMU 的模块机制是一套"宏驱动注册 + 构建期元数据提取 + 运行时按需加载"的完整闭环:module_init 族宏负责把初始化回调按八个阶段登记入链,DSO_STAMP 版本戳保证主程序与模块来自同一次构建,module_load 按三级目录搜索并递归处理依赖,modinfo 注解宏与 modinfo-collect.py/modinfo-generate.py 则让 QEMU 能够在运行时根据 QOM 类型、QemuOpts 配置组精确定位并加载实现它的模块。理解这套机制后,无论是阅读 QEMU 的设备注册流程、排查模块加载失败(版本不匹配、架构不匹配、依赖缺失),还是为自定义驱动添加模块支持,都能做到有的放矢。进一步深读推荐以下文件:

登录后查看全文
qemu