- 深入Linux设备驱动程序内核机制
- 陈学松
- 21051字
- 2020-08-28 15:47:46
1.3 模块的加载过程
在用户空间,用insmod这样的命令来向内核空间安装一个内核模块,本节将详细讨论模块加载时的内核行为。当调用“insmod demodev.ko”来安装demodev.ko这样的内核模块时, insmod会首先利用文件系统的接口将其数据读取到用户空间的一段内存中,然后通过系统调用sys_init_module让内核去处理模块加载的整个过程。
1.3.1 sys_init_module(第一部分)
sys_init_module的函数原型为:
long sys_init_module(void __user *umod, unsigned long len, const char __user *uargs);
其中,第一参数umod是指向用户空间demodev.ko文件映像数据的内存地址,第二参数len是该文件的数据大小,第三参数uargs是传给模块的参数在用户空间下的内存地址。
在sys_init_module函数中,加载模块的任务主要是通过调用load_module函数来完成的,该函数的定义为:
<kernel/module.c> static struct module *load_module(void __user *umod, unsigned long len, const char __user *uargs)
所有参数同sys_init_module函数中的完全一样,实际上在sys_init_module函数的一开始便会调用该函数,调用时传入的实参完全来自于sys_init_module函数,没有经过任何的处理或者修改。
为了更清楚地解释模块加载时的内核行为,我们把sys_init_module分为两个部分:第一部分是调用load_module,完成模块加载最核心的任务;第二部分是在模块被成功加载到系统之后的后续处理。我们将在讨论完load_module部分之后再继续讨论sys_init_module的第二部分。不过,在继续load_module话题之前,先要看一个内核中非常重要的数据结构——struct module。
1.3.2 struct module
load_module函数的返回值是一个struct module类型的指针,struct module是内核用来管理系统中加载的模块时使用的一个非常重要的数据结构,一个struct module对象代表着现实中一个内核模块在Linux系统中的抽象,该结构的定义如下(删除了一些trace和unused symbol相关的部分):
<include/linux/module.h> struct module { enum module_state state; /* Member of list of modules */ struct list_head list; /* Unique handle for this module */ 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; const unsigned long*crcs; unsigned int num_syms; /*Kernel parameters.*/ struct kernel_param*kp; unsigned int num_kp; /*GPL-only exported symbols.*/ unsigned int num_gpl_syms; const struct kernel_symbol*gpl_syms; const unsigned long*gpl_crcs; /*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); /*If this is non-NULL,vfree after init()returns*/ void*module_init; /*Here is the actual code+data,vfree'd on unload.*/ void*module_core; /*Here are the sizes of the init and core sections*/ unsigned int init_size,core_size; /*The size of the executable code in each section. */ unsigned int init_text_size,core_text_size; /* Size of RO sections of the module (text+rodata) */ unsigned int init_ro_size, core_ro_size; /* Arch-specific module values */ struct mod_arch_specific arch; unsigned int taints; /*same bits as kernel:tainted*/ #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). */ 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 */ struct module_notes_attrs *notes_attrs; #endif #ifdef CONFIG_SMP /* Per-cpu data. */ void __percpu *percpu; unsigned int percpu_size; #endif /*The command line arguments(may be mangled). People like keeping pointers to this stuff */ char *args; #ifdef CONFIG_MODULE_UNLOAD /* What modules depend on me? */ struct list_head source_list; /* What modules do I depend on? */ struct list_head target_list; /* Who is waiting for us to be unloaded */ struct task_struct *waiter; /* Destruction function. */ void (*exit)(void); struct module_ref { unsigned int incs; unsigned int decs; } __percpu *refptr; #endif #ifdef CONFIG_CONSTRUCTORS /* Constructor functions. */ ctor_fn_t *ctors; unsigned int num_ctors; #endif };
我们很快就会在后续的模块加载部分看到使用这些成员的具体代码,现在先把一些重要的成员变量简单描述如下:
enum module_state state
用于记录模块加载过程中不同阶段的状态,module_state的定义如下:
enum module_state { //模块被成功加载进系统时的状态 MODULE_STATE_LIVE, //模块正在加载中 MODULE_STATE_COMING, //模块正在卸载中 MODULE_STATE_GOING, }; struct list_head list
用来将模块链接到系统维护的内核模块链表中,内核用一个链表来管理系统中所有被成功加载的模块。
char name[MODULE_NAME_LEN]
模块名称。
const struct kernel_symbol *syms
内核模块导出的符号所在起始地址。
const unsigned long *crcs
内核模块导出符号的校验码所在起始地址。
struct kernel_param *kp
内核模块参数所在的起始地址。
int (*init)(void)
指向内核模块初始化函数的指针,在内核模块源码中由module_init宏指定。
struct list_head source_list struct list_head target_list
用来在内核模块间建立依赖关系。
1.3.3 load_module
作为内核模块加载器中最核心的函数,load_module负责最艰苦的模块加载全过程。我们将仔细讨论该函数,因为除了可以了解内核模块加载的幕后机制之外,还能了解到一些非常有趣的特性,诸如内核模块如何调用内核代码导出的函数,被加载的模块如何向系统中其他的模块导出自己的符号,以及模块如何接收外部的参数等。在介绍这部分内容时,如果完全按照内核代码的顺序依序进行的话,逻辑上可能会显得比较凌乱。所以此处文字组织的基本思路是:将load_module函数按照各主要功能分成若干部分,各部分在下文中的出现顺序尽可能维持在代码中的出现顺序;如果某些功能之间存在着某种依赖关系,比如有A和B两个功能,A功能的叙述需要用到B功能中提供的机制,则先介绍B功能;独立于功能模块之外的一些基础设施,比如某些功能性函数,则尽量往前放。
模块ELF静态的内存视图
如图1-2所示,用户空间程序insmod首先通过文件系统接口读取内核模块demodev.ko的文件数据,将其放在一块用户空间的存储区域中(图中void *umod所示)。然后通过系统调用sys_init_module进入到内核态,同时将umod指针作为参数传递过去(同时传入的还有umod所指向的空间大小len和存放有模块参数的地址空间指针uargs)。
图1-2 insmod构造的ELF静态内存视图
sys_init_module调用load_module,后者将在内核空间利用vmalloc分配一块大小同样为len的地址空间,如图1-2中Elf_Ehdr *hdr所示。然后通过copy_from_user函数的调用将用户空间的文件数据复制到内核空间中,从而在内核空间构造出demodev.ko的一个ELF静态的内存视图。接下来的操作都将以此视图为基础,为使叙述简单起见,我们称该视图为HDR视图(图1-2下方点画线椭圆部分)。HDR视图所占用的内存空间在load_module结束时通过vfree予以释放。
a字符串表(String Table)
字符串表是ELF文件中的一个section,用来保存ELF文件中各个section的名称或符号名,这些名称以字符串的形式存在。图1-3给出了一个具体的字符串表实例:
图1-3 字符串表
由图1-3可见,字符串表中各个字符串的构成和C语言中的字符串完全一样,都以'\0'作为一个字符串的结束标记。由index指向的字符串是从字符串表第index个字符开始,直到遇到一个'\0'标记,如果index处恰好是'\0',那么index指向的就是个空串(null string)。
在驱动模块所在的ELF文件中,一般有两个这样的字符串表section,一个用来保存各section名称的字符串,另一个用来保存符号表中每个符号名称的字符串。虽然同样都是字符串表section,但是得到这两个section的基地址的方法并不一样。
section名称字符串表的基地址为char *secstrings = (char *)hdr + entry[hdr->e_shstrndx].sh_offset。而获得符号名称字符串表的基地址则有点绕:首先要遍历Section header table中所有的entry,去找一个entry[i].sh_type = SHT_SYMTAB的entry, SHT_SYMTAB表明这个entry所对应的section是一符号表。这种情况下,entry[i].sh_link是符号名称字符串表section在Section header table中的索引值,换句话说,符号名称字符串表所在section的基地址为char *strtab = (char *)hdr + entry[entry[i].sh_link]. sh_offset。
如此,若想获得某一section的名称(假设该section在Section header table中的索引值是i),那么用secstrings + entry[i].sh_name即可。
至此,load_module函数通过以上计算获得了section名称字符串表的基地址secstrings和符号名称字符串表的基地址strtab,留作将来使用。
HDR视图的第一次改写
在获得了section名称字符串表的基地址secstrings和符号名称字符串表的基地址strtab之后,函数开始第一次遍历Section header table中的所有entry,将每个entry中的sh_addr改写为entry[i].sh_addr = (size_t)hdr + entry[i].offset,这样entry[i].sh_addr将指向该entry所对应的section在HDR视图中的实际存储地址。
在遍历过程中,如果发现CONFIG_MODULE_UNLOAD宏没有定义,表明系统不支持动态卸载一个模块,这样,对于名称为“.exit”的section,将来就没有必要把它加载到内存中,内核代码于是清除对应entry中sh_flags里面的SHF_ALLOC标志位。
相对于刚复制到内核空间的HDR视图,HDR视图的第一次改写只是在自身基础上修改了Section header table中的某些字段,其他方面没有任何变化。接下来在“HDR视图的第二次改写”一节中将会看到改写后的HDR视图会再次被改写,在那里,HDR视图中的绝大部分section会被搬移到一个新的内存空间中,那也是它们最终的内存位置。
find _ sec函数
内核用find_sec来寻找某一section在Section header table中的索引值,其函数原型为:
static unsigned int find_sec(Elf_Ehdr *hdr, Elf_Shdr *sechdrs, const char *secstrings, const char *name);
函数返回该section的索引值,如果没有找到对应的section,则返回0。该函数的前两个参数分别是ELF文件的ELF header和section header。因为函数要查找的是某一section的name,所以第三个参数就是前面提到的secstrings,第四个参数则是要查找的section的name。函数的具体实现过程非常简单:遍历Section header table中所有的entry(忽略没有SHF_ALLOC标志的section,因为这样的section最终不占有实际内存地址),对每一个entry,先找到其所对应的section name,然后和第四个参数进行比较,如果相等,就找到对应的section,返回该section在Section header table中的索引值。
在对HDR视图进行第一次改写之后,内核通过调用find_sec,分别查找以下名称的section:“.gnu.linkonce.this_module”,“__versions”和“.modinfo”。查找的索引值分别保存在变量modindex、versindex和infoindex中,以备将来使用。
struct module类型变量mod初始化
1.3.2节中提到了struct module是内核用来表示一个模块的非常重要的数据结构。在load_module函数中定义有一个struct module类型的变量mod,该变量的初始化是通过模块ELF文件中一个名为“.gnu.linkonce.this_module”的section来完成的。
ELF文件中出现的这个section其实是模块的编译工具链完成的,与设备驱动程序员无关。如果我们仔细看一下编译后的模块所在的目录,一定会发现一个扩展名为“.mod.c”的文件,打开该文件,会发现有如下定义:
struct module __this_module __attribute__((section(".gnu.linkonce.this_module"))) = { .name = KBUILD_MODNAME, .init = init_module, #ifdef CONFIG_MODULE_UNLOAD .exit = cleanup_module, #endif .arch = MODULE_ARCH_INIT, };
其中的__attribute__((section(".gnu.linkonce.this_module")))部分很清楚地揭示了内核模块ELF文件中“.gnu.linkonce.this_module”section出现的根源。
这段定义还有一个比较有趣的地方在于对struct module结构体中的init和exit成员变量的初始化:
.init = init_module .exit = cleanup_module
直觉告诉我们,这里的init和exit应该指向我们的驱动程序源码中定义的模块初始化和退出函数,然而经过和实际的设备驱动程序源码对比,有些读者也许会很失望,在驱动程序的源码中定义的初始化和退出函数并不是init_module和cleanup_module。这其实是拜module_init和module_exit宏所赐,它们利用了gcc提供的别名技术(__attribute__(alias))。
#define module_init(initfn) \ static inline initcall_t__inittest(void) \ {return initfn;} \ int init_module(void) __attribute__((alias(#initfn)));
该宏定义的核心是最后一句,它将init_module函数的别名设定为initfn,而后者正是我们在设备驱动程序中定义的模块初始化函数。总之,模块的构造工具链为我们安插了一个“.gnu.linkonce.this_module”section,并初始化了其中的一些成员。在模块加载过程中, load_module函数将利用这个section中的数据来初始化mod变量。
模块被加载到内存中之后,内核通过find_sec函数查找到“.gnu.linkonce.this_module”section在Section header table中所对应的索引值modindex,这样通过下面这行简单的代码就得到了“.gnu.linkonce.this_module”section在内存中的实际地址。
mod = (void *)sechdrs[modindex].sh_addr;
于是,在第一次改写的HDR视图的基础上,mod指针指向了实际的struct module所在的内存地址。接下来我们会看到,在HDR视图第二次被改写后,mod指针将会重新指向“.gnu.linkonce.this_module”section在内存中的最终地址。
HDR视图的第二次改写
在这次改写中,HDR视图中绝大多数的section会被搬移到新的内存空间中,之后会根据这些section新的内存地址再次改写图1-2中的HDR视图,使其中Section header table中各entry的sh_addr指向新的也是最终的内存地址。
在为那些需要移动的section分配新的内存空间地址之前,内核需要决定出HDR视图中哪些section需要移动,如果移动的话要移动到什么位置。内核代码中layout_sections函数用来做这件事,在layout_sections函数中,内核会遍历HDR视图中的每一个section,对每一个标记有SHF_ALLOC的section,将其划分到两大类section当中:CORE和INIT。
为了完成这种分类,layout_sections函数首先为标记了SHF_ALLOC的section定义了四种类型:code、read-only data、read-write data和small data。任何一个标记了SHF_ALLOC的section必定属于这四类中的一类。之后,对应每一个分类,函数都会遍历Section header table中的所有项,将section name不是以".init"开始的section划归为CORE section,并且修改HDR视图中Section header table中对应entry的sh_entsize,用以记录当前section在CORE section中的偏移量。
entry[i].sh_entsize = mod->core_size;
同时用struct module结构中的成员变量core_size记录下到当前正在操作的section为止CORE section的空间大小。
mod->core_size += entry[i].sh_size;
对于CORE section中的code section,内核用struct module结构中的core_text_size来记录。
对于INIT section的分类,和CORE section的划分基本一样,不同的地方在于属于INIT section的section,其name必须以".init"开始,内核用struct module结构中的成员变量init_size来记录当前INIT section空间的大小。
mod->init_size += entry[i].sh_size;
对于INIT section中的code section,内核用struct module结构中的init_text_size来记录。
在对section进行搬移之前,接下来会有个对符号表的处理,内核代码中通过调用layout_symtab函数来完成。Linux的内核源码中根据是否启用了内核配置选项CONFIG_KALLSYMS给出了layout_symtab函数的两种不同的定义。
如果没有启用CONFIG_KALLSYMS,那么layout_symtab函数就是个空函数,不做任何事情。CONFIG_KALLSYMS是一个决定内核映像中是否保留所有符号的配置选项,在内核配置文件Kconfig中,可以看到如下说明:
Say Y here to let the kernel print out symbolic crash information and symbolic stack backtraces. This increases the size of the kernel somewhat, as all symbols have to be loaded into the kernel image.
简言之,这是个为了方便系统调试而增加的选项,启用它的代价就是导致最终内核映像文件变大(当然占用的系统内存也会相应增加)。
在启用了CONFIG_KALLSYMS选项的Linux源码树基础上编译内核模块,会导致内核模块也会保留模块中的所有符号,这些符号都放在ELF符号表section中。由于在内核模块的ELF文件中,符号表所在的section没有SHF_ALLOC标志,所以上面提到的layout_sections函数不会把符号表section划到CORE section或者是INIT section中,这也是为什么要通过另外一个函数layout_symtab来把符号表搬移到CORE section内存区中的原因。
在对内核模块ELF文件中的section进行了CORE和INIT的划分之后,内核调用vmalloc相关的函数为CORE section和INIT section分配对应的内存空间,基地址分别记录在mod->module_core和mod->module_init中,然后把对应的section数据搬移到其在CORE section和INIT section内存空间的最终位置上。显然,在把各section搬移到其新的内存地址之后,内核需要改写HDR视图中的Section header table中对应entry的sh_addr,以使其指向新的地址。
注意,由于此时“.gnu.linkonce.this_module”section是一个带有SHF_ALLOC标志的可写数据section,也会被搬移到CORE section内存空间中,所以必须更新mod变量使之指向新的内存地址。
mod = (void *)entry[modindex].sh_addr;
这里之所以要对HDR视图中的某些section做这样的搬移,是因为在模块加载过程结束时,系统会释放掉HDR视图所在的内存区域,不仅如此,在模块初始化工作完成后,INIT section所在的内存区域也会被释放掉。由此可见,当一个模块被成功加载进系统,初始化工作完成之后,最终留下的仅仅是CORE section中的内容,因此CORE section中的数据应是模块在系统中整个存活期会使用到的数据。
如此处理之后,我们在图1-2的基础上得到了图1-4:
图1-4 模块加载时的section搬移
模块导出的符号
我们知道模块不仅可以使用内核或者其他模块导出的符号,而且可以向外部导出自己的符号,模块导出符号使用的宏和内核导出符号所使用的完全一样:EXPORT_SYMBOL、EXPORT_SYMBOL_GPL和EXPORT_SYMBOL_FUTURE。由1.2节对这些宏的代码分析可知,内核模块会把导出的符号分别放到“__ksymtab”、“__ksymtab_gpl”和“__ksymtab_gpl_future”section中。如果一个内核模块向外界导出了自己的符号,那么将由模块的编译工具链负责生成这些导出符号section,而且这些section都带有SHF_ALLOC标志,所以在模块加载过程中会被搬移到CORE section区域中。如果模块没有向外界导出任何符号,那么在模块的ELF文件中,将不会产生这些section。
显然,内核需要对模块导出的符号进行管理,以便在处理其他模块中那些“未解决的引用”符号时能够找到这些符号。内核对模块导出的符号的管理使用到了struct module结构中如下的成员变量:
struct module { … /* Exported symbols */ const struct kernel_symbol *syms; const unsigned long *crcs; unsigned int num_syms; /* GPL-only exported symbols. */ unsigned int num_gpl_syms; const struct kernel_symbol *gpl_syms; const unsigned long *gpl_crcs; /* 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; … }
在把HDR视图中的section搬移到最终的CORE section和INIT section之后,内核通过对HDR视图中Section header table的查找,获得“__ksymtab”、“__ksymtab_gpl”和“__ksymtab_gpl_future”section在CORE section中的地址,将其记录在mod->syms、mod->gpl_syms和mod->gpl_future_syms中,代码片段如下:
i = find_sec(…, "__ksymtab",…); mod->syms = (struct kernel_symbol *)entry[i].sh_addr; j = find_sec(…, "__ksymtab_gpl ",…); mod->syms_gpl = (struct kernel_symbol *)entry[i].sh_addr; k = find_sec(…, "__ksymtab_gpl_future ",…); mod->gpl_future_syms = (struct kernel_symbol *)entry[i].sh_addr;
如此,内核通过这些变量将可得到模块导出的符号的所有信息,如图1-5所示。读者将在接下来的“find_symbol函数”部分中看到这些变量的具体用途。
图1-5 内核模块导出的符号
find_symbol函数
在模块加载过程中,find_symbol是个非常重要的函数,顾名思义,它用来查找一个符号。该函数的原型如下:
const struct kernel_symbol *find_symbol(const char *name, struct module **owner, const unsigned long **crc, bool gplok, bool warn);
其中,第一个参数表示要查找的符号名,第二个参数用以表明符号可能所在的模块。
在深入到这个函数内部之前,有必要先介绍几个数据结构,这几个数据结构将在find_symbol函数中用到。
struct symsearch { const struct kernel_symbol *start, *stop; const unsigned long *crcs; enum { NOT_GPL_ONLY, GPL_ONLY, WILL_BE_GPL_ONLY, } licence; bool unused; };
struct symsearch用来对应要查找的每一个符号表section,换句话说,对要查找的每个符号表section,内核代码都要为之产生一个struct symsearch类型的实例。结构体中的成员变量start和stop分别指向对应section的开始和结束地址,bool型的unused成员用来表示内核是否配置了CONFIG_UNUSED_SYMBOLS选项,不过这个选项是“非主流”的,长远看这个选项最终会消失,因此本书只在这里提一下,在后续的章节中将忽略所有该选项被启用时才起作用的代码。另一个比较重要的成员是enum型的licence,GPL_ONLY表示符号只提供给满足GPL协议的模块使用,NOT_GPL_ONLY表示不一定要只给满足GPL协议的模块使用,WILL_BE_GPL_ONLY表示将来只提供给满足GPL协议的模块使用。再提醒一下,NOT_GPL_ONLY符号由EXPORT_SYMBOL负责导出,GPL_ONLY符号由EXPORT_SYMBOL_GPL负责导出,WILL_BE_GPL_ONLY符号由EXPORT_SYMBOL_GPL_FUTURE负责导出。
struct find_symbol_arg { /* Input */ const char *name; bool gplok; bool warn; /* Output */ struct module *owner; const unsigned long *crc; const struct kernel_symbol *sym; };
find_symbol_arg用做查找符号的标识参数,可以看到其大部分数据成员与find_symbol函数原型中的参数完全一致,其中的kernel_symbol是一个用以表示内核符号构成的数据结构,在前面的“EXPORT_SYMBOL的内核实现”一节中介绍过。
以下仔细分析find_symbol的功能,其源代码如下:
<kernel/module.c> const struct kernel_symbol *find_symbol(const char *name, struct module **owner, const unsigned long **crc, bool gplok, bool warn) { struct find_symbol_arg fsa; fsa.name = name; fsa.gplok = gplok; fsa.warn = warn; if (each_symbol(find_symbol_in_section, &fsa)) { if (owner) *owner = fsa.owner; if (crc) *crc = fsa.crc; return fsa.sym; } DEBUGP("Failed to find symbol %s\n", name); return NULL; }
函数首先构造被查找模块的标识参数fsa,然后通过each_symbol来查找符号。each_symbol是用来进行符号查找的主要函数,为节约篇幅起见,这里不再摘录其源代码,而是直接讲述其主要功能框架。
总体上,each_symbol函数可以分成两个部分:第一部分是在内核导出的符号表中查找对应的符号,如果找到,就通过fsa返回该符号的信息,否则,再进行第二部分的查找;第二部分是在系统中已加载的模块(系统中所有已成功加载的模块都以链表的形式保存在一个全局变量modules中)的导出符号表中查找对应的符号,如果找到就通过fsa返回该符号的信息,否则函数返回false。图1-6展示了find_symbol在查找一个符号时的搜索路径:
图1-6 find_symbol查找符号时的搜索路径
第一部分在对内核符号表进行查找时,首先构造一个struct symsearch类型的数组arr。
static const struct symsearch arr[] = { { __start___ksymtab, __stop___ksymtab, __start___kcrctab, NOT_GPL_ONLY, false }, { __start___ksymtab_gpl, __stop___ksymtab_gpl, __start___kcrctab_gpl, GPL_ONLY, false }, { __start___ksymtab_gpl_future, __stop___ksymtab_gpl_future, __start___kcrctab_gpl_future, WILL_BE_GPL_ONLY, false }, };
注意这里的__start___ksymtab、__start___kcrctab和__stop___ksymtab等变量已经在前面的“EXPORT_SYMBOL的内核实现”一节中交代过,它们在内核的链接脚本中定义,由链接器负责产生,由内核源码负责声明,现在到了使用它们的时候了。
接下来函数通过调用each_symbol_in_section查询内核的导出符号表, each_symbol_in_section的核心代码如下(经过适当改写):
<kernel/module.c> static bool each_symbol_in_section(const struct symsearch *arr,struct module *owner,void *fsa) { unsigned int i, j; for (j = 0; j < ARRAY_SIZE(arr); j++) { for (i = 0; i < arr[j].stop - arr[j].start; i++) if (find_symbol_in_section(&arr[j], owner, i, fsa)) return true; } return false; }
为了在内核的导出符号表中查找某一指定的符号名,each_symbol_in_section函数使用了两层for循环:外层j引导的for循环用来遍历符号可能所在的内核导出符号表中的各section;内层i引导的for循环用来遍历外层for循环所指定的section中的每个struct kernel_symbol类型的元素。对于每个kernel_symbol,都会调用find_symbol_in_section函数。
为了清楚地理解内核加载模块时如何处理“未解决的引用”符号,有必要仔细分析一下find_symbol_in_section函数的主要功能。因为对Linux下的设备驱动程序员而言,几乎每天都在和这个功能打交道,清楚地理解其内核机制,将来一旦在加载模块时出现相关问题,也可以将其快速定位并最终解决。另外,对于带有“_GPL”后缀的符号名,在写驱动程序的内核模块时常常会遇到,然而其背后到底蕴涵着怎样的设计理念呢?通过分析find_symbol_in_section函数,就可以得到所需的答案。
find_symbol_in_section函数的完整源代码如下: <kernel/module.c> static bool find_symbol_in_section(const struct symsearch *syms, struct module *owner, unsigned int symnum, void *data) { struct find_symbol_arg *fsa = data; if (strcmp(syms->start[symnum].name, fsa->name) != 0) return false; if (!fsa->gplok) { if (syms->licence == GPL_ONLY) return false; if (syms->licence == WILL_BE_GPL_ONLY && fsa->warn) { printk(KERN_WARNING "Symbol %s is being used " "by a non-GPL module, which will not "
"be allowed in the future\n", fsa->name); printk(KERN_WARNING "Please see the file " "Documentation/feature-removal-schedule.txt " "in the kernel source tree for more details.\n"); } } fsa->owner = owner; fsa->crc = symversion(syms->crcs, symnum); fsa->sym = &syms->start[symnum]; return true; }
函数首先用strcmp函数来比较kernel_symbol结构体中的name与fsa中的name(正在查找的符号名,即要加载的内核模块中出现的“未解决的引用”的符号)是否匹配,如果不匹配,那么函数直接返回false。
fsa->gplok和fsa->warn的设定最早是在find_symbol函数中,是通过后者的函数参数传入的。fsa->warn主要用来控制警告信息的输出。fsa->gplok用来表示当前的模块是不是满足GPL协议(GPL module或non-GPL module),fsa->gplok = true表明这是个GPL module,否则就是non-GPL module。内核判断一个模块是否GPL兼容,要使用到本章后面的“模块的信息”部分中的内容。
对于一个non-GPL module而言,它不能使用内核导出的属于GPL_ONLY的那些符号,所以即使要查找的符号匹配上一个属于GPL_ONLY的符号,也不能认为查找成功。但是如果要查找的符号匹配上一个属于WILL_BE_GPL_ONLY的符号,因为这个导出的符号“将要成为GPL_ONLY”,所以即使现在还不是GPL_ONLY,查找姑且算是成功的,不过即便如此,内核对模块将来对该符号的成功使用没有保障,所以应该给出一个警告信息。对于一个GPL module而言,一切好说,可以使用内核导出的所有符号。
函数如果成功查找到符号,利用传进来的data指针将符号相关信息传给上层调用的函数。
至此,find_symbol的第一部分,即在内核导出的符号表中查找指定的符号已经结束。如果指定的符号没有出现在内核导出的符号表中,那么将进入find_symbol函数的第二部分。
下面开始介绍find_symbol的第二部分,在系统已经加载的模块导出的符号表中查找符号。内核为达成此目的,需要在加载一个内核模块时完成下面两件事。
第一,模块成功加载进系统之后,需要将表示该模块的struct module类型变量mod加入到modules中,后者是一个全局的链表变量,用来记录系统中所有已加载的模块。
<kernel/module.c> static LIST_HEAD(modules); list_add_rcu(&mod->list, &modules);
第二,模块导出的符号信息记录在mod的相关成员变量中,这个过程的详细描述参见本章前面的“模块导出的符号”部分。
each_symbol用来在系统所有已加载的模块导出的符号中查找某一指定符号,其核心代码片段如下:
if (each_symbol_in_section(arr, ARRAY_SIZE(arr), NULL, fn, data)) return true; list_for_each_entry_rcu(mod, &modules, list) { struct symsearch arr[] = { { mod->syms, mod->syms + mod->num_syms, mod->crcs, NOT_GPL_ONLY, false }, { mod->gpl_syms, mod->gpl_syms + mod->num_gpl_syms, mod->gpl_crcs, GPL_ONLY, false }, { mod->gpl_future_syms, mod->gpl_future_syms + mod->num_gpl_future_syms, mod->gpl_future_crcs, WILL_BE_GPL_ONLY, false }, }; if (each_symbol_in_section(arr, ARRAY_SIZE(arr), mod, fn, data)) return true; }
相对于find_symbol的第一部分(在内核导出的符号表中查找某一符号),第二部分唯一的区别在于构造的arr数组。函数在全局链表modules中遍历所有已加载的内核模块,对其中的每一模块都构造一个新的arr数组,然后在其中查找特定的符号。
对“未解决的引用”符号(unresolved symbol)的处理
前文中已多次提到内核模块ELF文件中的“未解决的引用”符号,所谓的“未解决的引用”符号,就是模块的编译工具链在对模块进行链接生成最终的.ko文件时,对于模块中调用的一些函数,最简单的比如printk函数,链接工具无法在该模块的所有目标文件中找到这个函数的具体指令码(因为这个函数是在Linux的内核源代码中实现的,其指令码存在于编译内核生成的目标文件中,模块的链接工具显然不会也不应该去查找内核的目标文件),所以就会将这个符号标记为“未解决的引用”,对它的处理将一直延续到内核模块被加载时(处理的核心是在内核或者是其他内核模块导出的符号中找到这个“未解决的引用”符号,继而找到该符号所在的内存地址,从而最终形成正确的函数调用)。
Linux内核中,一个名为simplify_symbols的函数用来实现这一功能,这是个很有意思的函数,我们不妨仔细看一下它的代码。
<kernel/module.c> /* Change all symbols so that st_value encodes the pointer directly. */ static int simplify_symbols(struct module *mod, const struct load_info *info) { Elf_Shdr *symsec = &info->sechdrs[info->index.sym]; Elf_Sym *sym = (void *)symsec->sh_addr; unsigned long secbase; unsigned int i; int ret = 0; const struct kernel_symbol *ksym; for (i = 1; i < symsec->sh_size / sizeof(Elf_Sym); i++) { const char *name = info->strtab + sym[i].st_name; switch (sym[i].st_shndx) { case SHN_COMMON: /*We compiled with-fno-common. These are not supposed to happen. */ DEBUGP("Common symbol: %s\n", name); printk("%s: please compile with -fno-common\n", mod->name); ret = -ENOEXEC; break; case SHN_ABS: /* Don't need to do anything */ DEBUGP("Absolute symbol: 0x%08lx\n", (long)sym[i].st_value); break; case SHN_UNDEF: ksym = resolve_symbol_wait(mod, info, name); /*Ok if resolved. */ if (ksym && !IS_ERR(ksym)) { sym[i].st_value = ksym->value; break; } /*Ok if weak. */ if (!ksym && ELF_ST_BIND(sym[i].st_info) == STB_WEAK) break; printk(KERN_WARNING "%s: Unknown symbol %s (err %li)\n", mod->name, name, PTR_ERR(ksym)); ret = PTR_ERR(ksym) ?: -ENOENT; break; default: /* Divert to percpu allocation if a percpu var. */ if (sym[i].st_shndx == info->index.pcpu) secbase = (unsigned long)mod_percpu(mod); else secbase = info->sechdrs[sym[i].st_shndx].sh_addr; sym[i].st_value += secbase; break; } } return ret; }
简言之,在加载模块的过程中,simplify_symbols函数用来为当前正在加载的模块中所有“未解决的引用”符号产生正确的目标地址。对这段代码的透彻理解需要读者熟悉ELF文件格式规范的相关概念,我们不可能在本书中全面介绍ELF文件格式,但是为了让读者能理解上面的代码,还是从代码的角度出发,将其中所涉及的一些有关ELF文件的概念予以简单介绍。
代码中的Elf_Sym定义的是符号表中的元素,具体定义如下:
struct Elf_Sym { Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; unsigned char st_info; unsigned char st_other; Elf32_Half st_shndx; };
其中,st_name是符号名在符号名称字符串表中的索引值,详见本章前面的“字符串表(String Table)”部分。st_value是符号所在的内存地址。simplify_symbols函数的唯一功能就是在加载模块时重新生成正确的st_value值。st_shndx是该符号所在的section在Section header table中的索引值。但是该值还有一些特殊的定义。对于符号表,它是ELF文件中的一个section,这个section就是由一系列struct Elf_Sym型元素所构成的一个数组,每个元素代码一个符号。
在对符号表的概念有了基本了解之后,回过头来看看simplify_symbols函数的代码实现。函数首先通过一个for循环遍历符号表中的所有符号,对于每一个符号都会根据该符号的st_shndx值分情况进行处理。前面刚刚提到st_shndx,通常情况下,该值表示符号所在section的索引值,为方便叙述,我们称这种符号为一般符号。对于一般符号来说,它的st_value在ELF文件中的值是从其所在section起始处算起的一个偏移量,代码中在switch的default分支下进行处理:先得到符号所在section的最终内存地址,然后加上它在section中的偏移量,这样就得到了符号的最终内存地址。
除了一般符号,还有些符号的st_shndx具有特殊的含义,典型的如SHN_ABS和SHN_UNDEF,前者表明该符号具有绝对地址,因此simplify_symbols函数无须对这种情况予以任何处理,后者表明该符号是一“undefined symbol”,其实就是我们一直说的“未解决的引用”符号。这种情况下simplify_symbols函数会调用resolve_symbol函数来处理该未定义符号,后者会调用find_symbol函数去查找该符号(详细的查找过程见本章前面的“find_symbol函数”部分),如果找到了,就把它在内存中的实际地址赋值给st_value。
如此,经过simplify_symbols函数的调用之后,内核模块符号表中的所有符号就都有了正确的st_value值,也即都有了正确的内存地址。
到目前为止,一切关于符号相关的处理貌似都很完美,然而情况真是如此吗?如果当前正在加载的模块中一个“未解决的引用”符号是由别的内核模块导出的,情况会怎样呢?如果读者的探索精神足够强烈,想想那些由内核模块导出的符号吧。由前面的内容可知,“__ksymtab”、“__ksymtab_gpl”和“__ksymtab_gpl_future”section都被搬移到了最终的内存地址处,而且这些地址也被表示模块的mod变量记录在案,但是这些section中的内容呢?
回头看看图1-5“内核模块导出的符号”,每个“__ksymtab”、“__ksymtab_gpl”和“__ksymtab_gpl_future”section都是由struct kernel_symbol类型的元素所构成的数组。到目前为止,如果仔细考察每个元素的话,会发现其中的value成员依然是内核模块在静态编译时产生的地址。换句话说,根本不是这些符号在模块被加载进系统之后在内存中的实际地址。这显然不是我们想要的效果:想想本节前半部分提到的对模块中“未解决的引用”符号的处理,如果在别的模块中找到的符号其内存地址只是当初该模块在静态链接时填入的地址,那么对该符号的引用必然导致错误的内存访问。这是个很严重的问题。而Linux内核对这一问题的处理便引出了下一部分的内容——重定位。
重定位(relocation)
重定位主要用来解决静态链接时的符号引用与动态加载时实际符号地址不一致的问题,上节结束部分提到的模块导出的符号地址,就是一个典型的需要重定位的例子。仔细讨论重定位的内容不是件简单的事情,因为重定位的任务包含很多方面的内容,尤其是跟体系架构相关的一些微妙晦涩的技术细节。考虑到本书的主题定位,也许在这里详细讲述重定位的技术细节并不是件很有价值的事情:篇幅把握得不够理想,很可能就冲淡了本章关于内核模块加载这一主线。消耗大量的篇幅和读者大量的时间,所涉及的主题在现实中却难有用武之地。但是重定位毕竟是内核加载过程中一个很重要的步骤,仔细权衡之下,笔者决定采取一个相对折中的方案,在讨论重定位时就事论事。本节就以上节末尾提出的问题来展开重定位的话题。
如果模块有用EXPORT_SYMBOL导出的符号,那么模块的编译工具链会为这个模块的ELF文件生成一个独立的特殊section:“.rel__ksymtab”,它专门用于对“__ksymtab”section的重定位,称为relocation section。这个section是由下面的数据结构元素形成的一个数组。
typedef struct elf32_rel { Elf32_Addr r_offset; Elf32_Word r_info; } Elf32_Rel;
先来看看Linux源码中用于内核模块加载时重定位的代码:
<kernel/module.c> static int apply_relocations(struct module *mod, const struct load_info *info) { unsigned int i; int err = 0; /* Now do relocations. */ for (i = 1; i < info->hdr->e_shnum; i++) { unsigned int infosec = info->sechdrs[i].sh_info; /* Not a valid relocation section? */ if (infosec >= info->hdr->e_shnum) continue; /* Don't bother with non-allocated sections */ if (!(info->sechdrs[infosec].sh_flags & SHF_ALLOC)) continue; if (info->sechdrs[i].sh_type == SHT_REL) err = apply_relocate(info->sechdrs, info->strtab, info->index.sym, i, mod); else if (info->sechdrs[i].sh_type == SHT_RELA) err = apply_relocate_add(info->sechdrs, info->strtab, info->index.sym, i, mod); if (err < 0) break; } return err; }
代码用一个for循环来遍历HDR视图中Section header table中所有的entry。对于一个重定位的section,其entry中的sh_type的值为SHT_REL或者SHT_RELA,分别对应两种不同的重定位方式,我们拿第一种类型SHT_REL来说事。对于sh_type = SHT_REL的section而言,其Section header中的sh_info成员指明了被重定位的section在Section header table中的索引值,代码中用info变量来表示。
在遍历的过程中,如果发现了一个sh_type = SHT_REL的section,系统就调用apply_relocate函数来执行重定位,后者是个体系结构相关的函数。总体上,该函数对模块导出符号的重定位原理是,根据重定位元素中的r_offset以及relocation section header entry中的sh_info得到需要修改的导出符号struct kernel_symbol中value所在的内存地址:
Elf32_Rel*rel=(void*)entry[i].sh_addr; //entry[i]对应当前正在处理的relocation section int ksymtabidx = entry[i].sh_info; Elf32_Shdr * ksymtabsec = &entry[ksymtabidx]; unsigned long location = ksymtabsec->sh_addr + rel->r_offset;
然后根据重定位元素中的r_info获得需要定位的符号在符号表中的偏移量:
offset = ELF32_R_SYM(rel->r_info);
因为符号表section的基地址很容易获得,于是就可以获得需要重定位的符号在符号表中对应的Elf32_Sym型元素:
sym=((Elf32_Sym*)symsec->sh_addr)+offset; //symsec->sh_addr为符号表section基地址
所以,最终导出符号的地址被修改。
这一过程简单地说,就是根据导出符号所在section的relocation section,结合导出符号表section,修改导出符号的地址为在内存中最终的地址值。如此,内核模块导出符号的地址在系统执行完重定位之后被更新为正确的值。
模块参数
内核模块在用insmod命令加载时,可以通过诸如以下的命令向模块传递一些参数:
insmod demodev.ko dolphin=10 bobcat=5
其中dolphin=10和bobcat=5就是向模块传递的参数,dolphin和bobcat是参数名,10和5是具体的参数值。当然为了能正确接收外部的参数,内核模块本身在源代码中必须用module_param宏声明模块可以接收的参数。在上面的例子中,模块应该使用诸如module_param(dolphin, int, 0)来声明一个模块参数,例如下面的代码片段:
<demodev.c> #include <linux/module.h> #include <linux/kernel.h> int dolphin; int bobcat; module_param(dolphin, int, 0); module_param(bobcat, int, 0); static int demodev_init(void) { printk("dolphin=%d,bobcat=%d\n", dolphin, bobcat); return 0; } static void demodev_exit(void) { printk("+demodev_exit!\n"); } module_init(demodev_init); module_exit(demodev_exit);
从本章稍后的讨论中可以得知,内核模块加载器对模块参数的构造(初始化)过程发生在对模块初始化函数demodev_init的调用之前,所以在demodev_init函数被调用时,已经可以得到从命令行传过来的实际参数。
Linux源码中module_param宏相关的完整定义如下:
<include/linux/moduleparam.h> #define __module_param_call(prefix, name, ops, arg, isbool, perm) \ /*Default value instead of permissions?*/ \ static int__param_perm_check_##name__attribute__((unused))= \ BUILD_BUG_ON_ZERO((perm)<0||(perm)>0777||((perm)&2)) \ +BUILD_BUG_ON_ZERO(sizeof(""prefix)>MAX_PARAM_PREFIX_LEN); \ static const char__param_str_##name[]=prefix#name; \ static struct kernel_param __moduleparam_const __param_##name \ __used \ __attribute__ ((unused,__section__ ("__param"),aligned(sizeof(void *)))) \ ={__param_str_##name,ops,perm,isbool?KPARAM_ISBOOL:0, \ { arg } } #define module_param_cb(name,ops,arg,perm) \ __module_param_call(MODULE_PARAM_PREFIX, \ name, ops, arg, __same_type((arg), bool *), perm) #define__MODULE_PARM_TYPE(name,_type) \ __MODULE_INFO(parmtype, name##type, #name ":" _type) #define module_param_named(name,value,type,perm) \ param_check_##type(name,&(value)); \ module_param_cb(name,¶m_ops_##type,&value,perm); \ __MODULE_PARM_TYPE(name, #type) #define module_param(name,type,perm) \ module_param_named(name, name, type, perm)
基本上,上述的宏系列会在一个名为“__param”的section中定义一些变量。这段宏的定义细究起来可能稍嫌晦涩。这里不妨以本节开始的例子来展开该宏(除去一些跟调试相关的微末细节),以便使读者对module_param宏有一具体的印象,module_param(dolphin, int, 0)展开后如下:
param_check_int(dolphin, &( dolphin)); static int__param_perm_check_dolphin __attribute__((unused))= \ static const char__param_str_dolphin[]="dolphin"; \ static struct kernel_param __moduleparam_const __param_dolphin \ __used \ __attribute__ ((unused,__section__ ("__param"),aligned(sizeof(void *)))) \ ={__param_str_dolphin,¶m_ops_int,0,0, \ { &dolphin } }
可见module_param(dolphin, int, 0)在“__param”section中定义了一个类型为struct kernel_param的静态常量。struct kernel_param的定义如下:
<include/linux/moduleparam.h> struct kernel_param { const char *name; const struct kernel_param_ops *ops; u16 perm; u16 flags; union { void *arg; const struct kparam_string *str; const struct kparam_array *arr; }; };
其中name为参数名,perm为对sysfs文件系统中模块参数的访问许可,定义在结构体struct kernel_param_ops对象ops中的成员函数(set和get)用来在模块mod的args成员和模块的参数section间拷贝数据,最后的union为指向参数的指针。
__used和unused主要用来避免编译器产生警告信息,因为此处声明的__param_dolphin变量在模块源码的其他部分并不会被使用。
param_check_int宏用来检测变量dolphin在module_param宏之前是否定义,因为struct kernel_param中的union联合体只是用来放置模块使用的参数所在地址,如果之前该参数没有定义,就不可能生成&dolphin的值。所以我们的内核模块示例源代码demodev.c在用module_param声明模块参数之前,要首先定义出这些参数。
int dolphin; //首先定义 module_param(dolphin, int, 0); //然后再声明模块参数
如果在设备驱动程序中忘了先定义参数变量dolphin,只用module_param(dolphin, int, 0)来声明模块参数,将会得到如下编译错误:
root@AMDLinuxFGL:/home/dennis/Linux/book/chap01# make make -C/lib/modules/2.6.39/buildM=/home/dennis/Linux/book/chap01modules make[1]: Entering directory `/home/dennis/Linux/kernel/linux-2.6.39' CC[M] /home/dennis/Linux/book/chap01/demodev.o /home/dennis/Linux/book/chap01/demodev.c: In function '__check_dolphin': /home/dennis/Linux/book/chap01/demodev.c:5: error: 'dolphin' undeclared (first use in this function) /home/dennis/Linux/book/chap01/demodev.c:5: error: (Each undeclared identifier is reported only once /home/dennis/Linux/book/chap01/demodev.c:5: error: for each function it appears in.) /home/dennis/Linux/book/chap01/demodev.c: At top level: /home/dennis/Linux/book/chap01/demodev.c:5: error: 'dolphin' undeclared here (not in a function) /home/dennis/Linux/book/chap01/demodev.c:5: warning: type defaults to 'int' in declaration of 'type name' make[2]: *** [/home/dennis/Linux/book/chap01/demodev.o] Error 1 make[1]: *** [_module_/home/dennis/Linux/book/chap01] Error 2 make[1]: Leaving directory `/home/dennis/Linux/kernel/linux-2.6.39' make: *** [default] Error 2
在上面的宏展开的实例中,可以看到指向参数的指针值被设定为“{ &dolphin }”,在模块静态链接期间,&dolphin指令不可能生成其最终的运行期地址,因此模块参数所在的“__param”section需要有一个对应的relocation section“.rel__param”,用来完成对参数指针的重定位,这样才能把命令行中的参数值正确复制到模块的“__param”section中。
除了module_param之外,Linux系统下还有另外两个宏module_param_array和module_param_string,分别用来设定数组型和字符串型参数,本书不再赘述。
下面讨论“insmod demodev.ko dolphin=10 bobcat=5”中携带的参数值如何为模块所用,不看代码也应该可以猜想出命令行中的参数值应该会被复制到模块的参数中,这样模块在开始使用参数dolphin之前,其值已经被insmod命令行中的实际值所改写。图1-7展示了命令行参数传递到模块的“__param”section的全过程:
图1-7 内核模块参数传递过程示意图
在实际的内核源代码中,sys_init_module函数的最后一个参数const char __user *uargs清楚地表明这是由用户空间传递过来的放置模块参数的内存地址,在insmod一个模块时所携带的参数将以字符串的形式向内核空间传递。然后在load_module函数中,通过strndup_user的调用将用户空间的模块参数复制到内核空间。
args = strndup_user(uargs, ~0UL >> 1);
strndup_user函数内部会调用kmalloc为在内核空间保存模块参数字符串分配一段内存区域,然后通过copy_from_user将模块参数从用户空间复制到内核空间。
接着,在HDR视图的section被搬移到CORE和INIT section之后,load_module通过下面的section_objs函数调用取得“__param”section在内存空间的最终地址,并记录在struct module的struct kernel_param *kp成员变量中。
mod->kp = section_objs(hdr, sechdrs, secstrings, "__param", sizeof(*mod->kp), &mod->num_kp);
mod->num_kp为“__param”section中struct kernel_param对象的个数。此后mod->args参数也将指向内核空间中保留参数字符串的内存区域。
最后调用parse_args函数将mod->args中的参数值复制到“__param”section中对应的参数。
parse_args(mod->name, mod->args, mod->kp, mod->num_kp, NULL);
parse_args函数的主要流程是,针对命令行中出现的每一个参数,用其参数名与“__param”section中出现的每一个struct kernel_param对象的name成员进行匹配,如果匹配成功即认为找到了对应的参数,然后通过struct kernel_param中的set函数指针将参数值复制到struct kernel_param中arg、str或者arr所指向的地址空间。对于本节开始的例子,set指针指向param_set_int函数,后者在Linux内核源码中由STANDARD_PARAM_DEF宏(kernel/params.c)负责定义,展开后的param_set_int如下:
int param_set_int(const char *val, struct kernel_param *kp) { long l; int ret; if (!val) return -EINVAL; ret = strict_strtol (val, 0, &l); if (ret == -EINVAL || ((int)l != l)) return -EINVAL; *((int*)kp->arg)=l; //将命令行中的参数值复制到“__param”section的kp中 return 0; }
param_set_int函数调用之后,模块“__param”section中“dolphin”和“bobcat”所对应的struct kernel_param对象中的arg成员将被赋值为10和5,也就是在insmod命令行中传入的模块实际参数值。这之后,内核模块将可以通过自身的dolphin和bobcat变量引用到这些传入的参数值。
如果将这一过程简单总结一下的话,应该是在把命令行的参数值复制到模块的参数这个过程中,module_param宏所定义的“__param”section起了桥梁的作用,通过“__param”section,内核可以找到模块中定义的参数所在的内存地址,继而可以用命令行中的参数值改写之。因为内核模块加载器在解析命令行参数时,对命令行中参数的构成格式有严格的要求,在参数名与参数值之间只能用“=”,且不能有空格,如果把前面的命令行写成如下形式(“dolphin=”和“10”之间有个空格):
insmod demodev.ko dolphin= 10 bobcat=5
那么将会得到如下信息:
insmod: error inserting 'demodev.ko': -1 Unknown symbol in module
dmesg中针对这个错误的输出如下:
[262282.558333] demodev: Unknown parameter `dolphin'
至此,我们已经理解了内核模块的参数机制,包括模块源码中如何声明参数,内核模块加载时如何把命令行中的参数复制到模块中等。
模块间的依赖关系
实际运行的系统中并不是只加载一个模块,模块可以随时添加进系统,也可以随时被卸载。这些内核模块之间并不是完全相对独立的,比如当一个模块引用到另一个模块中导出的符号时,这两个模块间就建立了依赖关系。因此依赖关系只存在于模块与模块之间,模块与内核之间不构成依赖关系,因为在模块生存期间我们不可能去卸载内核,当然内核也不可能引用到模块导出的符号。内核必须能跟踪模块间的这种依赖关系,只有这样,如果由于存在依赖关系卸载一个模块有可能影响到系统的稳定性,内核才可能采取必要的措施防止这种情况发生。
内核用struct module数据结构中定义的如下成员变量来跟踪模块间的这种依赖关系:
<include/linux/module.h> #ifdef CONFIG_MODULE_UNLOAD /* What modules depend on me? */ struct list_head source_list; /* What modules do I depend on? */ struct list_head target_list; #endif
其中的struct list_head source_list和struct list_head target_list用来构建有依赖关系模块的链表,对该链表的使用要结合数据结构struct module_use:
<include/linux/module.h> struct module_use { struct list_head source_list; struct list_head target_list; struct module *source, *target; };
显然,模块间的这种依赖关系只有当模块能够被卸载时才有可能出现问题,对一个没有启用CONFIG_MODULE_UNLOAD宏的系统而言,因为模块被禁止卸载,因此内核无须对依赖关系做出处理。
模块的依赖关系的建立最早发生在当前模块对象mod被加载时,模块加载函数调用resolve_symbol函数来解决其中一些“未解决的引用”符号。如果成功地在其他模块导出的符号中找到了指定的符号,那么resolve_symbol函数会将导出这一“未解决的引用”符号的模块记录在一个变量struct module *owner中,然后调用ref_module(mod, owner)在模块mod和owner之间建立依赖关系。ref_module函数在做一些必要的安全性检查之后调用add_module_usage(mod, owner)在mod和owner模块间建立依赖关系,add_module_usage函数的定义如下:
<kernel/module.c> static int add_module_usage(struct module *a, struct module *b) { struct module_use *use; DEBUGP("Allocating new usage for %s.\n", a->name); use = kmalloc(sizeof(*use), GFP_ATOMIC); if (!use) { printk(KERN_WARNING "%s: out of memory loading\n", a->name); return -ENOMEM; } use->source = a; use->target = b; list_add(&use->source_list, &b->source_list); list_add(&use->target_list, &a->target_list); return 0; }
函数首先调用kmalloc分配一struct module_use型内存空间use,然后将use中的source指向mod模块,target指向owner模块,同时将use的target_list加入mod中的target_list指向的双向链表,将use的source_list加入owner中的source_list指向的双向链表。图1-8展示了通过struct module_use对象在三个模块间建立依赖关系的技术细节:
图1-8 mod_A和mod_B模块依赖于owner模块
图1-8中,模块mod_A和mod_B均依赖于模块owner,mod_A和mod_B之间则没有依赖关系。owner模块先加入系统,它导出一个函数为模块mod_A和mod_B所使用。图中显示mod_A先于mod_B加入系统,在mod_A加入系统时,模块加载器创建了use_A对象在mod_A和owner间建立依赖关系。随后mod_B加入了系统,因为它和owner模块间存在依赖关系,加载器同样创建了use_B对象在mod_B和owner间建立关联。从图中可以看到, use_A对象中的source_list成员已不再指向owner->source_list,而是指向了use_B->source_list,后者则和owner->source_list建立了直接的链接关系。
如此,mod_A和mod_B模块可以通过遍历其target_list成员知道所依赖的所有模块,而owner模块则可以通过遍历其source_list成员知道所有依赖于自己的模块。
当从系统中卸载一个模块时,系统必须确保没有其他模块依赖于该模块,根据上面的讨论,只要模块结构中的source_list是一空链表,就表明没有其他模块依赖于它。相关代码在模块卸载部分讨论。
模块的版本控制
版本控制主要用来解决内核模块和内核之间的接口一致性问题。所谓内核模块和内核之间的接口,简单地说是指由内核导出并被内核模块调用的那些符号。产生这种问题的根源在于内核模块和内核作为独立实体各自分开编译。想象一下,一个在Linux 2.6.18内核源码树基础上编译出来的内核模块能否成功加载到内核版本号为2.6.35的Linux系统中?如果模块使用的一个接口在2.6.35版本中已被改变或者废弃,那么前者将会因为无法找到一个未经定义的符号而导致加载失败,后者虽然有可能加载成功,但因为使用到了一个内核已经废弃的接口而需承担相应的风险。
因此,内核和模块之间必须协商出一种机制确保上述问题不会出现。Linux系统对此的解决方案是使用接口的校验和,也叫接口CRC校验码。这种方法的基本思想非常简单,根据函数的参数生成一个大小为4字节的CRC校验码,当双方校验码相等时视为相同接口,否则为不同接口。
为了确保这种机制能够正常工作,内核必须首先启用CONFIG_MODVERSIONS这个宏,在此基础上内核模块在编译时也必须启用CONFIG_MODVERSIONS,否则模块将会因为出现无法解决的未定义符号错误导致加载失败。显然这是个需要双方共同协作才能解决的问题。如果内核编译时没有启用CONFIG_MODVERSIONS,那么系统将不会启用本节所说的方案,即使被加载的模块是在另一个启用CONFIG_MODVERSIONS的内核源码树的基础上编译出来的。
下面首先从内核的角度来看看CONFIG_MODVERSIONS宏对内核导出符号产生的影响。在前面“EXPORT_SYMBOL的内核实现”一节中我们看到了内核用于导出符号的EXPORT_SYMBOL相关宏的定义,其中出现了一个__CRC_SYMBOL宏,如果在内核编译时启用了CONFIG_MODVERSIONS,即对内核启用了版本控制特性,那么__CRC_SYMBOL的定义为:
#define__CRC_SYMBOL(sym,sec) \ extern void*__crc_##sym__attribute__((weak)); \ static const unsigned long__kcrctab_##sym \ __used \ __attribute__((section("__kcrctab"sec),unused)) \ = (unsigned long) &__crc_##sym;
如此,对于EXPORT_SYMBOL(my_exp_function)的例子, 将会在原来的基础上新增如下的定义:
extern void * __crc_ my_exp_function; static const unsigned long __kcrctab_my_exp_function = (unsigned long) &__crc_my_exp_function;
__kcrctab_my_exp_function用来保存__crc_my_exp_function变量地址并将其放在一个名为“__kcrctab”的section中(对于EXPORT_SYMBOL_GPL和EXPORT_SYMBOL_GPL_FUTURE,其所在的section的名称分别为“__kcrctab_gpl”和“__kcrctab_gpl_future”)。
由此可见,如果内核编译时启用了CONFIG_MODVERSIONS宏,那么对于每一个导出的符号,都会生成一个对应的CRC校验码。反之,如果内核编译时没有启用CONFIG_MODVERSIONS宏,那么系统将不会为导出的符号产生CRC校验码。“__kcrctab”等section存在的意义在于当符号查找时,可以通过该section找到对应符号的校验码,以此来判断模块所使用的接口是否和当前正运行的内核提供的接口相匹配。
CONFIG_MODVERSIONS宏对内核的另一个影响存在于加载内核模块的过程中。前面在“对‘未解决的引用’符号(unresolved symbol)的处理”部分中提到,对于模块中出现的未定义的符号,内核会调用resolve_symbol函数予以解决,在resolve_symbol函数的内部会调用find_symbol来查找该符号。如果成功查找到,则函数接下来会调用check_version对这种接口进行校验码的验证。在没有启用CONFIG_MODVERSIONS的系统中,这个函数直接返回1,因此没有启用CONFIG_MODVERSIONS的内核不会进行接口一致性的检验。
接下来看一下CONFIG_MODVERSIONS对内核模块的影响。
首先,前面提到的CONFIG_MODVERSIONS宏对内核导出符号的影响同样适用于模块导出的符号。
其次,启用CONFIG_MODVERSIONS会导致模块的编译工具链在模块最终的ELF文件中产生一个名为“__versions”的section,打开模块源码所在目录下的.mod.c文件,会发现类似如下代码:
static const struct modversion_info ____versions[]
__used
__attribute__((section("__versions"))) = {
{0x58334a4a,"module_layout"注 },
{ 0x6980fe91, "param_get_int" },
{ 0xff964b25, "param_set_int" },
{ 0xb72397d5, "printk" },
{ 0xb4390f9a, "mcount" },
};
注:2.6.35及以后版本的内核会为启用CONFIG_MODVERSIONS的模块生成一个名为“module_layout”的未定义符号,启用了CONFIG_MODVERSIONS的2.6.35版本内核在加载时会检查“module_layout”符号和对应的CRC校验码。
代码定义了一个类型为struct modversion_info的数组____versions,放在“__versions”section中。struct modversion_info的定义如下:
<include/linux/module.h> struct modversion_info { unsigned long crc; char name[MODULE_NAME_LEN]; };
可见当编译内核模块时,若对应的内核配置文件中启用了CONFIG_MODVERSIONS,则模块最终的ELF文件中会生成一个“__versions”section,该section会将模块中的所有“未解决的引用”符号名和对应的校验码放入其间。在前面的那个.mod.c的示例中,printk作为模块的一个“未解决的引用”符号被放在了“__versions”section中,工具链为其产生的CRC校验码为0xb72397d5。
在分析完启用CONFIG_MODVERSIONS宏对内核和内核模块双方的影响后,再回过头来看一看当模块加载时处理“未解决的引用”符号时是如何对接口一致性进行验证的。这种验证是通过在resolve_symbol函数里调用check_version函数完成的,check_version函数的完整定义如下:
<kernel/module.c> #ifdef CONFIG_MODVERSIONS 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.*/ 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); 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; } #else static inline int check_version(Elf_Shdr *sechdrs, unsigned int versindex, const char *symname, struct module *mod, const unsigned long *crc, const struct module *crc_owner) { return 1; } #endif
在定义了CONFIG_MODVERSIONS的前提下,check_version用一个for循环在“__versions”section中进行遍历,对每一个struct modversion_info元素和找到的符号名symname进行匹配,如果匹配成功,再进行接口的校验码比较,如果校验码相等,说明模块所使用的接口和内核导出的接口是一致的,否则产生版本不匹配的错误。
关于校验码的计算[13],内核源码树中提供了一个产生CRC校验码的工具genksyms,它位于Linux内核源码的scripts/genksyms目录下。内核模块编译工具链通过它来生成导出符号的CRC校验码。比如下面的demodev.h文件,其中导出了一个函数符号my_exp_function:
关于校验码生成算法,本书不作详细讨论,读者可以简单认为:VCRC=f(导出函数名, 函数的参数表)。
<demodev.h> int my_exp_function(int a, int b); EXPORT_SYMBOL(my_exp_function);
那么使用如下命令就可以看到genksyms为my_exp_function生成的CRC校验和:
root@AMDLinuxFGL:/home/dennis/book# gcc -E demodev.h -D__GENKSYMS__ -D__KERNEL__ | ./genksyms>demodev.crc
打开demodev.crc文件,可以看到类似下面的内容:
__crc_my_exp_function = 0x48a0010f;
genksyms只为由EXPORT_SYMBOL系列的宏导出符号生成CRC校验码。对于内核和内核模块而言,它们在各自编译链接阶段均独立使用genksyms来为导出的符号和那些“未解决的引用”符号生成校验和。
图1-9展示了接口校验码在验证接口有效性时的一个具体例子,图中的模块A使用到了内核导出的一个函数demofunc,该模块当前的.ko文件最初是在内核源码树的2.6.18版本上编译而成的,现在要在运行内核B(版本号为2.6.35)的Linux系统上加载该模块。现在的问题是,内核导出的函数demofunc在从版本2.6.18到2.6.35的演变过程中发生了改变,新版本内核源码中该函数原型较旧版本多出了一个参数。如果内核在加载模块时没有版本控制机制,那么在运行新版本的Linux系统中加载该模块时会发生什么呢?答案是结果不确定,内核也许会正常运行,也许会崩溃,但无论如何这是个潜在的危险行为。
图1-9 新旧内核导出的接口CRC不匹配
如果内核A和B在当初配置时都启用了版本控制机制,那么它们除了导出demofunc这个符号外,还会分别为这个接口生成对应的CRC校验码。当图中的模块A在内核A源码树的基础上进行编译时,模块的构造工具链也会负责为模块中这个“未解决的引用”符号demofunc生成CRC校验码,因为模块编链工具链和内核工具链使用的CRC校验码生成工具是完全一样的,所以它们都会获得针对demofunc函数接口的一个CRC校验码,图中的值为0xa206cef4。
而对于图中的内核B而言,虽然使用了同样的CRC校验码生成工具,但因为函数多出了一个参数,所以内核B得到的demofunc函数接口校验码为0x74522d16。这样,当将图中的模块A向内核B加载时,会因为两者的CRC校验码不匹配而导致无法加载该模块,从而避免可能造成内核不稳定的危险行为。
基于上面的讨论,我们建议在对内核进行配置时启用CONFIG_MODVERSIONS选项,这样编译出的内核在模块加载时就会启动版本控制机制。如果模块在一个没有启用CONFIG_MODVERSIONS宏的内核源码树基础上进行编译链接,模块的构造工具将不会为模块中导出的符号和那些“未解决的引用”符号生成CRC校验码,如果将这样的模块安装到一个启用了CONFIG_MODVERSIONS的Linux系统中,加载该模块就会在版本控制环节出现问题,这种情况下即使强行加载也会导致内核的污染。
最后要强调的是,对于CONFIG_MODVERSIONS机制,模块的编译工具链只对导出的符号产生CRC校验码,最明显的,内核在编译过程中所有导出的符号都会被记录到一个名为“Module.symvers”的文件中,下面是该文件的一部分:
root@AMDLinuxFGL:/home/dennis/Linux/kernel/linux-2.6.39# cat Module.symvers | grep printk 0xc60f75ec__ftrace_vprintkvmlinux EXPORT_SYMBOL_GPL 0x5ebefe4bv4l_printk_ioctl drivers/media/video/videodev EXPORT_SYMBOL 0xbdd295f0trace_vprintk vmlinux EXPORT_SYMBOL_GPL 0x50eedeb8printk vmlinux EXPORT_SYMBOL
其中每行第二列就是导出的符号,第一列是该导出符号的CRC校验码,第三列是导出该符号的模块,第四列是导出符号的类型。
如果一个独立编译的内核模块,比如demodev.ko,引用到了内核导出的符号,比如printk,那么在demodev.ko的编译链接过程中,工具链会到内核源码所在的目录下查找Module.symvers文件,将得到的printk的CRC校验码记录到demodev.ko的“__versions”section中,换句话说,工具链不会在使用导出符号的地方重新为之生成一个CRC校验码。在demodev模块成功编译后,读者可以在其源码所在的目录下发现一个名为“demodev.mod.c”的文件,在该文件中可以发现类似下面的内容:
<demodev.mod.c> static const struct modversion_info ____versions[] __used __attribute__((section("__versions"))) = { { 0xe1c343b8, "module_layout" }, { 0x50eedeb8, "printk" }, { 0xb4390f9a, "mcount" }, };
从中可以看到printk函数的校验码和内核源码树中Module.symvers中的完全一样。
从这里还可以引申出一个有趣的问题,如果有两个模块A和B,A导出的一个符号“a_sym”为B所用,因为A和B都是各自独立编译链接,意味着A导出的符号及其CRC校验码将放在A所在目录的Module.symvers文件中,这样在编译链接B模块时将会产生一个WARNING,大意是"a_sym" [/home/dennis/Linux/B.ko] undefined!,即便在B模块源码中加入extern … a_sym这样的声明也无法消除这个WARNING,产生该WARNING的原因是B模块只能找到Linux内核导出符号所在的Module.symvers文件,而无法找到A模块产生的Module.symvers文件,所以在模块编译阶段无法确定最终在模块加载时是否能找到这个“a_sym”符号,因此只是简单地给了个WARNING。从表象上看,这个WARNING尚不是致命的,因为还有模块加载器这最后一道防线,正如前面所讨论的,它会到内核及所有加载进系统的内核模块所导出的符号表中查找“a_sym”,假如在B模块加载前A模块已经被加载进系统,那么有理由相信模块加载器是可以帮B模块找到“a_sym”符号的,但是遗憾的是模块B通常都不可能被加载成功,dmesg对此给出的提示信息是“B: no symbol version for a_sym”云云。所以此时再去查看B模块的.mod.c文件,在它的versions[]里一定不会有“a_sym”的身影,因为工具链根本得不到它的CRC校验码。知道了原因问题就好解决了,最简单的,把A模块Module.symvers文件的内容添加到Linux内核源码树中的Module.symvers文件中,这样就可以消除WARNING而且B模块也可以成功加载。如果此时再看B模块的.mod.c文件,就会发现“a_sym”已经出现在versions[]中,并且modinfo显示B模块有了个依赖关系depends:
root@AMDLinuxFGL:/home/dennis/Linux/book/chap09/framewk/device#modinfo B.ko filename: B.ko description: Asimplekernelmodule author: dennischen@AMDLinuxFGL license: GPL srcversion: 7093EDF7B908CDD5212F5F1 depends: A vermagic: 2.6.39SMPmod_unloadmodversions586
模块的信息(modinfo)
模块的最终ELF文件中都会有一个名为“.modinfo”的section,这个section以文本的形式保留着模块的一些相关信息。在Linux环境下可以用modinfo工具来查看一个模块存储的信息,比如下面modinfo输出的demodev.ko模块的信息:
root@AMDLinuxFGL:/home/dennis/Linux/book/chap01# modinfo demodev.ko filename: demodev.ko srcversion: D2FF50581DA6C0AF3F6B3EC depends: vermagic: 2.6.39SMPmod_unloadmodversions686 parm: dolphin:int parm: bobcat:int
模块的源码中用MODULE_INFO宏来向该section添加模块信息,MODULE_INFO宏的相关定义如下:
<include/linux/module.h> #ifdef MODULE #define ___module_cat(a,b) __mod_ ## a ## b #define __module_cat(a,b) ___module_cat(a,b) #define__MODULE_INFO(tag,name,info) \ static const char__module_cat(name,__LINE__)[] \ __used \ __attribute__((section(".modinfo"),unused)) = __stringify(tag) "=" info #else /*!MODULE*/ #define __MODULE_INFO(tag, name, info) #endif /* Generic info of form tag = "info" */ #define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info)
在MODULE没有定义的情况下,MODULE_INFO是个空定义。而对于内核模块而言, MODULE_INFO在“.modinfo”section中定义了一个类似"tag=info"的字符串,内核中通过调用get_modinfo函数来获得tag所在字符串的值info。
模块加载过程中,需要获得“.modinfo”section中的相关信息以便进一步处理,这些信息包括:
● 模块的license
模块的license在模块源码中以MODULE_LICENSE宏引出,该宏的主体就是MODULE_INFO:
#define MODULE_LICENSE(_license) MODULE_INFO(license, _license)
内核模块加载过程中,会调用license_is_gpl_compatible来确定模块的license是否与GPL兼容:
<include/linux/license.h> static inline int license_is_gpl_compatible(const char *license) { return (strcmp(license, "GPL") == 0 || strcmp(license, "GPL v2") == 0 || strcmp(license, "GPL and additional rights") == 0 || strcmp(license, "Dual BSD/GPL") == 0 || strcmp(license, "Dual MIT/GPL") == 0 || strcmp(license, "Dual MPL/GPL") == 0); }
非以上形式的license被认为与GPL不兼容,这样的模块在加载进系统后会导致内核被污染,内核用mod对象的unsigned int taints成员记录一个模块是否会污染内核:
mod->taints |= (1U << 0);
这样,以后就可以通过mod->taints来判断要加载的模块是否GPL兼容。而对于运行中的系统是否被污染,内核用一个unsigned long型全局变量tainted_mask来表示,在系统因故障挂起时,tainted_mask会影响系统调试信息的输出,以告之内核是否已被污染。
另外,non-GPL的模块无法使用内核或其他内核模块用EXPORT_SYMBOL_GPL导出的符号,在加载这样的模块时将出现"Unknown symbol in module"类似的错误信息。
● 模块的vermagic
内核和内核模块的vermagic都是通过MODULE_INFO定义的一个VERMAGIC_STRING字符串,后者实际上是一个生成字符串的宏,该宏会根据不同的内核配置信息生成不同的字符串。模块加载过程中会检查模块中的vermagic是否和当前运行的内核定义的vermagic一致,如果不一致加载将失败,dmesg命令会发现类似下面的错误信息:
demodev: version magic '2.6.39 SMP mod_unload 586' should be '2.6.39 SMP mod_unload modversions586'
上面的信息输出对应着load_module函数如下的代码片段:
<kernel/module.c> static noinline struct module *load_module(void __user *umod, unsigned long len, const char __user *uargs) { … modmagic = get_modinfo(sechdrs, infoindex, "vermagic"); /* This is allowed: modprobe --force will invalidate it. */ if (!modmagic) { err = try_to_force_load(mod, "bad vermagic"); if (err) goto free_hdr; } else if (!same_magic(modmagic, vermagic, versindex)) { printk(KERN_ERR "%s: version magic '%s' should be '%s'\n", mod->name, modmagic, vermagic); err = -ENOEXEC; goto free_hdr; } … }
该代码片段首先在当前模块的“.modinfo”section中查找vermagic对应的字符串modmagic,如果找到则和当前内核的vermagic字符串进行比较,以确定两者是否匹配,不匹配的话模块将加载失败。读者可以通过如下命令查看一个模块的“.modinfo”section的内容:
root@AMDLinuxFGL:/home/dennis/Linux/book/chap01#readelf-p.modinfodemodev.ko Stringdumpofsection'.modinfo':
[ 0] parmtype=bobcat:int
[ 14] parmtype=dolphin:int
[ 40] srcversion=D2FF50581DA6C0AF3F6B3EC
[ 63] depends=
[ 80] vermagic=2.6.39SMPmod_unloadmodversions686
所以对于demodev.ko这个模块而言,“.modinfo”section中vermagic对应的字符串就为"2.6.39 SMP mod_unload modversions 686",打开模块源码目录下的demodev.mod.c文件,可以看到下列信息:
MODULE_INFO(vermagic, VERMAGIC_STRING);
内核模块中用来产生vermagic的MODULE_INFO是通过scripts/mod/modpost.c文件自动生成的,内核模块开发者无须在源码中显式添加这一信息。
前面已经提到,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_VERMAGIC
读者很容易将VERMAGIC_STRING所组合的信息与"2.6.39 SMP mod_unload modversions 686"这样的具体字符串对应起来。因为不同的内核源码树的配置信息不一定相同,所以从这个角度而言,vermagic也可以看做是模块版本控制的一部分。
1.3.4 sys_init_module(第二部分)
load_module函数完成模块加载几乎所有的艰苦工作之后,重新返回到sys_init_module,后者在load_module的基础上所做的事情就很简单了,主要有:
调用模块的初始化函数
“struct module类型变量mod初始化”部分中已经提到了mod中init函数指针是如何指向模块源码中的初始化函数,现在由sys_init_module负责调用它,相关代码如下:
<kernel/module.c> sys_init_module(void__user*umod,unsigned long len, const char__user* uargs) { … if (mod->init != NULL) ret = do_one_initcall(mod->init); if (ret < 0) { /*Init routine failed:abort. Try to protect us from buggy refcounters. */ mod->state = MODULE_STATE_GOING; synchronize_sched(); module_put(mod); blocking_notifier_call_chain(&module_notify_list, MODULE_STATE_GOING, mod); free_module(mod); wake_up(&module_wq); return ret; } … }
从以上代码可以看出,内核模块可以不提供模块初始化函数。如果模块提供了初始化函数,那么它将在do_one_initcall函数内部被调用。如果模块初始化函数被成功调用,那么模块就算是被加载进了系统,因此需要更新模块的状态为MODULE_STATE_LIVE:
mod->state = MODULE_STATE_LIVE;
释放INIT section所占用的空间
模块一旦被成功加载,HDR视图和INIT section所占的内存区域将不再会被用到,因此需要释放它们。在sys_init_module函数中,释放INIT section所在内存区域由函数module_free完成,后者调用vfree来释放INIT section区域(mod->module_init):
module_free(mod, mod->module_init);
HDR视图所占空间的释放实际上发生在load_module函数的最后部分:
<kernel/module.c> static noinline struct module *load_module(void __user *umod, unsigned long len, const char __user *uargs) { … vfree(hdr); … }
模块成功加载进系统之后,正在运行的内核和系统中所有加载的模块关系如图1-10所示:
图1-10 内核与内核模块
内核用一全局变量modules链表记录系统中所有已加载的模块。
呼叫模块通知链
Linux内核提供了一个很有趣的特性,也就是所谓的通知链(notifier call chain),这个特性不只是在模块的加载过程中会使用到,在内核系统的其他组件中也常常会使用到。通过通知链,模块或者其他的内核组件可以对向其感兴趣的一些内核事件进行注册,当该事件发生时,这些模块或者组件当初注册的回调函数将会被调用。内核模块机制中实现的模块通知链module_notify_list就是内核中众多通知链中的一条。通知链背后的实现机制其实很简单,通过链表的形式,内核将那些注册进来的关注同类事件的节点构成一个链表,当某一特定的内核事件发生时,事件所属的内核组件负责遍历该通知链上的所有节点,调用节点上的回调函数。所有的通知链头部都是一个struct blocking_notifier_head类型的变量,该类型的定义为:
<include/linux/notifier.h> struct blocking_notifier_head { struct rw_semaphore rwsem; struct notifier_block *head; };
这里以内核模块的通知链module_notify_list为例,来讨论一下通知链的实现原理。module_notify_list为一全局变量,用来管理所有对内核模块事件感兴趣的通知节点,其定义为:
<kernel/module.c> static BLOCKING_NOTIFIER_HEAD(module_notify_list);
上述定义其实是定义并初始化了一个类型为struct blocking_notifier_head的对象module_notify_list。如果一个内核模块想了解当前系统中所有与模块相关的事件,可以调用register_module_notifier向内核注册一个节点对象,该节点对象中包含有一个回调函数。当register_module_notifier函数成功向系统注册了一个回调节点之后,系统中所有那些模块相关的事件发生时都会调用到这个回调函数。为了具体了解幕后的机制,下面先来看看register_module_notifier函数在内核源码中的实现:
<kernel/module.c> int register_module_notifier(struct notifier_block * nb) { return blocking_notifier_chain_register(&module_notify_list, nb); }
函数的参数是个struct notifier_block型指针,代表一个通知节点对象。struct notifier_block的定义为:
<include/linux/notifier.h> struct notifier_block { int (*notifier_call)(struct notifier_block *, unsigned long, void *); struct notifier_block *next; int priority; };
其中,notifier_call就是所谓的通知节点中的回调函数,next用来构成通知链,priority代表一个通知节点优先级,用来决定通知节点在通知链中的先后顺序,数值越大代表优先级越高。
blocking_notifier_chain_register最终通过调用notifier_chain_register函数将一个通知节点加入module_notify_list管理的链表。在向一个通知链中加入新节点时,系统会把各节点的priority作为一个排序关键字进行简单排序,其结果是越高优先级的节点越靠近头节点,当有事件发生时,最先被通知。图1-11展示了多个模块在同一条通知链上调用了register_module_notifier,由此形成的该调用链结构示意图:
图1-11 内核中的一条通知链构成示意图
与register_module_notifier相反,如果要从一条通知链中注销一个通知节点,那么应该使用unregister_module_notifier函数,该函数的原型为:
int unregister_module_notifier(struct notifier_block * nb);
以上讨论了如何向/从系统中的一条通知链注册/注销一个通知节点,接下来看看当某个特定的内核事件发生时,通知链上各节点的回调函数如何被触发。以内核模块加载过程为例, sys_init_module函数在调用完load_module之后,会通过blocking_notifier_call_chain函数来通知调用链module_notify_list上的各节点,例如,如果load_module函数成功返回,表明模块加载的大部分工作已经完成,此时sys_init_module会通过调用blocking_notifier_call_chain函数来通知module_notify_list上的节点,一个模块正在被加入系统(MODULE_STATE_COMING):
<kernel/module.c> sys_init_module(void__user*umod,unsigned long len, const char__user* uargs) { … mod = load_module(umod, len, uargs); if (IS_ERR(mod)) return PTR_ERR(mod); blocking_notifier_call_chain(&module_notify_list, MODULE_STATE_COMING, mod); … }
上面代码段中的blocking_notifier_call_chain函数将使通知链module_notify_list上的各节点的回调函数均被调用,其实现原理可以简单概括为遍历module_notify_list上的各节点,依次调用各节点上的notifier_call函数。读者从源码中也可以发现,对于模块加载的其他阶段(MODULE_STATE_LIVE和MODULE_STATE_GOING),内核模块加载器也都会调用blocking_notifier_call_chain函数来通知module_notify_list上的各个通知节点。
最后用一个实际的例子来加深读者对模块通知链的理解,下面所列内核模块modnoti.ko的源码展示了如何利用模块通知链来监听系统中与模块相关的事件:
#include <linux/module.h> #include <linux/kernel.h> #include <linux/slab.h> static struct notifier_block *pnb = NULL; static char *mstate[] = {"LIVE", "COMING", "GOING"}; //通知节点对象pnb上的回调函数 int get_notify(struct notifier_block *p, unsigned long v, void *m) { printk("module <%s> is %s, p->priority=%d\n", ((struct module *)m)->name, mstate[v], p->priority); return 0; } static int hello_init(void) { //分配一个struct notifier_block通知节点对象 pnb = kzalloc(sizeof(struct notifier_block), GFP_KERNEL); if(!pnb) return -1; //通知节点上的回调函数 pnb->notifier_call = get_notify; pnb->priority = 10; register_module_notifier(pnb); printk("A listening module is coming...\n"); return 0; } static void hello_exit(void) { unregister_module_notifier(pnb); kfree(pnb); printk("A listening module is going\n"); } module_init(hello_init); module_exit(hello_exit);
当通过“insmod modnoti.ko”把该内核模块加入系统后,在dmesg的输出中可以发现如下的输出信息:
root@AMDLinuxFGL:/home/dennis/book/gene-module#dmesg [230330.738545]Alisteningmoduleiscoming... [230330.738555]module<modnoti>isLIVE,p->priority=10
因为modnoti.ko中向通知链module_notify_list注册一个节点的动作发生在模块的初始化函数中,所以虽然sys_init_module函数在模块初始化函数前调用了blocking_notifier_call_chain(&module_notify_list, MODULE_STATE_COMING, mod),但是此时modnoti模块的通知节点还没有出现在module_notify_list通知链中,因而modnoti只能接收到发生在模块初始化函数之后的blocking_notifier_call_chain(&module_notify_list, MODULE_STATE_LIVE, mod)的通知消息。
在成功加载完modnoti.ko模块后,再通过“insmod demodev.ko”来加载另一个内核模块,此时dmesg的输出又多出了如下两行:
[230357.414452] module <demodev> is COMING, p->priority=10 [230357.414467] module <demodev> is LIVE, p->priority=10
对比sys_init_module函数源码,读者应该很容易理解为何出现上述两行输出。
1.3.5 模块的卸载
相对于模块的加载,从系统中卸载一个模块的任务则要轻松得多。将一个模块从系统中卸载,使用rmmod命令,比如“rmmod demodev”。rmmod通过系统调用sys_delete_module来完成卸载工作,该函数原型如下:
long sys_delete_module(const char __user * name_user, unsigned int flags);
find_module函数
sys_delete_module函数首先将来自用户空间的欲卸载模块名用strncpy_from_user函数复制到内核空间:
if (strncpy_from_user(name, name_user, MODULE_NAME_LEN-1) < 0) return -EFAULT;
然后调用find_module函数在内核维护的模块链表modules中利用name来查找要卸载的模块。find_module函数的定义如下:
<kernel/module.c> /* Search for module by name: must hold module_mutex. */ struct module *find_module(const char *name) { struct module *mod; list_for_each_entry(mod, &modules, list) { if (strcmp(mod->name, name) == 0) return mod; } return NULL; }
函数通过list_for_each_entry在modules链表中遍历每一个模块mod,通过前面的讨论我们知道,全局变量modules用来管理系统中所有已加载的模块形成的链表。如果查找到指定的模块,则函数返回该模块的mod结构,否则返回NULL。
检查模块依赖关系
如果sys_delete_module函数成功查找到了要卸载的模块,那么接下来就要检查是否有别的模块依赖于当前要卸载的模块,为了系统的稳定,一个有依赖关系的模块不应该从系统中卸载掉。在前面“模块间的依赖关系”部分中已经讨论了内核如何跟踪系统中各模块之间的依赖关系,这里只需检查要卸载模块的source_list链表是否为空,即可判断这种依赖关系,相关代码段为:
if (!list_empty(&mod->source_list)) { /* Other modules depend on us: get rid of them first. */ ret = -EWOULDBLOCK; goto out; }
free_module函数
如果一切正常,sys_delete_module函数最后会调用free_module函数来做模块卸载末期的一些清理工作,包括更新模块的状态为MODULE_STATE_GOING,将卸载的模块从modules链表中移除,将模块占用的CORE section空间释放,释放模块从用户空间接收的参数所占的空间等,函数的实现相对比较直白,这里就不再仔细讨论了。