加载一个模块命令:
insmod demodev.ko
本章介绍:
模块的加载过程;
模块如何引用内核或者其它模块中的函数与变量;
模块本身导出的函数与变量如何被别的内核模块所用;
模块的参数传递机制;
模块之间的依赖关系;
模块中的版本控制;
1.1内核模块的文件格式:
elf格式:(可重定位的目标文件)
输出信息命令:
file demodev.ko
readelf demodev.ko (读取elf文件信息的工具)
结构说明:
ELF Header :52字节(e_type、e_shoff头表 Section header table在文件中的偏移量、e_shsize、e_shnum、e_shstrndx )
Section部分
Section header table
1.2模块如何导出符号
分三步:
1、定义:EXPORT _SYMBOL 宏定义
2、链接脚本、链接器处理;
3、模块加载时使用导出符号;
例如:导出my_exp_function()
1、module.h
有EXPORT_SYMBOL的完整定义,请参考;
也有内核符号结构的定义:
struct kernel_symbol
{
unsigned long value;
const *name;
}
2、具体实现:
static const char * __kstrtab_my_exp_function = "my_exp_function";
static const struct kernel_symbol __ksymtab_my_exp_function = {(unsigned long)& my_exp_function,__kstrtab_my_exp_function};
3、上面的 __kstrtab_my_exp_function会被放在名为“__ksymtab_strings” 的section中;
__ksymtab_my_exp_function会被放在名为“__ksymtab” 的section中;
4、链接脚本告诉连接器: 所有名为__ksymtab放在内核或内核模块的 名为__ksymtab的section中;参考vmlinux.lds 链接脚本文件。
__ksymtab: AT(ADDR(__ksymtab )-0xc0000000)
{ __start___ksymtab=.;*(__ksymtab)__stop___ksymtab=.; }
......
}
1.3模块的加载过程
在用户空间,insmod demodev.ko :
1、insmod用文件系统接口,读取demodev.ko到用户空间的内存中,然后通过系统调用sys_init_module 让内核处理模块加载整个过程;
1.3.1 sys_init_module:调用load_module ,参考源码;
1.3.2 struct module (module.h有完整定义)
struct module{
enum module_state state; //状态:成功加载、加载中、卸载中;
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_moudle
功能包括:
1、模块如何调用内核导出的函数;
2、如何导出自己的符号给别的模块用;
3、如何接收参数;
模块ELF的静态内存视图:
说明:1、insmod首先通过文件系统接口,读取的摩的demodev.ko到用户空间 void* umod;
2、系统调用sys_init_module进入内核态,同时将umod指针传递过去(另两个参数:len 长度,uargs 模块参数);
3、 调用load_module: vmalloc分配大小为len的地址空间,然后copy_from_user 复制到内核空间,叫HDR视图,这个视图所占用内存,load_module结束是会释放vfree。
字符串表
驱动模块所在的ELF文件中,有两个字符串表section:
1、section名称的字符串表: char * secstrings = (char*)hdr +entry[hdr->e_shstrndx].sh_offset;
2、每个符号名称的字符串表; 先遍历section header table, 找到类型为SHT_SYMTAB的entry其序号为 i,它的成员entry[i].sh_link就是符号名称字符串表在整个section header table的索引值 ,即char* strtab= (char*)hdr + entry[entry[i].sh_link].sh_offset;
到这里,就得到俩个基地址:
secstrings :section名称基地址;
strtab: 符号名称串表的基地址;
HDR视图的第一次改写:
获得2个基地址后,load _module函数遍历Section header table 中所有entry,执行entry[i].sh_addr=(size_t)hdr+entry[i].offset;
entry[i].sh_addr指向了所对应的section在HDR视图中的实际地址;
/*找section 在Section header table中的索引值*/
static unsigned int find_sec(const struct load_info *info, const char *name)
struct module类型变量mod的初始化
1、load _module定义一个变量mod;
2、ELF里有一个section叫".gnu.linkonce.this_module",这个section是有编译工具链完成,与设备程序员无关,所以,编译后,在模块目录会有一个文件X.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,
};
3、module_init 和 module_exit 宏利用别名技术,把模块初始化函数指向了initfn,这个正是我们在设备驱动程序模块中定义的初始化函数。
模块加载到内存后,内核通过find_sec函数找出".gnu.linkonce.this_module" seciton 在Section Header table的索引值modindex,然后通过下面的代码:
mod = (void*)sechdrs[modindex].sh_addr;
就得到了".gnu.linkonce.this_module"这个seciton在内存中的实际地址;
这样mod第一次指向了struct module所在的内存地址,下一节mod将在section重定位后指向".gnu.linkonce.this_module" seciton的最终地址。
HDR视图的第二次改写(各section的重定位):
重定位函数: layout_sections
遍历HDR视图中的每一个seciton,如果有SHF_ALLOC标志(4个类型:code、read-only data、read-write data 和small data),就归为两大类:CORE和INIT。
并对每一类,函数都会遍历section header table中的所有项,将section name 不是".init"开始的seciton划归为CORE section,并且修改HDR视图中Seciton header table中对应entry的sh_entrsize,用来记录当前section在CORE section中的偏移量。
entry[i].sh_entsize = mod->core_size;
mod->core_size += entry[i].sh_entsize; 同理,
mod->core_txt_size 记录code section的大小;
mod->init_size 记录INIT sectioin
mod->init_text_size 记录INIT sectioin里的code section;
对section进行搬移前,layout_symtab函数处理符号表:
如果内核没有启用CONFIG_KALLSYMS(内核映像是否保留所有符号,保留的代价是变大许多),layout_symtab是一个空函数;
由于在内核模块的ELF文件中,符号表所在的section没有SHF_ALLOC标志,所以layout_sections不会把符号表section划到CORE section或INIT section中,所有要通过另一个函数layout_symtab把符号表搬移到CORE section中。
对内核模块的ELF进行了CORE和INIT划分后,内核调用vmalloc相关的函数为CORE 段和INIT段分配内存,基地址分别记录在:
mod->module_core、mod->module_init中,然后把对应的段数据搬移到CORE 段和INIT段的最终位置上。显然需要改写HDR视图中 Section header table中对应的entry的sh_addr,以使其指向最终地址。
mod要指向新的内存地址:".gnu.linkonce.this_module" seciton是一个SHF_ALLOC标志的可写数据段,也会被搬移到CORE section中,所有mod要指向新的内存地址:
mod=(void*)entry[modindex].sh_addr;
HDR视图某一些段搬移的原因:因为模块加载结束后,系统会释放HDR视图区,模块初始化完成后,INIT section内存区也被释放。
所以当一个模块加载后,最终留下的是CORE 段中的内容,CORE段中的数据是模块整个生命周期会用到的数据。
模块导出符号:
模块导出符号所用的宏和内核导出符号完全一样:
EXPROT_SYMBOL
EXPROT_SYMBOL_GPL
EXPROT_SYMBOL _FUTURE
导出的符号放在:"__ksymtab" 、" __ksymtab_gpl"、"__ksymtab_gpl_future" section中;
在HDR视图中的section搬移到最终的CORE seciton和INIT section之后,内核通过对HDR视图中的Seciton header table查找,获得"__ksymtab" 、" __ksymtab_gpl"、"__ksymtab_gpl_future"在CORE seciton中的地址,记录在mod的成员中:
i=find_sec(...,""__ksymtab,...);
mod->sysms= entry[i].sh_addr;
.....
mod->sysms_gpl=
mod->gpl_future_sysms=
这些变量供内核查找符号时用:find_symbol 函数;
find_symbol 函数:
1、构造被查找模块的参数fsa;
2、调用each_symbol函数:
a 遍历内核导出符号表,对每一项调用find_symbol_in_section(),找到则通过传进来的*data指针,返回到上层函数;
find_symbol_in_section函数会处理未解决的引用,并做以下匹配:
符号分:GPL_ONLY, WILL_BE_GPL_ONLY, non-GPL module不能使用GPL_ONLY类型的那些符号;
b 遍历系统已经加载的模块(所有模块的链表全局变量modules中)导出的符号表;
前提:模块加载后,表示该模块的struct module 类型的变量mod需要加入到全局变量链表modules里;
模块导出的符号记录在mod的相关成员中;
动作: 对每一个模块构造一个符号数组arr,然后在其中找相应的符号;
simplify_symbols函数( 未解决的引用符号的处理):
链接工具会把链接过程中找不到的符号(如printk)标记为未解决的引用的符号,当模块加载时,在内核其他加载模块导出符号表中查找这个符号,形成正确的调用;
符号表项结构:
typedef struct elf32_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;
} Elf32_Sym;
符号表seciton就是由上述结构组成的数组;
st_shndx的值把符号分为以下三类型:
1、SHN_ABS:绝对地址,不能重定位;
2、SHN_UNDEF:未解决的引用,需要重新找到正确地址;
3、default :一般符号,在本模块中能够找到的符号;
该函数遍历符号表数组,算出所有符号的st_value值,对于未解决的符号,调用find_symbol函数(在内核符号表和模块符号表)查找正确地址;
重定位:
到目前为止,各符号表项里的内容还是静态链接时写入的内容;
如果模块有导出符号,编译工具链会在该模块的ELF文件生成一个seciton:“.rel_ksymtab",他专门用于对"__ksymtab“section的重定位。叫relocation section;
typedef struct elf32_rel {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
重定位函数:
static int apply_relocations(Elf32_Shdr *sechdrs,
const char *strtab,
unsigned int symindex,
unsigned int relsec,
struct module *me)
功能:根据导出符号所在secton的relocation section,结合导出符号表section,修改导出符号的地址为在内存中的最终地址,到此内核模块导出的符号已经重定位完成。
模块参数:
insmod demodev.ko dolphin=10 bobcat=5
为了能正确接收参数,内核模块本身源码必须用module_param 宏声明;
#include <linux/mousule.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("dophin=%d,bobcat=%%d\n",dolphin,bobcat);
return 0;
}
static void demodev_exit(void)
{
printk("+demodev_exit!\n");
}
module_init(demodev_inti);
module_exit(demodev_exit);
内核加载器对模块参数的初始化发生在模块初始化函数demodev_init调用之前,所有在demodev_init中已经可以得到从命令行传过来的实际参数了。
module_param(dolphin,int,0)在“__param" 的section中定义一个类型为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见copy数据,最后的union为指向参数的指针;
还有另外两个宏用来定义字符串和数组类型的参数:module_param_array 和 module_param_string。
load_module 函数中,通过strndup_user的调用,将用户空间的参数复制到内核空间:args = strndup_user(uargs,~0UL>>1);
参数的最终内核空间地记录在module的成员struct kernel_param *kp中。
模块间的依赖关系:
假设:mod_A、mod_B依赖模块owner,那么mod_A、mod_B可以通过遍历其targer_list成员知道所依赖的所有模块,owner可以通过遍历其source_list成员知道所有依赖于自己的模块;
当系统中卸载一个模块时,系统必须确保没有其他模块依赖于该模块,即判断source_list是否为空?
模块版本控制:
内核和内核模块都需要配置CONFIG_MODVERSION,每一个导出接口都会生成一个CRC的校验和,当系统解决“未解决的引用”时,会通过check_version()函数完成版本检测;
模块信息:
通过MODULE_INFO宏来定义要添加的模块信息;
模块的license:
模块的license在模块的源码中以MOEdULE_LICENSE的宏引出;
模块的vermagic:
内核和内核模块的vermagic都是通过MODULE_INFO定义的一个VERMAGIC_STRING字符串,后者是一个生成字符串的宏,会根据不同的内核配置信息生成不同的字符串。模块加载过程中,会检查模块中的vermagic是否和当前运行的内核定义的vermagic一致,如果不一致,加载失败。
sys_init_module第二部分:
load_module做完所有的加载工作后,返回sys_init_module,后者接下来做:
1、调用模块的初始化函数;成功后mod->state = MODULE_STATE_LIVE; 状态修改为
2、释放INIT section所占用的空间;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);
.......
}
模块成功加载后,全局变量modules 记录了一个链表;runtime的关系图如下:
3、呼叫模块通知链;
module_notify_list是众多内核通知链中一条,当一个特定内核事件发生时,事件所属的内核组件负责遍历通知链的所有节点,调用节点上的回调函数;
module_notify_list是一个全局变量;
函数register_module_notifier 向内核注册一个节点,该节点对象包含一个回调函数;
函数unregister_module_notifier 注销一个节点;
函数blocking_notifier_call_chain 通知module_notify_list上的各个通知节点;(MODULE_STATE_LIVE和MODULE_STATE_GOING都会被通知到各个节点)
模块的卸载:
命令:rmmod demodev
系统调用:sys_delete_module
1、首先将来在用户空间的要卸载的模块名复制到内核空间;
2、调用find_module函数在链表modules中查找要卸载的模块,返回该模块的mod结构;
3、检查模块间的依赖关系;其source_list 是否为空?不为空,则不能卸载;
4、调用free_module 做一些清理工作(如更新模块状态MODULE_STATE_GOING,从链表里摘除,释放模块占用的CORE section空间,释放从用户空间接收的参数空间等);