LINUX内核模块

《深入LINUX设备驱动程序内核机制》第一章笔记

    驱动菜鸟,基本上(99.9%)照抄原文。

内核模块:可以在系统运行期间动态扩展系统功能而无须重新启动系统,更无须为这些新增的功能编译一个新的系统内核映像。

1.1 内核模块的文件格式

        .ko格式的内核模块,其文件数据组织形式是ELF格式,即普通的可重定位目标文件。LINUX中常见的可执行程序都是以ELF的形式存在。

    ELF文件总体上分为三大部分:头部的ELF header,中间的Section和尾部的Section header table。


    ELF header: 52字节。

    Section: ELF文件主体,当模块被内核加载时,会根据各自属性被重新分配到新的内存区域。而有些section可能只起到辅助作用,运行时不占用实际的内存空间。

    Section header table: 位于文件视图的末尾,由若干个Section header entry 组成,每个entry具有相同的数据结构类型。

1.2 EXPORT_SYMBOL的内核实现

    内核模块不可避免的要调用内核函数,作为独立编译链接的内核模块,必须要解决这种静态链接无法完成的符号引用问题(ELF文件中称为“未解决的引用”),处理“未解决的引用”问题的本质是在模块加载期间找到当前“未解决的引用”符号在内存中的实际目标地址。内核和内核模块通过符号表的形式向外部导出符号的相关信息,即使用EXPORT_SYMBOL宏。

    使用EXPORT_SYMBOL(my_exp_function)导出符号“my_exp_function”,实际上是通过struct kernel_symbol的一个对象告诉外部世界关于这个符号的两点信息:符号名称和地址。

    由EXPORT_SYMBOL导出的符号,与一般变量定义唯一的不同点在于,它们被放到了特定的section中。对于这些section的使用要经过一个中间环节,即链接脚本与链接器部分。例如链接脚本会告诉链接器,将目标文件中的名为“__ksymtab”的section放置在最终内核(或者是内核模块)映像文件的名为“__ksymtab”的section中。

    把所有的向外界导出的符号统一放到一个特殊的section里面,是为了在加载其他模块时用来处理那些“未解决的引用”符号。链接器声明了一些变量,而内核为使用这些链接器的变量也做了声明,这些变量会在对内核或者内核模块的导出符号表进行查找时用到。

1.3 模块的加载过程

    在用户空间,使用insmod这样的命令向内核空间安装一个内核模块。当调用“insmod demodev.ko”这样的命令来加载demodev.ko内核模块时,insmod会首先利用文件系统的接口将其数据读取到用户空间的一段内存中,然后通过系统调用sys_init_module让内核去处理模块加载的整个过程。

1.3.1 sys_init_module


    umod指向用户空间demodev.ko文件映像数据的内存地址,len是数据大小,uargs是传给模块的参数所在的用户空间的内存地址。

    而在sys_init_module中,可以分为两个部分。

    第一部分,加载模块的最核心的任务通过load_module函数完成。


    load_module完成之后再进行第二部分的后续处理。


    load_module的返回变量是struct module类型,下面介绍。

1.3.2 struct module结构体

    struct module结构体代表一个模块在linux系统中的抽象。





1.3.3 load_module



*模块ELF静态的内存视图(HDR)

    insmod将demodev.ko放到用户空间的内存中,然后通过系统调用sys_init_module进入到内核态,sys_init_module调用load_module,load_module在内核空间利用vmalloc分配一块同样为len的地址空间,然后通过copy_from_user将用户空间的数据复制到内核空间,从而在内核空间构造处理demodev.ko的一个ELF静态的内存视图(HDR).

    HDR视图所占用的内存在load_module结束时通过vfree释放。


*字符串表

    字符串表(string table)是ELF文件中的一个section, 用来保存ELF文件中各个section的名称或符号名。这些名称以字符串的形式存在。


    驱动模块所在的ELF文件中,一般有两个这样的字符串表section,一个用来保存各section名称的字符串,另一个用来保存符号表中的每个符号名称的字符串。


    load_module通过计算获得section名称字符串表的基地址secstrings和符号名称字符串表的基地址strtab,留作将来使用。

*HDR视图的第一次改写

    在获得了section名称字符串表的基地址secstrings和符号名称字符串表的基地址strtab后,load_module函数第二次遍历Section header table中的所有entry, 将每个entry中的sh_addr改写为对应的section在HDR视图中的实际地址。若未定义CONFIG_MODULE_UNLOAD宏,则清除名称为.exit的section对应的entry的sh_flags里面的SHF_ALLOC标志位。

    本次改写只修改了section header table 中的某些字段,其他方面没有任何变化。

