Linux下的内核模块机制
2017-06-20
Linux的内核模块机制允许开发者动态的向内核添加功能,我们常见的文件系统、驱动程序等都可以通过模块的方式添加到内核而无需对内核重新编译,这在很大程度上减少了操作的复杂度。模块机制使内核预编译时不必包含很多无关功能,把内核做到最精简,后期可以根据需要进行添加。而针对驱动程序,因为涉及到具体的硬件,很难使通用的,且其中可能包含了各个厂商的私密接口,厂商几乎不会允许开发者把源代码公开,这就和linux的许可相悖,模块机制很好的解决了这个冲突,允许驱动程序后期进行添加而不合并到内核。OK,下面结合源代码讨论下模块机制的实现。
类似于普通的可执行文件,模块经过编译后得到.ko文件,其本身也是可重定位目标文件,类似于gcc -c 得到的.o目标文件。对于可重定位的概念,请参考PE文件格式(虽然是windows下的,但是原理类似)。
既然是重定位文件,在把模块加载到内核的时候就需要进行重定位,回想下用户可执行文件的重定位,一般如果一个程序的可执行文件总能加载到自己的理想位置,所以对于用户可执行文件,一般不怎么需要重定位,而对于动态库文件就不同了,库文件格式是一致的,但是可能需要加载多个库文件,那么有些库文件必然无法加载到自己的理想位置,就需要进行重定位。而内核模块由于和内核共享同一个内核地址空间,更不能保证自己的理想地址不被占用,所以一般情况内核模块也需要进行重定位。在加载到内核时,还有一个重要的工作即使解决模块之间的依赖,模块A中引用了其他模块的函数,那么在加载到内核之前其实模块A并不知道所引用的函数地址,因此只能做一个标记,在加载到内核的时候在根据符号表解决引用问题!这些都是在加载内核的核心系统调用sys_init_module完成。
内核中的数据结构
每一个内核模块在内核中都对应一个数据结构module,所有的模块通过一个链表维护。所以有些恶意模块企图通过从链表摘除结构来达到隐藏模块的目的。部分成员列举如下:
struct module { enum module_state state; /* Member of list of modules */ struct list_head list;//所有的模块构成双链表,包头为全局变量modules /* Unique handle for this module */ char name[MODULE_NAME_LEN];//模块名字,唯一,一般存储去掉.ko的部分 /* 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;//导出符号信息,指向一个kernel_symbol的数组,有num_syms个表项。 const unsigned long *crcs;//同样有num_syms个表项,不过存储的是符号的校验和 unsigned int num_syms; /* Kernel parameters. */ struct kernel_param *kp; unsigned int num_kp; /* GPL-only exported symbols. */ unsigned int num_gpl_syms;//具体意义同上面符号,但是这里只适用于GPL兼容的模块 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 #ifdef CONFIG_MODULE_SIG /* Signature was verified. */ bool sig_ok; #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);//模块初始化函数指针 /* If this is non-NULL, vfree after init() returns */ void *module_init;/如果该函数不为空,则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;//对应于上面的init和core函数,决定各自占用的大小 /* 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; 。。。。。。 #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);//卸载退出函数,模块中定义的exit函数 。。。。。。 };
依赖关系
模块间的依赖关系通过两个节点source_list和target_list记录,前者记录那些模块依赖于本模块,后者记录本模块依赖于那些模块。节点通过module_use记录,module_use如下
struct module_use { struct list_head source_list; struct list_head target_list; struct module *source, *target; };
每个module_use记录一个映射关系,注意这里把source和target放在一个一个结构里,因为一个关系需要在源模块和目标模块都做记录。如果模块A依赖于模块B,则生成一个module_use结构,其中source_list字段链入模块B的module结构的source_list链表,而source指针指向模块A的module结构。而target_list加入到模块A中的target_list链表,target指针指向模块B的模块结构,参考下面代码。
static int add_module_usage(struct module *a, struct module *b) { struct module_use *use; pr_debug("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; }
符号信息
内核模块几乎不会作为完全独立的存在,均需要引用其他模块的函数,而这一机制就是由符号机制保证的。参考前面的module数据结构,在
const struct kernel_symbol *syms;//导出符号信息,指向一个kernel_symbol的数组,有num_syms个表项。
const unsigned long *crcs;//同样有num_syms个表项,不过存储的是符号的校验和
unsigned int num_syms;
syms指针指向一个符号数组,也可以称之为符号表,不过是局部的符号表。看下kernel_symbol结构
struct kernel_symbol { unsigned long value; const char *name; };
结构很简单,value记录符号地址,而name自然就是符号名字了。原理很简单,借助于find_symbol函数看下内核如果解决位引用的符号
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_section(find_symbol_in_section, &fsa)) { if (owner) *owner = fsa.owner; if (crc) *crc = fsa.crc; return fsa.sym; } pr_debug("Failed to find symbol %s\n", name); return NULL; }
首先把参数信息封装成一个find_symbol_arg结构,然后调用了each_symbol_section,并传入了在section中查找symbol的函数find_symbol_in_section
bool each_symbol_section(bool (*fn)(const struct symsearch *arr, struct module *owner, void *data), void *data) { struct module *mod; 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 }, #ifdef CONFIG_UNUSED_SYMBOLS { __start___ksymtab_unused, __stop___ksymtab_unused, __start___kcrctab_unused, NOT_GPL_ONLY, true }, { __start___ksymtab_unused_gpl, __stop___ksymtab_unused_gpl, __start___kcrctab_unused_gpl, GPL_ONLY, true }, #endif }; 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 }, #ifdef CONFIG_UNUSED_SYMBOLS { mod->unused_syms, mod->unused_syms + mod->num_unused_syms, mod->unused_crcs, NOT_GPL_ONLY, true }, { mod->unused_gpl_syms, mod->unused_gpl_syms + mod->num_unused_gpl_syms, mod->unused_gpl_crcs, GPL_ONLY, true }, #endif }; if (mod->state == MODULE_STATE_UNFORMED) continue; if (each_symbol_in_section(arr, ARRAY_SIZE(arr), mod, fn, data)) return true; } return false; }
首先考虑的自然是内核自身的符号,根据优先顺序,定义了一个数组,内核中的导出符号记录在全局的结构中,顺序分别是__start___ksymtab、__start___ksymtab_gpl、__start___ksymtab_gpl_future、__start___ksymtab_unused、__start___ksymtab_unused_gpl。然后调用each_symbol_in_section进行遍历数组,针对每一个项,调用find_symbol_in_section进行查找。如果内核中的符号没有包含指定符号,则需要查找其他加载模块的符号表,这就是局部符号表,方法类似,不过是表指针记录在module结构中而不是全局的。不在赘述。看下find_symbol_in_section
static bool find_symbol_in_section(const struct symsearch *syms, struct module *owner, void *data) { struct find_symbol_arg *fsa = data; struct kernel_symbol *sym; sym = bsearch(fsa->name, syms->start, syms->stop - syms->start, sizeof(struct kernel_symbol), cmp_name); if (sym != NULL && check_symbol(syms, owner, sym - syms->start, data)) return true; return false; }
该函数是根据是个符号表的起始和结束区间对符号进行查找,具体查找工作有bsearch完成,通过二分查找key,即符号名字。算法挺简单,我们也看下
void *bsearch(const void *key, const void *base, size_t num, size_t size, int (*cmp)(const void *key, const void *elt)) { size_t start = 0, end = num; int result; while (start < end) { size_t mid = start + (end - start) / 2; result = cmp(key, base + mid * size); if (result < 0) end = mid; else if (result > 0) start = mid + 1; else return (void *)base + mid * size; } return NULL; }
找到一个结果就调用cmp进行比较,cmp为开始传递进来的比较函数,本质还是调用strcmp函数。有这里可以看出,符号表种符号是有顺序的,即通过首字母进行排列,首字母相同则按照第二个字母,以此类推。这样在找到symbol后会对其进行校验,如果没有找到就直接返回false了……
使用未导出的函数
1、定义函数指针
2、声明函数
3、查找符号表
以内核中的lookup_swap_cache函数为例,函数在内核中未导出,不能直接使用。通过查找符号表,把函数地址强制转化成函数指针,可以为我们所用。
1、定义函数指针
函数原始定义如下:struct page *lookup_swap_cache(swp_entry_t swp)
typedef struct page* (*LOOKUPSWAPCACHE)(swp_entry_t);
2、声明函数
LOOKUPSWAPCACHE lookup_swap_cache_chen;
3、赋值
lookup_swap_cache_chen=(LOOKUPSWAPCACHE)kallsyms_lookup_name("lookup_swap_ cache");
这样就可以在自己的模块中使用了lookup_swap_cache_chen函数了。需要包含头文件#include <linux/kallsyms.h>
用户空间信息
一下信息摘自:http://www.blogbus.com/wanderer-zjhit-logs/172382425.html
内核符号表(kernel symbol table)变量名或者函数名组成,每一项是符号和地址的序对,就像域名和ip地址,格式如下:
[root@rx6600 boot]# head System.map
000000000479c4a0 A phys_start
a000000000000600 A __start_gate_mckinley_e9_patchlist
a000000000000604 A __end_gate_mckinley_e9_patchlist
a000000000000604 A __end_gate_vtop_patchlist
a000000000000604 A __start_gate_fsyscall_patchlist
a000000000000604 A __start_gate_vtop_patchlist
对于系统的oop消息、或者通过gdb的调试消息,都需要根据该对照表,将内核熟悉的函数地址转化为用户熟悉的函数名称,便于用户进行故障定位、运行监控。
内核符号表存储位置
System.map
磁盘中真实存在的文件,存储内核中静态编译的函数和变量地址,每个新编译内核对应一个System.map文件,当klogd输出内核消息时,会通过/boot/System.map来将函数、变量地址转换为名称,方便用户理解。该文件对应不同的编译内核有对应的实现文件。
/proc/kallsyms
内核启动时候创建,共oops时定位错误,文件大小总为0,包含当前内核导出的、可供使用的变量或者函数
相似点:都是内核函数、变量的符号表,结构一致;对于可导出的内核变量、函数,其运行时在物理内存中的位置是一样的。
区别:
两者侧重点不同,System.map文件面向内核,对于内核中的没有导出的变量或者函数名,比如kthread_create_list链表头指针,也有其相应的内核地址,该文件一般是只读的、固定大小的,没有动态添加模块中的变量、函数名;而System.map在内核启动过程中创建,并实时更新,反映的是系统的当前最新情况,其内部也包含内核或者是已加载模块导出的函数、变量名称。所以和System.map文件有差别,且文件动态变化,大小不固定。
- 注:/proc/kmsg文件保存了内核从最开始启动到正常运行时的所有内核输出消息,是内核在运行过程中通过printk输出的。
- 如果klogd启动,klogd读取/proc/kmsg文件的内容,然后通过syslogd程序,写到/var/log/messages文件中,当然,syslogd可以通过syslogd.conf文件进行配置。利用dmesg,其实也是读取/proc/kmsg文件内容,然后显示到终端。
- dmesg和klogd都是利用了System.map文件将内核地址转化为对应的函数名称,方便用户调试。
- 在内核运行出现问题时,一般由于引用了一个无效指针造成的oops错误,如果在应用层,一般应用程序不可能从段错误(即引用无效地址)中恢复,但是由于内核稳定性比较高,一般只是会将该内核模块杀死,并使系统维持在一个稳定状态;如果出现更严重情况,即内核出现panic,就会宕机重启。
暂时写到这里把……
以马内利
参考资料:
1、linux内核3.10.1源码
2、深入linux内核架构