3. 内核模块加载流程详解:从用户态 insmod 到内核态 sys_init_module 的完整路径
内核模块的加载是一个跨越用户态与内核态的复杂过程。理解从 insmod 命令到内核 sys_init_module 系统调用的完整路径,是后续使用 kprobe/jprobe 进行动态追踪的基础。本章将逐层拆解这一路径,涵盖用户态工具行为、系统调用接口、内核模块加载器核心逻辑以及关键数据结构。
3.1 整体加载流程概览
下图展示了从用户态 insmod 到内核态模块初始化完成的宏观流程,包含 5 个关键阶段:
关键观察: 整个加载路径中,
load_module() 函数是工作量最密集的阶段,它负责将 ELF 格式的 .ko 文件转换为内核可以运行的内存映像。kprobe/jprobe 通常挂载在 load_module() 和 do_init_module() 这两个函数上,以捕获模块加载的中间状态。
3.2 用户态:insmod 与 modprobe
用户态模块加载工具主要有两个:insmod 和 modprobe。它们的职责分工如下表所示:
| 工具 | 主要职责 | 依赖处理 | 系统调用 |
|---|---|---|---|
| insmod | 直接加载指定的 .ko 文件 | 不处理依赖 | sys_init_module |
| modprobe | 智能加载,自动解决依赖 | 读取 /lib/modules/$(uname -r)/modules.dep | sys_init_module(内部调用 insmod) |
insmod 的执行流程非常直接:
- 打开 .ko 文件:使用
open()系统调用获取文件描述符。 - 读取文件内容到用户缓冲区:使用
read()或mmap()将整个 ELF 文件读入内存。 - 调用
sys_init_module:将用户缓冲区指针和模块参数传递给内核。 - 等待内核返回:内核完成加载后,
insmod根据返回值打印成功或失败信息。
调试切入点: 在用户态,可以使用
strace insmod mymodule.ko 观察系统调用序列。你会看到 openat、read、init_module 三个关键调用。这是验证加载流程是否按预期执行的第一道关卡。
3.3 系统调用:sys_init_module 与 sys_finit_module
内核提供了两个系统调用用于模块加载:
- sys_init_module:传统接口,接收用户态内存指针和长度。
- sys_finit_module:较新的接口,接收文件描述符,由内核内部读取文件内容。
两个系统调用的最终都会汇聚到同一个核心函数 load_module()。其调用链如下:
内核版本差异: 在较新的内核(5.x+)中,
sys_init_module 内部会调用 load_module,而 sys_finit_module 则先通过 kernel_read_file_from_fd() 读取文件内容,再调用 load_module。调试时需确认目标内核版本对应的具体实现路径。
3.4 内核态核心:load_module() 函数深度解析
load_module() 是模块加载的“心脏”,定义在 kernel/module.c 中。它执行以下关键步骤:
| 步骤 | 函数/操作 | 说明 | kprobe 挂载点 |
|---|---|---|---|
| 1 | ELF 头部校验 | 检查魔数、架构、版本等 | layout_and_allocate |
| 2 | 模块内存布局 | 分配核心内存、初始化段、调试段等 | layout_sections |
| 3 | 符号解析 | 解析模块导出的符号和引用的内核符号 | simplify_symbols |
| 4 | 重定位 | 根据架构执行重定位(x86_64: apply_relocate_add) | apply_relocate_add |
| 5 | 模块参数处理 | 解析并设置模块参数 | parse_args |
| 6 | 模块依赖检查 | 验证依赖模块是否已加载 | resolve_symbol_wait |
以下代码片段展示了 load_module() 中调用这些子函数的典型顺序(简化版):
/* kernel/module.c (简化伪代码) */
static int load_module(struct load_info *info, const char __user *uargs, int flags)
{
struct module *mod;
int err;
// 步骤1: 分配模块内存并布局
err = layout_and_allocate(info, flags);
if (err)
return err;
// 步骤2: 解析模块符号
err = simplify_symbols(mod, info);
if (err)
goto out;
// 步骤3: 应用重定位
err = apply_relocations(mod, info);
if (err)
goto out;
// 步骤4: 更新模块参数
err = parse_args(mod->name, mod->args, mod->num_args, ...);
if (err)
goto out;
// 步骤5: 将模块加入系统链表
list_add_rcu(&mod->list, &modules);
// 步骤6: 调用 do_init_module() 执行 init 函数
err = do_init_module(mod);
if (err)
goto out;
return 0;
out:
module_deallocate(mod);
return err;
}
调试策略: 在
simplify_symbols 和 apply_relocate_add 上设置 jprobe,可以捕获模块加载时的符号解析失败和重定位错误。这是排查“Unknown symbol”和“Relocation truncated”等经典错误的最有效手段。
3.5 模块初始化:do_init_module() 的执行
当 load_module() 完成所有准备工作后,调用 do_init_module() 执行模块的初始化函数。该函数的执行流程如下:
- 设置模块状态为
MODULE_STATE_COMING:通知内核模块正在初始化。 - 调用模块的
init函数:即 .ko 文件中的module_init()宏指定的函数。 - 检查返回值:
- 返回 0:模块初始化成功,状态变为
MODULE_STATE_LIVE。 - 返回非 0:模块初始化失败,执行清理并卸载。
- 返回 0:模块初始化成功,状态变为
- 释放 init 段内存:如果模块标记了
__init,内核会释放 init 段占用的内存。
3.6 关键数据结构:struct module 与 load_info
在整个加载过程中,有两个核心数据结构贯穿始终:
| 数据结构 | 定义位置 | 作用 | 关键字段 |
|---|---|---|---|
| struct module | include/linux/module.h | 表示一个已加载的内核模块 | name, state, init, exit, symtab, num_syms, modules_which_use_me |
| struct load_info | kernel/module.c (内部) | 加载过程中的临时上下文 | hdr (ELF 头部指针), len, mod, sym, strmap, index |
struct module 的 state 字段是调试时最常用的指标之一,其枚举值定义如下:
/* include/linux/module.h */
enum module_state {
MODULE_STATE_LIVE, /* 模块正常运行 */
MODULE_STATE_COMING, /* 模块正在加载/初始化 */
MODULE_STATE_GOING, /* 模块正在卸载 */
MODULE_STATE_UNFORMED, /* 模块尚未设置 */
};
实战技巧: 在 kprobe 处理函数中,通过
module_state_name(mod->state) 可以打印模块状态的字符串表示。结合 mod->name 和 mod->init 函数指针,可以精确追踪每个模块的加载进度。
3.7 加载路径中的错误处理与回滚
内核模块加载器设计了完善的错误处理机制。当 load_module() 中任何一步失败时,都会执行回滚操作:
- 符号解析失败:释放已分配的内存,返回
-ENOEXEC或-ENOSYS。 - 重定位失败:释放模块内存,返回
-ENOEXEC。 - 参数解析失败:释放模块内存,返回
-EINVAL。 - init 函数执行失败:调用
module_put()并设置状态为MODULE_STATE_GOING,最终释放模块。
下图展示了错误回滚的路径:
常见陷阱: 模块加载失败后,
module_deallocate() 会释放 struct module 本身。如果在 kprobe 处理函数中访问已释放的 mod 指针,会导致内核崩溃。务必在 kprobe 处理函数中检查模块状态,避免访问 MODULE_STATE_GOING 或已释放的模块。
3.8 本章小结
本章详细拆解了从用户态 insmod 到内核态 sys_init_module 的完整加载路径。核心要点总结如下:
- 用户态入口:
insmod读取 .ko 文件并调用sys_init_module。 - 系统调用层:
sys_init_module和sys_finit_module汇聚到load_module()。 - 核心加载器:
load_module()执行 ELF 解析、符号解析、重定位、参数处理等关键步骤。 - 模块初始化:
do_init_module()执行模块的 init 函数,并管理状态转换。 - 错误回滚:任何步骤失败都会触发
module_deallocate()清理资源。