*find_sec函数

    

    内核用find_sec函数来寻找某一section在section header table中的索引值。
    对每一个entry,find_sec先遍历section header table中所有的entry,找到其对应的section name, 然后和第四个参数name进行比较,如果相等,就找到对应的section,返回该section在section header table中的索引值。
    对HDR视图第一次改写后, 内核通过调用find_sec,查找到“.gnu.linkonce.this_module”,"__versions"和“.modinfo”。查找到的索引值分别保存在变量modindex、versindex和infoindex中,备用。

*struct module类型变量mod初始化
   模块的编译工具链为我们安插了一个“.gnu.linkonce.this_module”section, 并初始化了其中一些成员。在模块加载过程中,load_module将利用这个section中的数据来初始化mod变量。
    使用modindex索引值得到 “.gnu.linkonce.this_module”section的实际地址:
        mod = (void *)sechdrs[modindex].sh_addr;
   在第一次改写HDR视图的基础上,mod指向了实际的struct module所在的内存地址;在第二次改写HDR视图之后,mod将会重新指向其在内存中的最终地址。

*HDR视图第二次改写
  这次改写中,HDR视图绝大多数的section被搬移到了新的内存空间中,之后会根据这些新的内存空间,重新修改HDR视图,使其中的section header table中各entry的sh_addr指向新的也是最终的内存地址。
    内核中layout_sections函数用来做这件事,遍历HDR视图中的每一个section,对每一个标记有SHF_ALLOC的section,将其划分为CORE和INIT两大类。section name不是以“.init”开始的section划分为CORE section,属于INIT section的section,其name必须以“.init”开始。
    如果内核启用了CONFIG_KALLSYMS,内核会使用layout_symtab函数将模块的符号表section(非SHF_ALLOC类型)搬移到CORE section,方便调试,缺点是增大了映像体积和内存消耗。

    模块加载过程结束时,系统会释放掉HDR视图所在的内存区域。不仅如此,在模块初始化完成之后,INIT section所在的内存区域也会被释放掉。最终留下的只有CORE section中的内容,CORE section中的数据是模块在系统中整个存活期会用到的数据。

*模块导出的符号

    模块导出的符号使用的宏和内核导出符号所使用的完全一样:EXPORT_SYMBOL、EXPORT_SYMBOL_GPL和EXPORT_SYMBOL_FUTURE。内核模块会把导出的符号分别放到“__ksymtab”、“__ksymtab_gpl”和“__ksymtab_gpl_future”section中。模块便宜工具链负责生成这些导出符号section,这些section都带有SHF_ALLOC标志,加载过程中会被搬移到CORE section区域中。

    内核需要对模块导出的符号进行管理,以便解决那些“为解决的引用”符号。为此用到了struct module结构体变量mod。内核通过使用函数find_sec对HDR视图中Section header table查找,获得“__ksymtab”、“__ksymtab_gpl”和“__ksymtab_gpl_future”section在CORE section中的位置,将其记录在mod->syms、mod->gpl_syms和mod->gpl_future_syms中。如此内核将可以得到模块导出符号的所有信息。


    在find_symbol函数中可以看到这些变量的具体用途。

*find_symbol函数

    该函数用于查找一个符号。首先构造一个struct find_symbol_arg 格式的标志参数fsa,然后通过each_symbol来查找符号。each_symbol是用来进行符号查找的主要函数。

    总体上each_symbol函数可以分成两个部分:第一部分是在内核导出的符号表中查找对应的符号;第二部分是在系统中已加载的模块(全局变量modules)的导出符号表中查找。如果找到就通过fsa返回该符号的信息,否则函数返回flase。


*对“为解决的引用”的符号的处理

    在模块加载的过程中,内核函数simplify_symbols函数用来为当前加载的模块中所有“为解决的引用”符号产生正确的目标地址。

*重定位 

    解决静态链接时的符号引用与动态加载时实际符号地址不一致的问题。

*模块参数

    模块内部使用module_param声明一个参数。声明参数之前要先定义参数变量。


    模块加载时,可以通过诸如如下命令向模块传递一些参数:

        insmod demodev.ko dolphin=10 bobcat=5

    


