新闻  |   论坛  |   博客  |   在线研讨会
解析 Linux 内核可装载模块的版本检查机制
电子禅石 | 2020-06-24 15:12:20    阅读:46001   发布文章

内核可装载模块概述

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. 模块在内核中完成连接

图 1. 模块在内核中完成连接

发展到 Linux 2.6,内核中越来越多的功能被模块化。这是由于可装载模块相对内核有着易维护,易调试的特点。可装载模块还为内核节省了内存空间,因为模块一般是在真正需要时才被加载。根据模块作用,可装载模块还可分三大类型:设备驱动、文件系统和系统调用。另须指出的是,虽然可装载模块是从用户空间加载到内核空间的,但是并非用户空间的程序。

模块的版本检查

Linux 的迅速发展致使相邻版本的内核之间亦存在较大的差异,即在版本补丁号(Patch Level,即内核版本号的第四位数)相邻的内核之间。为此 Linux 的开发者为了保证内核的稳定,Linux 在加载模块到内核时对模块采用了版本校验机制。当被期望加载模块的系统环境与模块的构建环境相左时,通常会出现如清单 1 所示的装载模块失败。

清单 1. 装载模块失败

1
2
3
4
5
# insmod ./hello/hello.ko
insmod: error inserting './hello/hello.ko': -1 Invalid module format
 
# dmesg | grep hello
[ 9206.599843] hello: disagrees about version of symbol module_layout

清单 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

1
2
3
4
5
6
7
8
9
10
11
/* kernel/module.c */
#ifdef CONFIG_MODVERSIONS
void module_layout(struct module *mod,
      struct modversion_info *ver,
      struct kernel_param *kp,
      struct kernel_symbol *ks,
      struct tracepoint * const *tp)
{
}
EXPORT_SYMBOL(module_layout);
#endif

清单 3. 结构体 modversion_info

1
2
3
4
5
6
/* include/linux/module.h */
struct modversion_info
{
    unsigned long crc;
    char name[MODULE_NAME_LEN];
};

正如您所想,函数 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 信息

1
2
3
4
5
6
7
8
9
# uname – r
2.6.38-10-generic
 
# modinfo ./hello/hello.ko
filename:       ./hello/hello.ko
license:        Dual BSD/GPL
srcversion:     31FE72DA6A560C890FF9B3F
depends:       
vermagic:       2.6.38-9-generic SMP mod_unload modversions

通常,内核与模块的导出符号的 CRC 值被保存在文件 Module.symvers 中,该文件需在开启内核配置选项 CONFIG_MODVERSIONS 之后并完全编译内核获得(或者您也可在编译外部模块后获得该文件,保存的是模块的导出符号的 CRC 信息),其信息的保存格式如清单 5 所示。

清单 5. 导出符号的 CRC 值

1
2
0x1de386dd  module_layout  vmlinux  EXPORT_SYMBOL
<CRC>        <Symbol>  <module>

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. 模块的两层版本校验过程

图 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

1
2
3
4
5
6
7
8
9
10
11
12
13
/* kernel/module.c */
static int check_modinfo(struct module *mod, struct load_info *info)
{
 const char *modmagic = get_modinfo(info, "vermagic");
 
 ...
 } else if (!same_magic(modmagic, vermagic, info->index.vers)) {
   ...
 }
 ...
 
 return 0;
}

须指出的是模块的 vermagic 信息来自内核头文件 include/linux/vermagic.h 中的宏 VERMAGIC_STRING,其中宏 UTS_RELEASE 保存了内核版本信息(见 清单 7)。与其关联的头文件 include/generated/utsrelease.h 需经内核预编译生成,即通过命令 make 或 make modules_prepare 等。

清单 7. 宏 VERMAGIC_STRING

1
2
3
4
5
6
7
8
9
/* kernel/module.c */
static const char vermagic[] = VERMAGIC_STRING;
 
/* include/linux/vermagic.h */
#define VERMAGIC_STRING                         \
    UTS_RELEASE " "                         \
    MODULE_VERMAGIC_SMP MODULE_VERMAGIC_PREEMPT             \
    MODULE_VERMAGIC_MODULE_UNLOAD MODULE_VERMAGIC_MODVERSIONS   \
    MODULE_ARCH_VERMAGICLINE

模块的装载与卸载

上述中,我们在装载模块时使用了工具 insmod。在 Linux 2.6 中,工具 insmod 被重新设计并作为工具集 module-init-tools 中的一个程序,其通过系统调用 sys_init_module(您可查看头文件 include/asm-generic/unistd.h)衔接了模块的版本检查,模块的装载等功能(如 图 3所示)。module-init-tools 是为 2.6 内核设计的运行在 Linux 用户空间的模块装卸载工具集,其包含的程序 rmmod 用于卸载当前内核中的模块。

