深入linux设备驱动程序内核机制(第一章) 读书笔记

来源:互联网 发布:mac os 10.12.1 beta5 编辑:程序博客网 时间:2024/04/29 09:16
第一章  内核模块
1.1 内核模块的文件模式

   本文 欢迎转载,
原文地址: blog.csdn.net/dyron


    . 内核模块都是ko形式存在的, 类似于动态库的so, 数据组织形式上是elf(Executable and
      Linkable Format)格式, 是一种普通的可重定位的目标文件。

    . elf模式中,我们关心头部的elf header, 中间的section和尾部的section header table.
    elf header:  大小52byte, 位于文件头部。
    section:    elf文件的主体, 当模块被内核加载时, 内核会动态的重新分配内存区域(当然
        其中的辅助性section不会占实际的内存空间)
    setction header table: 部于文件未尾, 由若干个section header entry组成。

1.2 EXPORT_SYMBOL的内核实现

    . EXPORT_SYMBOL宏功能的完整实现需要三部分达成: EXPORT_SYMBOL宏定义部分, 链接脚本
    链接器部分和使用导出符号部分。

    . EXPORT_SYMBOL的宏定义
    /* For every exported symbol, place a struct in the __ksymtab section */    #define __EXPORT_SYMBOL(sym, sec)                               \            extern typeof(sym) sym;                                 \            __CRC_SYMBOL(sym, sec)                                  \            static const char __kstrtab_##sym[]                     \            __attribute__((section("__ksymtab_strings"), aligned(1))) \            = MODULE_SYMBOL_PREFIX #sym;                            \            static const struct kernel_symbol __ksymtab_##sym       \            __used                                                  \            __attribute__((section("___ksymtab" sec "+" #sym), unused))     \            = { (unsigned long)&sym, __kstrtab_##sym }    #define EXPORT_SYMBOL(sym)                                      \            __EXPORT_SYMBOL(sym, "")    #define EXPORT_SYMBOL_GPL(sym)                                  \            __EXPORT_SYMBOL(sym, "_gpl")    #define EXPORT_SYMBOL_GPL_FUTURE(sym)                           \            __EXPORT_SYMBOL(sym, "_gpl_future")

    实际是实义了一个static const char类型和一个kernel_symbole的结构体, 与常用
    变量不同的是, char放在了__ksymtab_strings的section中, kernel_symbole结构体放在了
    ___ksymtab的section中, 链接器会将___ksymtab的section放在映像文件中的___ksymtab中,
    这部分工作是在lds链接脚本中做的。

