内核学习之一:Linux内核模块技术探秘

本文深入探讨了内核模块的加载过程、模块间依赖关系、参数传递、符号导出与重定位、版本控制及模块信息等内容。通过具体实例展示了模块如何在内核环境中导入、导出符号并进行参数化配置。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

加载一个模块命令:

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空间,释放从用户空间接收的参数空间等);

 

 

 

 

 

 

 

     

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

          

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值