图 3. 模块的装卸载

图 3. 模块的装卸载

表 1. 工具集 module-init-tools 中的部分程序

值得一提的是在 module-init-tools 中可用于模块装卸载的程序 modprobe。程序 modprobe 的内部函数调用过程正如您所想与 insmod 类似,只是其装载过程会查找一些模块装载的配置文件,且 modprobe 在装载模块时可解决模块间的依赖性,即若有必要,程序 modprobe 会在装载一个模块时自动加载该模块依赖的其他模块。

其他一些细节

从用户空间装载模块到内核时,Linux 还对用户权限进行了检查。模块的装载须是获得 CAP_SYS_MODULE 权限的超级用户,这正是模块装载时最先检查的内容(见 图 2)。在 Linux 2.6 中,模块在构建时生成了一些临时文件,如 .o 文件、.mod.o 文件等。了解这些文件的生成有助于我们更好的理解 Linux 2.6 的内核模块构建过程以及版本信息的检查等内容。文件 .o 是模块代码(即 .c 文件)经编译后获得的目标文件,文件 .mod.o 则对应文件 .mod.c。文件 <module>.mod.c 是对 <modulue>.c 的扩展,清单 8展示了文件 kobject-example.mod.c 的内容 ( 即模块 kobject-example.ko 的 .mod.c 文件 ),您可见到与模块版本检查相关三个小节。

清单 8. 文件 kobject-example.mod.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# cat ./kobject/kobject-example.mod.c
 ...
 
 MODULE_INFO(vermagic, VERMAGIC_STRING);
 
 struct module __this_module
 __attribute__((section(".gnu.linkonce.this_module"))) = {
 ...
 };
 
 static const struct modversion_info ____versions[]
 __used
 __attribute__((section("__versions"))) = {
 ...
 };
 
 static const char __module_depends[]
 __used
 __attribute__((section(".modinfo"))) =
"depends=";
 
 MODULE_INFO(srcversion, "B06F9B8B7AB52AEED247B9F");

清单 8 中显示了模块 kobject-example.ko 中的三个 section 以及宏 MODULE_INFO,最后一行 srcversion 则需开启内核配置选项 MODULE_SRCVERSION_ALL。经上述,我们知道这三个 section 正是模块版本检查的附加信息。我们通过工具 objdump 查看 .modinfo 小节(见 清单 9, 即模块的 vermagic 信息)。<module>.ko 的附加信息合并自文件 <module>.o 与文件 <module>.mod.o。内核工具 modpost 完成了一这步骤,且该工具是 Linux 2.6 内核模块构建时所必须的。

清单 9. 使用工具 objdump 查看 .modinfo 小节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# objdump --section=.modinfo -s hello/hello.o
 
hello/hello.o:     file format elf64-x86-64
 
Contents of section .modinfo:
0000 6c696365 6e73653d 4475616c 20425344  license=Dual BSD
0010 2f47504c 00                          /GPL.
 
# modinfo hello/hello.o
filename:       hello/hello.o
license:        Dual BSD/GPL
 
# objdump --section=.modinfo -s hello/hello.mod.o
...
# objdump --section=.modinfo -s hello/hello.ko
...

经上述,我们可知内核树的顶层 Makefile 文件包含了内核版本的信息,且该信息经编译后被添加到模块的(头文件 include/generated/utsrelease.h 保存的内核版本信息来自顶层 Makefile)。表 1中,工具 lsmod 打开文件 /proc/modules 查询当前内核中已装载的模块(见清单 10),文件 /proc/modules 还被 rmmod 在卸载模块时使用。另外,若您在装载模块 hello.ko 后没能在终端下看到相应的字符串输出,则需检查文件 /proc/sys/kernel/printk,并重设下消息级别。

清单 10. 工具 lsmod 的使用

1
2
3
4
5
6
7
8
9
10
# insmod ./kobject/kobject-example.ko
 
# ls /sys/kernel/kobject_example/
bar  baz  foo
 
# lsmod | grep kobject
kobject_example        12857  0
 
# cat /proc/modules | grep kobject
kobject_example 12857 0 - Live 0xffffffffa0523000

Linux 2.6 构建模块时工具 modpost 被 scripts/Makefile.modpost 调用,生成 <module>.mod.c 及文件 Module.symvers(见 清单 15)。在开启内核选项 CONFIG_MODVERSIONS 之后,文件 Makefile.Build 会调用工具 genksyms(现位于内核树 scripts/genksyms 目录下,在 Linux 2.4 时是模块工具集 Modutils 的一部分)生成 CRC 信息(见 清单 11)。其中代码 call cmd_gensymtypes 就是对工具 genksyms 的调用。另外一个较为明晰的方式是,使用工具 objdump 或 readelf 查看相关的 ELF 小节,并使用 make – n 查看模块构建过程。

