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了……