1.3 模块加载过程
    . 在insmod向内核空间安装一个内核模块时, insmod会利用文件系统的接口将ko的数据读到用户
    空间的一段内存中, 然后通过sys_init_module让内核去处理模块加载的整个过程。
    
    1.3.1 sys_init_module
    asmlinkage long sys_init_module(void __user *umod, unsigned long len, const char      __user *uargs);

        第1个参数指向用户空间内存地址,第2个参数指示文件大小, 第三个参数指向传给模块的参数地址
    实际的加载动作由load_module来完成, 参数与sys_init_module完全相同。

    1.3.2 struct module
    load_module的返回值是一个struct module类型的指针, 一个struct module对象代表着现实中的一个内核
    模块.
 
    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];    ................    }

    1.3.3 load_module
    
    sys_init_module调用load_module, 将内核空间利用vmalloc分配一块大小等于模块的地址空间, 将用
    户空间的数据复制到内核空间。
    
        字符串表是elf文件中的一个section, 用来保存elf文件中各个section的名称或符号表名.
    计算section名称的方法
    char *secstrings = (char *)hdr + entry[hdr->e_shstrndx].sh_offset.
    计算符号表名称字符串表的基地址的方法
    
    .遍历section header table中的所有entry, 找一个entry[i].sh_type = SHT_SYMTAB的entry, SHT_SYM
    TAB    表明这个entry所对应的section是一个符号表。
    
    .entry[i].sh_link是符号名称字符串表在section在section header table中的索引, 所以基地址就为
    char *strtab = (char*)hdr+entry[entry[i].sh_link].sh_offset

    至此load_module通过以上计算就可以获得section名称字符串表的基地址secstrings和符号名称字符串表
    的基地址strtab.


    HDR视图的第一次改写
    
    开始遍历section header table中所有的entry, 进行如下改动,以将entry的sh_addr指向HDR中的实际地
    址。
    entry[i].sh_addr = (size_t)hdr+entry[i].offset

    如果发现CONFIG_MODULE_UNLOAD宏没有定义, 表示不支持动态卸载,就将.exit的sh_flags中SHF_ALLOC
    标志位清除掉。

    find_sec函数

    find_sec用来寻找某一section在section header table中的索引值,原型为

    static unsigned in find_sec(Elf_Ehdr *hdr, Elf_Shdr *sechdrs, const char *secstrings,const char *name);

    返回值为section的索引,未找到返回0. 前两个参数为elf header, section header, 第三个为
    secstrings, 第四个为要查找的section的name.

    遍历section header table所有的entry, 对每一个entry,先找到section name 然后与第四个参数比较
    ,相等就返回该section在section header table中的索引值。

    struct module类型变量mod初始化
    load_module函数中定义了一个struct module类型的变量mod, 该变量的初始化是通过模块elf文件中的一
    个名为".gnu.linkonce.this_module"的section来完成的。

    gcc的别名技术, (__attribute__(alias)), int init_module(void)__attribute__((alias(#initfn)));

    HDR视图的第二次改写
    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视图中对应的sh_entsize.

    CONFIG_KALLSYMS是一个决定内核映像中是否保留所有符号的选项。

    模块导出的符号
    内核在把HDR视图中的section搬移到最终的core section和init section后, 内核会查找HDR视图中
    section header table, 获得__ksymtab, __ksymtab_gpl和__ksymtab_gpl_future section在core
    section的地址, 记录在mod->syms, gpl_syms, gpl_future_symc中。

    find_symbol函数
    find_symbol是用来查找一个符号
    const struct kernel_symbol *find_symbol(const char *name, struct module **owner,                        const unsigned long **crc, bool gplok, bool warn);

    第一个参数表示要找的符号名, 第二个参数表明符号可能所在的模块。
    struct symsearch中的enum licence,
    GPL_ONLY表示符号只提供给满足GPL协议的模块使用.  [EXPORT_SYMBOL_GPL]
    NOT_GPL_ONLY表示不一定只给满足GPL协议的模块使用. [EXPORT_SYMBOL]
    WILL_BE_GPL_ONLY表示将来只提供给满足GPL协议的模块使用。[EXPORT_SYMBOL_GPL_FUTURE]

    find_symbol_arg中,通过each_symbol函数查找,第一部分在内核导出的符号表中查找对应的符号, 找
    到就返回对应的符号信息, 否则,再进行第二部分查找,在系统已加载的模块中查找(以一个链表的形
    式存在).

?????? 链接器将kernel链接到固定地址, 且将__ksymtab_gpl等变量链接到kernel的固定地址+偏移

    find_symbol_in_section函数, 首先比较当前kernel_symbol结构何事的name是否匹配, 再查fsa->gplok
    , fas->warn如果查找到的模块需要GPL权限, 如果不具有GPL权限就算名称匹配也不会成功。

    find_symbol的第二部分,
    模块成功加载到系统之后, 表示模块的struct module变量mod加入到modules中,一个全局的链表变量,
    记录系统中的模块

    对“未解决的引用”符号的处理
    simplify_symbols函数的代码实现, 函数首先通过一个for循环遍历符号表中的所有符号, 根据Elf_sym
    的st_shndx查找到SHN_UNDEF, 这就是未解决的引用, 调用solve_symbol来处理, 后者会调用find_symb
    ol来查找符号, 找到了就将它在内存中的实际值赋值给st_value, 这样未解决的引用就有了正确的内存
    地址。

    重定位
    重定位主要用来解决静态链接时的符号引用与动态加载时实际符号地址不一致的问题.
       如果模块有用EXPORT_SYMBOL导出符号, 那么这个模块的elf文件会生成一个独立的section:".rel_ksymta
       b", 它专门用于对"__ksymtab" section的重定位, 称为relocation section.

       apply_relocations是用来执行重定位的代码, 其中用for循环来遍历HDR视图中section header table中
       所有的entry, 需要重定位的section, 其entry中的sh_type值为SHT_REL/SHT_RELA, 分别对应不同的重
       定位方式, section header中的sh_info指明被重定位的section在SHT中的索引值。

       在遍历中,如果发现一个sh_type= SHT_REL的section, 系统就调用apply_relocate函数执行重定位, 根
       据重定位元素中的r_offset以及relocation section header entry中的sh_info得到的需要修改的导出符
       号struct kernel_symbol中value所在的内存地址。
    
       根据重定位元素中的r_info获得符号在符号表中的偏移量, 符号表的section基地址很容易获得, 于是就
       可以获得需要重定位的符号在符号表中对应的elf32_sym型元素。 所以导出的符号地址被修改。

    模块参数
    模块必需使用module_param宏声明模块可以接收的参数, 如module_param(dolphin, int, 0), 内核模块
    加载器对参数的构造过程发生在模块初始化函数之前, 所以在模块构造函数中就可以得到命令行传过来
    的实际参数。

    命令行的参数值复制到模块的参数过程中, module_param宏定义的"__param"section起到桥梁的作用,
    通过"__param"section, 内核找到模块中定义参数所在内存地址, 然后用命令行中参数值修改之。

    模块间的信赖关系
    struct list_head source_list和struct list_head target_list 用来构建有信赖关系模块的链表, 对
    链表的使用要结合数据结构struct module_use:

    如果有依赖关系, 在solve_symbol函数中将会导出这"未解决的引用"符号模块记录在变量struct module
           *owner中, 调用ref_module(mod, owner)在模块mod和owner之间建立信赖关系。 ref_module做过检查后
    调用add_module_usage(mod, owner)在mod和owner模块间建立信赖关系.

    模块的版本控制
    linux系统对此的解决方案是使用接口的校验和,也叫接口crc校验码, 为了确保这种机制能够正常运行
    , 内核必须启用CONFIG_MODVERSIONS宏, 模块编译时也需启用此宏, 否则加载不成功。

    这样将会启用__CRC_SYMBOL宏, __kcrctab_my_exp_function用来保存__crc_my_exp_function变量地址
    并将其放在一个名为__kcrctab的section中. 可见如果内核启用了CONFIG_MODVERSIONS宏, 每一个导出
    的符号都会生成一个crc校验码
    check_version用一个for循环在__versions section中进行遍历, 对每一个struct modversion_info元
    素和找到的符号名symname进行匹配, 如果匹配成功就进行接口的校验码比较。

    独立编译的内核模块,如果引用到了内核导出一符号, 在模块编译的过程中, 工具链会到内核源码所
    在目录下查找Module.symvers文件, 将得到的printk的crc校验码记录到模块的__versions section中。

    模块的信息
    模块的最终elf文件中都会有一个名为.modinfo的section, 这个section以广西的形式保留着模块的一些
    相关信息。
    可以使用modinfo工具来查看模块的信息。

    模块的license. MODULE_LICENSE();
    
    1.3.4 sys_init_module(第二部分)
    
    调用模块的初始化函数, 模块可以不提供初始化函数。
    
    释放init section所占用的空间, 模块一旦被加载成功,HDR视图和INIT section所占的内存区将不会再
    被用到,因此需要释放它们。 module_free将执行释放,module_free将调用vfree来释放INIT section.

    呼叫模块通知链:
        通过通知链,模块或者其它内核组件可以对向其感兴趣的一些内核事件进行注册, 当该事件发生时
        , 这些模块或组件的回调函数将会被调用。
        
        module_notify_list就是内核中从多通知链中的一条, 通过register_module_notifier向内核注册
        一个节点对象,该节点对象中包含一个回调函数, 注册后, 系统中所有的模块相关事件发生时都会
        调用到这个回调函数。
        
    1.3.5 模块的卸载
    .find_module函数
        sys_delete_module函数首先将用户空间的模块名用strncpy_from_user复制到内核空间, 然后调用
        find_module函数在内核模块链表modules中利用name来查找要卸载的模块。

    .检查模块信赖关系
        检查信赖链表, 如果不存在信赖模块,即可卸载。

        .free_module函数
        做清理工作, 包括更新模块的状态为MODULE_STATE_GOING, 将模块从modules链表中移除, 将模块
        占用的CORE section空间释放, 释放模块从用户空间接收的参数所占空间等。


本文 欢迎转载,

原文地址: blog.csdn.net/dyron


1.4 本章小结
    系统中所有的模块都以链表的形式被加入到内核当中, 模块编译时需要指定一个内核源码树, 这不是出
    于链接的需要, 而是模块需要内核源码头文件中的一些定义, 包括以头文件形式出现的内核配置信息。
    因此.ko文件总是基于一个特定的内核源码树构成, 所以在不同版本的内核中运行.ko, 有可能会引用潜
    在问题, 为了防止这一个问题, 内核引入了版本控制机制。


原创粉丝点击