日期 :2026-09-04 · 参考源码版本:Linux 5.15.49
版权声明 本文为原创技术文章,著作权归作者所有。未经作者书面授权,严禁任何形式的转载、摘编、复制、建立镜像、商业使用。
写在前面
每个内核驱动开发者都写过这样几行代码:
c
MODULE_LICENSE("GPL v2");
module_init(hello_init);
module_exit(hello_exit);
它们看起来只是"填表"式的模板,但实际上,这几行代码背后是一套完整的工程方案:如何让同一份驱动源码,既能编译成运行时可插拔的 .ko 模块,又能静态编进内核镜像,而源码一行不改。
本文围绕 include/linux/module.h 与 include/linux/moduleparam.h,剖析 MODULE_* 系列模块信息宏 (声明许可证、作者、所需固件等模块描述信息)与 module_init/module_exit 的实现。
阅读本文你将了解:
MODULE_LICENSE等模块信息宏如何借 ELF 段在编译期"登记"信息,谁在何时消费它- 为什么同一套
module_init宏,在内建与模块两种形态下展开成完全不同的代码init_module这个固定函数名背后的 ABI 约定来自哪里(答案在构建工具 modpost,而非内核)MODULE_DEVICE_TABLE一个"别名声明"如何串起驱动的自动加载__inittest、__CFI_ADDRESSABLE这些冷门部件分别在防什么错
一、问题:一份源码,两种形态
Linux 驱动的分发形态有两种:
| 内建(built-in) | 模块(loadable module) | |
|---|---|---|
| Kbuild 配置 | obj-y |
obj-m |
| 产物 | 链入 vmlinux |
独立 xxx.ko |
| 初始化时机 | 内核启动阶段 do_initcalls() |
insmod/modprobe 时 |
| 可卸载 | 否(module_exit 无效) |
是(rmmod) |
| 入口约定 | 函数指针进 initcall 段 | 固定符号 init_module |
两种形态的注册要求截然不同:内建代码需要把初始化函数注册进启动流程的某个级别;模块则需要把入口地址、许可证、设备表等信息以标准格式 写进 .ko 文件,供内核加载器和用户态工具解析。
如果让驱动作者为两种形态各写一套注册代码,#ifdef 将污染所有驱动源码。内核的做法是把这一差异交给宏处理:由编译开关 MODULE 决定展开路径 。构建系统(Kbuild)根据 obj-m / obj-y 决定是否在编译命令行传入 -DMODULE,如图 1 所示。
图 1:同一份源码的两条编译路径
MODULE宏是区分两种形态的唯一开关。本文所有宏的"双重展开",都由它决定。
二、核心设计思想
2.1 模块信息写入 ELF 段
MODULE_LICENSE、MODULE_AUTHOR 这些宏不产生任何可执行代码,它们只做一件事:在 .modinfo 段里放一个 tag=value 格式的字符串。信息在编译期写入 ELF 文件,消费方有两类:
- 用户态工具 :
modinfo命令直接读.ko的.modinfo段展示许可证、作者;depmod从中提取别名生成modules.alias - 内核加载器 :
kernel/module.c的get_modinfo()在加载时遍历同一批字符串,做许可证检查、命名空间检查
一份编译期生成的数据,多方按约定格式读取------这是内核里典型的"数据接口优于函数接口"设计:写入方(驱动)与读取方(工具/内核)通过 ELF 段解耦,谁也不需要链接谁。
2.2 固定名称的入口 ABI
模块加载器需要一个确定的入口。Linux 的约定是:每个 .ko 必须提供名为 init_module / cleanup_module 的符号 。驱动作者的函数名五花八门,于是用 GCC 的 alias 属性把固定名"别名"到真实函数上。这个约定由构建工具 modpost 强制执行,是跨越内核与 kbuild 的 ABI 契约。
2.3 用宏吸收编译形态差异
同一套宏,靠 #ifdef MODULE 展开成两套代码,驱动源码对编译形态保持完全无感知。这是一种 复杂度转移:宁可让宏的实现复杂,也要让宏的使用简单。内核里有数万个调用点,宏定义处的一次简化会放大数万倍。
三、模块信息宏:MODULE_* 家族
3.1 共同的落点:MODULE_INFO 的展开
所有 MODULE_* 信息宏最终都汇聚到 __MODULE_INFO:
c
/* include/linux/moduleparam.h */
#define __MODULE_INFO(tag, name, info) \
static const char __UNIQUE_ID(name)[] \
__used __section(".modinfo") __aligned(1) \
= __MODULE_INFO_PREFIX __stringify(tag) "=" info
短短几行,每个修饰符都有明确用途:
__section(".modinfo"):把字符串放进自定义段。这是整个机制的基石------信息成为 ELF 文件结构的一部分,而非运行期数据__used:这个静态数组没有任何代码引用它,不标记则链接器会将其丢弃__aligned(1):取消默认对齐,多个字符串在段内紧凑排列。消费方按strlen + 1逐个遍历,紧凑排列让遍历逻辑最简单__UNIQUE_ID(name):展开为带__COUNTER__(编译器内建计数器)的唯一符号名。同一个编译单元里写多个MODULE_LICENSE或多个MODULE_FIRMWARE时,数组名不会冲突
前缀 __MODULE_INFO_PREFIX 按 MODULE 宏二选一:
c
/* include/linux/moduleparam.h */
#ifdef MODULE
#define __MODULE_INFO_PREFIX /* empty */
#else
#define __MODULE_INFO_PREFIX KBUILD_MODNAME "."
#endif
模块形态下前缀为空,因为每个 .ko 是独立文件,tag=value 不会相互冲突;内建形态下多个编译单元的 .modinfo 会汇总,KBUILD_MODNAME. 前缀(如 hello.)标记每条信息的归属。
__stringify(tag) 用了两层宏的间接字符串化:
c
/* include/linux/stringify.h */
#define __stringify_1(x...) #x
#define __stringify(x...) __stringify_1(x)
若只用一层 #x,当参数 x 本身是宏时不会展开,得到的是宏名字符串;先经过一层替换再 #,才能拿到宏的值。
最终,一个 .ko 的 .modinfo 段大致如图 2 所示。
图 2:.modinfo 段中的 tag=value 字符串
name、vermagic等条目不是驱动作者写的,而是 modpost 自动生成的(scripts/mod/modpost.c),用于加载时的版本魔术字校验。
3.2 其余模块信息宏:一行 MODULE_INFO 的封装
理解了共同落点,其余信息宏都只是给不同 tag 换了个名字:
c
/* include/linux/module.h */
#define MODULE_LICENSE(_license) MODULE_FILE MODULE_INFO(license, _license)
#define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)
#define MODULE_DESCRIPTION(_description) MODULE_INFO(description, _description)
#define MODULE_FIRMWARE(_firmware) MODULE_INFO(firmware, _firmware)
#define MODULE_IMPORT_NS(ns) MODULE_INFO(import_ns, #ns)
| 宏 | 生成的 .modinfo 条目 |
读取方 |
|---|---|---|
MODULE_LICENSE |
license=... |
内核 GPL 兼容性检查、modinfo |
MODULE_AUTHOR / MODULE_DESCRIPTION |
author=... / description=... |
modinfo |
MODULE_FIRMWARE |
firmware=... |
固件子系统(request_firmware 加载路径) |
MODULE_IMPORT_NS |
import_ns=... |
内核符号命名空间检查 |
其中两个值得展开。
MODULE_LICENSE 不只是注释。 内核加载器读到 license 后会调用 license_is_gpl_compatible() 判断(kernel/module.c):非 GPL 兼容许可证会给内核打上 taint 标记 ,且该模块不得绑定 EXPORT_SYMBOL_GPL 导出的符号 。源码注释里那串 "Dual BSD/GPL" 等可接受标识符,就是这份白名单。它同时支撑社区治理:modinfo 让用户自查系统是否纯净,维护者可拒绝含专有模块的缺陷报告。
MODULE_IMPORT_NS 是符号命名空间的"访问许可"。 自 5.x 起,内核可把一批导出符号归入命名空间(如 USB_STORAGE)。模块想使用这些符号,必须显式声明导入;加载时内核逐条校验 import_ns 条目,未声明的导入直接拒绝加载,把"隐式依赖"变成"显式契约"。
3.3 MODULE_FILE:为什么 LICENSE 的展开里多了它
注意 MODULE_LICENSE 展开里排在最前面的 MODULE_FILE:
c
/* include/linux/module.h */
#ifdef MODULE
#define MODULE_FILE
#else
#define MODULE_FILE MODULE_INFO(file, KBUILD_MODFILE);
#endif
仅内建形态写一条 file=路径。它服务于构建期:kbuild 据此生成 modules.builtin------记录哪些"模块"其实已编进内核的清单。modprobe 查到目标在清单内时直接跳过加载。模块形态下此信息无意义(.ko 文件本身就是自己),故为空展开。
3.4 MODULE_DEVICE_TABLE:一个别名串起自动加载
c
/* include/linux/module.h */
#ifdef MODULE
/* Creates an alias so file2alias.c can find device table. */
#define MODULE_DEVICE_TABLE(type, name) \
extern typeof(name) __mod_##type##__##name##_device_table \
__attribute__ ((unused, alias(__stringify(name))))
#else /* !MODULE */
#define MODULE_DEVICE_TABLE(type, name)
#endif
这可能是最费解的一个宏。它声明了一个只有名字、没有函数体 的外部符号:__mod_pci__xxx_device_table,通过 alias 属性让它与驱动的设备 ID 表 xxx 等价。
为什么不直接找 xxx?因为设备表是个普通的静态结构体数组,名字由驱动作者随意起,没有可检索的命名规律 。这个宏用 __mod_<type>__<name>_device_table 的固定模式为设备表生成了一个符合命名约定的检索符号 。构建期的 file2alias(scripts/mod/file2alias.c)扫描 .ko 中所有符合该模式的符号,读出设备 ID 表内容,为每一条 ID 计算 alias=pci:vXXXdYYY... 形式的 modalias 字符串 写入 .modinfo。此后,自动加载链路得以闭合,如图 3。
图 3:MODULE_DEVICE_TABLE 驱动的自动加载链路
内建形态下宏为空展开:驱动已在内核里,不存在"加载"问题,自然无需别名。这个细节再次体现"一套宏两种形态"的设计------同一语义,按形态裁剪到最小必要动作。
四、module_init / module_exit:一套宏,两条路径
4.1 内建形态:注册进 initcall 段
MODULE 未定义时,module_init 转手交给 initcall 机制:
c
/* include/linux/module.h */
#ifndef MODULE
#define module_init(x) __initcall(x);
#define module_exit(x) __exitcall(x);
#endif
__initcall(x) 在 include/linux/init.h 中等价于 device_initcall(x),最终在 .initcall6.init 段放入一个指向 x 的函数指针:
c
/* include/linux/init.h */
#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn) __define_initcall(fn, 6)
编译产物形如(节选自预处理输出):
c
static initcall_t __initcall____KBUILD_MODNAME__128_17_hello_init6
__used __section__(".initcall6" ".init") = hello_init;
符号名中的 6 就是 initcall 级别。内核启动时 do_initcalls() 按 0→7 的段顺序逐级调用所有注册函数,驱动初始化由此汇入启动流程。数字级别提供了依赖排序 :总线(subsys_initcall,4)先于设备驱动(device_initcall,6),驱动自然可以假定它依赖的总线已经就绪。
module_exit 走 __exitcall,但内建代码永不卸载,这个指针实际不会被调用------保留注册仅为语义完整性。
4.2 模块形态:别名、类型检查与 CFI
MODULE 已定义时,展开骤然变复杂:
c
/* include/linux/module.h */
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) \
__attribute__((alias(#initfn))); \
__CFI_ADDRESSABLE(init_module, __initdata);
int init_module(void) ... alias(#initfn)------固定名称 ABI 的落地。 加载器只认 init_module 这个名字。用 alias 属性让 init_module 成为 initfn 的别名:两个符号指向同一地址,驱动作者保有自己的命名自由。
这个约定真正的执法者是构建工具 modpost。scripts/mod/modpost.c 扫描 .ko 的符号表:
c
/* scripts/mod/modpost.c */
if (strcmp(symname, "init_module") == 0)
mod->has_init = 1;
if (strcmp(symname, "cleanup_module") == 0)
mod->has_cleanup = 1;
随后 modpost 为每个 .ko 生成一个 struct module __this_module(位于 .gnu.linkonce.this_module 段),把 .init = init_module 写进去。内核加载器(finit_module/init_module 系统调用 → load_module)读取这个结构体,拿到入口指针后由 do_init_module() 执行 do_one_initcall(mod->init)。完整流程如图 4 所示。
图 4:module_init 在两种形态下的展开与执行路径
__inittest------编译期类型门卫。 宏还附带定义了一个内联函数,函数体只有 return initfn;。initcall_t 是 int (*)(void),若 initfn 的签名与之不符(比如带了参数),这条 return 语句直接编译报错。把运行期才能发现的调用约定错误,提前到编译期 ,代价是一个多余的、__maybe_unused 抑制告警的内联函数。__exittest 同理。
__copy(initfn)------属性继承。 __copy(include/linux/compiler_attributes.h)让 init_module 继承 initfn 声明上的属性(可见性、CFI 规范性等),保证别名符号与本体在链接器与 CFI 眼中行为一致。
__CFI_ADDRESSABLE(init_module, __initdata)------控制流完整性适配。 开启 Clang CFI 时,include/linux/cfi.h 将其展开为一个跳转表项 __cfi_jt_init_module。内核通过函数指针调用 mod->init 时,CFI 要校验目标类型;直接调用真实函数会绕过跳转表导致校验失败。于是加载器的 cfi_init()(kernel/module.c)专门查找 __cfi_jt_init_module,把 mod->init 改指为跳转表项。冷门,但在开启 CFI 的内核上缺了它模块就无法加载。
4.3 module_exit:完全对称的镜像
c
/* include/linux/module.h */
#define module_exit(exitfn) \
static inline exitcall_t __maybe_unused __exittest(void) \
{ return exitfn; } \
void cleanup_module(void) __copy(exitfn) \
__attribute__((alias(#exitfn))); \
__CFI_ADDRESSABLE(cleanup_module, __exitdata);
结构上逐项对应 module_init:别名 cleanup_module、类型门卫 __exittest、CFI 跳转表项。rmmod 触发 delete_module 系统调用时执行 mod->exit。两点差异:内建形态下完全空操作(不可卸载的代码无需清理路径);且模块可以选择不写 module_exit------不可卸载模块(如带 __init 数据的)是合法存在。
五、设计哲学小结
回看全文,module.h 的这些宏示范了几条可迁移的设计原则:
- 将编译形态差异集中到宏定义处 。
obj-m/obj-y的形态差异由MODULE宏的展开吸收,数万个驱动源码无需编写任何#ifdef。复杂度并未减少,而是集中于宏定义一处,调用方因此保持简洁 - 用数据段做接口 。
.modinfo以tag=value字符串为契约,写入方与多个消费方(modinfo、depmod、内核加载器)彻底解耦,新增一种信息只需新增一个 tag,无需改动任何读取方 - 用固定命名约定换零成本对接 。
init_module这个固定符号名虽然限制了入口命名,却换来了 modpost 与内核加载器之间一条极简的约定;驱动侧的命名自由由alias属性补偿 - 把错误前移 。
__inittest用一个多余的内联函数,把入口签名错误从运行期崩溃提前到编译期报错 - 属性修饰符皆有明确用途 。
__used、__aligned(1)、__copy、__CFI_ADDRESSABLE均非装饰,理解它们就是理解链接器与加载器的约束
附录 A:延伸阅读
include/linux/module.h--- 所有MODULE_*宏定义include/linux/moduleparam.h---__MODULE_INFO、模块参数机制include/linux/init.h--- initcall 分级机制、.initcallN.init段kernel/module.c--- 模块加载器:load_module、do_init_module、get_modinfoscripts/mod/modpost.c---__this_module生成、固定符号名检查scripts/mod/file2alias.c--- 设备表、modalias 字符串
附录 B:宏速查总表
| 宏 | 作用 |
|---|---|
MODULE_LICENSE |
声明许可证;供内核做 GPL 兼容性检查,非兼容模块被标记 taint 且不能使用 EXPORT_SYMBOL_GPL 符号 |
MODULE_AUTHOR |
声明作者,modinfo 可见 |
MODULE_DESCRIPTION |
声明模块功能描述,modinfo 可见 |
MODULE_FIRMWARE |
声明运行时需要的固件文件名 |
MODULE_IMPORT_NS |
声明导入的符号命名空间,未声明则不能使用该空间的导出符号 |
MODULE_DEVICE_TABLE |
为设备 ID 表生成固定命名的别名符号,构建期据此生成 modalias,支撑驱动自动加载 |
module_init(fn) |
注册入口函数:内建时挂入 initcall 段随启动调用;模块时以 init_module 别名供加载器调用 |
module_exit(fn) |
注册卸载函数:模块时以 cleanup_module 别名供 rmmod 调用;内建时不生效 |
⚠️ 内建形态下
module_exit注册的指针永远不会被调用;依赖卸载逻辑清理资源的驱动,内建时资源生命周期等同内核生命周期。
版权声明(重申) 本文为原创技术文章,著作权归作者所有。未经作者书面授权,严禁任何形式的转载、摘编、复制、建立镜像、商业使用。
Copyright © 2026. All rights reserved. Unauthorized reproduction, distribution, or commercial use is strictly prohibited.