sys_init_module 系统调用完整实现分析
01 | 概述
sys_init_module 是 Linux 内核中用于加载内核模块(.ko 文件)的系统调用。用户态程序(如 insmod、modprobe)通过该系统调用将内核模块的 ELF 二进制数据从用户空间传递到内核空间,由内核完成模块的解析、重定位、符号解析和初始化执行。
系统调用原型
c
SYSCALL_DEFINE3(init_module, void __user *, umod,
unsigned long, len, const char __user *, uargs)
参数说明:
umod:指向用户空间内核模块 ELF 镜像的指针len:镜像的大小(字节数)uargs:传递给模块的参数串(用户空间字符串)
返回值: 成功返回 0,失败返回负的错误码
02 | 总体流程图
sys_init_module(umod, len, uargs)
│
├── [1] may_init_module()
│ 权限和能力检查
│
├── [2] copy_module_from_user(umod, len, &info)
│ 从用户空间拷贝模块数据到内核
│
└── [3] load_module(&info, uargs, 0)
核心加载流程(详见第三节)
03 | 调用链详细分析
3.1 入口函数:sys_init_module
c
// 位置: module.c:4158-4176
SYSCALL_DEFINE3(init_module, void __user *, umod,
unsigned long, len, const char __user *, uargs)
{
int err;
struct load_info info = { };
err = may_init_module();
if (err)
return err;
pr_debug("init_module: umod=%p, len=%lu, uargs=%p\n",
umod, len, uargs);
err = copy_module_from_user(umod, len, &info);
if (err)
return err;
return load_module(&info, uargs, 0);
}
关键数据结构:struct load_info
c
// 位置: module-internal.h:11-29
struct load_info {
const char *name; // 模块名称
struct module *mod; // 指向临时拷贝中的 module 结构体
Elf_Ehdr *hdr; // ELF 头指针
unsigned long len; // 模块数据长度
Elf_Shdr *sechdrs; // section headers 数组
char *secstrings, *strtab; // section 名字符串表, 符号字符串表
unsigned long symoffs, stroffs, init_typeoffs, core_typeoffs;
struct _ddebug *debug; // 动态调试信息
unsigned int num_debug; // 动态调试条目数
bool sig_ok; // 签名验证是否通过
struct {
unsigned int sym, str, mod, vers, info, pcpu;
} index; // 关键 section 的索引
};
3.2 may_init_module --- 权限检查
c
// 位置: module.c:3821-3827
static int may_init_module(void)
{
if (!capable(CAP_SYS_MODULE) || modules_disabled)
return -EPERM;
return 0;
}
调用链分析:
| 调用接口 | 功能说明 |
|---|---|
capable(CAP_SYS_MODULE) |
检查当前进程是否具有 CAP_SYS_MODULE 能力(通常需要 root 权限) |
modules_disabled |
全局标志位,通过 /proc/sys/kernel/modules_disabled 控制,设为 1 后禁止加载任何模块 |
错误返回: -EPERM (Operation not permitted)
3.3 copy_module_from_user --- 从用户空间拷贝模块数据
c
// 位置: module.c:3123-3153
static int copy_module_from_user(const void __user *umod, unsigned long len,
struct load_info *info)
{
int err;
info->len = len;
// 长度必须至少大于 ELF header
if (info->len < sizeof(*(info->hdr)))
return -ENOEXEC;
// LSM安全检查: 内核加载数据前回调
err = security_kernel_load_data(LOADING_MODULE, true);
if (err)
return err;
// 分配内核空间内存
info->hdr = __vmalloc(info->len, GFP_KERNEL | __GFP_NOWARN);
if (!info->hdr)
return -ENOMEM;
// 分块从用户空间拷贝数据
if (copy_chunked_from_user(info->hdr, umod, info->len) != 0) {
err = -EFAULT;
goto out;
}
// LSM安全检查: 内核加载数据后回调
err = security_kernel_post_load_data((char *)info->hdr, info->len,
LOADING_MODULE, "init_module");
out:
if (err)
vfree(info->hdr);
return err;
}
调用链分析:
| 调用接口 | 功能说明 |
|---|---|
security_kernel_load_data(LOADING_MODULE, true) |
LSM(Linux Security Module)钩子,在内核加载数据之前 进行安全检查。LOADING_MODULE 标识数据类型为模块加载。true 表示加载全部内容后会有后续检查 |
| `__vmalloc(info->len, GFP_KERNEL | __GFP_NOWARN)` |
copy_chunked_from_user(info->hdr, umod, info->len) |
分块拷贝函数(见下文 3.3.1) |
security_kernel_post_load_data(...) |
LSM 钩子,在内核加载数据之后进行安全检查 |
vfree(info->hdr) |
错误路径下释放已分配的 vmalloc 内存 |
3.3.1 copy_chunked_from_user --- 分块拷贝
c
// 位置: module.c:3073-3086
#define COPY_CHUNK_SIZE (16*PAGE_SIZE) // 通常是 64KB
static int copy_chunked_from_user(void *dst, const void __user *usrc,
unsigned long len)
{
do {
unsigned long n = min(len, COPY_CHUNK_SIZE);
if (copy_from_user(dst, usrc, n) != 0)
return -EFAULT;
cond_resched(); // 让出 CPU,避免长时间占用
dst += n;
usrc += n;
len -= n;
} while (len);
return 0;
}
| 调用接口 | 功能说明 |
|---|---|
copy_from_user(dst, usrc, n) |
架构相关的用户空间到内核空间拷贝函数。每次最多拷贝 16*PAGE_SIZE 字节,确保不会因单次拷贝过大导致问题 |
cond_resched() |
条件性调度点,在 CONFIG_PREEMPT=n 时允许内核主动让出 CPU,防止长时间禁用抢占 |
3.4 load_module --- 模块加载核心函数
这是整个模块加载流程的核心,包含 20+ 个步骤。下面按执行顺序逐一分析。
c
// 位置: module.c:3936-4156
static int load_module(struct load_info *info, const char __user *uargs,
int flags)
步骤 1:module_sig_check --- 模块签名验证
c
// 位置: module.c:2900-2958
static int module_sig_check(struct load_info *info, int flags)
功能: 检查模块末尾是否包含签名信息(~Module signature appended~\n 标记),如果有则验证签名。
调用链:
| 调用接口 | 功能说明 |
|---|---|
memcmp(mod + info->len - markerlen, MODULE_SIG_STRING, markerlen) |
检查模块末尾是否有签名魔数标记 |
mod_verify_sig(mod, info) |
验证模块的 PKCS#7 数字签名。使用内核密钥环中的公钥验证,同时通过 info->len 获取去除签名后的实际模块大小 |
is_module_sig_enforced() |
检查内核是否强制要求模块签名(CONFIG_MODULE_SIG_FORCE 或内核启动参数 module.sig_enforce) |
security_locked_down(LOCKDOWN_MODULE_SIGNATURE) |
检查内核是否处于锁定模式,锁定模式下禁止加载未签名模块 |
错误返回: -EKEYREJECTED(签名被拒绝),-ENODATA(无签名),-ENOPKG(不支持的加密算法),-ENOKEY(密钥不可用)
步骤 2:elf_validity_check --- ELF 有效性检查
c
// 位置: module.c:2981-3069
static int elf_validity_check(struct load_info *info)
功能: 验证模块 ELF 文件的结构完整性。
检查项:
| 检查内容 | 说明 |
|---|---|
info->len >= sizeof(Elf_Ehdr) |
总长度至少大于 ELF 头 |
memcmp(hdr->e_ident, ELFMAG, SELFMAG) |
ELF 魔数验证(\x7fELF) |
hdr->e_type == ET_REL |
必须是可重定位文件类型 |
elf_check_arch(hdr) |
架构相关的 ELF 检查(机器类型等) |
hdr->e_shentsize == sizeof(Elf_Shdr) |
section header 大小验证 |
hdr->e_shoff < info->len |
section 表偏移不越界 |
section 名称表索引有效性 (e_shstrndx) |
必须在有效范围内 |
validate_section_offset(info, strhdr) |
节名称字符串表偏移验证 |
| 字符串表 NUL 结尾检查 | 确保字符串操作安全 |
| 第 0 号 section 检查 | 必须为 SHT_NULL 类型 |
| 遍历所有 section | 检查偏移、大小、名称有效性 |
validate_section_offset(子函数):
c
// 位置: module.c:2960-2973
static int validate_section_offset(struct load_info *info, Elf_Shdr *shdr)
{
unsigned long secend;
secend = shdr->sh_offset + shdr->sh_size;
if (secend < shdr->sh_offset || secend > info->len)
return -ENOEXEC;
return 0;
}
检查 section 的偏移+大小是否溢出或超过模块总长度。
步骤 3:setup_load_info --- 建立加载信息
c
// 位置: module.c:3196-3246
static int setup_load_info(struct load_info *info, int flags)
功能: 解析 ELF 的 section headers,定位关键 section,建立基本信息。
调用链:
| 调用接口 | 功能说明 |
|---|---|
find_sec(info, ".modinfo") |
查找 .modinfo section(包含模块元信息) |
get_modinfo(info, "name") |
从 .modinfo 中提取 name=xxx 字段作为模块名 |
遍历 section 找 SHT_SYMTAB |
定位符号表和对应的字符串表,设置 info->index.sym 和 info->index.str |
find_sec(info, ".gnu.linkonce.this_module") |
查找包含 struct module 的 section,设置 info->mod 指向临时拷贝 |
find_sec(info, "__versions") |
查找模块版本信息 section(CONFIG_MODVERSIONS) |
find_pcpusec(info) |
查找 per-cpu 数据 section |
find_sec 函数: 遍历所有 section headers,根据名称字符串匹配目标 section,返回索引或 0(未找到)。
get_modinfo 函数: 解析 .modinfo section 中的 key=value 格式字符串,根据 key 查找并返回 value 指针。
步骤 4:blacklisted --- 模块黑名单检查
c
// 位置: module.c:3536-3553
static bool blacklisted(const char *module_name)
功能: 检查模块名是否在黑名单中。黑名单由内核启动参数 module_blacklist=mod1,mod2,... 指定,用于阻止特定模块的加载。
错误返回: -EPERM
步骤 5:rewrite_section_headers --- 重写 section headers
c
// 位置: module.c:3160-3186
static int rewrite_section_headers(struct load_info *info, int flags)
功能: 将 section header 中的 sh_addr 设置为临时拷贝中的实际地址(info->hdr + sh_offset),并标记不需要分配的 section。
| 操作 | 说明 |
|---|---|
设置 sh_addr = hdr + sh_offset |
使 section 地址指向临时拷贝中的实际位置 |
| 非 CONFIG_MODULE_UNLOAD 时 | 移除 .exit section 的 SHF_ALLOC 标志 |
| 版本/modinfo section | 移除 SHF_ALLOC 标志(由内核单独管理) |
步骤 6:check_modstruct_version --- 模块结构版本检查
c
// 位置: module.c:1356-1391
static inline int check_modstruct_version(const struct load_info *info,
struct module *mod)
功能: 检查模块编译时使用的 struct module 的 CRC 校验和是否与当前运行内核一致。主要用于 CONFIG_MODVERSIONS 配置下确保 ABI 兼容性。
same_magic 函数(子函数): 对比模块的 vermagic 字符串与内核的 vermagic(包含内核版本、编译器版本、架构等信息)。
步骤 7:layout_and_allocate --- 布局和分配内存
c
// 位置: module.c:3555-3612
static struct module *layout_and_allocate(struct load_info *info, int flags)
这是最核心的内存布局和分配步骤。
7.1 check_modinfo --- 模块信息检查
c
// 位置: module.c:3248-3290
static int check_modinfo(struct module *mod, struct load_info *info, int flags)
| 子调用 | 功能说明 |
|---|---|
get_modinfo(info, "vermagic") |
获取 vermagic 字符串 |
same_magic(modmagic, vermagic, info->index.vers) |
对比模块的 vermagic 与内核 vermagic 是否匹配 |
try_to_force_load(mod, "bad vermagic") |
若允许强制加载(CONFIG_MODULE_FORCE_LOAD),则只加 taint 标记,不返回错误 |
get_modinfo(info, "intree") |
检查是否内核树内模块,否则打 TAINT_OOT_MODULE 标记 |
check_modinfo_retpoline(mod, info) |
检查模块是否使用 retpoline 编译(缓解 Spectre 漏洞) |
check_modinfo_livepatch(mod, info) |
检查是否为 livepatch 模块,设置 mod->klp 标志并打 TAINT_LIVEPATCH |
get_modinfo(info, "staging") |
检查是否为 staging 目录模块,打 TAINT_CRAP 标记 |
set_license(mod, ...) |
设置模块许可证信息 |
7.2 module_frob_arch_sections --- 架构相关 section 调整
c
int __weak module_frob_arch_sections(Elf_Ehdr *hdr, Elf_Shdr *sechdrs,
char *secstrings, struct module *mod)
平台相关的弱函数,允许架构代码在布局前修改 section 内容和大小。
7.3 module_enforce_rwx_sections --- 强制执行 RWX section
检查并确保模块的代码段、只读数据段和读写数据段正确分离,为后续设置内存权限做准备。
7.4 layout_sections --- section 布局
c
// 位置: module.c:2466-2584
static void layout_sections(struct module *mod, struct load_info *info)
功能: 按照类似链接器的规则排布所有 SHF_ALLOC section。布局顺序为:
- 可执行代码段 (
SHF_EXECINSTR | SHF_ALLOC) - 只读数据段 (
SHF_ALLOC) - ro_after_init 数据段 (
SHF_RO_AFTER_INIT | SHF_ALLOC) - 读写数据段 (
SHF_WRITE | SHF_ALLOC) - 小数据段 (
ARCH_SHF_SMALL | SHF_ALLOC)
分别对 core_layout 和 init_layout 执行两遍相同的布局。
子函数 get_offset:
c
// 位置: module.c:2451-2460
static long get_offset(struct module *mod, unsigned int *size,
Elf_Shdr *sechdr, unsigned int section)
{
*size += arch_mod_section_prepend(mod, section);
ret = ALIGN(*size, sechdr->sh_addralign ?: 1);
*size = ret + sechdr->sh_size;
return ret;
}
计算每个 section 的对齐偏移,并将偏移量写入 sh_entsize 字段。init 段的偏移量最高位设置 INIT_OFFSET_MASK 以区分 core/init。
7.5 layout_symtab --- 符号表布局
计算符号表和字符串表的大小并记录偏移。
7.6 move_module --- 移动到最终位置
c
// 位置: module.c:3407-3468
static int move_module(struct module *mod, struct load_info *info)
| 子调用 | 功能说明 |
|---|---|
module_alloc(mod->core_layout.size) |
分配模块 core 区域的内存(通常是 vmalloc 可执行区域) |
kmemleak_not_leak(ptr) |
通知 kmemleak 此指针不是内存泄漏(被 struct module 引用) |
memset(ptr, 0, ...) |
清零分配的内存 |
module_alloc(mod->init_layout.size) |
分配 init 区域内存(模块初始化后释放) |
kmemleak_ignore(ptr) |
通知 kmemleak 忽略 init 区域(因为将被释放) |
memcpy(dest, src, size) |
将所有 SHF_ALLOC section 从临时拷贝复制到最终位置,更新 sh_addr |
7.7 kmemleak_load_module --- 标记模块内存
通知 kmemleak 模块的各个区域(core、init、per-cpu、data)不是内存泄漏。
步骤 8:audit_log_kern_module --- 审计日志
c
audit_log_kern_module(mod->name);
向内核审计子系统记录模块加载事件。
步骤 9:add_unformed_module --- 添加到模块列表
c
// 位置: module.c:3834-3866
static int add_unformed_module(struct module *mod)
功能: 在投入更多资源之前,先将模块加入全局模块链表,确保模块名唯一。
| 子调用 | 功能说明 |
|---|---|
mod->state = MODULE_STATE_UNFORMED |
设置模块初始状态 |
find_module_all(mod->name, ...) |
查找是否已有同名模块 |
wait_event_interruptible(module_wq, finished_loading(mod->name)) |
如果同名模块正在加载,等待其完成 |
finished_loading(mod->name) |
检查同名模块是否已加载完成 |
mod_update_bounds(mod) |
更新全局模块地址边界 (module_addr_min/module_addr_max) |
list_add_rcu(&mod->list, &modules) |
将模块加入 RCU 保护的全局模块链表 |
mod_tree_insert(mod) |
将模块插入地址查找树(用于快速地址→模块映射) |
步骤 10:add_taint_module --- 模块签名污染标记
c
// (在 CONFIG_MODULE_SIG 下)
if (!mod->sig_ok) {
add_taint_module(mod, TAINT_UNSIGNED_MODULE, LOCKDEP_STILL_OK);
}
如果签名验证未通过,给内核打上 TAINT_UNSIGNED_MODULE 标记。
步骤 11:percpu_modalloc --- 分配 Per-CPU 内存
c
err = percpu_modalloc(mod, info);
为模块分配 per-CPU 变量所需的内存。必须在模块唯一性确认后进行,以避免临时 per-CPU 内存耗尽。
步骤 12:module_unload_init --- 初始化卸载支持
c
// 位置: module.c:825-840
static int module_unload_init(struct module *mod)
| 操作 | 说明 |
|---|---|
atomic_set(&mod->refcnt, MODULE_REF_BASE) |
初始化引用计数基准值 |
INIT_LIST_HEAD(&mod->source_list) |
初始化源模块列表(依赖本模块的模块) |
INIT_LIST_HEAD(&mod->target_list) |
初始化目标模块列表(本模块依赖的模块) |
atomic_inc(&mod->refcnt) |
加载期间持有引用,防止被卸载 |
步骤 13:init_param_lock --- 初始化参数锁
c
init_param_lock(mod);
初始化模块参数访问互斥锁。
步骤 14:find_module_sections --- 查找可选 section
c
// 位置: module.c:3292-3405
static int find_module_sections(struct module *mod, struct load_info *info)
功能: 定位并解析模块的各个可选 section,设置 struct module 中对应的指针。
| section 名称 | 存储字段 | 说明 |
|---|---|---|
__param |
mod->kp, mod->num_kp |
模块参数描述 |
__ksymtab |
mod->syms, mod->num_syms |
导出的内核符号 |
__kcrctab |
mod->crcs |
导出符号的 CRC |
__ksymtab_gpl |
mod->gpl_syms |
GPL-only 导出符号 |
__ksymtab_gpl_future |
mod->gpl_future_syms |
Future GPL 导出符号 |
.ctors / .init_array |
mod->ctors, mod->num_ctors |
全局构造函数 |
.noinstr.text |
mod->noinstr_text_start |
noinstr 代码段 |
__tracepoints_ptrs |
tracepoint 指针 | (CONFIG_TRACEPOINTS) |
__jump_table |
mod->jump_entries |
jump label 表 (CONFIG_JUMP_LABEL) |
_ftrace_events |
mod->trace_events |
ftrace 事件 (CONFIG_EVENT_TRACING) |
__ex_table |
mod->extable |
异常表 |
子函数:
section_objs(info, name, obj_size, num):获取 section 内容为对象数组section_addr(info, name):获取 section 内容的地址
步骤 15:check_module_license_and_versions --- 许可证检查
c
// 位置: module.c:3470-3509
static int check_module_license_and_versions(struct module *mod)
| 检查项 | 说明 |
|---|---|
| ndiswrapper/driverloader/lve | 特殊模块污染检查:这些已知模块使用 add_taint(TAINT_PROPRIETARY_MODULE) |
| MODVERSIONS CRC 检查 | 确保有导出符号就有对应的 CRC(CONFIG_MODVERSIONS) |
步骤 16:setup_modinfo --- 设置模块信息属性
c
// 位置: module.c:2618-2627
static void setup_modinfo(struct module *mod, struct load_info *info)
遍历 modinfo_attrs 数组(包含 version、srcversion、vermagic 等),从 .modinfo section 中读取值并通过各属性的 setup 回调保存到 struct module 中。
步骤 17:simplify_symbols --- 符号解析
c
// 位置: module.c:2338-2405
static int simplify_symbols(struct module *mod, const struct load_info *info)
功能: 解析模块中所有符号的最终地址。这是模块加载中最关键的步骤之一。
对符号表中的每个符号按类型处理:
| 符号类型 | 处理方式 |
|---|---|
SHN_COMMON |
忽略(__gnu_lto* 前缀)或报错(应使用 -fno-common 编译) |
SHN_ABS |
绝对符号,无需修改 |
SHN_LIVEPATCH |
livepatch 符号,由 livepatch 框架解析 |
SHN_UNDEF (未定义符号) |
调用 resolve_symbol_wait(mod, info, name) 在内核中查找 |
| 其他 (已定义符号) | 加上其 section 的基地址 (sh_addr);per-cpu 变量使用 mod_percpu(mod) |
关键子调用:
| 调用接口 | 功能说明 |
|---|---|
resolve_symbol_wait(mod, info, name) |
在内核符号表和已加载模块中查找符号。如果符号所属模块正在加载,则等待其完成。查找成功返回 kernel_symbol 结构体指针 |
kernel_symbol_value(ksym) |
获取内核符号的实际地址值 |
ignore_undef_symbol(machine, name) |
检查是否为可忽略的弱符号(特定架构的特殊符号) |
mod_percpu(mod) |
获取模块 per-cpu 区域的基地址 |
步骤 18:apply_relocations --- 应用重定位
c
// 位置: module.c:2407-2440
static int apply_relocations(struct module *mod, const struct load_info *info)
功能: 遍历所有 section,对重定位 section(SHT_REL 或 SHT_RELA)执行重定位操作。
| 重定位类型 | 处理函数 | 说明 |
|---|---|---|
SHF_RELA_LIVEPATCH |
klp_apply_section_relocs() |
livepatch 专用重定位 |
SHT_REL |
apply_relocate(sechdrs, strtab, symindex, i, mod) |
标准 REL 重定位(架构相关) |
SHT_RELA |
apply_relocate_add(sechdrs, strtab, symindex, i, mod) |
标准 RELA 重定位(架构相关) |
架构相关的重定位函数(如 ARM、x86)负责根据重定位类型修正确代码/数据中的地址引用。
步骤 19:post_relocation --- 重定位后处理
c
// 位置: module.c:3630-3644
static int post_relocation(struct module *mod, const struct load_info *info)
| 子调用 | 功能说明 |
|---|---|
sort_extable(mod->extable, ...) |
对异常表条目排序(用于内核异常处理快速查找) |
percpu_modcopy(mod, ...) |
将 per-cpu 模板数据复制到已分配的 per-cpu 区域 |
add_kallsyms(mod, info) |
为模块设置 kallsyms(内核符号表)信息 |
module_finalize(info->hdr, info->sechdrs, mod) |
架构相关的模块最终化处理(如 ARM 上的 PLT 处理等) |
步骤 20:flush_module_icache --- 刷新指令缓存
c
// 位置: module.c:3511-3524
static void flush_module_icache(const struct module *mod)
{
if (mod->init_layout.base)
flush_icache_range((unsigned long)mod->init_layout.base,
(unsigned long)mod->init_layout.base
+ mod->init_layout.size);
flush_icache_range((unsigned long)mod->core_layout.base,
(unsigned long)mod->core_layout.base
+ mod->core_layout.size);
}
功能: 因为已修改代码段(应用了重定位),刷新指令缓存确保 CPU 获取最新指令。
子函数 flush_icache_range: 架构相关的 I-Cache 刷新函数。在 ARM 上通常使用 flush_icache_range() 或 CP15 协处理器指令。
步骤 21:strndup_user --- 复制模块参数
c
mod->args = strndup_user(uargs, ~0UL >> 1);
从用户空间复制模块参数串(最大 INT_MAX 长度)。
步骤 22:dynamic_debug_setup --- 动态调试设置
c
dynamic_debug_setup(mod, info->debug, info->num_debug);
设置模块的动态调试(pr_debug/dev_dbg 等)控制。
步骤 23:ftrace_module_init --- Ftrace 初始化
c
ftrace_module_init(mod);
注册模块的 ftrace 相关信息和 mcount 调用点位置。必须在 MODULE_STATE_UNFORMED 状态下调用。
步骤 24:complete_formation --- 完成模块成型
c
// 位置: module.c:3868-3896
static int complete_formation(struct module *mod, struct load_info *info)
| 子调用 | 功能说明 |
|---|---|
verify_exported_symbols(mod) |
检查导出符号是否与已有符号冲突(必须在 module_mutex 锁下调用) |
module_bug_finalize(info->hdr, info->sechdrs, mod) |
解析模块的 __bug_table section,注册 BUG/WARN 信息 |
module_enable_ro(mod, false) |
设置模块只读区域为只读(代码段、rodata 段) |
module_enable_nx(mod) |
设置模块非可执行区域为不可执行(NX/DEP) |
module_enable_x(mod) |
设置模块可执行区域 |
mod->state = MODULE_STATE_COMING |
将状态切换为 COMING(kallsyms 等可见,但 strong_try_module_get() 忽略) |
module_enable_ro、module_enable_nx、module_enable_x 这三个函数通过 set_memory_ro()、set_memory_nx()、set_memory_x() 等修改页表权限,利用硬件 MMU 实现内存保护。
步骤 25:prepare_coming_module --- 准备模块到来
c
// 位置: module.c:3898-3914
static int prepare_coming_module(struct module *mod)
| 子调用 | 功能说明 |
|---|---|
ftrace_module_enable(mod) |
启用模块的 ftrace 功能 |
klp_module_coming(mod) |
通知 livepatch 子系统新模块到来(livepatch 可能打补丁) |
blocking_notifier_call_chain_robust(&module_notify_list, MODULE_STATE_COMING, MODULE_STATE_GOING, mod) |
调用模块通知链上的所有回调。如果任何回调失败,则回滚(切换到 GOING 状态并重调) |
步骤 26:parse_args --- 解析模块参数
c
after_dashes = parse_args(mod->name, mod->args, mod->kp, mod->num_kp,
-32768, 32767, mod,
unknown_module_param_cb);
功能: 解析用户传入的模块参数串,调用各参数对应的 set 函数。参数值范围限定为 [-32768, 32767]。
- 返回非 NULL:有
--后的参数被忽略 - 返回 ERR_PTR:参数解析失败
步骤 27:mod_sysfs_setup --- Sysfs 设置
c
err = mod_sysfs_setup(mod, info, mod->kp, mod->num_kp);
在 /sys/module/<name>/ 下创建 sysfs 目录和属性文件(parameters、sections、version 等)。
步骤 28:copy_module_elf --- Livepatch ELF 拷贝
c
if (is_livepatch_module(mod)) {
err = copy_module_elf(mod, info);
}
如果是 livepatch 模块,保存模块的 ELF 信息以便后续打补丁时使用。
步骤 29:free_copy --- 释放临时拷贝
c
// 位置: module.c:3155-3158
static void free_copy(struct load_info *info)
{
vfree(info->hdr);
}
释放 copy_module_from_user 中分配的临时内存。
步骤 30:trace_module_load --- 跟踪事件
c
trace_module_load(mod);
通过 ftrace tracepoint 记录模块加载事件。
步骤 31:do_init_module --- 执行模块初始化
c
// 位置: module.c:3705-3819
static noinline int do_init_module(struct module *mod)
这是加载流程的最后一步,真正"启动"模块。
| 执行步骤 | 调用接口 | 功能说明 |
|---|---|---|
| 1 | kmalloc(sizeof(*freeinit), GFP_KERNEL) |
分配 init 释放跟踪结构 |
| 2 | current->flags &= ~PF_USED_ASYNC |
清除异步标志(追踪模块 init 是否使用 async) |
| 3 | do_mod_ctors(mod) |
调用模块的全局构造函数(.ctors / .init_array section) |
| 4 | do_one_initcall(mod->init) |
调用模块的 init_module() 函数。这是 module_init 宏注册的函数 |
| 5 | mod->state = MODULE_STATE_LIVE |
模块进入 LIVE 状态,成为"一等公民" |
| 6 | blocking_notifier_call_chain(&module_notify_list, MODULE_STATE_LIVE, mod) |
通知各子系统模块已激活 |
| 7 | kobject_uevent(&mod->mkobj.kobj, KOBJ_ADD) |
发送 uevent 到用户空间(触发 udev 规则等) |
| 8 | async_synchronize_full() |
如果 init 期间产生了异步任务,等待所有异步任务完成 |
| 9 | ftrace_free_mem(mod, ...) |
释放 init 区域中的 ftrace 内存 |
| 10 | module_put(mod) |
释放初始化期间持有的引用计数 |
| 11 | trim_init_extable(mod) |
裁剪异常表(移除 init 段的异常表条目) |
| 12 | rcu_assign_pointer(mod->kallsyms, &mod->core_kallsyms) |
切换到 core kallsyms(指向 core_layout) |
| 13 | module_enable_ro(mod, true) |
再次锁定只读区域(包含 ro_after_init 数据段) |
| 14 | mod_tree_remove_init(mod) |
从模块地址树中移除 init 段 |
| 15 | module_arch_freeing_init(mod) |
架构相关的 init 清理(如释放 PLT 等) |
| 16 | 清空 init_layout 字段 | 将 base/size/ro_size 等全部置 0 |
| 17 | llist_add(&freeinit->node, &init_free_list) + schedule_work(&init_free_wq) |
通过工作队列异步释放 init 段内存 |
init 段释放机制: 初始化成功后,init 段通过 do_free_init 工作队列延迟释放。延迟释放的原因是在 RCU 宽限期之后才能安全释放(因为 kallsyms 可能在禁用抢占的情况下遍历这些地址)。
04 | 模块状态机
模块在加载过程中经历以下状态转换:
UNFORMED ──→ COMING ──→ LIVE ──→ GOING
↑ ↑ ↑ ↑
add_unformed complete_ do_init_ 失败/
_module formation module 卸载
| 状态 | 说明 |
|---|---|
MODULE_STATE_UNFORMED |
模块已分配内存但未完成布局 |
MODULE_STATE_COMING |
模块正在初始化中,kallsyms 可见 |
MODULE_STATE_LIVE |
模块已完全激活,正常运行 |
MODULE_STATE_GOING |
模块正在被卸载 |
05 | 数据流图
用户空间 内核空间
======== ========
insmod/modprobe
│
│ init_module(umod, len, uargs)
▼
│ copy_module_from_user ───────► info->hdr (vmalloc 临时拷贝)
│
│ load_module
│ ├─ module_sig_check ────────► 签名验证
│ ├─ elf_validity_check ──────► ELF 校验
│ ├─ setup_load_info ─────────► info->mod, info->index 设置
│ ├─ layout_and_allocate
│ │ ├─ layout_sections ─────► 计算 core/init 布局大小
│ │ └─ move_module ─────────► module_alloc (最终位置)
│ │ 拷贝 section 到最终位置
│ ├─ simplify_symbols ────────► 符号地址解析
│ ├─ apply_relocations ───────► 重定位修正
│ ├─ post_relocation ─────────► 异常表排序, kallsyms
│ ├─ complete_formation ──────► RO/NX 内存权限设置
│ ├─ prepare_coming_module ───► 通知链 + ftrace
│ ├─ parse_args ──────────────► 参数解析
│ ├─ mod_sysfs_setup ─────────► /sys/module/xxx/
│ ├─ free_copy ───────────────► vfree(临时拷贝)
│ │
│ └─ do_init_module
│ ├─ do_mod_ctors ───────► 执行构造函数
│ ├─ do_one_initcall ────► 调用 module_init()
│ ├─ kobject_uevent ─────► KOBJ_ADD 事件
│ └─ schedule_work ──────► 异步释放 init 段
│
▼
返回 0 (成功)
06 | 总结
insmod加载模块时,调用sys_init_module的过程其实相当于自己买一个高达模型进行拼接的过程,或者说 insmod 把 .ko 当成"带补丁的工程图纸",解析后分配真实运行内存,修正所有地址依赖,最后调用初始化函数
| 步骤 | 内核做的事 | 类比 |
|---|---|---|
| 1 | 从用户空间读 .ko | 组装高达的"图纸包" |
| 2 | vmalloc 暂存 | 把图纸包放在临时工作台上 |
| 3 | 解析 ELF | 拆开图纸,区分"一次性工具"和"长期结构" |
| 4 | 分配 core / init 内存 | 规划哪里放柱子、哪里放工具 |
| 5 | 拷贝 section | 按图纸搭建真正家具 |
| 6 | 重定位 | 把所有"这里放钉子"的位置,按真实位置打孔 |
| 7 | 执行 init | 第一次开灯、测试 |
| 8 | 释放 init 段 | 收走一次性使用的手套和说明书 |