Linux Module
catalog
1. 概述
2. 使用模块
3. 插入和删除模块
4. 自动化与热插拔
5. 版本控制
1. 概述
模块(module)是一种向Linux内核添加设备驱动程序、文件系统及其他组件的有效方法,而无需连编新内核或重启系统,模块消除了宏内核的许多限制,模块有许多优点
1. 通过使用模块,内核发布者能够预先编译大量驱动程序,但并不会造成内核镜像发生膨胀,在自动检测硬件(例如USB)或用户提示之后,安装例程选择适当的模块并将其添加到内核中
2. 内核开发者可以将试验性的代码打包到模块中,模块可以卸载、修改代码或重新打包后可以重新加载,这使得可以快速测试新特性,无需每次都重启系统
模块几乎可以无缝地插入到内核,模块代码导出一些函数,可以由其他核心模块(包括持久编译到内核中的代码)使用,同样,在模块代码需要卸载时,模块和内核剩余部分之间的关联,也会相应终止
Relevant Link:
2. 使用模块
添加和移除模块涉及几个系统调用,这些通常由modtils工具包调用
0x1: 添加和移除
从用户的角度来看,模块可通过两个不同的系统程序添加到运行的内核中
1. modprobe: 考虑了各个模块之间可能出现的依赖性,即在一个模块依赖于一个或多个合作者模块的功能,modprobe在识别出目标模块所依赖的模块之后,在内核也会使用Insmod
2. insmod: 只加载一个单一的模块到内核中,而该模块可能只信赖内核中已存在的代码,并不关注所依赖的代码是通过模块动态加载,还是持久编译到内核中
模块文件是可重定位的,可重定位文件的函数都不会引用绝对地址,而只是指向代码中的相对地址,因此可以在内存的任意偏移地址加载,当然在映像加载到内存中时,映像中的地址要由动态链接器ld.so进行适当的修改,内核模块也是一样的,内核模块中的地址也是相对的,而不是绝对的,但重定位的工作由内核自身执行,而不是动态装载器
在处理init_module系统调用时,模块代码首先复制到内核内存中,接下来是重定位工作和解决模块中未定义的引用,因为模块使用了持久编译到内核中的函数,在模块本身编译时无法确定这些函数的地址,所以需要在这里处理未定义的引用,这些函数定义在内核的基础代码中,因为已经加载到内存,为此,内核提供了一个所有导出函数的列表,该列表给出所有导出函数的内存地址和对应函数名,可以通过proc文件系统访问
cat /proc/kallsyms
这是对于内核中持久编译的代码和动态添加的模块导入的代码的数组,其数组项用于将符号分配到虚拟地址空间中对应的地址
0x2: 依赖关系
一个模块还可以依赖一个或多个其他模块,在向内核添加模块时,还需要考虑下列问题
1. 内核提供的函数符号表,可以在模块加载时动态扩展其长度,模块可以指定其代码中哪些函数可以导出,哪些函数仅供内部使用
2. 如果模块之间有依赖,那么向内核添加模块的顺序很重要,否则会造成函数引用的地址无法解析,modutils标准工具集中的depmod工具可用于计算系统的各个模块之间的依赖关系,每次系统启动时或新模块安装后,通常会运行该程序,找到的依赖关系保存在一个列表中,默认情况下,写入/lib/modules/2.6.18-308.el5(对应ersion)/modules.dep中,该信息由modprobe处理,该工具在现存的依赖关系能够自动解决的情况下向内核插入模块
depmod分析所有可用的模块的二进制代码,对每个模块建立一个列表,包含所有已定义符号和未解决的引用,最后将各个模块的列表彼此进行比较,如果模块A包含的一个符号在模块B中是未解决的引用,则意味着模块B依赖模块A,接下来在依赖文件中以B:A的形式增加一项,即确认了上述事实,模块引用的大多数符号都定义在内核中,而不是定义在其他模块中
0x3: 查询模块信息
有一些额外信息,是直接存储在模块二进制文件中,并且指定了模块用途的文本描述,这可以使用modinfo工具查询
1. 驱动的开发者
2. 驱动程序的简短描述
3. 可以传递给模块的配置参数
4. 指定支持的设备
5. 该模块按何种许可证分发(重要)
0x4: 自动加载
通常,模块的装载发起于用户空间,由用户或者自动化脚本启动,在处理模块时,为达到更大的灵活性并提高透明度,内核自身也能够请求加载模块,由于在用户空间完成这些比内核空间容易得多,内核将工作委托给一个辅助进程kmod,kmod并不是一个永久性的守护进程,内核会按需启动它
内核源代码中,很多不同地方调用了request_module,借助该函数,内核试图通过在没有用户介入的情况下自动加载代码,使得尽可能透明地访问那些委托给模块的功能
3. 插入和删除模块
用户空间和内核的模块实现之间的接口,包括以下几个系统调用
1. init_module: 将一个新模块插入到内核中,用户空间工具只需要提供二进制数据,所有其他工作(特别是重定位和解决引用)由内核自身完成
2. delete_module: 从内核移除一个模块(前提是该模块的代码不再使用,并且其他模块也不再使用该模块导出的函数,否则强行卸载会造成内核地址引用错误PANIC)
3. reque_module(非系统调用): 用于从内核端加载模块,它不仅仅用于加载模块,还用于实现热插拔功能
0x1: 模块的表示
我们接下来讨论在内核中表示模块(及其属性),其中,module是最重要的数据结构,内核中驻留的每个模块,都分配了该结构的一个实例
\linux-2.6.32.63\include\linux\module.h
struct module
{
/*
state表示模块的当前状态
enum module_state
{
MODULE_STATE_LIVE, //正常运行
MODULE_STATE_COMING, //装载期间
MODULE_STATE_GOING, //正在移除
};
*/
enum module_state state;
/*
Member of list of modules, 用作模块链表的链表元素
list是一个标准的链表元素,在内核使用,将所有加载模块保存到一个双链表中,链表的表头定义为全局变量modules
*/
struct list_head list;
/*
Unique handle for this module 该模块的唯一句柄
name指定了模块的名称,该名称必须是唯一的,内核中会使用该名称来引用模块
*/
char name[MODULE_NAME_LEN];
/* Sysfs stuff. */
struct module_kobject mkobj;
struct module_attribute *modinfo_attrs;
const char *version;
const char *srcversion;
struct kobject *holders_dir;
/* Exported symbols 导出的符号 */
const struct kernel_symbol *syms;
//crcs也是num_syms个数组项的数组,存储了导出符号的校验和,用于实现版本控制
const unsigned long *crcs;
unsigned int num_syms;
/* Kernel parameters. */
struct kernel_param *kp;
unsigned int num_kp;
/* GPL-only exported symbols. 只适用于GPL的导出符号 */
unsigned int num_gpl_syms;
const struct kernel_symbol *gpl_syms;
const unsigned long *gpl_crcs;
#ifdef CONFIG_UNUSED_SYMBOLS
/* unused exported symbols. */
const struct kernel_symbol *unused_syms;
const unsigned long *unused_crcs;
unsigned int num_unused_syms;
/* GPL-only, unused exported symbols. */
unsigned int num_unused_gpl_syms;
const struct kernel_symbol *unused_gpl_syms;
const unsigned long *unused_gpl_crcs;
#endif
/* symbols that will be GPL-only in the near future. */
const struct kernel_symbol *gpl_future_syms;
const unsigned long *gpl_future_crcs;
unsigned int num_gpl_future_syms;
/* Exception table 异常表 */
unsigned int num_exentries;
struct exception_table_entry *extable;
/* Startup function. 初始化函数 */
int (*init)(void);
/*
模块的二进制数据分为连个部分
1. 初始化部分: 装载结束后都可以丢弃,例如初始化函数
2. 核心部分: 包含了正常运行期间需要的所有数据
*/
/* If this is non-NULL, vfree after init() returns 如果不是NULL,则在init()返回后调vfree释放 */
void *module_init;
/* Here is the actual code + data, vfree'd on unload. 这是实际的代码和数据,在卸载时调用vfree释放*/
void *module_core;
/* Here are the sizes of the init and core sections module_init、module_core两个内存区的长度 */
unsigned int init_size, core_size;
/* The size of the executable code in each section. 上述两个内存区中可执行代码的长度 */
unsigned int init_text_size, core_text_size;
/* Arch-specific module values 特定于体系结构的值 */
struct mod_arch_specific arch;
/*
same bits as kernel:tainted
如果模块会污染内核,则设置taints,污染意味着内核怀疑该模块做了一些有害的事情可能妨碍内核的正确运行,模块可能因为两个原因污染内核
1. 如果模块的许可证是专有的,或不兼容GPL(即没有声明GPL),那么在模块载入内核时,会使用TAINT_PROPRIETARY_MODULE
2. TAINT_FORCED_MODULE表示该模块是强制装载的,如果模块中没有提供版本信息,也称作版本魔数(version magic),或模块和内核某些符号的版本不一致,那么可以请求强制装载
*/
unsigned int taints;
#ifdef CONFIG_GENERIC_BUG
/* Support for BUG */
unsigned num_bugs;
struct list_head bug_list;
struct bug_entry *bug_table;
#endif
#ifdef CONFIG_KALLSYMS
/*
* We keep the symbol and string tables for kallsyms.
* The core_* fields below are temporary, loader-only (they
* could really be discarded after module init).
kallsyms的符号表和字符串表
KALLSYMS是一个配置选项(但只用于嵌入式系统,在普通计算机上总是启用的),启用该选项后,将在内存中建立一个列表,保存内核自身和加载模块中定义的所有符号(否则只存储导出的函数)
如果oops消息(内核检测到背离常规的行为,例如反引用NULL指针),不仅输出16进制数字(地址),还要输出涉及函数的名称,那么该选项就很有用
*/
Elf_Sym *symtab, *core_symtab;
unsigned int num_symtab, core_num_syms;
char *strtab, *core_strtab;
/* Section attributes 模块中各段的属性 */
struct module_sect_attrs *sect_attrs;
/* Notes attributes note属性 */
struct module_notes_attrs *notes_attrs;
#endif
/* Per-cpu data. per-cpu数据,它在模块装载时初始化 */
void *percpu;
/* The command line arguments (may be mangled). People like keeping pointers to this stuff */
char *args;
#ifdef CONFIG_TRACEPOINTS
struct tracepoint *tracepoints;
unsigned int num_tracepoints;
#endif
#ifdef CONFIG_TRACING
const char **trace_bprintk_fmt_start;
unsigned int num_trace_bprintk_fmt;
#endif
#ifdef CONFIG_EVENT_TRACING
struct ftrace_event_call *trace_events;
unsigned int num_trace_events;
#endif
#ifdef CONFIG_FTRACE_MCOUNT_RECORD
unsigned long *ftrace_callsites;
unsigned int num_ftrace_callsites;
#endif
#ifdef CONFIG_MODULE_UNLOAD
/*
What modules depend on me? 依赖当前模块的模块
modules_which_use_me用作一个链表元素,将模块连接到内核用于描述模块间依赖关系的数据结构中
*/
struct list_head modules_which_use_me;
/*
Who is waiting for us to be unloaded 等待当前模块卸载的进程
waiter是一个指针,指向导致模块卸载并且正在等待该操作结束的进程的task_struct实例
*/
struct task_struct *waiter;
/* Destruction function. 析构函数 */
void (*exit)(void);
/*
引用计数
系统中每个CPU,都对应到该数组中的一个数组项,该项指定了系统中有多少地方使用了该模块
*/
#ifdef CONFIG_SMP
char *refptr;
#else
local_t ref;
#endif
#endif
#ifdef CONFIG_CONSTRUCTORS
/* Constructor functions. */
ctor_fn_t *ctors;
unsigned int num_ctors;
#endif
};
0x2: 依赖关系和引用
如果模块B使用了模块A提供的函数,那么模块A和模块B之间就存在依赖关系,可以用两种不同的方式来看这种关系
1. 模块B依赖模块A,除非模块A已经驻留在内核内存,否则模块B无法装载
2. 模块B引用模块A,除非模块B已经移除,否则模块A无法从内核移除,即所有引用模块A的模块都已经从内核移除。在内核中,这种关系称之为模块B使用模块A
为了正确管理这些依赖关系,内核需要引入另一个数据结构
\linux-2.6.32.63\kernel\module.c
/* modules using other modules */
struct module_use
{
struct list_head list;
struct module *module_which_uses;
};
依赖关系的网络通过module_use、module->modules_which_usr_me成员共同建立起来
1. 对每个使用了模块A中的函数的模块B,都会创建一个module_use的新实例
/*
struct module_use
{
struct list_head list;
struct module *module_which_uses;
};
*/
2. 将新module_use实例的module_which_uses指针指向模块B的module实例
3. 该新module_use实例将添加到模块A的module实例中的modules_which_use_me链表
根据这些信息,内核很容易计算出使用特定模块的其他内核模块
如果试图装载一个模块,却因为依赖的模块不存在,而导致一部分未定义的符号无法解决,内核将返回错误码并放弃装载,需要明白的是,依赖关系的处理,需要由用户空间的modprobe来处理
内核提供了already_uses函数,来判断模块A是否需要另一个模块B
\linux-2.6.32.63\kernel\module.c
/* Does a already use b? */
static int already_uses(struct module *a, struct module *b)
{
struct module_use *use;
/*
如果模块A依赖模块B,则模块B的modules_which_use_me链表中必定至少有一个链表元素的module_which_uses成员指向了模块A的module实例的指针
*/
list_for_each_entry(use, &b->modules_which_use_me, list)
{
if (use->module_which_uses == a)
{
DEBUGP("%s uses %s!\n", a->name, b->name);
//如果找到一个匹配项,则依赖关系确实存在
return 1;
}
}
DEBUGP("%s does not use %s!\n", a->name, b->name);
return 0;
}
use_module用于建立模块A和模块B之间的关系,即模块A需要模块B才能正确运行
int use_module(struct module *a, struct module *b)
{
struct module_use *use;
int no_warn, err;
//already_uses首先检查该关系是否已经建立
if (b == NULL || already_uses(a, b)) return 1;
/*
If we're interrupted or time out, we fail.
将模块B的引用计数器加1,使之不能从内核移除,因为模块A坚决要求模块B驻留在内存中
static inline int strong_try_module_get(struct module *mod)
{
if (mod && mod->state == MODULE_STATE_COMING)
return -EBUSY;
if (try_module_get(mod))
return 0;
else
return -ENOENT;
}
*/
if (wait_event_interruptible_timeout( module_wq, (err = strong_try_module_get(b)) != -EBUSY, 30 * HZ) <= 0)
{
printk("%s: gave up waiting for init of module %s.\n",
a->name, b->name);
return 0;
}
/* If strong_try_module_get() returned a different error, we fail. */
if (err)
return 0;
DEBUGP("Allocating new usage for %s.\n", a->name);
use = kmalloc(sizeof(*use), GFP_ATOMIC);
if (!use)
{
printk("%s: out of memory loading\n", a->name);
module_put(b);
return 0;
}
use->module_which_uses = a;
list_add(&use->list, &b->modules_which_use_me);
no_warn = sysfs_create_link(b->holders_dir, &a->mkobj.kobj, a->name);
return 1;
}
EXPORT_SYMBOL_GPL(use_module);
对于内核模块的依赖的概念,我们需要明白本质包含的两种概念层次
1. 模块安装/卸载的依赖关系
模块A、模块B依次安装,模块B使用了模块A中的某个函数,则模块B依赖于模块A,模块A如果要卸载则必须等待模块B卸载完成之后才可以继续进行
2. 模块运行期间函数调用的引用依赖关系
模块A装载进内核内存,并导出了一些函数,用户态或内核态发起了对这个函数的调用,每次调用对增加了一次对这个模块A的引用计数,模块A如果想卸载必须等待这个引用计数降为0时才可以继续进行
0x3: 模块的二进制结构
模块使用ELF二进制结构,模块中包含了几个额外的段,普通的程序或库中不会出现,我们着重讨论这些额外的段
1. __ksymtab、__ksymtab_gpl、__ksymtab_gpl_future: 包含一个符号表,包括了模块导出的所有符号
2. __param: 存储了模块可接受的参数有关信息
3. .modinfo: 存储哎加载当前模块之间,内核中必须先行加载的所有其他模块的名称,即该特定模块所依赖的其他模块
4. .exit.text: 包含了在该模块从内核移除时,所需使用的代码(和可能的数据),该信息并未保存在普通代码段中,这样如果内核配置中未启用移除模块的选项,就不必将该段载入内存
5. .init_text: 初始化函数(和数据)使用一个独立的段,因为初始化完成后,相关的代码和数据就不再需要,因而可以从内存移除
6. .gnu_linkonce_this_module: 提供了struct module的一个实例,其中存储了模块的名称(name)、和指向二进制文件中的初始化函数和清理函数(init、cleanup)的指针,根据本段,内核即可判断特定的二进制文件是否为模块,如果没有该段,则拒绝装载文件
在模块自身和所依赖的所有其他内核模块都已经编译完成之前,上述的一些段是无法生成的,例如列出模块所有依赖关系的段,因为源代码中没有明确给出依赖关系信息,内核必须通过分析目标模块的未解决引用和所有其他模块导出的符号,来获取该信息
生成模块需要执行下述的几个步骤
1. 模块源代码中的所有C文件都编译为普通的.o目标文件
2. 在为所有模块产生目标文件后,内核可以分析它们,找到附加信息(例如模块依赖关系),保存在一个独立文件中,也编译为一个二进制文件
3. 将前述两个步骤产生的二进制文件链接起来,生成最终的模块
1. 初始化和清理函数
模块的初始化函数和清理函数,保存在.gnu.linkonce.module段中的module实例中
<init.h>中的module_init宏、module_exit宏用于定义init函数、exit函数,每个模块都包含上述两个宏定义的代码,用于定义init、exit函数
__init、__exit前缀有助于将这两个函数放置到二进制代码的正确的段中
\linux-2.6.32.63\include\linux\init.h
#define __init __section(.init.text) __cold notrace
#define __initdata __section(.init.data)
#define __initconst __section(.init.rodata)
#define __exitdata __section(.exit.data)
#define __exit_call __used __section(.exitcall.exit)
2. 导出符号
内核为导出符号提供了两个宏
1. EXPORT_SYMBOL: 用于一般的导出符号
2. EXPORT_SYMBOL_GPL: 只用于GPL兼容代码的导出符号
\linux-2.6.32.63\include\linux\module.h
/* For every exported symbol, place a struct in the __ksymtab section */
#define __EXPORT_SYMBOL(sym, sec) \
extern typeof(sym) sym; \
__CRC_SYMBOL(sym, sec) \
static const char __kstrtab_##sym[] \
__attribute__((section("__ksymtab_strings"), aligned(1))) \
= MODULE_SYMBOL_PREFIX #sym; \
static const struct kernel_symbol __ksymtab_##sym \
__used \
__attribute__((section("__ksymtab" sec), unused)) \
= { (unsigned long)&sym, __kstrtab_##sym }
#define EXPORT_SYMBOL(sym) \
__EXPORT_SYMBOL(sym, "")
#define EXPORT_SYMBOL_GPL(sym) \
__EXPORT_SYMBOL(sym, "_gpl")
#define EXPORT_SYMBOL_GPL_FUTURE(sym) \
__EXPORT_SYMBOL(sym, "_gpl_future")
3. 一般模块信息
模块的.modinfo段包含了一般信息,使用MODULE_INFO设置
/* Generic info of form tag = "info" */
#define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info)
这个段中并没有什么特别重要的元素,我们来重点关注"基本的版本控制"这个元素信息,".modinfo"段中总是会存储某些必不可少的版本控制信息,无论内核的版本控制特性是否启用,这使得可以从各种内核配置中区分出特别影响整个内核源代码的那些配置,这些可能需要一个单独的模块集合,在模块编译的第二阶段期间,下列代码会链接到每个模块中
module.mod.c
MODULE_INFO(vermagic, VERMAGIC_STRING);
//VERMAGIC_STRING是一个字符串,表示内核配置的关键特性
\linux-2.6.32.63\include\linux\vermagic.h
#define VERMAGIC_STRING \
UTS_RELEASE " " \
MODULE_VERMAGIC_SMP MODULE_VERMAGIC_PREEMPT \
MODULE_VERMAGIC_MODULE_UNLOAD MODULE_VERMAGIC_MODVERSIONS \
MODULE_ARCH_VERMAGIC
内核自身和每个模块中都会存储VERMAGIC_STRING的一份副本,因为不同处理器可用的特性可能相差很多,例如,如果模块编译时特意对Pentium4处理器进行优化,那么可能无法插入为Athlon处理器编译的内核中
0x4: 插入模块
init_module系统调用是用户空间和内核之间用于装载新模块的接口,关于init_module的相关知识,请参阅另一篇文章
//搜索:3. LKM模块加载原理
0x5: 移除模块
从内核移除模块比插入模块简单得多,关于delete_module的相关知识,请参阅另一篇文章
//搜索:4. LKM模块卸载原理
4. 自动化与热插拔
模块不仅可以根据用户指令或自动化脚本装载,还可以由内核自身请求装载,这种装载机制,在下面两种情况很有用处
1. 内核确认一个需要的功能当前不可用,例如,需要装载一个文件系统,但内核不支持,内核可以尝试加载所需的模块,然后重试装载文件系统
2. 一个新设备连接到可热插拔的总线(USB、FireWire、PCI等),内核检测到新设备并自动装载包含适当驱动程序的模块
//在这两种情况下,内核都依赖用户空间的实用程序,根据内核提供的信息,实用程序找到适当的模块并按惯例将其插入内核
0x1: kmod实现的自动加载
在内核发起的模块自动装载特性中,\linux-2.6.32.63\kernel\kmod.c中的__request_module是主要的函数,模块的名称(或一般占位符)需要传递给该函数。请求模块的操作必须显式建立在内核中,逻辑上一般出现在以下场合
1. 内核因为没有可用的驱动程序而导致分配特定的资源失败
2. 内核中此类场景有很多,例如IDE驱动程序在探测现存的设备时会尝试加载设备所需的驱动程序,为此必须直接指定所需驱动程序的模块名
0x2: 热插拔
在新设备连接到可热插拔的总线(或移除)时,内核再次借助用户空间应用程序来确保装载正确的驱动程序,与通常插入模块的过程相比,这里有必要执行几个额外的任务,例如
1. 根据设备标识字符串,找到正确的驱动程序
2. 进行一些配置工作
//这里使用/sbin/udevd完成这些工作
需要明白的是,内核不仅在设备插入与移除时会向用户空间提供消息,实际上内核在很多一般事件发生时,都会发送消息,例如
1. 在一个新硬盘连接到系统时,内核不仅提供有关该事件的信息,还发送通知,提供该设备上已经找到的分区信息
2. 设备模型的每部分都可以向用户层发送注册和撤销注册事件
//实际上内核可能发送的消息,组成饿了一个相当庞大和广泛的集合
我们通过一个具体的USB的例子来更好地说明这个问题,考虑一个USB存储棒附接到系统,但此时提供USB海量存储(mass storage)支持的模块尚未载入内核,系统想要自动地将设备装载到文件系统,以便用户可以立即访问它,为此,需要执行以下步骤
1. USB宿主机控制器在总线上检测到一个新设备并报告给其设备驱动程序,宿主机控制器分配一个新的device实例并调用usb_new_device注册它
2. usb_new_device触发对kobject_uevent的调用,该函数对所述对象kobject实例,调用其中注册的特定于子系统的事件通知程序
3. 对USB设备对象,usb_uevent用作通知函数,该函数准备一个消息,其中包含了所有必要的信息,使得udevd能够对新的USB海量存储设备的插入、作出适当反应
//udevd守护进程可以检查来源于内核的所有消息
5. 版本控制
不断改变的内核源代码对驱动程序的程序设计有一定影响,特别是只提供二进制代码的专有驱动程序,在实现新特性或修订总体设计时,通常必须修改内核各个部分之间的接口,以处理新的情况或支持性能和设计方面的改进。当然,开发者会尽可能将改动限制到驱动程序不直接使用的那些内部函数,但这并不能排除内核偶尔修改"公开的"接口,很显然,模块接口也会受到此类修改的影响。但是对于厂商发布、只提供二进制代码的驱动程序来说,情况会有所不同,用户不得不等待新驱动的开发和发布,这种情况会引起一整套问题,包括
1. 如果模块使用一个废弃的接口,不仅会损害模块的正常功能,而且系统很可能PANIC
2. SMP和单处理器系统的接口不同,需要两个二进制版本,如果装载了错误的版本同样可能导致系统崩溃
因此,引入了一个细粒度的方法,从而考虑到内核中各个例程的改变,我们无需考虑实际的模块和内核实现,需要考虑的问题是,如果模块要再不同的内核版本下运作,那么其调用的接口不能改变,所用的方法很简单,但却能很好地解决版本控制问题
0x1: 校验和方法
基本思想是使用函数或过程的参数,生成一个CRC校验和,该校验和是一个4字节数字,如果函数接口修改了,校验和也会发生变化,这使得内核能够推断出新版本已经不再兼容旧版本
1. 生成校验和
内核源代码附带的genksym工具在编译时自动创建,用于生成函数的校验和2. 将校验和编译到模块和内核中
内核必须将genksym提供的信息合并到模块的二进制代码中,供后续使用,CRC版本控制检测本质上是比较两个东西
1. 扫描当前模块中所使用的内核导出函数,这是从装载目标机器的内核中动态获取并计算的,当然这里面还要处理模块间依赖关系
2. 模块自身保存的一份CRC Magic数值,这是模块在编译的时候计算并保存的
0x2: 版本控制函数
我们知道,内核使用辅助函数check_version确定模块所需版本的符号是否与内核中可用符号的版本匹配
\linux-2.6.32.63\kernel\module.c
/*
1. sechdrs: 模块段头的一个指针
2. versindex: __version段的索引
3. symname: 将要处理符号的名称
4. mod: 指向模块数据结构的一个指针
5. crc: 指向内核提供的对应符号校验和的一个指针,该校验和在解析该符号时由__find_symbol动态提供
*/
static int check_version(Elf_Shdr *sechdrs,
unsigned int versindex,
const char *symname,
struct module *mod,
const unsigned long *crc,
const struct module *crc_owner)
{
unsigned int i, num_versions;
struct modversion_info *versions;
/*
Exporting module didn't supply crcs? OK, we're already tainted.
导出模块没有提供校验和,那么内核已经被污染了,函数直接返回1,这意味着版本检查已经成功,因为如果没有信息可用,那么检查也不会失败
*/
if (!crc)
return 1;
/* No versions at all? modprobe --force does this. */
if (versindex == 0)
return try_to_force_load(mod, symname) == 0;
versions = (void *) sechdrs[versindex].sh_addr;
num_versions = sechdrs[versindex].sh_size
/ sizeof(struct modversion_info);
/*
内核遍历该模块所引用的所有符号,从中搜索对应项,并比较模块中存储的校验和与内核返回的校验和
1. 如果两者匹配,则内核返回1
2. 否则发出一条警告信息,并且函数返回0
*/
for (i = 0; i < num_versions; i++)
{
if (strcmp(versions[i].name, symname) != 0)
continue;
if (versions[i].crc == maybe_relocated(*crc, crc_owner))
return 1;
DEBUGP("Found checksum %lX vs module %lX\n", maybe_relocated(*crc, crc_owner), versions[i].crc);
goto bad_version;
}
printk(KERN_WARNING "%s: no symbol version for %s\n", mod->name, symname);
return 0;
bad_version:
printk("%s: disagrees about version of symbol %s\n", mod->name, symname);
return 0;
}
Copyright (c) 2015 LittleHann All rights reserved