基于 Linux Kernel 源码深度分析 关键文件:
include/linux/module.h|include/linux/moduleparam.h|include/linux/init.h|kernel/module/main.c|kernel/module/signing.c
- 内核模块概述
- .ko ELF 文件格式深度解析
- 模块加载流程(load_module)
- 符号导出与解析机制
- 模块参数系统
- 模块卸载机制
- 模块签名验证
- initcall 机制与优先级体系
- 模块内存保护
- 常见模块开发模式
- 符号解析详细流程剖析
- 模块内核符号命名空间
- 模块的 livepatch 支持
- 模块系统的 RCU 并发模型
- 模块构建系统(Kbuild)
- 深入案例:xt_RATEEST 模块
- 模块调试技术
- 模块与 BPF 的交互
- 关键数据结构关系图
- 工具链与调试命令速查
Linux 内核面临一个根本矛盾:硬件种类繁多、驱动数量庞大,但系统内存有限,不可能把所有驱动代码都常驻内存。内核模块(Loadable Kernel Module,LKM)是这一矛盾的工程化解法。
模块本质是一段可被动态链接进内核地址空间的 ELF 目标文件(.ko),它运行在内核空间,与内核共享同一套虚拟地址,具备与内核代码完全相同的权限。这是模块与用户态动态库(.so)的本质区别——.so 通过 MMU 地址映射运行在用户空间,.ko 直接映射进内核地址空间。
静态编译(built-in) 模块化(loadable)
+-------------------------+ +------------------------+
启动速度 | 快,无需加载过程 | | 需运行时 insmod/modprobe |
内存占用 | 始终驻留,不可释放 | | 按需加载,用后可卸载 |
初始化顺序 | 严格按 initcall 级别 | | insmod 时即运行 init() |
符号可见性 | 内核全局可见 | | 需显式 EXPORT_SYMBOL |
调试能力 | 难以热更新 | | 可重复加载(开发期方便) |
安全性 | 无法被用户态任意注入 | | 需签名验证等机制 |
适用场景 | 核心调度器、内存管理 | | 驱动、文件系统、netfilter |
+-------------------------+ +------------------------+
通常的实践是:核心调度器、内存管理、VFS 框架静态编译;设备驱动、网络过滤器、文件系统做成模块。make menuconfig 中每个选项的 m(module)选项正是这一机制的体现。
用户空间
┌──────────────────────────────────────────┐
│ insmod / modprobe / rmmod │
│ ↓ syscall (init_module / finit_module)│
└──────────────────────────────────────────┘
│
▼
内核空间(同一虚拟地址空间)
┌──────────────────────────────────────────────────────┐
│ │
│ vmalloc 区域 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ module A │ │ module B │ │ module C │ ... │
│ │ .text │ │ .text │ │ .text │ │
│ │ .data │ │ .data │ │ .data │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ 内核核心代码区(.text/.data) │
│ ┌──────────────────────────────┐ │
│ │ vmlinux(内核主体) │ │
│ │ __ksymtab(导出符号表) │ │
│ └──────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
.ko 文件是 ELF Type = ET_REL(可重定位目标文件),与普通 .o 文件相同,但包含了内核模块特有的 section。链接器没有对 .ko 做最终链接,重定位表项还保留着,由内核在加载时完成最终重定位。
.ko 文件布局(ELF ET_REL):
+--------------------+ 偏移 0
| ELF Header (64B) | e_type=ET_REL, e_machine=EM_X86_64
| | e_shoff → 节头表偏移, e_shnum → 节数量
+--------------------+
| |
| Section Data | 各节的实际内容(字节流)
| |
| .text | 机器指令
| .init.text | __init 函数(初始化后可释放)
| .exit.text | __exit 函数
| .data | 已初始化全局变量
| .bss | 未初始化全局变量(仅记录大小)
| .rodata | 只读数据(字符串常量等)
| .modinfo | MODULE_INFO() 生成的键值对
| .gnu.linkonce.this_module │ struct module 原型
| __param | 模块参数描述符(kernel_param 数组)
| __ksymtab | EXPORT_SYMBOL() 导出的符号表
| __ksymtab_gpl | EXPORT_SYMBOL_GPL() 导出的符号表
| __kcrctab | CRC 校验表(CONFIG_MODVERSIONS)
| .symtab | ELF 符号表
| .strtab | 符号名字符串表
| .rela.text | .text 节的重定位表
| .rela__param | 参数节重定位表
| |
+--------------------+
| Section Header Table| 节头表:每个 Elf64_Shdr 64 字节
+--------------------+
包含模块的可执行代码。.init.text 子节包含所有用 __init 标记的函数,这些函数在模块初始化完成后会被释放。include/linux/init.h:45 中的宏定义:
// include/linux/init.h:45
#define __init __section(".init.text") __cold __latent_entropy \
__no_kstack_erase
#define __initdata __section(".init.data")
#define __initconst __section(".init.rodata")
#define __exitdata __section(".exit.data")
#define __exit_call __used __section(".exitcall.exit")__cold 属性告诉编译器这是冷路径代码,生成时会放到函数末尾,减少 icache 压力。__latent_entropy 用于内核熵池初始化。__no_kstack_erase 禁用该函数的栈擦除(节省初始化阶段的性能开销)。
.data:已初始化的全局/静态变量(读写).bss:未初始化的全局/静态变量,ELF 中仅记录大小,不存储实际数据,由内核加载器在分配内存后memset为零.rodata:只读数据,加载后映射为只读页.data..ro_after_init:加载时可写,init()完成后变为只读(对应mod_mem_type中的MOD_RO_AFTER_INIT,见include/linux/module.h:323).data..read_mostly:主要被读取的数据,编译器/链接器将其聚合在同一缓存行,减少写入时的缓存失效
.modinfo 节由 MODULE_INFO() 宏填充,存储键值对字符串,每个字符串以 \0 结尾,格式为 key=value(include/linux/moduleparam.h:33-39):
// include/linux/moduleparam.h:33
#define MODULE_INFO(tag, info) \
static_assert( \
sizeof(info) - 1 == __builtin_strlen(info), \
"MODULE_INFO(" #tag ", ...) contains embedded NUL byte"); \
static const char __UNIQUE_ID(modinfo)[] \
__used __section(".modinfo") __aligned(1) \
= __MODULE_INFO_PREFIX __stringify(tag) "=" infoMODULE_LICENSE() 正是通过这个宏实现(include/linux/module.h:230):
// include/linux/module.h:230
#define MODULE_LICENSE(_license) MODULE_FILE MODULE_INFO(license, _license)MODULE_ALIAS() 同理(include/linux/module.h:163):
// include/linux/module.h:163
#define MODULE_ALIAS(_alias) MODULE_INFO(alias, _alias).modinfo 节的内容可通过 modinfo <module.ko> 命令查看,内核加载时用这些信息进行各种检查(如 vermagic 版本魔数校验)。
modinfo 常见字段示例(以当前工作区 net/netfilter/xt_DSCP.c 为例):
license=GPL
author=Harald Welte <laforge@netfilter.org>
description=Xtables: DSCP/TOS field match
alias=ipt_dscp
alias=ip6t_dscp
alias=ipt_tos
alias=ip6t_tos
vermagic=6.x.x SMP preempt mod_unload ...
这对应 net/netfilter/xt_DSCP.c:16-22 的声明:
// net/netfilter/xt_DSCP.c:16
MODULE_AUTHOR("Harald Welte <laforge@netfilter.org>");
MODULE_DESCRIPTION("Xtables: DSCP/TOS field match");
MODULE_LICENSE("GPL");
MODULE_ALIAS("ipt_dscp");
MODULE_ALIAS("ip6t_dscp");这是模块中最特殊的节,它包含模块自身的 struct module 实例(__this_module)。内核加载时将该节的数据复制到内核管理的内存中,并填充运行时字段。
在 include/linux/init.h:173-174 中:
// include/linux/init.h:173
#ifdef MODULE
extern struct module __this_module;
#define THIS_MODULE (&__this_module)
#else
#define THIS_MODULE ((struct module *)0)
#endif模块代码通过 THIS_MODULE 指针引用自身的 struct module,用于引用计数、注册时标识所属模块等。
module_param() 最终通过 __module_param_call 宏将一个 struct kernel_param 变量放入 __param 节(include/linux/moduleparam.h:299-306):
// include/linux/moduleparam.h:299
#define __module_param_call(prefix, name, ops, arg, perm, level, flags) \
static_assert(sizeof(""prefix) - 1 <= __MODULE_NAME_LEN); \
static const char __param_str_##name[] = prefix #name; \
static struct kernel_param __moduleparam_const __param_##name \
__used __section("__param") \
__aligned(__alignof__(struct kernel_param)) \
= { __param_str_##name, THIS_MODULE, ops, \
VERIFY_OCTAL_PERMISSIONS(perm), level, flags, { arg } }struct module 是内核中描述一个已加载模块的核心数据结构,定义在 include/linux/module.h:397-585。其关键字段如下:
// include/linux/module.h:397
struct module {
enum module_state state; // 模块状态:LIVE/COMING/GOING/UNFORMED
struct list_head list; // 全局 modules 链表节点
char name[MODULE_NAME_LEN]; // 模块名(最长 56 字节)
struct module_kobject mkobj; // sysfs kobject
struct module_attribute *modinfo_attrs;
const char *version;
const char *srcversion;
struct kobject *holders_dir;
/* 导出符号 */
const struct kernel_symbol *syms; // EXPORT_SYMBOL 符号表
const u32 *crcs; // 符号 CRC(MODVERSIONS)
unsigned int num_syms;
/* GPL-only 符号 */
unsigned int num_gpl_syms;
const struct kernel_symbol *gpl_syms;
const u32 *gpl_crcs;
bool using_gplonly_symbols;
#ifdef CONFIG_MODULE_SIG
bool sig_ok; // 签名验证结果
#endif
/* 异常表 */
unsigned int num_exentries;
struct exception_table_entry *extable;
/* 初始化/清理函数指针 */
int (*init)(void);
/* 内存区域:MOD_TEXT/MOD_DATA/MOD_RODATA/... */
struct module_memory mem[MOD_MEM_NUM_TYPES] __module_memory_align;
struct mod_arch_specific arch; // 架构特定数据
unsigned long taints; // 污点标志
/* KALLSYMS */
struct mod_kallsyms __rcu *kallsyms;
struct mod_kallsyms core_kallsyms;
char *args; // 模块参数字符串
#ifdef CONFIG_MODULE_UNLOAD
struct list_head source_list; // 依赖本模块的模块列表
struct list_head target_list; // 本模块依赖的模块列表
void (*exit)(void); // 清理函数指针
atomic_t refcnt; // 引用计数
#endif
} ____cacheline_aligned __randomize_layout;__randomize_layout 标记表示在开启 CONFIG_RANDSTRUCT 时,结构体字段顺序会被随机化以对抗内核堆喷攻击。
enum mod_mem_type 定义了模块内存的七种类型(include/linux/module.h:318-329):
// include/linux/module.h:318
enum mod_mem_type {
MOD_TEXT = 0, // 代码段(可执行)
MOD_DATA, // 数据段(读写)
MOD_RODATA, // 只读数据
MOD_RO_AFTER_INIT, // init 后变为只读
MOD_INIT_TEXT, // __init 代码(初始化后释放)
MOD_INIT_DATA, // __initdata(初始化后释放)
MOD_INIT_RODATA, // __initconst(初始化后释放)
MOD_MEM_NUM_TYPES,
};内核提供两个加载模块的系统调用,均定义在 kernel/module/main.c:
用户空间工具调用链:
insmod <module.ko>
│
├─ 旧式:syscall(SYS_init_module, buf, len, args)
│ ↓
│ SYSCALL_DEFINE3(init_module) [main.c:3570]
│ → copy_module_from_user() 将 .ko 复制到内核
│ → load_module(&info, uargs, 0)
│
└─ 新式:syscall(SYS_finit_module, fd, args, flags)
↓
SYSCALL_DEFINE3(finit_module) [main.c:3735]
→ idempotent_init_module()
→ init_module_from_file()
→ load_module(&info, uargs, flags)
init_module 系统调用实现(kernel/module/main.c:3570):
// kernel/module/main.c:3570
SYSCALL_DEFINE3(init_module, void __user *, umod,
unsigned long, len, const char __user *, uargs)
{
int err;
struct load_info info = { };
err = may_init_module(); // 检查 CAP_SYS_MODULE 权限
if (err)
return err;
err = copy_module_from_user(umod, len, &info); // 复制 ELF 到内核
if (err)
return err;
return load_module(&info, uargs, 0);
}finit_module(kernel/module/main.c:3735)是更现代的接口,接受文件描述符而非内存指针,支持通过文件签名做幂等性检查,避免同一模块被并发加载两次。
load_module() 是整个加载过程的核心函数(kernel/module/main.c:3358),其完整流程如下:
load_module(info, uargs, flags)
│
├─ 1. module_sig_check() 签名校验(最早执行)
│
├─ 2. elf_validity_cache_copy() ELF 头合法性校验 + 缓存节头表
│
├─ 3. early_mod_check() 早期模块检查(vermagic、许可证等)
│
├─ 4. layout_and_allocate() 计算内存布局 + module_alloc() 分配
│ └─ 为 7 种 mod_mem_type 分别分配 vmalloc 区域
│
├─ 5. add_unformed_module() 加入 modules 全局链表(状态=UNFORMED)
│
├─ 6. percpu_modalloc() 分配 per-CPU 数据
│
├─ 7. module_unload_init() 初始化引用计数和依赖链表
│
├─ 8. find_module_sections() 定位各特殊节(__param、__ksymtab 等)
│
├─ 9. check_export_symbol_versions() 检查 CRC 版本兼容性
│
├─ 10. setup_modinfo() 从 .modinfo 节提取 version/srcversion
│
├─ 11. simplify_symbols() 符号解析:对 .symtab 中的未定义符号
│ └─ resolve_symbol() → find_symbol() → 查内核符号表或其他模块
│
├─ 12. apply_relocations() 应用重定位表(.rela.text 等)
│
├─ 13. post_relocation() 重定位后的修正工作
│
├─ 14. flush_module_icache() 刷新指令缓存
│
├─ 15. complete_formation() 将模块状态升为 COMING
│
├─ 16. prepare_coming_module() 通知链(MODULE_STATE_COMING)
│
├─ 17. parse_args() 解析命令行参数到 module->kp
│
├─ 18. mod_sysfs_setup() 创建 /sys/module/<name>/ 目录
│
├─ 19. free_copy() 释放临时内核缓冲区(ELF 文件副本)
│
└─ 20. do_init_module() 调用 mod->init(),释放 __init 内存
elf_validity_cache_copy() 完成基本合法性检查后,内核通过以下辅助函数操作 ELF 结构(kernel/module/main.c:236-313):
// kernel/module/main.c:236
static unsigned int find_sec(const struct load_info *info, const char *name)
{
unsigned int i;
for (i = 1; i < info->hdr->e_shnum; i++) {
Elf_Shdr *shdr = &info->sechdrs[i];
if ((shdr->sh_flags & SHF_ALLOC)
&& strcmp(info->secstrings + shdr->sh_name, name) == 0)
return i;
}
return 0;
}load_info 结构体缓存了 ELF 头指针、节头表数组、节字符串表等信息,避免重复解析。每个节通过 Elf_Shdr(即 Elf64_Shdr)描述,关键字段:
sh_name 节名字符串在 .shstrtab 中的偏移
sh_type SHT_PROGBITS(代码/数据)/ SHT_RELA(重定位)/ SHT_SYMTAB 等
sh_flags SHF_ALLOC(需分配内存)/ SHF_EXECINSTR(可执行)/ SHF_WRITE
sh_addr 运行时地址(加载前为 0,加载后填入虚拟地址)
sh_offset 节在文件中的字节偏移
sh_size 节的字节大小
sh_link 关联节索引(符号表 → 字符串表)
sh_addralign 对齐要求
sh_entsize 条目大小(用于符号表、重定位表等固定大小条目的节)
layout_and_allocate() 负责计算各内存区域的布局并调用 execmem_alloc()(底层为 vmalloc 区域)分配内存:
内核虚拟地址空间(64位 x86_64):
ffffffff80000000 ← 内核核心代码(vmlinux .text)
...
ffffffffa0000000 ← 模块区域开始(MODULES_VADDR)
│
│ ┌──────────────────┐
│ │ xt_DSCP .text │ MOD_TEXT(可执行,只读)
│ │ xt_DSCP .rodata │ MOD_RODATA(不可执行,只读)
│ │ xt_DSCP .data │ MOD_DATA(读写)
│ │ xt_DSCP .init.* │ MOD_INIT_TEXT/DATA(临时,用后释放)
│ └──────────────────┘
│ ┌──────────────────┐
│ │ xt_HL .text │
│ ...
│
ffffffffc0000000 ← 模块区域结束(MODULES_END)
每种 mod_mem_type 都由独立的 module_memory_alloc() 处理(kernel/module/main.c:1312):
// kernel/module/main.c:1312
static int module_memory_alloc(struct module *mod, enum mod_mem_type type)
{
unsigned int size = PAGE_ALIGN(mod->mem[type].size);
enum execmem_type execmem_type;
void *ptr;
mod->mem[type].size = size;
// 代码和数据使用不同的分配器
if (mod_mem_type_is_data(type))
execmem_type = EXECMEM_MODULE_DATA;
else
execmem_type = EXECMEM_MODULE_TEXT;
ptr = execmem_alloc_rw(execmem_type, size);
if (!ptr)
return -ENOMEM;
mod->mem[type].is_rox = execmem_is_rox(execmem_type);
if (!mod->mem[type].is_rox)
kmemleak_not_leak(ptr);
memset(ptr, 0, size); // 清零(.bss 依赖此操作)
mod->mem[type].base = ptr;
return 0;
}加载过程中,.symtab 里 st_shndx == SHN_UNDEF 的符号(即外部引用)必须在内核或其他模块中找到对应的实现。simplify_symbols() 遍历所有符号,对未定义的符号调用 resolve_symbol()。
find_symbol() 函数在两个层面查找(kernel/module/main.c:388-424):
// kernel/module/main.c:388
bool find_symbol(struct find_symbol_arg *fsa)
{
static const struct symsearch arr[] = {
// 内核核心导出的普通符号
{ __start___ksymtab, __stop___ksymtab,
__start___kcrctab, NOT_GPL_ONLY },
// 内核核心导出的 GPL-only 符号
{ __start___ksymtab_gpl, __stop___ksymtab_gpl,
__start___kcrctab_gpl, GPL_ONLY },
};
struct module *mod;
unsigned int i;
// 先在内核核心符号表中查找
for (i = 0; i < ARRAY_SIZE(arr); i++)
if (find_exported_symbol_in_section(&arr[i], NULL, fsa))
return true;
// 再在所有已加载模块的符号表中查找
list_for_each_entry_rcu(mod, &modules, list, ...) {
struct symsearch arr[] = {
{ mod->syms, mod->syms + mod->num_syms,
mod->crcs, NOT_GPL_ONLY },
{ mod->gpl_syms, mod->gpl_syms + mod->num_gpl_syms,
mod->gpl_crcs, GPL_ONLY },
};
...
}
return false;
}find_exported_symbol_in_section() 使用 bsearch() 在排序好的 kernel_symbol 数组中二分查找(kernel/module/main.c:362-382),因此导出符号表必须在构建时排序(由链接脚本保证)。
GPL-only 符号检查在此处进行:如果模块没有 GPL 兼容许可证(即 fsa->gplok == false),则不允许访问 GPL-only 符号(kernel/module/main.c:368):
// kernel/module/main.c:368
if (!fsa->gplok && syms->license == GPL_ONLY)
return false;重定位是将符号引用转换为实际内存地址的过程。对于 x86_64,重定位类型主要有:
R_X86_64_PC32 32 位 PC 相对寻址(call/jmp 指令)
R_X86_64_PLT32 PLT 相对(函数调用)
R_X86_64_64 64 位绝对地址
R_X86_64_32S 32 位符号扩展绝对地址
重定位表项(Elf64_Rela)包含:
r_offset:需要修改的位置(相对节起始的偏移)r_info:高 32 位为符号表索引,低 32 位为重定位类型r_addend:加数
内核的 apply_relocate_add() 函数(架构特定,如 arch/x86/kernel/module.c)对每条重定位项:
- 从符号表找到符号的运行时地址(经过
simplify_symbols()已填好) - 根据重定位类型计算最终值(如 PC 相对 = 目标地址 - 当前指令地址 - 4)
- 将计算结果写入
r_offset指向的位置
do_init_module() 是真正执行 init() 函数并释放 __init 内存的函数(kernel/module/main.c:3017):
// kernel/module/main.c:3017
static noinline int do_init_module(struct module *mod)
{
struct mod_initfree *freeinit;
// 记录 init 区域地址,供后续释放
freeinit->init_text = mod->mem[MOD_INIT_TEXT].base;
freeinit->init_data = mod->mem[MOD_INIT_DATA].base;
freeinit->init_rodata = mod->mem[MOD_INIT_RODATA].base;
do_mod_ctors(mod); // 调用 C++ 构造函数(如有)
// 执行模块的 init() 函数
if (mod->init != NULL)
ret = do_one_initcall(mod->init);
// init 成功:将模块状态设为 LIVE
mod->state = MODULE_STATE_LIVE;
// 通知链:MODULE_STATE_LIVE
blocking_notifier_call_chain(&module_notify_list,
MODULE_STATE_LIVE, mod);
// 等待异步探测完成
if (!mod->async_probe_requested)
async_synchronize_full();
// 释放 ftrace 追踪的 init_text 条目
ftrace_free_mem(mod, mod->mem[MOD_INIT_TEXT].base, ...);
mutex_lock(&module_mutex);
module_put(mod); // 释放初始化期间持有的引用
// 切换 kallsyms 到只含核心符号的版本
rcu_assign_pointer(mod->kallsyms, &mod->core_kallsyms);
// 将 ro_after_init 区域设为只读
module_enable_rodata_ro_after_init(mod);
// 从 mod_tree 中删除 init 节点
mod_tree_remove_init(mod);
// 异步通过工作队列释放 init 内存
llist_add(&freeinit->node, &init_free_list);
schedule_work(&init_free_wq);
}__init 内存之所以要异步释放(通过工作队列),是因为 do_init_module() 本身就运行在 init() 函数的调用链中,不能在返回之前释放 init_text(否则会释放仍在使用的栈帧)。
do_free_init() 是工作队列处理函数(kernel/module/main.c:2982):
// kernel/module/main.c:2982
static void do_free_init(struct work_struct *w)
{
struct llist_node *pos, *n, *list;
struct mod_initfree *initfree;
list = llist_del_all(&init_free_list);
// 等待 RCU 宽限期:确保没有读者还在访问 init 区域符号
synchronize_rcu();
llist_for_each_safe(pos, n, list) {
initfree = container_of(pos, struct mod_initfree, node);
execmem_free(initfree->init_text);
execmem_free(initfree->init_data);
execmem_free(initfree->init_rodata);
kfree(initfree);
}
}EXPORT_SYMBOL() 和 EXPORT_SYMBOL_GPL() 定义在 include/linux/export.h:89-90:
// include/linux/export.h:89
#define EXPORT_SYMBOL(sym) _EXPORT_SYMBOL(sym, "")
#define EXPORT_SYMBOL_GPL(sym) _EXPORT_SYMBOL(sym, "GPL")两者最终展开为 ___EXPORT_SYMBOL() 内联汇编(include/linux/export.h:33):
// include/linux/export.h:33
#define ___EXPORT_SYMBOL(sym, license, ns...) \
.section ".export_symbol","a" ASM_NL \
__export_symbol_##sym: ASM_NL \
.asciz license ASM_NL \
.ascii ns "\0" ASM_NL \
__EXPORT_SYMBOL_REF(sym) ASM_NL \
.previous在 C 代码中使用时(include/linux/export.h:75-79):
// include/linux/export.h:75
#define __EXPORT_SYMBOL(sym, license, ns) \
extern typeof(sym) sym; \
__ADDRESSABLE(sym) \
__GENDWARFKSYMS_EXPORT(sym) \
asm(__stringify(___EXPORT_SYMBOL(sym, license, ns)))这会在 .export_symbol 节中生成一个条目,包含:许可证字符串 + 命名空间字符串 + 符号地址引用。构建工具链(scripts/mod/modpost.c)在链接期间将 .export_symbol 处理成最终的 __ksymtab 和 __ksymtab_strings。
最终生成的内核符号表布局如下:
__ksymtab(kernel_symbol 数组,按符号名排序):
┌─────────────────────────────────────────┐
│ struct kernel_symbol { │
│ int value_offset; // 相对偏移→符号地址 │
│ int name_offset; // 相对偏移→符号名 │
│ int namespace_offset; // 命名空间 │
│ } │
├─────────────────────────────────────────┤
│ struct kernel_symbol { ... } │ (排序后)
│ ... │
└─────────────────────────────────────────┘
__ksymtab_strings(符号名字符串池):
"alloc_pages\0blk_mq_init_queue\0copy_from_user\0..."
CONFIG_HAVE_ARCH_PREL32_RELOCATIONS 开启时使用 PC 相对 32 位偏移(而非绝对地址),这样符号表本身可以放在只读节中,减少运行时重定位需求。
内核核心符号表的边界通过链接器符号暴露(kernel/module/main.c:390-396):
// kernel/module/main.c:390
static const struct symsearch arr[] = {
{ __start___ksymtab, __stop___ksymtab,
__start___kcrctab, NOT_GPL_ONLY },
{ __start___ksymtab_gpl, __stop___ksymtab_gpl,
__start___kcrctab_gpl, GPL_ONLY },
}; EXPORT_SYMBOL EXPORT_SYMBOL_GPL
+--------------------+ +--------------------+
许可证标记 | license=""(空) | | license="GPL" |
查找许可 | NOT_GPL_ONLY | | GPL_ONLY |
可用模块 | 任何模块 | | 仅 GPL 兼容许可模块 |
拒绝条件 | 无 | | !fsa->gplok 时拒绝 |
典型用途 | 通用 API | | 内核内部接口 |
+--------------------+ +--------------------+
gplok 标志在 resolve_symbol() 时由模块许可证决定。许可证字符串解析在 kernel/module/main.c 中通过 license_is_gpl_compatible() 函数完成,判断 .modinfo 中 license= 字段是否包含 "GPL" 变体。
resolve_symbol() 是符号解析的核心函数(kernel/module/main.c:1231),其执行顺序如下:
// kernel/module/main.c:1231
static const struct kernel_symbol *resolve_symbol(struct module *mod,
const struct load_info *info,
const char *name,
char ownername[])
{
struct find_symbol_arg fsa = {
.name = name,
// GPL 检查:专有模块不允许使用 GPL-only 符号
.gplok = !(mod->taints & (1 << TAINT_PROPRIETARY_MODULE)),
.warn = true,
};
mutex_lock(&module_mutex);
if (!find_symbol(&fsa))
goto unlock;
// 记录是否使用了 GPL-only 符号(供后续报告)
if (fsa.license == GPL_ONLY)
mod->using_gplonly_symbols = true;
// 污点继承:被依赖的模块如果 tainted,依赖方也继承污点
if (!inherit_taint(mod, fsa.owner, name)) {
fsa.sym = NULL;
goto getname;
}
// CRC 版本检查(CONFIG_MODVERSIONS)
if (!check_version(info, name, mod, fsa.crc)) {
fsa.sym = ERR_PTR(-EINVAL);
goto getname;
}
// 命名空间检查
err = verify_namespace_is_imported(info, fsa.sym, mod);
// 建立模块依赖关系(增加被依赖模块的引用计数)
err = ref_module(mod, fsa.owner);
...
}resolve_symbol_wait()(kernel/module/main.c:1287)在此基础上增加了等待机制:若目标符号所在模块正在初始化(MODULE_STATE_COMING),则等待最多 30 秒:
// kernel/module/main.c:1294
if (wait_event_interruptible_timeout(module_wq,
!IS_ERR(ksym = resolve_symbol(mod, info, name, owner))
|| PTR_ERR(ksym) != -EBUSY,
30 * HZ) <= 0) {
pr_warn("%s: gave up waiting for init of module %s.\n",
mod->name, owner);
}kallsyms_lookup_name() 提供了在运行时按名称查找内核符号地址的能力:
kallsyms_lookup_name("schedule")
│
├─ 在 kallsyms_names 表中二分查找
│ (该表在编译时由 scripts/kallsyms.c 生成)
│
└─ 返回符号的运行时地址(unsigned long)
注意:自 Linux 5.7 起,kallsyms_lookup_name() 不再对内核外部模块暴露(未被 EXPORT_SYMBOL),模块必须使用 __symbol_get() 或其他接口。
开启 CONFIG_MODVERSIONS 时,每个导出符号都有一个 32 位 CRC,在构建时由 genksyms 工具根据符号的类型签名计算(kernel/module/main.c:331-335):
// kernel/module/main.c:331
#ifndef CONFIG_MODVERSIONS
#define symversion(base, idx) NULL
#else
#define symversion(base, idx) ((base != NULL) ? ((base) + (idx)) : NULL)
#endifCRC 不匹配时,insmod 默认拒绝加载(MODULE_INIT_IGNORE_MODVERSIONS 标志可以跳过此检查,但会导致内核 taint)。
module_param() 是模块参数的主要入口宏,定义在 include/linux/moduleparam.h:139-140:
// include/linux/moduleparam.h:139
#define module_param(name, type, perm) \
module_param_named(name, name, type, perm)展开链:
module_param(debug, int, 0644)
↓
module_param_named(debug, debug, int, 0644)
↓ include/linux/moduleparam.h:162
param_check_int(debug, &debug); // 类型安全检查
module_param_cb(debug, ¶m_ops_int, &debug, 0644);
__MODULE_PARM_TYPE(debug, "int"); // 写入 .modinfo
↓ include/linux/moduleparam.h:188
__module_param_call(MODULE_PARAM_PREFIX, debug, ¶m_ops_int, &debug, 0644, -1, 0)
↓ include/linux/moduleparam.h:299
// 生成静态变量,放入 __param 节:
static const char __param_str_debug[] = "debug";
static struct kernel_param const __param_debug
__used __section("__param")
__aligned(__alignof__(struct kernel_param))
= { "debug", THIS_MODULE, ¶m_ops_int,
0644, -1, 0, { &debug } }
struct kernel_param 是每个模块参数的描述符(include/linux/moduleparam.h:82-94):
// include/linux/moduleparam.h:82
struct kernel_param {
const char *name; // 参数名(如 "debug")
struct module *mod; // 所属模块
const struct kernel_param_ops *ops; // set/get 操作集
const u16 perm; // sysfs 权限(如 0644)
s8 level; // initcall 级别(-1 = 任意时机)
u8 flags; // KERNEL_PARAM_FL_UNSAFE 等
union {
void *arg; // 变量地址(普通类型)
const struct kparam_string *str; // 字符串参数
const struct kparam_array *arr; // 数组参数
};
};struct kernel_param_ops 定义了参数的操作接口(include/linux/moduleparam.h:60-69):
// include/linux/moduleparam.h:60
struct kernel_param_ops {
unsigned int flags; // KERNEL_PARAM_OPS_FL_NOARG
int (*set)(const char *val, const struct kernel_param *kp); // 写参数
int (*get)(char *buffer, const struct kernel_param *kp); // 读参数
void (*free)(void *arg); // 模块卸载时释放(用于 charp)
};param_check_##type 宏在编译期做类型检查。以 int 类型为例:
// 内核预定义(概念等效)
#define param_check_int(name, p) __param_check(name, p, int)
#define __param_check(name, p, type) \
static inline type *__check_##name(void) { return(p); }这通过返回类型不兼容在编译时产生错误,防止开发者传入错误类型的变量地址。
支持的内置类型(include/linux/moduleparam.h:130-137):
byte, hexint, short, ushort, int, uint, long, ulong
charp 字符指针(内核自动 kstrdup/kfree)
bool 布尔值(0/1, y/n, Y/N)
invbool 反向布尔(N=true)
数组参数通过 module_param_array() 宏支持,底层使用 struct kparam_array:
// include/linux/moduleparam.h:105
struct kparam_array {
unsigned int max; // 最大元素数
unsigned int elemsize; // 单个元素大小
unsigned int *num; // 实际元素数指针
const struct kernel_param_ops *ops; // 元素操作集
void *elem; // 数组基地址
};模块加载后,mod_sysfs_setup() 在 sysfs 下创建:
/sys/module/<modname>/
├── parameters/ ← 各模块参数
│ ├── debug ← 对应 module_param(debug, ...)
│ ├── timeout
│ └── ...
├── refcnt ← 引用计数
├── initstate ← live / coming / going
├── srcversion ← 源码 MD5
└── sections/ ← ELF 节地址(需 CAP_SYS_ADMIN)
├── .text
├── .data
└── ...
读取参数值时,sysfs 调用 kernel_param_ops->get();写入时调用 set()。写操作受 param_lock 互斥锁保护(include/linux/module.h:430)。
如果参数 perm == 0,则不在 sysfs 中创建对应文件(仅支持内核命令行设置)。
模块加载时,insmod module.ko param1=value1 param2=value2 中的参数字符串经由 load_module() 传入,最终由 parse_args() 解析(kernel/module/main.c:3485):
// kernel/module/main.c:3485
after_dashes = parse_args(mod->name, mod->args, mod->kp, mod->num_kp,
-32768, 32767, mod,
unknown_module_param_cb);parse_args() 逐个解析 key=value 对,通过名称匹配找到对应的 kernel_param,然后调用其 ops->set() 函数设置变量值。
模块卸载由 SYSCALL_DEFINE2(delete_module, ...) 处理,定义在 kernel/module/main.c:776:
// kernel/module/main.c:776
SYSCALL_DEFINE2(delete_module, const char __user *, name_user,
unsigned int, flags)
{
struct module *mod;
char name[MODULE_NAME_LEN];
int ret, forced = 0;
// 1. 权限检查:需要 CAP_SYS_MODULE
if (!capable(CAP_SYS_MODULE) || modules_disabled)
return -EPERM;
mutex_lock(&module_mutex);
// 2. 在 modules 链表中查找
mod = find_module(name);
// 3. 检查是否有其他模块依赖本模块
if (!list_empty(&mod->source_list)) {
ret = -EWOULDBLOCK;
goto out;
}
// 4. 检查模块状态
if (mod->state != MODULE_STATE_LIVE) {
ret = -EBUSY;
goto out;
}
// 5. 检查 init 有但 exit 无的情况
if (mod->init && !mod->exit) {
forced = try_force_unload(flags);
if (!forced) {
ret = -EBUSY;
goto out;
}
}
// 6. 尝试停止模块(减引用计数)
ret = try_stop_module(mod, flags, &forced);
mutex_unlock(&module_mutex);
// 7. 调用 exit() 函数
if (mod->exit != NULL)
mod->exit();
// 8. 通知链
blocking_notifier_call_chain(&module_notify_list,
MODULE_STATE_GOING, mod);
// 9. 释放模块资源
free_module(mod);
}模块引用计数通过 atomic_t refcnt 字段(include/linux/module.h:569)和三个函数管理:
// kernel/module/main.c:907
bool try_module_get(struct module *module)
{
bool ret = true;
if (module) {
if (likely(module_is_live(module) &&
atomic_inc_not_zero(&module->refcnt) != 0))
trace_module_get(module, _RET_IP_);
else
ret = false; // 模块正在卸载,获取失败
}
return ret;
}
EXPORT_SYMBOL(try_module_get);
// kernel/module/main.c:923
void module_put(struct module *module)
{
if (module) {
ret = atomic_dec_if_positive(&module->refcnt);
WARN_ON(ret < 0);
trace_module_put(module, _RET_IP_);
}
}
EXPORT_SYMBOL(module_put);引用计数使用规则:
try_module_get(mod) ← 使用模块前调用(失败说明模块正在卸载)
│
↓ 使用模块功能...
│
module_put(mod) ← 使用完毕后调用
MODULE_REF_BASE(值为 1)是模块加载时的基础引用计数。卸载时先减去 MODULE_REF_BASE,如果结果为 0 说明没有其他用户在使用(kernel/module/main.c:730):
// kernel/module/main.c:730
static int try_release_module_ref(struct module *mod)
{
int ret;
ret = atomic_sub_return(MODULE_REF_BASE, &mod->refcnt);
BUG_ON(ret < 0);
if (ret)
ret = atomic_add_unless(&mod->refcnt, MODULE_REF_BASE, 0);
return ret;
}模块间依赖通过双向链表维护(kernel/module/main.c:660-674):
模块 A(依赖方) 模块 B(被依赖方)
┌──────────────┐ ┌──────────────┐
│ target_list │←──────────→│ source_list │
│ (我依赖谁) │ module_use │ (谁依赖我) │
└──────────────┘ └──────────────┘
struct module_use {
struct list_head source_list; // 挂在 B->source_list
struct list_head target_list; // 挂在 A->target_list
struct module *source; // = A
struct module *target; // = B
};
当模块 A 首次引用模块 B 的符号时,ref_module(A, B) 被调用,创建 module_use 节点并插入两侧链表。卸载 A 时,module_unload_free() 遍历 A->target_list,对每个目标模块调用 module_put()。
尝试卸载 B 时,如果 B->source_list 非空(说明仍有模块依赖 B),直接返回 -EWOULDBLOCK,用户会看到 rmmod: ERROR: Module b is in use by: a。
module_exit() 宏将清理函数注册为 cleanup_module 的别名(include/linux/module.h:139-144):
// include/linux/module.h:139(MODULE 宏定义版本)
#define module_exit(exitfn) \
static inline exitcall_t __maybe_unused __exittest(void) \
{ return exitfn; } \
void cleanup_module(void) __copy(exitfn) \
__attribute__((alias(#exitfn))); \
___ADDRESSABLE(cleanup_module, __exitdata);cleanup_module 是传统的 ABI 接口名,内核通过 mod->exit 函数指针调用,该指针在 find_module_sections() 时通过查找 cleanup_module 符号地址来初始化。
__exit 标记的函数(include/linux/init.h:79):
// include/linux/init.h:79
#define __exit __section(".exit.text") __exitused __cold notrace如果模块不支持卸载(未定义 module_exit()),则 mod->exit 为 NULL,此时 delete_module() 会在 mod->init && !mod->exit 处返回 -EBUSY。
当有代码正在执行模块内的函数时(引用计数 > MODULE_REF_BASE),卸载会被拒绝。但有两种绕过机制:
-
强制卸载(
CONFIG_MODULE_FORCE_UNLOAD):使用rmmod -f传入O_TRUNC标志,try_force_unload()强行将模块状态设为 GOING,并给内核打TAINT_FORCED_RMMOD污点标志(kernel/module/main.c:714-720)。 -
__module_put_and_kthread_exit():工作在内核线程中运行模块代码时,线程退出前调用此函数同时释放引用并退出线程(kernel/module/main.c:228-232):
// kernel/module/main.c:228
void __noreturn __module_put_and_kthread_exit(struct module *mod, long code)
{
module_put(mod);
kthread_exit(code);
}
EXPORT_SYMBOL(__module_put_and_kthread_exit);内核提供三个签名相关配置选项:
CONFIG_MODULE_SIG 启用签名检查基础设施(但不强制要求签名)
CONFIG_MODULE_SIG_FORCE 强制要求所有模块必须有有效签名
CONFIG_MODULE_SIG_SHA256 使用 SHA-256 摘要算法(可选 SHA-384/512)
签名强制执行状态在运行时通过 sig_enforce 变量控制(kernel/module/signing.c:22-23):
// kernel/module/signing.c:22
static bool sig_enforce = IS_ENABLED(CONFIG_MODULE_SIG_FORCE);
module_param(sig_enforce, bool_enable_only, 0644);bool_enable_only 类型确保该参数只能从 false 改为 true,不可逆。这允许系统运行时动态开启强制签名模式(如进入 lockdown 状态时)。
签名附加在 .ko 文件末尾,格式如下:
.ko 文件布局(带签名):
┌──────────────────────────────────┐
│ ELF 内容(原始 .ko 数据) │ ← modlen 字节
├──────────────────────────────────┤
│ PKCS#7 签名数据(DER 编码) │ ← sig_len 字节
├──────────────────────────────────┤
│ struct module_signature (16B) │ ← 固定结尾
│ algo: 签名算法 │
│ hash: 哈希类型 │
│ id_type: PKEY_ID_PKCS7 │
│ signer_len: 签名者名称长度 │
│ key_id_len: 密钥 ID 长度 │
│ sig_len: PKCS#7 数据长度(BE) │
├──────────────────────────────────┤
│ "~Module signature appended~\n" │ ← MODULE_SIG_STRING 标记
└──────────────────────────────────┘
签名验证核心函数(kernel/module/signing.c:43-68):
// kernel/module/signing.c:43
int mod_verify_sig(const void *mod, struct load_info *info)
{
struct module_signature ms;
size_t sig_len, modlen = info->len;
// 从文件末尾读取 module_signature 结构
memcpy(&ms, mod + (modlen - sizeof(ms)), sizeof(ms));
// 检查结构合法性(算法、标志等)
ret = mod_check_sig(&ms, modlen, "module");
if (ret)
return ret;
// 计算实际签名数据长度和 ELF 内容长度
sig_len = be32_to_cpu(ms.sig_len);
modlen -= sig_len + sizeof(ms);
info->len = modlen; // 告知调用者实际 ELF 长度
// 通过 PKCS#7 验证签名
return verify_pkcs7_signature(mod, modlen,
mod + modlen, sig_len,
VERIFY_USE_SECONDARY_KEYRING,
VERIFYING_MODULE_SIGNATURE,
NULL, NULL);
}module_sig_check() 处理各种签名状态(kernel/module/signing.c:70-125):
module_sig_check(info, flags)
│
├─ 1. 检查文件末尾是否有 MODULE_SIG_STRING 标记
│ │
│ ├─ 有标记 → 调用 mod_verify_sig()
│ │ │
│ │ ├─ 验证成功:info->sig_ok=true,返回 0
│ │ └─ 验证失败:err = -EKEYREJECTED 等致命错误
│ │
│ └─ 无标记 → err = -ENODATA("unsigned module")
│
├─ 2. 处理非致命错误(-ENODATA / -ENOPKG / -ENOKEY):
│ │
│ ├─ is_module_sig_enforced()?
│ │ ├─ 是:pr_notice("rejected") → 返回 -EKEYREJECTED
│ │ └─ 否:调用 security_locked_down()
│ │ └─ lockdown 模式下也会拒绝
│ │
│ └─ 非 lockdown、非强制:允许加载(但 sig_ok=false)
│
└─ 3. 致命错误(内存不足、签名格式错误等):直接返回错误码
签名验证使用的公钥来自内核内置的证书(VERIFY_USE_SECONDARY_KEYRING):
内核密钥环体系:
.builtin_trusted_keys(内置信任密钥环)
│ CONFIG_MODULE_SIG_KEY 指定的密钥(构建时嵌入)
│ CONFIG_SYSTEM_TRUSTED_KEYS 指定的额外证书
│
.secondary_trusted_keys(次级信任密钥环)
│ 可以在运行时添加(需 CAP_SYS_ADMIN)
│
模块签名验证使用 VERIFY_USE_SECONDARY_KEYRING,
即在两个密钥环中都查找匹配的证书
scripts/sign-file 工具使用 CONFIG_MODULE_SIG_KEY 指定的私钥(默认 certs/signing_key.pem,构建时自动生成)对模块进行签名,生成的公钥证书被编译进内核的 .builtin_trusted_keys 密钥环。
内核启动需要按照严格的依赖顺序初始化各个子系统:先初始化总线控制器,再初始化挂在总线上的设备;先初始化文件系统,再挂载根文件系统。initcall 机制通过编译时分类、链接时排序来实现这一需求,无需手动维护复杂的初始化调用图。
内核定义了 8 个(加上 early 和 rootfs,共 10 个)初始化优先级(include/linux/init.h:284-316):
// include/linux/init.h:284
#define early_initcall(fn) __define_initcall(fn, early)
// 级别 0(pure):纯粹的变量初始化,无依赖
#define pure_initcall(fn) __define_initcall(fn, 0)
// 级别 1(core):核心子系统(kmem_cache、workqueue等)
#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
// 级别 2(postcore):总线子系统初始化后
#define postcore_initcall(fn) __define_initcall(fn, 2)
// 级别 3(arch):体系结构相关初始化
#define arch_initcall(fn) __define_initcall(fn, 3)
// 级别 4(subsys):子系统初始化(PCI、USB等总线)
#define subsys_initcall(fn) __define_initcall(fn, 4)
// 级别 5(fs):文件系统注册
#define fs_initcall(fn) __define_initcall(fn, 5)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
// 级别 6(device):设备驱动初始化(最常用)
#define device_initcall(fn) __define_initcall(fn, 6)
// 级别 7(late):延迟初始化(系统基本就绪后)
#define late_initcall(fn) __define_initcall(fn, 7)
// __initcall 等价于 device_initcall(级别 6)
#define __initcall(fn) device_initcall(fn)include/linux/init.h:152-161 中定义了各级别的边界符号:
// include/linux/init.h:152
extern initcall_entry_t __initcall_start[];
extern initcall_entry_t __initcall0_start[];
extern initcall_entry_t __initcall1_start[];
extern initcall_entry_t __initcall2_start[];
extern initcall_entry_t __initcall3_start[];
extern initcall_entry_t __initcall4_start[];
extern initcall_entry_t __initcall5_start[];
extern initcall_entry_t __initcall6_start[];
extern initcall_entry_t __initcall7_start[];
extern initcall_entry_t __initcall_end[];这些符号在链接脚本(include/asm-generic/vmlinux.lds.h)中定义,将各 .initcallN.init 节聚合并标记边界。
以 subsys_initcall(pci_driver_init) 为例展开(非 LTO、非 PREL32 架构,include/linux/init.h:263-265):
// include/linux/init.h:263
#define ____define_initcall(fn, __unused, __name, __sec) \
static initcall_t __name __used \
__attribute__((__section__(__sec))) = fn;
// 展开结果:
static initcall_t __initcall_pci_driver_init4
__used
__attribute__((__section__(".initcall4.init")))
= pci_driver_init;LTO 场景下,include/linux/init.h:234-235 需要生成全局 stub 函数:
// include/linux/init.h:237
#define __define_initcall_stub(__stub, fn) \
int __init __stub(void); \
int __init __stub(void) \
{ \
return fn(); \
} \
__ADDRESSABLE(__stub)对于支持 CONFIG_HAVE_ARCH_PREL32_RELOCATIONS 的架构(如 ARM64),使用 PC 相对 32 位偏移(include/linux/init.h:255-261):
// include/linux/init.h:255(PREL32 版本)
#define ____define_initcall(fn, __stub, __name, __sec) \
__define_initcall_stub(__stub, fn) \
asm(".section \"" __sec "\", \"a\" \n" \
__stringify(__name) ": \n" \
".long " __stringify(__stub) " - . \n" \
".previous \n");内核启动时(init/main.c 中的 kernel_init_freeable())调用 do_initcalls():
// init/main.c(概念)
static initcall_level_t initcall_levels[] = {
__initcall0_start, // pure
__initcall1_start, // core
__initcall2_start, // postcore
__initcall3_start, // arch
__initcall4_start, // subsys
__initcall5_start, // fs
__initcall6_start, // device(最多)
__initcall7_start, // late
__initcall_end,
};
static void do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level, command_line);
}do_one_initcall() 是执行单个 initcall 的函数(include/linux/init.h:136),它处理返回值检查、调试打印(CONFIG_INIT_CALL_DEBUG)和异常捕获。
关键区别:module_init() 的行为随编译方式不同而不同(include/linux/module.h:80-145):
module_init(fn)
│
├─ 静态编译(!MODULE):等价于 __initcall(fn)
│ 即 device_initcall(fn),放入 .initcall6.init 节
│ 在内核启动时由 do_initcalls() 调用
│ include/linux/module.h:89:
│ #define module_init(x) __initcall(x);
│
└─ 模块化(MODULE):
生成 init_module 别名(指向 fn)
在 insmod 时由 do_init_module() 中的
do_one_initcall(mod->init) 调用
include/linux/module.h:131:
#define module_init(initfn) \
static inline initcall_t __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) \
__attribute__((alias(#initfn)));
include/linux/module.h:112-126 展示了当以模块形式编译时,early_initcall() 到 late_initcall() 等所有级别宏都等价于 module_init():
// include/linux/module.h:112
#define early_initcall(fn) module_init(fn)
#define core_initcall(fn) module_init(fn)
#define postcore_initcall(fn) module_init(fn)
#define arch_initcall(fn) module_init(fn)
#define subsys_initcall(fn) module_init(fn)
#define fs_initcall(fn) module_init(fn)
#define device_initcall(fn) module_init(fn)
#define late_initcall(fn) module_init(fn)现代内核实现了严格的模块内存权限分区,防止代码注入和数据执行攻击(CONFIG_STRICT_MODULE_RWX):
模块内存区域权限矩阵:
内存类型 读 写 执行 对应 mod_mem_type
─────────────────────────────────────────────────────
MOD_TEXT ✓ ✗ ✓ 代码段(机器指令)
MOD_RODATA ✓ ✗ ✗ 只读数据
MOD_RO_AFTER_INIT ✓ ✗→✓ ✗ init 后变为只读
MOD_DATA ✓ ✓ ✗ 读写数据
MOD_INIT_TEXT ✓ ✗ ✓ __init 代码(临时)
MOD_INIT_DATA ✓ ✓ ✗ __initdata(临时)
MOD_INIT_RODATA ✓ ✗ ✗ __initconst(临时)
权限设置通过 set_memory_ro()、set_memory_nx() 等函数实现。module_memory 结构体中的 is_rox 字段标记该区域是否已经处于只读+可执行状态(include/linux/module.h:357-364):
// include/linux/module.h:356
struct module_memory {
void *base;
bool is_rox; // read-only + executable
unsigned int size;
#ifdef CONFIG_MODULES_TREE_LOOKUP
struct mod_tree_node mtn;
#endif
};complete_formation() 调用 module_enable_rodata_ro() 将 MOD_RODATA 节设置为只读,调用 module_enable_nx() 禁止 MOD_DATA 的执行权限。这些操作在模块加载完成、符号重定位全部完成之后进行——重定位期间需要临时的写权限。
do_init_module() 完成后,进一步调用 module_enable_rodata_ro_after_init() 处理 MOD_RO_AFTER_INIT 区域(kernel/module/main.c:3087)。
jump_label(又名静态 key)是内核中一个精妙的性能优化机制,与模块系统深度集成。
// 典型用法(内核追踪点开关):
DEFINE_STATIC_KEY_FALSE(tracepoint_enabled);
if (static_branch_unlikely(&tracepoint_enabled)) {
do_expensive_trace_work();
}当 tracepoint_enabled 为 false 时,if 条件被编译为一条 nop 指令(5 字节,x86_64),不产生任何分支预测开销。当状态改变时(static_branch_enable()),内核通过 text_poke_bp() 动态修改该指令为 jmp,实现零开销的条件检查。
模块加载时,jump_entry_update_module() 负责初始化模块内所有 jump_label 条目(struct module 中的 jump_entries 字段,include/linux/module.h:512-514):
// include/linux/module.h:511
#ifdef CONFIG_JUMP_LABEL
struct jump_entry *jump_entries;
unsigned int num_jump_entries;
#endif模块卸载时,必须撤销所有 jump_label 的修改,否则内核代码区会残留指向已卸载模块的 jmp 指令,导致内核崩溃。
开启 CONFIG_RANDOMIZE_BASE(KASLR)时,模块加载地址也会随机化(CONFIG_RANDOMIZE_MODULE_REGION_FULL)。module_alloc() 使用 get_random_u32() 在允许范围内随机选择分配起始地址。
struct module 结构体本身也用 __randomize_layout 标记(include/linux/module.h:585),在开启 CONFIG_RANDSTRUCT 时字段顺序随机化,防止攻击者通过已知偏移访问关键字段。
开启 CONFIG_CFI_CLANG 时,模块的函数指针调用会被 CFI 保护。struct module 中的 kcfi_traps 字段记录 CFI 陷阱表(include/linux/module.h:424-426):
// include/linux/module.h:423
#ifdef CONFIG_ARCH_USES_CFI_TRAPS
s32 *kcfi_traps;
s32 *kcfi_traps_end;
#endifCFI 为每个间接调用目标(函数指针目标)生成类型签名校验,如果调用目标类型不匹配(如利用漏洞劫持函数指针后调用了错误类型的函数),内核会主动崩溃而不是执行攻击者控制的代码。
#include <linux/module.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Author <email>");
MODULE_DESCRIPTION("Example module");
static int __init my_init(void)
{
pr_info("my_module: loaded\n");
return 0;
}
static void __exit my_exit(void)
{
pr_info("my_module: unloaded\n");
}
module_init(my_init);
module_exit(my_exit);关键点:
__init标记my_init:模块初始化后,该函数占用的内存会被释放__exit标记my_exit:若模块静态编译,该函数永不被调用,链接器可以丢弃module_init()/module_exit()展开为init_module/cleanup_module别名
platform_driver 是最常用的设备驱动框架,通过 module_platform_driver() 宏简化注册:
#include <linux/module.h>
#include <linux/platform_device.h>
static int my_driver_probe(struct platform_device *pdev)
{
dev_info(&pdev->dev, "probe called\n");
return 0;
}
static void my_driver_remove(struct platform_device *pdev)
{
dev_info(&pdev->dev, "remove called\n");
}
static const struct of_device_id my_driver_dt_ids[] = {
{ .compatible = "vendor,my-device" },
{ }
};
MODULE_DEVICE_TABLE(of, my_driver_dt_ids);
static struct platform_driver my_driver = {
.probe = my_driver_probe,
.remove = my_driver_remove,
.driver = {
.name = "my-device",
.of_match_table = my_driver_dt_ids,
},
};
module_platform_driver(my_driver);
MODULE_LICENSE("GPL");module_platform_driver(drv) 展开为:
#define module_platform_driver(__platform_driver) \
module_driver(__platform_driver, \
platform_driver_register, \
platform_driver_unregister)最终等效于在 init 中调用 platform_driver_register(),在 exit 中调用 platform_driver_unregister()。
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
static dev_t dev_num;
static struct cdev my_cdev;
static const struct file_operations my_fops = {
.owner = THIS_MODULE, // 引用计数保护
.read = my_read,
.write = my_write,
.open = my_open,
.release = my_release,
};
static int __init my_init(void)
{
int ret;
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0)
return ret;
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret) {
unregister_chrdev_region(dev_num, 1);
return ret;
}
return 0;
}
static void __exit my_exit(void)
{
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");.owner = THIS_MODULE 是防止卸载竞争的关键——VFS 在调用 fops 中的函数之前会调用 try_module_get(file->f_op->owner),使引用计数增加,确保模块在 fops 函数执行期间不被卸载。
当前工作区中的 net/netfilter/xt_DSCP.c 是一个典型的 netfilter 匹配器模块,展示了完整的模块开发模式(net/netfilter/xt_DSCP.c:64-110):
// net/netfilter/xt_DSCP.c:64
static struct xt_match dscp_mt_reg[] __read_mostly = {
{
.name = "dscp",
.family = NFPROTO_IPV4,
.checkentry = dscp_mt_check,
.match = dscp_mt,
.matchsize = sizeof(struct xt_dscp_info),
.me = THIS_MODULE, // 持有模块引用
},
{
.name = "dscp",
.family = NFPROTO_IPV6,
.checkentry = dscp_mt_check,
.match = dscp_mt6,
.matchsize = sizeof(struct xt_dscp_info),
.me = THIS_MODULE,
},
// ... tos 匹配器
};
static int __init dscp_mt_init(void)
{
return xt_register_matches(dscp_mt_reg, ARRAY_SIZE(dscp_mt_reg));
}
static void __exit dscp_mt_exit(void)
{
xt_unregister_matches(dscp_mt_reg, ARRAY_SIZE(dscp_mt_reg));
}
module_init(dscp_mt_init);
module_exit(dscp_mt_exit);__read_mostly 属性(展开为 __section(".data..read_mostly"))将该数组放入只读优化的数据节,这些数据主要被读取,很少修改,可以被多个 CPU 缓存在同一缓存行而不产生伪共享问题。
.me = THIS_MODULE 在每个 xt_match 中保存模块指针,当 netfilter 子系统使用这个匹配器时会调用 try_module_get(match->me),防止在匹配规则仍被引用时卸载模块。
其余工作区中修改的 netfilter 模块(xt_HL.c、xt_RATEEST.c、xt_TCPMSS.c)同样遵循这一模式:通过 xt_register_target() / xt_register_match() 注册,通过 .me = THIS_MODULE 管理引用计数。
resolve_symbol_wait()(kernel/module/main.c:1287)解决了并发加载场景:
场景:modprobe 同时加载模块 A 和 B,A 依赖 B 的符号
CPU 0 CPU 1
────── ──────
load_module(A) load_module(B)
simplify_symbols(A) add_unformed_module(B)
resolve_symbol("B::foo") → B 状态 = UNFORMED
│
├─ find_symbol() 找到 B::foo
│ 但 B->state == MODULE_STATE_UNFORMED
│ → 返回 -EBUSY
│
└─ wait_event_interruptible_timeout(module_wq, ...)
等待最多 30 秒...
complete_formation(B)
→ B 状态 = COMING
→ wake_up_all(&module_wq)
│
├─ 重新调用 resolve_symbol("B::foo")
│ B 状态 = COMING → strong_try_module_get(B) 返回 -EBUSY
└─ 继续等待...
do_init_module(B)
→ B 状态 = LIVE
→ wake_up_all(&module_wq)
│
└─ resolve_symbol("B::foo") 成功
mod->taints 字段记录模块的污点标志(include/linux/module.h:460)。当模块被加载时,可能触发以下污点:
TAINT_PROPRIETARY_MODULE (P) - 加载了非 GPL 许可证模块
TAINT_FORCED_MODULE (F) - insmod -f 强制加载版本不匹配的模块
TAINT_OUT_OF_TREE (O) - 加载了非内核树中的模块
TAINT_UNSIGNED_MODULE (E) - 加载了未签名模块
TAINT_LIVEPATCH (K) - 加载了 livepatch 模块
TAINT_FORCED_RMMOD (R) - 强制卸载了模块
污点通过 inherit_taint() 传播:如果模块 A 依赖一个带有 TAINT_PROPRIETARY_MODULE 的模块 B,则 A 也会继承这个污点,相应地 A 就不能再访问内核的 GPL-only 符号(kernel/module/main.c:1256)。
Linux 5.4 引入了内核符号命名空间。在此之前,EXPORT_SYMBOL_GPL() 的粒度太粗——任何 GPL 兼容模块都可以使用所有 GPL-only 符号,没有进一步的访问控制。命名空间提供了更细粒度的符号可见性控制。
导出带命名空间的符号使用 EXPORT_SYMBOL_NS_GPL()(include/linux/export.h:92):
// include/linux/export.h:91
#define EXPORT_SYMBOL_NS(sym, ns) __EXPORT_SYMBOL(sym, "", ns)
#define EXPORT_SYMBOL_NS_GPL(sym, ns) __EXPORT_SYMBOL(sym, "GPL", ns)模块要使用这些符号,必须显式导入该命名空间(include/linux/module.h:302):
// include/linux/module.h:302
#define MODULE_IMPORT_NS(ns) MODULE_INFO(import_ns, ns)
// 使用示例:
MODULE_IMPORT_NS(USB);verify_namespace_is_imported() 在 resolve_symbol() 中检查模块的 .modinfo 中是否声明了对应命名空间(kernel/module/main.c:1266)。
include/linux/export.h:94 中的特殊导出宏,将符号限定为只能被特定模块使用:
// include/linux/export.h:94
#define EXPORT_SYMBOL_FOR_MODULES(sym, mods) __EXPORT_SYMBOL(sym, "GPL", "module:" mods)Linux livepatch(内核热补丁,CONFIG_LIVEPATCH)允许在不重启系统的情况下修复内核代码缺陷。struct module 中的 livepatch 相关字段(include/linux/module.h:547-553):
// include/linux/module.h:547
#ifdef CONFIG_LIVEPATCH
bool klp; // 这是一个 livepatch 模块
bool klp_alive; // livepatch 仍然激活
struct klp_modinfo *klp_info; // 保留的 ELF 信息
#endif普通模块加载完成后,内核会释放 ELF 解析用的临时数据。但 livepatch 模块需要这些数据来支持函数替换时的重定位计算,因此 load_module() 在检测到 is_livepatch_module(mod) 时会调用 copy_module_elf() 保留 ELF 信息(kernel/module/main.c:3501):
// kernel/module/main.c:3501
if (is_livepatch_module(mod)) {
err = copy_module_elf(mod, info);
if (err < 0)
goto sysfs_cleanup;
}insmod livepatch_fix.ko
│
klp_enable_patch()
│
对每个需要替换的函数:
klp_patch_func()
│
├─ 找到原始内核函数地址
├─ 使用 ftrace 在函数入口安装 trampoline
└─ trampoline 跳转到 livepatch 模块中的新函数
等待所有线程从旧函数返回(consistency model)
│
替换完成:系统继续运行,使用新函数实现
卸载 livepatch 模块时,klp_module_going() 负责撤销所有函数替换(kernel/module/main.c:838)。
全局 modules 链表(kernel/module/main.c:76)受到两种同步机制保护:
访问类型 同步机制
─────────────────────────────────────────
写操作(加/删模块) module_mutex(互斥锁)
读操作(遍历链表) RCU(读-拷贝-更新)
list_add_rcu() / list_del_rcu() 确保链表修改对 RCU 读者是安全的。find_symbol() 函数(在符号查找热路径上)只需要 RCU 读锁,不需要 module_mutex,从而避免了符号解析的锁竞争(kernel/module/main.c:404):
// kernel/module/main.c:404
list_for_each_entry_rcu(mod, &modules, list,
lockdep_is_held(&module_mutex)) {
...
}mod_tree 是一个基于 latch-tree 的区间树,用于快速将内存地址映射到所属模块(kernel/module/main.c:83):
// kernel/module/main.c:83
struct mod_tree_root mod_tree __cacheline_aligned = {
.addr_min = -1UL, // 初始为最大值,表示无模块
};__module_address() 函数通过此树将任意地址(如崩溃时的 PC 值)映射到对应模块,时间复杂度 O(log n),相比线性扫描 modules 链表快得多。latch-tree 是一种 RCU 友好平衡二叉树,维护两份树结构交替使用,读者无需锁即可安全遍历。
__mod_update_bounds() 维护模块地址范围的最小/最大值缓存(kernel/module/main.c:97-116),用于快速排除明显不在模块区域内的地址。
内核树内的模块 Makefile 格式:
# 内核树内模块示例
obj-$(CONFIG_E1000) += e1000.o
e1000-objs := e1000_main.o e1000_hw.o e1000_ethtool.oobj-$(CONFIG_E1000) 中 y 为静态编译,m 为编译为模块(.ko),空则不编译。多个 .o 文件会被链接器以 ld -r(可重定位合并)方式合并成一个 .ko。
源文件(.c)
│
▼ gcc -c -DMODULE -D__KERNEL__ ...
│
中间目标(.o)
│
▼ ld -r(可重定位合并多个 .o)
│
module.o
│
▼ scripts/mod/modpost(生成 module.mod.c)
│ - 处理 .export_symbol 节 → __ksymtab
│ - 生成 .modinfo 中的 vermagic
│ - 检查节不匹配(section mismatch)
│
module.mod.o
│
▼ ld -r module.o module.mod.o
│
module.ko
│
▼(可选)scripts/sign-file
│
module.ko(带签名)
scripts/mod/modpost 是 Kbuild 中非常关键的工具:
- 解析所有
.o中的.export_symbol节,生成最终的__ksymtab和 CRC 表 - 生成
module.mod.c,其中包含模块的vermagic字符串和依赖信息 - 检查节不匹配(如在非 init 函数中引用了
__init数据),在构建时产生警告
KDIR ?= /lib/modules/$(shell uname -r)/build
default:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean运行 make 时,会切换到内核构建目录,以内核的 Kbuild 基础设施处理 M=$(PWD) 指定的模块目录。树外模块加载时会被打上 TAINT_OUT_OF_TREE 污点(O 标志)。
当前工作区修改了 net/netfilter/xt_RATEEST.c,该模块实现了 netfilter 的流量速率估计匹配器,展示了复杂模块的两级资源管理设计。
xt_RATEEST 模块注册了两个组件:一个 target(用于设置速率估计器)和一个 match(用于匹配速率条件):
netfilter 规则链
│
├─ RATEEST target(接收数据包时更新速率计数器)
│ └─ xt_rateest_tg():记录字节数和包数,更新 EWMA 统计
│
└─ rateest match(匹配时检查速率)
└─ xt_rateest_mt():与阈值比较,决定是否匹配
速率估计器(struct xt_rateest)通过哈希表管理,多个 target 规则可以引用同一个估计器实例。
级别 1(模块引用计数):
try_module_get(match->me) ← netfilter 在使用匹配规则时调用
module_put(match->me) ← 规则被删除时调用
级别 2(估计器引用计数):
xt_rateest_get(name) ← match 规则引用同一估计器时调用
xt_rateest_put(est) ← match 规则被删除时调用
模块必须等到所有级别 2 的引用都释放(即没有规则引用任何估计器),才能安全卸载。这是内核模块开发中常见的"资源对象 + 模块引用"双层引用计数模式。
do_init_module() 被故意声明为 noinline(kernel/module/main.c:3017),就是为了给 GDB 脚本 lx-symbols 提供可靠的断点目标:
# 获取模块各节的加载地址
cat /sys/module/<modname>/sections/.text
cat /sys/module/<modname>/sections/.data
# 在 GDB 中加载符号
(gdb) add-symbol-file <module.ko> \
-s .text <text_address> \
-s .data <data_address> \
-s .bss <bss_address>cat /proc/modules
# 输出格式:名称 大小 使用计数 依赖模块 状态 地址
xt_dscp 16384 2 - Live 0xffffffffc0a00000
xt_hl 16384 4 xt_dscp Live 0xffffffffc0b00000各字段含义:
16384:模块占用内存字节数(四舍五入到页面大小)2:当前引用计数减去MODULE_REF_BASE(module_refcount()返回值)xt_dscp:依赖本模块的模块列表Live:模块状态(Live/Loading/Unloading)0xffffffffc0a00000:模块代码段基地址(mod->mem[MOD_TEXT].base)
当发生内核 oops 时,调用栈中会标注涉及模块的函数:
[<ffffffffc0a01234>] dscp_mt+0x14/0x30 [xt_dscp]
[<ffffffffc0b05678>] xt_check_match+0x48/0x120 [x_tables]
括号内的模块名由 __module_text_address() 解析得到:给定内存地址,在 mod_tree 中查找包含该地址的模块。TAINT 信息也会在 oops 中显示(如 Tainted: P O),告知内核社区系统中加载了专有模块(P)或树外模块(O)。
内核的动态调试基础设施与模块系统深度集成。struct module 中包含 dyndbg_info 字段(include/linux/module.h:582-584):
// include/linux/module.h:582
#ifdef CONFIG_DYNAMIC_DEBUG_CORE
struct _ddebug_info dyndbg_info;
#endif模块加载时,ddebug_add_module() 将模块中所有 pr_debug() / dev_dbg() 调用点注册到动态调试数据库。用户可以在运行时选择性地开启/关闭特定模块的调试输出:
# 开启 xt_dscp 模块的所有 pr_debug 输出
echo "module xt_dscp +p" > /sys/kernel/debug/dynamic_debug/control模块卸载时,ddebug_remove_module() 清理注册信息,避免悬挂指针。
struct module 中的 bpf_raw_events 字段(include/linux/module.h:502-504)允许模块注册 BPF 可访问的原始事件追踪点:
// include/linux/module.h:501
#ifdef CONFIG_BPF_EVENTS
unsigned int num_bpf_raw_events;
struct bpf_raw_event_map *bpf_raw_events;
#endif开启 CONFIG_DEBUG_INFO_BTF_MODULES 时,每个模块都会嵌入 BTF 类型信息(include/linux/module.h:505-510):
// include/linux/module.h:505
#ifdef CONFIG_DEBUG_INFO_BTF_MODULES
unsigned int btf_data_size;
unsigned int btf_base_data_size;
void *btf_data; // 模块自身的 BTF 数据
void *btf_base_data; // 引用的内核基础 BTF 数据
#endifBTF 使 BPF 验证器能够理解模块中数据结构的布局,实现类型安全的内核访问。bpftool 工具可以利用 BTF 提供模块级别的类型信息。
struct module(include/linux/module.h:397)
│
├─ list_head list → modules 全局链表(module_mutex 保护)
│
├─ module_kobject mkobj → /sys/module/<name>/ 目录
│ └─ module_param_attrs → /sys/module/<name>/parameters/
│
├─ kernel_symbol *syms → __ksymtab 节(NOT_GPL_ONLY 符号)
├─ kernel_symbol *gpl_syms → __ksymtab_gpl 节(GPL_ONLY 符号)
│
├─ kernel_param *kp → __param 节(模块参数数组)
│
├─ module_memory mem[] → 7 种内存区域(MOD_TEXT ... MOD_INIT_RODATA)
│ └─ mod_tree_node mtn → 按地址索引的红黑树(加速 __module_address())
│
├─ mod_kallsyms *kallsyms → 模块符号表(RCU 保护)
│
├─ int (*init)(void) → module_init() 注册的初始化函数
├─ void (*exit)(void) → module_exit() 注册的清理函数
│
├─ list_head source_list → 依赖本模块的模块链表
├─ list_head target_list → 本模块依赖的模块链表
│
└─ atomic_t refcnt → 引用计数(try_module_get/module_put)
模块加载完整状态机:
insmod / modprobe
│
▼
┌─────────────────┐
│ MODULE_STATE_ │
│ UNFORMED │ ← 正在解析/分配内存
└────────┬────────┘
│ add_unformed_module()
▼
┌─────────────────┐
│ MODULE_STATE_ │
│ COMING │ ← 正在执行 init()
└────────┬────────┘
│ do_init_module() 成功
▼
┌─────────────────┐
│ MODULE_STATE_ │◄──── 正常运行
│ LIVE │
└────────┬────────┘
│ rmmod(引用计数降为0)
▼
┌─────────────────┐
│ MODULE_STATE_ │
│ GOING │ ← 正在执行 exit()
└────────┬────────┘
│ free_module()
▼
(已从内存删除)
# 查看模块元信息
modinfo <module.ko>
# 加载模块(带参数)
insmod <module.ko> param1=value1
# 通过 modprobe 加载(自动处理依赖)
modprobe <modname> param1=value1
# 卸载模块
rmmod <modname>
rmmod -f <modname> # 强制卸载(需 CONFIG_MODULE_FORCE_UNLOAD)
# 查看已加载模块
lsmod # 等效 cat /proc/modules
cat /sys/module/<name>/refcnt
# 查看模块符号
cat /proc/kallsyms | grep <modname>
nm <module.ko>
# 读取/设置模块参数
cat /sys/module/<name>/parameters/<param>
echo value > /sys/module/<name>/parameters/<param>
# 查看 ELF 节信息
readelf -S <module.ko>
readelf -r <module.ko> # 重定位表
readelf -s <module.ko> # 符号表
# 查看模块签名
scripts/extract-module-sig.pl -d <module.ko>
openssl pkcs7 -inform der -print_certs -in sig.p7
# 对模块签名
scripts/sign-file sha256 certs/signing_key.pem certs/signing_key.x509 <module.ko>
# 动态调试
echo "module <name> +p" > /sys/kernel/debug/dynamic_debug/control
# 内核 taint 标志查看
cat /proc/sys/kernel/tainted由 Claude Code 分析生成