LKM 与直接编译到内核或典型程序的元素有根本区别。典型的程序有一个 main 函数,其中 LKM 包含 entry 和 exit 函数(在 2.6 版本,您可以任意命名这些函数)。当向内核插入模块时,调用 entry 函数,从内核删除模块时则调用 exit 函数。因为 entry 和 exit 函数是用户定义的,所以存在 module_init
和 module_exit
宏,用于定义这些函数属于哪种函数。LKM 还包含一组必要的宏和一组可选的宏,用于定义模块的许可证、模块的作者、模块的描述等等。图 1 提供了一个非常简单的 LKM 的视图。
2.6 版本的 Linux 内核提供了一个新的更简单的方法,用于构建 LKM。构建 LKM 时,可以使用典型的用户工具管理模块(尽管内部已经改变):标准 insmod
(安装 LKM),rmmod
(删除 LKM),modprobe
(insmod
和 rmmod
的包装器),depmod
(用于创建模块依赖项),以及 modinfo
(用于为模块宏查找值)。
剖析内核模块对象
LKM 只不过是一个特殊的可执行可链接格式(Executable and Linkable Format,ELF)对象文件。通常,必须链接对象文件才能在可执行文件中解析它们的符号和结果。由于必须将 LKM 加载到内核后 LKM 才能解析符号,所以 LKM 仍然是一个 ELF 对象。您可以在 LKM 上使用标准对象工具(在 2.6 版本中,内核对象带有后缀 .ko,)。例如,如果在 LKM 上使用 objdump
实用工具,您将发现一些熟悉的区段(section),比如 .text(说明)、.data(已初始化数据)和 .bss(块开始符号或未初始化数据)。
您还可以在模块中找到其他支持动态特性的区段。.init.text 区段包含 module_init
代码,.exit.text 区段包含 module_exit
代码(参见图 2)。.modinfo 区段包含各种表示模块许可证、作者和描述等的宏文本。
了解 LKM 的基础知识之后,现在我们进一步探索模块是如何进入内核的,以及在内核内部是如何管理模块的。
LKM 的生命周期
在用户空间中,insmod
(插入模块)启动模块加载过程。insmod
命令定义需要加载的模块,并调用 init_module
用户空间系统调用,开始加载过程。2.6 版本内核的 insmod
命令经过修改后变得非常简单(70 行代码),可以在内核中执行更多工作。insmod
并不进行所有必要的符号解析(处理 kerneld
),它只是通过 init_module
函数将模块二进制文件复制到内核,然后由内核完成剩余的任务。
init_module
函数通过系统调用层,进入内核到达内核函数 sys_init_module
(参见图 3)。这是加载模块的主要函数,它利用许多其他函数完成困难的工作。类似地,rmmod
命令会使 delete_module
执行 system call
调用,而 delete_module
最终会进入内核,并调用 sys_delete_module
将模块从内核删除。
在模块的加载和卸载期间,模块子系统维护了一组简单的状态变量,用于表示模块的操作。加载模块时,状态为 MODULE_STATE_COMING
。如果模块已经加载并且可用,状态为 MODULE_STATE_LIVE
。此外,卸载模块时,状态为 MODULE_STATE_GOING
。
模块加载细节
现在,我们看看加载模块时的内部函数(参见图 4)。当调用内核函数 sys_init_module
时,会开始一个许可检查,查明调用者是否有权执行这个操作(通过 capable
函数完成)。然后,调用 load_module
函数,这个函数负责将模块加载到内核并执行必要的调试(后面还会讨论这点)。load_module
函数返回一个指向最新加载模块的模块引用。这个模块加载到系统内具有双重链接的所有模块的列表上,并且通过 notifier 列表通知正在等待模块状态改变的线程。最后,调用模块的 init()
函数,更新模块状态,表明模块已经加载并且可用。
加载模块的内部细节是 ELF 模块解析和操作。load_module
函数(位于 ./linux/kernel/module.c)首先分配一块用于容纳整个 ELF 模块的临时内存。然后,通过 copy_from_user
函数将 ELF 模块从用户空间读入到临时内存。作为一个 ELF 对象,这个文件的结构非常独特,易于解析和验证。
下一步是对加载的 ELF 映像执行一组健康检查(它是有效的 ELF 文件吗?它适合当前的架构吗?等等)。完成健康检查后,就会解析 ELF 映像,然后会为每个区段头创建一组方便变量,简化随后的访问。因为 ELF 对象的偏移量是基于 0 的(除非重新分配),所以这些方便变量将相对偏移量包含到临时内存块中。在创建方便变量的过程中还会验证 ELF 区段头,确保加载的是有效模块。
任何可选的模块参数都从用户空间加载到另一个已分配的内核内存块(第 4 步),并且更新模块状态,表明模块已加载(MODULE_STATE_COMING
)。如果需要 per-CPU 数据(这在检查区段头时确定),那么就分配