3. 内核模块加载流程详解:从用户态 insmod 到内核态 sys_init_module 的完整路径

内核模块的加载是一个跨越用户态与内核态的复杂过程。理解从 insmod 命令到内核 sys_init_module 系统调用的完整路径,是后续使用 kprobe/jprobe 进行动态追踪的基础。本章将逐层拆解这一路径,涵盖用户态工具行为、系统调用接口、内核模块加载器核心逻辑以及关键数据结构。

3.1 整体加载流程概览

下图展示了从用户态 insmod 到内核态模块初始化完成的宏观流程,包含 5 个关键阶段:

内核模块加载宏观流程 内核模块加载宏观流程(用户态 → 内核态) 用户态 insmod 读取 .ko 文件 sys_init_module 系统调用入口 load_module() ELF 解析与重定位 do_init_module() 执行 init 返回用户态 insmod load_module() 内部关键子步骤 ELF 头部校验 (layout_and_allocate) 符号解析 (simplify_symbols) 重定位 (apply_relocate_add) 模块参数传递 (parse_args)
关键观察: 整个加载路径中,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 的执行流程非常直接:

  1. 打开 .ko 文件:使用 open() 系统调用获取文件描述符。
  2. 读取文件内容到用户缓冲区:使用 read() 或 mmap() 将整个 ELF 文件读入内存。
  3. 调用 sys_init_module:将用户缓冲区指针和模块参数传递给内核。
  4. 等待内核返回:内核完成加载后,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()。其调用链如下:

系统调用汇聚图 系统调用汇聚到 load_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() 执行模块的初始化函数。该函数的执行流程如下:

  1. 设置模块状态为 MODULE_STATE_COMING:通知内核模块正在初始化。
  2. 调用模块的 init 函数:即 .ko 文件中的 module_init() 宏指定的函数。
  3. 检查返回值:
    • 返回 0:模块初始化成功,状态变为 MODULE_STATE_LIVE。
    • 返回非 0:模块初始化失败,执行清理并卸载。
  4. 释放 init 段内存:如果模块标记了 __init,内核会释放 init 段占用的内存。
do_init_module() 状态机 do_init_module() 状态转换 MODULE_STATE_COMING (初始化中) 执行 module_init() (调用 init 函数) MODULE_STATE_LIVE MODULE_STATE_GOING 关键调试点 • 在 do_init_module() 入口设置 kprobe,捕获模块状态切换。 • 在 module_init() 函数内部设置 jprobe,观察初始化参数和返回值。 • 如果模块初始化卡死,检查 MODULE_STATE_COMING 状态是否超时。

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,最终释放模块。

下图展示了错误回滚的路径:

加载错误回滚路径 load_module() 错误回滚路径 正常流程: layout → symbol → reloc → args → list_add → do_init_module 错误回滚: 任意步骤失败 → goto out → module_deallocate(mod) → 返回错误码 调试提示 在 module_deallocate() 上设置 kprobe,可以捕获所有加载失败的模块及其错误原因。
常见陷阱: 模块加载失败后,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() 清理资源。