解析nbsp;Linuxnbsp;内核可装载模块的版本…

2024-01-14 08:48

本文主要是介绍解析nbsp;Linuxnbsp;内核可装载模块的版本…,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

作者: 王华东, 系统工程师, 自由职业者
王华东,自由职业者,对 Linux、开放源码感兴趣。

 

简介: 为保持 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. 模块在内核中完成连接
图 1. 模块在内核中完成连接

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


模块的版本检查

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


清单 1. 装载模块失败
                          # 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
                          #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
                          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 信息
                          # 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 值
                          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
                          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; } 

内核空间与用户空间

操作系统须负责程序的独立操作并保护资源不受非法访问,而这一功能在现代 CPU 中以设计不同的操作模式(级别)来实现。内核运行在 CPU 的最高级别,即内核态,也被称为超级用户态;而应用程序则运行在最低级别,即用户态。由此系统内存在 Linux 中可分为两个不同的区域:内核空间与用户空间。模块运行在内核空间里,而应用程序则运行在对应的用户空间中。

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


清单 7. 宏 VERMAGIC_STRING
                          static const char vermagic[] = VERMAGIC_STRING; #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 中的部分程序
名称说明
insmod装载模块到当前运行的内核中
rmmod从当前运行的内核中卸载模块
lsmod显示当前内核已加装的模块信息
modinfo检查与内核模块相关联的目标文件,并打印出所有得到的信息
modprobe利用 depmod 创建的依赖关系文件自动加载相关的模块
depmod创建一个内核可装载模块的依赖关系文件,modprobe 用它来自动加载模块

值得一提的是在 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
                          # 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 小节
                            # 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 的使用
                          # 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 的部分内容
                          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. 预编译内核模块
                          # 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 配置内核模块选项
                            # make menuconfig --- Enable loadable module support [ ]   Forced module loading [*]   Module unloading [ ]     Forced module unloading [*]   Module versioning support [*]   Source checksum for all modules 



清单 14. 模块相关的内核配置选项
                            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. 构建外部模块
                          # 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 文件,但这对于模块亦是重要的内容。


这篇关于解析nbsp;Linuxnbsp;内核可装载模块的版本…的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/604589

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

python: 多模块(.py)中全局变量的导入

文章目录 global关键字可变类型和不可变类型数据的内存地址单模块(单个py文件)的全局变量示例总结 多模块(多个py文件)的全局变量from x import x导入全局变量示例 import x导入全局变量示例 总结 global关键字 global 的作用范围是模块(.py)级别: 当你在一个模块(文件)中使用 global 声明变量时,这个变量只在该模块的全局命名空

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

内核启动时减少log的方式

内核引导选项 内核引导选项大体上可以分为两类:一类与设备无关、另一类与设备有关。与设备有关的引导选项多如牛毛,需要你自己阅读内核中的相应驱动程序源码以获取其能够接受的引导选项。比如,如果你想知道可以向 AHA1542 SCSI 驱动程序传递哪些引导选项,那么就查看 drivers/scsi/aha1542.c 文件,一般在前面 100 行注释里就可以找到所接受的引导选项说明。大多数选项是通过"_

OWASP十大安全漏洞解析

OWASP(开放式Web应用程序安全项目)发布的“十大安全漏洞”列表是Web应用程序安全领域的权威指南,它总结了Web应用程序中最常见、最危险的安全隐患。以下是对OWASP十大安全漏洞的详细解析: 1. 注入漏洞(Injection) 描述:攻击者通过在应用程序的输入数据中插入恶意代码,从而控制应用程序的行为。常见的注入类型包括SQL注入、OS命令注入、LDAP注入等。 影响:可能导致数据泄

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。