解析 Linux 内核可装载模块的版本检查机制-模块的版本检查
- UID
- 1066743
|
解析 Linux 内核可装载模块的版本检查机制-模块的版本检查
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(即内核可装载模块的版本校验选项)之后。所示为 module_layout 在内核文件 kernel/module.c 中的定义。
清单 2. 函数 module_layout1
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_info1
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,即循环冗余码校验)值(见 )。Linux 对可装载模块采取了两层验证:模块的 CRC 值校验和 vermagic 的检查。其中模块 CRC 值校验针对模块(内核)导出符号,是一种简单的 ABI(即 Application Binary Interface)一致性检查,中模块 hello.ko 加载失败的根本原因就是没有通过 CRC 值校验(即 module_layout 的 CRC 值与当前内核中的不符);而模块 vermagic(即 Version Magic String)则保存了模块编译时的内核版本以及 SMP 等配置信息(见 ,模块 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 中的字符信息,展示了内核中与模块版本校验相关的函数的调用过程(分别在函数 setup_load_info 和 check_modinfo 中完成校验)。Linux 使用 GCC 中的声明函数属性 __attribute__ 完成对模块的版本信息附加。构建的模块存在几个 section,如 .modinfo、.gnu.linkonce.this_module 和 __versions 等,这些 ELF 小节(即 section)保存了模块校验所需的信息(关于这些 section 信息的附加过程,您可查看模块构建时生成的文件 <module>.mod.c 及工具 modpost,见 和 )。
图 2. 模块的两层版本校验过程为了更好的理解可装载模块,我们查看内核头文件 include/linux/module.h,它不仅定义了上述中的 struct modversion_info(见 )还定义了 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 中函数 get_modinfo 用于获取内核中的 vermagic 信息,模块 vermagic 信息则被保存在了 ELF 的 .modinfo 小节中。
清单 6. 函数 check_modinfo1
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;
}
|
内核空间与用户空间操作系统须负责程序的独立操作并保护资源不受非法访问,而这一功能在现代 CPU 中以设计不同的操作模式(级别)来实现。内核运行在 CPU 的最高级别,即内核态,也被称为超级用户态;而应用程序则运行在最低级别,即用户态。由此系统内存在 Linux 中可分为两个不同的区域:内核空间与用户空间。模块运行在内核空间里,而应用程序则运行在对应的用户空间中。
须指出的是模块的 vermagic 信息来自内核头文件 include/linux/vermagic.h 中的宏 VERMAGIC_STRING,其中宏 UTS_RELEASE 保存了内核版本信息(见 )。与其关联的头文件 include/generated/utsrelease.h 需经内核预编译生成,即通过命令 make 或 make modules_prepare 等。
清单 7. 宏 VERMAGIC_STRING1
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
|
|
|
|
|
|
|