*模块间的依赖关系

(        )

*模块的版本控制

    CONFIG_MODVERSIONS启用。CRC校验码,根据函数的参数生成一个大小为4字节的CRC校验码,当双方校验码相等时视为相同接口,否则为不同接口。


*modinfo

    模块中用MODULE_INFO宏向“.modinfo”section添加模块信息。

    内核中使用get_modinfo函数来获得tag所在字符串的值info。

*模块的license

    模块中以MODULE_LICENSE宏引出。

    内核模块加载过程中,会调用license_is_gpl_compatible来确定模块的license是否与GPL兼容。

    不符合要求的模块加载进系统后会导致内核污染。内核用mod对象的unsigned int taints成员记录一个模块是否会污染内核。

    而对于运行中的系统是否已经被污染,内核用一个unsigned long型全局变量tainted_mask来表示。在系统因故障挂起的时候,tainted_mask会影响系统调试信息的输出,以告知内核是否已被污染。

    另外,non-GPL的模块无法使用内核或其他模块用EXPORT_SYMBOL_GPL导出的符号。加载这样的模块时会出现“Unknown symbol in module”类似的错误信息。

*模块的vermagic

    内核模块中用来产生vermagic的MODULE_INFO是通过scripts/mod/modpost.c文件自动生成的,内核模块开发者无须在源码中显式地添加这一信息。

    内核和内核模块的vermagic都是通过MODULE_INFO定义的一个VERMAGIC_STRING字符串,后者实际上是一个生成字符串的宏,该宏会根据不同的内核配置信息生成不同的字符串。

    模块加载过程中会检查模块中的vermagic是否和当前运行的内核定义的vermagic一致,如果不一致将加载失败(load_module函数中)。


1.3.4 sys_init_module(第二部分)

    load_module做完所有的艰苦工作之后,重新返回到sys_init_module,后者在load_module的基础上所做工作就很简单了(反正我读到这里是不相信的)。

*调用模块的初始化函数

    do_one_initcall(mod->init)

    如果模块初始化函数被成功调用,那么模块就算是被加载进了系统,因此需要更新模块的状态为MODULE_STATE_LIVE。

*释放INIT section所占用的空间

    模块一旦被成功加载,HDR视图和INIT section所占的内存区域将不会被用到,因此需要释放它们。在sys_init_module函数中,释放INIT section所在内存区域由函数module_free完成,module_free调用vfree来释放INIT section区域(mod->module_init)。

    模块成功加载之后如图:

       

    内核用一全局变量modules链表记录系统中所有已加载模块。

*呼叫模块通知链

    通过通知链,模块或者其他内核组件可以对其感兴趣的一些内核事件进行注册,当该事件发生时,这些模块或者组件当初注册的回掉函数将会被调用。内核模块机制中实现的模块通知链struct blocking_notifier_head module_notify_list就是内核中众多通知链中的一条。

    内核模块可以调用register_module_notifer向内核注册一个节点对象,该节点对象包含一个回掉函数。当成功注册之后,系统中所有那些模块相关的事件发生后,都会调用这个回调函数。

    

    内核模块加载的过程中,sys_init_module函数在调用完load_module之后,会通过blocking_notifier_call_chain函数来通知调用链module_notify_list上的各节点,一个模块正被加入系统(MODULE_STATE_COMING)。

    对于模块加载的其他阶段(MODULE_STATE_LIVE和MODULE_STATE_GOING),内核模块加载器也都会调用blocking_notifier_call_chain函数来通知module_notify_list上的各个节点。

1.3.5模块的卸载

    rmmod demodev

    rmmod通过系统调用sys_delete_module来完成卸载工作。

    sys_delete_module首先通过strncpy_from_user函数将欲卸载的模块名name复制到内核空间,然后调用find_module函数在内核维护的modules链表中遍历每一个mod结构,利用模块名name来查找要卸载的模块。

    如果sys_delete_module查找到了想要卸载的模块,接下来检查是否有其他模块依赖于当前模块(检查要卸载的模块的source_list链表是否为空)。为了系统稳定,一个被依赖的模块不应该被卸载掉。

    如果无其他模块依赖于当前模块,一切正常,则调用free_module函数来做模块卸载末期的一些清理工作:更新模块的状态为MODULE_STATE_GOING,将卸载的模块从module链表中移除,将模块占用的CORE section空间释放,释放模块从用户空间接收的参数所占的空间等。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值