简介: 为保持 Linux 内核的稳定与可持续发展,内核在发展过程中引进了可装载模块这一特性。内核可装载模块就是可在内核运行时加载到内核的一组代码。通常 , 我们会在两个版本不同的内核上装载同一模块失败,即使是在两个相邻的补丁级(Patch Level)版本上。这是因为内核在引入可装载模块的同时,对模块采取了版本信息校验。这是一个与模块代码无关,却与内核相连的机制。该校验机制保证了内核装载的模块是用户认可的,且安全的。本文将从内核模块发布者的角度思考模块版本检查机制,并从开发者与授权 root 用户的角度去使用及理解该机制。
内核可装载模块概述
Linux 在发展过程中(即自 Linux 1.2 之后)引进了模块这一重要特性,该特性提供内核可在运行时进行扩展。可装载模块(Loadable Kernel Module,即 LKM)也被称为模块,就是可在内核运行时加载到内核的一组目标代码(并非一个完整的可执行程序)。这就意味着在重构和使用可装载模块时并不需要重新编译内核。模块依据代码编写与编译时的位置可分:内部模块和外部模块,即 in-tree module 和 out-of-tree module,在内核树外部编写并构建的模块就是外部模块。如果只是认为可装载模块就是外部模块或者认为在模块与内核通讯时模块是位于内核的外部的,那么这在 Linux 下均是错误的。当模块被装载到内核后,可装载模块已是内核的一部分。另外,我们使用的 Linux 发行版在系统启动过程 initrd 中已使用了必要的模块,除非我们只讨论基础内核(base kernel)。本文主要是对 Linux 2.6 的外部模块进行讨论的。
可装载模块在 Linux 2.6 与 2.4 之间存在巨大差异,其最大区别就是模块装载过程变化(如 图 1所示,在 Linux 2.6 中可装载模块在内核中完成连接)。其他一些变化大致如下:
模块的后缀及装载工具;
对于使用模块的授权用户而言,模块最直观的改变应是模块后缀由原先的 .o 文件(即 object)变成了 .ko 文件(即 kernel object)。同时,在 Linux 2.6 中,模块使用了新的装卸载工具集 module-init-tools(工具 insmod 和 rmmod 被重新设计)。模块的构建过程改变巨大,在 Linux 2.6 中代码先被编译成 .o 文件,再从 .o 文件生成 .ko 文件,构建过程会生成如 .mod.c、.mod.o 等文件。
模块信息的附加过程;
在 Linux 2.6 中,模块的信息在构建时完成了附加;这与 Linux 2.4 不同,先前模块信息的附加是在模块装载到内核时进行的(在 Linux 2.4 时,这一过程由工具 insmod 完成)。
模块的标记选项。
在 Linux 2.6 中,针对管理模块的选项做了一些调整,如取消了 can_unload 标记(用于标记模块的使用状态),添加了 CONFIG_MODULE_UNLOAD 标记(用于标记禁止模块卸载)等。还修改了一些接口函数,如模块的引用计数。
图 1. 模块在内核中完成连接
发展到 Linux 2.6,内核中越来越多的功能被模块化。这是由于可装载模块相对内核有着易维护,易调试的特点。可装载模块还为内核节省了内存空间,因为模块一般是在真正需要时才被加载。根据模块作用,可装载模块还可分三大类型:设备驱动、文件系统和系统调用。另须指出的是,虽然可装载模块是从用户空间加载到内核空间的,但是并非用户空间的程序。
模块的版本检查
Linux 的迅速发展致使相邻版本的内核之间亦存在较大的差异,即在版本补丁号(Patch Level,即内核版本号的第四位数)相邻的内核之间。为此 Linux 的开发者为了保证内核的稳定,Linux 在加载模块到内核时对模块采用了版本校验机制。当被期望加载模块的系统环境与模块的构建环境相左时,通常会出现如清单 1 所示的装载模块失败。
清单 1. 装载模块失败
清单 1 中,模块 hello.ko 构建时的环境与当前系统不一致,导致工具 insmod 在尝试装载模块 hello.ko 到内核时失败。hello.ko 是一个仅使用了函数 printk 的普通模块(您可在示例源码中找到文件 hello/hello.c)。我们通过命令 dmesg(或者您也可以查看系统日志文件如 /var/log/messages 等,如果您启用了这些系统日志的话)获取模块装载失败的具体原因,模块 hello.ko 装载失败是由于模块中 module_layout 的导出符号的版本信息与当前内核中的不符。函数 module_layout 被定义在内核模块版本选项 MODVERSIONS(即内核可装载模块的版本校验选项)之后。清单 2所示为 module_layout 在内核文件 kernel/module.c 中的定义。
清单 2. 函数 module_layout
清单 3. 结构体 modversion_info
正如您所想,函数 module_layout 的第二个参数 ver 存储了模块的版本校验信息。结构体 modversion_info 中保存了用于模块校验的 CRC(Cyclic Redundancy Check,即循环冗余码校验)值(见 清单 3)。Linux 对可装载模块采取了两层验证:模块的 CRC 值校验和 vermagic 的检查。其中模块 CRC 值校验针对模块(内核)导出符号,是一种简单的 ABI(即 Application Binary Interface)一致性检查,清单 1中模块 hello.ko 加载失败的根本原因就是没有通过 CRC 值校验(即 module_layout 的 CRC 值与当前内核中的不符);而模块 vermagic(即 Version Magic String)则保存了模块编译时的内核版本以及 SMP 等配置信息(见 清单 4,模块 hello.ko 的 vermagic 信息),当模块 vermagic 与主机信息不相符时亦将终止模块的加载。
清单 4. 模块的 vermagic 信息
通常,内核与模块的导出符号的 CRC 值被保存在文件 Module.symvers 中,该文件需在开启内核配置选项 CONFIG_MODVERSIONS 之后并完全编译内核获得(或者您也可在编译外部模块后获得该文件,保存的是模块的导出符号的 CRC 信息),其信息的保存格式如清单 5 所示。
清单 5. 导出符号的 CRC 值
Linux 内核在进行模块装载时先完成模块的 CRC 值校验,再核对 vermagic 中的字符信息,图 2展示了内核中与模块版本校验相关的函数的调用过程(分别在函数 setup_load_info 和 check_modinfo 中完成校验)。Linux 使用 GCC 中的声明函数属性 __attribute__ 完成对模块的版本信息附加。构建的模块存在几个 section,如 .modinfo、.gnu.linkonce.this_module 和 __versions 等,这些 ELF 小节(即 section)保存了模块校验所需的信息(关于这些 section 信息的附加过程,您可查看模块构建时生成的文件 <module>.mod.c 及工具 modpost,见 清单 8和 清单 15)。
图 2. 模块的两层版本校验过程
为了更好的理解可装载模块,我们查看内核头文件 include/linux/module.h,它不仅定义了上述中的 struct modversion_info(见 清单 3)还定义了 struct module 等结构体。模块 CRC 值校验查看的是就是模块 __versions 小节的内容,即是附加的 struct modversion_info 信息。模块的 CRC 校验过程在函数 setup_load_info 中完成。Linux 使用 .gnu.linkonce.this_module 小节来解决模块对 struct module 信息的附加。文件 kernel/module.c 中的函数 check_modinfo 完成了主机与模块的 vermagic 值的对比(见 清单 6)。清单 6 中函数 get_modinfo 用于获取内核中的 vermagic 信息,模块 vermagic 信息则被保存在了 ELF 的 .modinfo 小节中。
清单 6. 函数 check_modinfo
内核空间与用户空间
操作系统须负责程序的独立操作并保护资源不受非法访问,而这一功能在现代 CPU 中以设计不同的操作模式(级别)来实现。内核运行在 CPU 的最高级别,即内核态,也被称为超级用户态;而应用程序则运行在最低级别,即用户态。由此系统内存在 Linux 中可分为两个不同的区域:内核空间与用户空间。模块运行在内核空间里,而应用程序则运行在对应的用户空间中。
须指出的是模块的 vermagic 信息来自内核头文件 include/linux/vermagic.h 中的宏 VERMAGIC_STRING,其中宏 UTS_RELEASE 保存了内核版本信息(见 清单 7)。与其关联的头文件 include/generated/utsrelease.h 需经内核预编译生成,即通过命令 make 或 make modules_prepare 等。
清单 7. 宏 VERMAGIC_STRING