2、内核模块基础:模块结构、模块加载入口函数(init_module)、模块卸载出口函数(cleanup_module)

内核模块是 Linux 内核在运行时动态加载和卸载的代码片段,它允许在不重新编译整个内核的情况下扩展内核功能。理解内核模块的基本结构、生命周期以及入口/出口函数,是使用 kprobe 和 jprobe 进行调试的前提。

2.1 内核模块的基本结构

一个标准的内核模块通常由以下几个核心部分组成,它们共同定义了模块的行为和生命周期:

组成部分 说明 是否必须
模块许可证 (MODULE_LICENSE) 声明模块使用的许可证,如 "GPL"、"Dual BSD/GPL" 等。非 GPL 许可证可能导致某些内核符号不可用。 是
模块作者/描述 (MODULE_AUTHOR / MODULE_DESCRIPTION) 提供模块的元信息,用于 modinfo 命令查看。 推荐
入口函数 (init_module 或 module_init) 模块加载到内核时自动执行的初始化函数。 是
出口函数 (cleanup_module 或 module_exit) 模块从内核卸载时自动执行的清理函数。 是
自定义功能代码 模块实现的具体功能,如文件系统、设备驱动、网络协议等。 按需
关键点: 内核模块运行在内核空间,拥有最高权限。任何编程错误(如空指针解引用、缓冲区溢出)都可能导致内核崩溃(Oops),因此调试工具(如 kprobe/jprobe)对于模块开发至关重要。

2.2 模块加载入口函数(init_module)

当用户执行 insmod 或 modprobe 命令加载模块时,内核会执行模块中定义的入口函数。该函数负责完成模块的初始化工作,例如注册设备、分配内存、创建文件系统等。

入口函数有两种定义方式:

  • 传统方式: 直接定义名为 int init_module(void) 的函数。
  • 宏方式(推荐): 使用 module_init() 宏,可以自定义函数名,更灵活。

入口函数必须返回 int 类型:

  • 返回 0 表示初始化成功,模块正常加载。
  • 返回 负数(如 -ENOMEM、-EINVAL)表示初始化失败,内核将回滚加载操作,模块不会驻留内存。
// 示例:使用 module_init 宏定义入口函数
#include <linux/module.h>
#include <linux/kernel.h>

static int __init my_module_init(void)
{
    printk(KERN_INFO "My module: 加载成功!\n");
    // 执行注册、分配等操作
    return 0; // 成功
}

static void __exit my_module_exit(void)
{
    printk(KERN_INFO "My module: 卸载成功!\n");
    // 执行注销、释放等操作
}

module_init(my_module_init);
module_exit(my_module_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kprobe Debugger");
MODULE_DESCRIPTION("一个简单的内核模块示例");
调试视角: 使用 jprobe 探测 init_module 函数,可以捕获模块加载时的参数和返回值,是分析模块初始化逻辑的有效手段。

2.3 模块卸载出口函数(cleanup_module)

当用户执行 rmmod 命令卸载模块时,内核会执行出口函数。该函数必须释放入口函数分配的所有资源,包括内存、中断、设备号、文件系统等,否则会导致资源泄漏。

出口函数同样有两种定义方式:

  • 传统方式: 直接定义名为 void cleanup_module(void) 的函数。
  • 宏方式(推荐): 使用 module_exit() 宏。

出口函数没有返回值(void),且必须确保在函数返回后,模块不再被任何内核路径引用。

常见陷阱: 如果出口函数未能正确释放资源,或者模块在被使用时强制卸载,可能导致内核崩溃或系统不稳定。kprobe 可以用于监控 cleanup_module 的执行路径,帮助定位资源泄漏问题。

2.4 模块加载与卸载的完整流程

下图展示了内核模块从加载到卸载的完整生命周期,以及入口/出口函数在其中的位置:

内核模块加载与卸载完整流程 用户态命令 内核模块加载器 (sys_init_module) 执行 init_module() (返回 0 成功,负数失败) 成功? 模块驻留内核 加载失败,回滚 执行 cleanup_module() 模块完全卸载 kprobe/jprobe 探测点 可监控入口/出口函数

2.5 模块加载过程中的关键数据结构

内核使用 struct module 结构体来管理每个已加载的模块。该结构体包含了模块的名称、状态、内存布局、符号表、依赖关系等关键信息。kprobe 在探测模块函数时,可以通过访问当前任务的 struct module 来获取模块上下文。

字段 类型 说明
name char [] 模块名称,如 "hello_module"
state enum module_state 模块状态:MODULE_STATE_LIVE(运行中)、MODULE_STATE_COMING(加载中)、MODULE_STATE_GOING(卸载中)
init void (*)(void) 指向模块入口函数的指针
exit void (*)(void) 指向模块出口函数的指针
module_core void * 模块核心代码在内存中的基地址
core_size unsigned int 模块核心代码的大小
syms struct kernel_symbol * 模块导出的符号表
调试应用: 在 kprobe 的 pre_handler 中,可以通过 kallsyms_lookup_name("module_name") 或遍历 modules 链表来获取目标模块的 struct module,进而检查其 state 字段,判断模块是否处于加载或卸载过程中。

2.6 模块加载过程中的关键时机点

对于使用 kprobe/jprobe 调试模块加载过程,以下时机点尤为重要:

  1. 模块加载器入口: sys_init_module 系统调用被触发时,可以捕获加载请求。
  2. 模块内存分配后: 内核为模块分配内核内存空间后,可以检查模块的代码布局。
  3. 入口函数执行前: 使用 jprobe 探测 init_module 函数,捕获初始化参数。
  4. 入口函数执行后: 检查返回值,判断初始化是否成功。
  5. 模块状态变更时: 模块状态从 MODULE_STATE_COMING 变为 MODULE_STATE_LIVE 时,表示加载完成。
实战建议: 在调试自定义模块时,建议在 init_module 和 cleanup_module 函数内部添加 printk 日志,配合 kprobe 的探测点,可以形成双重验证,确保调试数据的准确性。