QEMU 模块系统深度解析:module_init、动态加载与 modinfo 元数据机制
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 等开关控制。
从源码结构看,模块基础设施由三个层次构成:
- 公开 API 与宏定义:include/qemu/module.h 定义了全部注册宏、加载函数原型与 modinfo 注解宏,是整个机制的"契约";
- 运行时实现:util/module.c 负责初始化列表的管理、DSO 的动态加载、按 QOM 类型或 QemuOpts 反查模块;
- 构建期元数据流水线: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)
典型用法在仓库中随处可见,例如块驱动在文件末尾注册初始化函数:
- block/blkdebug.c:
block_init(bdrv_blkdebug_init); - block/blkio.c:
block_init(bdrv_blkio_init); - block/bochs.c、block/cloop.c 等绝大多数块驱动同样以
block_init(...)收尾。
另外两个宏用于"按子系统前缀加载模块",它直接封装了 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 完整实现了上述契约,其内部流程可概括为:
- 检查
g_module_supported()(GLib 的模块支持),不支持则报错返回-1; - 以
prefix + name拼出module_name,查询全局哈希表loaded_modules:已存在则直接返回2,否则先加入哈希表(防止递归/重复加载); - 依次把
QEMU_MODULE_DIR、CONFIG_QEMU_MODDIR、升级目录填入dirs[]; - 遍历
qemu_modinfo[]元数据:若本模块声明了arch则用module_check_arch校验架构匹配(不匹配直接报错);若本模块声明了deps依赖,则先递归module_load("", *sl, errp)加载依赖,依赖失败(返回值 ≤ 0)则本次加载失败;反之若发现"别的模块依赖本模块",则置export_symbols = true,使本模块符号导出到全局命名空间; - 在
dirs[]中逐个拼接%s/%s%s(目录 + 模块名 +CONFIG_HOST_DSOSUF,如.so)并用access(fname, F_OK)探测:ENOENT/ENOTDIR表示该目录没有此模块,继续下一个目录;其他错误(最常见EACCES)用error_setg_errno报错;找到则调用module_load_dso; - 全部目录都没有 → 返回
0(未安装,不视为错误); 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 的设备注册流程、排查模块加载失败(版本不匹配、架构不匹配、依赖缺失),还是为自定义驱动添加模块支持,都能做到有的放矢。进一步深读推荐以下文件:
- 模块 API 与全部注释:include/qemu/module.h
- 运行时实现:util/module.c
- 元数据收集与生成:scripts/modinfo-collect.py、scripts/modinfo-generate.py
- 宏使用实例:hw/display/qxl.c、block/blkdebug.c