清单 11. 文件 Makefile.Build 的部分内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ifndef CONFIG_MODVERSIONS
cmd_cc_o_c = $(CC) $(c_flags) -c -o $@ $<
else
cmd_cc_o_c = $(CC) $(c_flags) -c -o $(@D)/.tmp_$(@F) $<
cmd_modversions =              \
 if $(OBJDUMP) -h $(@D)/.tmp_$(@F) | grep -q __ksymtab; then  \
   $(call cmd_gensymtypes, $(KBUILD_SYMTYPES))    \
       > $(@D)/.tmp_$(@F:.o=.ver);      \
                 \
   $(LD) $(LDFLAGS) -r -o $@ $(@D)/.tmp_$(@F)     \
     -T $(@D)/.tmp_$(@F:.o=.ver);      \
   rm -f $(@D)/.tmp_$(@F) $(@D)/.tmp_$(@F:.o=.ver);  \
 else                \
   mv -f $(@D)/.tmp_$(@F) $@;        \
 fi;
endif

模块的构建与测试

为内核构建外部模块前,我们须准备一颗内核源码树(kernel source tree)。内核源码树就是一套包含系统配置及内核头文件的内核目录树。须指出的是 Linux 2.6 的内核源码树与 2.4 的不同,先前的内核只需一套内核头文件就可以了,但在 2.6 的内核源码树中还需存在一些目标文件及工具,如 scripts/mod/modpost 等。清单 12 所示是从内核源码进行内核模块预编译以此生成内核树,当然您也可使用 Linux 发行版的内核源码树(系统内核树一般存放在 /lib/modules/<kernel version>/build,如果存在的话)。

清单 12. 预编译内核模块

1
2
3
# make menuconfig
# make modules_prepare
#

当然,我们最先须根据主机的硬件信息产生内核配置文件 .config。您可使用命令 make menuconfig 或 make config 等来配置与模块相关的选项(清单 13与 清单 14相互对应,显示了模块相关的内核配置选项)。设置选项 CONFIG_MODULES=y 以及 CONFIG_MODVERSIONS=y 使内核支持模块的版本检查。另须注意的是,模块预编译并不生成 Module.symvers 文件,即使您开启了 CONFIG_MODVERSIONS 选项。因此最好的方式是完全编译 Linux 内核。

清单 13. 使用 make menuconfig 配置内核模块选项

1
2
3
4
5
6
7
# make menuconfig
--- Enable loadable module support
   [ ]   Forced module loading
   [*]   Module unloading
   [ ]     Forced module unloading
   [*]   Module versioning support
   [*]   Source checksum for all modules

清单 14. 模块相关的内核配置选项

1
2
3
4
5
6
CONFIG_MODULES
CONFIG_MODULE_FORCE_LOAD
CONFIG_MODULE_UNLOAD
CONFIG_MODULE_FORCE_UNLOAD
CONFIG_MODVERSIONS
CONFIG_MODULE_SRCVERSION_ALL

内核 2.6 时,我们常为模块的构建编写一个 Makefile 文件,但仍可使用类似内核 2.4 下的模块构建命令。清单 15 展示了外部模块构建的 make 命令,其中 $KDIR 是内核树的绝对路径,$MDIR 是期望构建的模块的绝对路径(若是内部模块则可使用 make CONFIG_EXT2_FS=m …)。

清单 15. 构建外部模块

1
2
3
4
5
6
7
8
9
10
# make -C $KDIR M=$MDIR [target]
 
# make -C /lib/modules/2.6.38-10-generic/build M=$PWD/hello  modules
make: Entering directory `/usr/src/linux-headers-2.6.38-10-generic'
CC [M] /home/harris/work/samples/hello/hello.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/harris/work/samples/hello/hello.mod.o
LD [M] /home/harris/work/samples/hello/hello.ko
make: Leaving directory `/usr/src/linux-headers-2.6.38-10-generic'

结束语

虽然本文已尽量集中描述可装载模块的版本检查机制,但是仍然涉及了非常多的内容。您需花一些时间来了解这些看似与模块不相关的内容,如 /proc、/sys 文件系统、ELF(即 Executable and Linkable Format)格式等,以此更好的全面理解内核可装载模块以及模块版本版本检查机制。另外,由于文章讨论的是 Linux 2.6 的外部模块,没有清晰的讲述内核的 Kbuild 系统及 Makefile 文件,但这对于模块亦是重要的内容。

https://www.ibm.com/developerworks/cn/linux/l-cn-kernelmodules/

*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。

参与讨论
登录后参与讨论
属于自己的技术积累分享,成为嵌入式系统研发高手。
推荐文章
最近访客