再谈内核模块加载(一)—背景知识

版权声明:本文为CSDN博主「ashimida@」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lidan113lidan/article/details/119743237

更多内容可关注微信公众号  

  几年前写过一篇内核模块加载的文章<内核模块的加载> ,最近又基于5.2内核重新读了一遍模块加载的流程,此文则结合一些新的认知重新整理而成.


1.内核模块的编译分为两个阶段,可分别称为stage1和stage2:

  • stage1主要负责将模块源码(*.c)编译为对应的目标(x.o)文件
  • stage2主要负责对每个模块生成一个.mod.c文件,其中记录了ko所需要的其他信息,将其编译为.mod.o,最终同模块的x.o共同链接(ld -r)为一个目标文件,这个目标文件即为最终的可加载模块(x.ko)
  其中 stage2是个批量操作,即不论内核要编译多少个ko,都会通过一次stage2全部完成;
  在stage2中会调用modpost程序,其输入是所有模块的*.o和内核的vmlinux,其输出是为每个*.o生成对应的*.mod.c文件,且将内核和内部模块中所有EXPORT_SYMBOL_XXX的符号信息都输出到Module.symvers中(如果开启CONFIG_MODVERSIONS则同时输出符号的CRC值). 
  Modules.symvers的作用主要是在外部模块编译时告知外部模块当前内核有哪些导出符号,以及这些导出符号的CRC值.
  模块编译的细节可参考

2.mod.c文件的内容

  .mod.c文件是在Stage2编译ko时批量生成的,Stage2会调用modpost工具(同时一次传入所有要处理的模块的*.o文件);modpost工具中会为所有输入的%.o(除了vmlinux.o)生成对应的%.mod.c文件,其内容包括:
  1) 默认的.mod.c文件头(main=>add_header)
    这里主要包括三个内容:
    * 在.modinfo段增加字符串变量vermagic ,其内容为"vermagic=$(VERMAGIC_STRING)", 记录内核版本信息 ,如:
tangyuan@ubuntu:~/qemu_aarch64_device/linux-4.19.109$ strings lib/xxhash.ko |grep vermagic
vermagic=4.19.109-g6c232927e-dirty SMP preempt mod_unload modversions aarch64

    * 在.modinfo段增加字符串变量name,其内容为当前模块名,如:

tangyuan@ubuntu:~/qemu_aarch64_device/linux-4.19.109$ strings ./net/ipv6/ipv6.ko|grep "name="
name=ipv6

    * struct module __this_modules,此结构体是一个内置的struct modules,其中记录着模块的初始化函数等信息

      每个模块(ko)二进制中都有一个记录其struct modules的段,段名为".gnu.linkonce.this_modules"。
    源码如下:
//*.mod.c文件都拥有相同的文件头,生成此头文件的代码在./scripts/mod/modpost.c中
#include                                                                                                                      
#include 
#include 
#include 

BUILD_SALT;

//MODULE_INFO(tag,name)宏的作用是在.modinfo段添加变量 字符串变量tag = "tag = info"
//VERMAGIC_STRING为内核版本信息
MODULE_INFO(vermagic, VERMAGIC_STRING);
//KBUILD_MODNAME是cc时传入的参数,其在Makefile.lib中定义:
//modname_flags  = -DKBUILD_MODNAME=$(call name-fix,$(modname))
MODULE_INFO(name, KBUILD_MODNAME);

__visible struct module __this_module
__attribute__((section(".gnu.linkonce.this_module"))) = {
    .name = KBUILD_MODNAME,
    .init = init_module,
    .arch = MODULE_ARCH_INIT,
};
  2) 记录当前是in-tree/out-tree模块
    已经集成在内核中的模块为in-tree模块,外部模块为out-tree模块(在不私自改动源码的情况下),对于in-tree模块要增加(main=>add_intree_flag):
//*.mod.c文件都拥有相同的文件头,生成此头文件的代码在./scripts/mod/modpost.c中
MODULE_INFO(intree, "Y");

    也就是在.modinfo段增加了字符串变量intree,其内容为"intree=Y"(对于out-tree的模块直接没有此字段)

  3) x86等平台若提供retpoline支持(防御spectre攻击),则会增加  MODULE_INFO(retpoline, \"Y\")
  4) 对于drivers/staging目录的模块(测试中模块),会增加 MODULE_INFO(staging, \"Y\");
  5) 若开启 CONFIG_MODVERSIONS(CRC校验), 则在__versions段增加____versions数组记录所有 导入符号 的CRC
    ____versions数组的内容如下:
static const struct modversion_info ____versions[]
__used __attribute__((section("__versions"))) = {
    { 0xe57c29f2, "module_layout" },
    { 0x633475c7, "static_key_enable" },
    { 0x2cdf87a1, "proc_dointvec_minmax" },
    { 0x609f1c7e, "synchronize_net" },
    { 0xa0c9d45, "inet_peer_base_init" },
    ......
};

   内核模块为____versions数组单独分配了一个段__versions,需要注意的是____versions数组中存储的实际上都是当前模块中未定义的,但在内核或其他模块中已定义的符号(若二者都没定义则报错了),____versions数组的作用是在模块装载时确定其未定义符号(导入符号)是否与内核中此符号的定义匹配,故其中并不需要保存当前模块的任何其他符号(如导出符号,EXPORT_SYMBOL_XXX)的信息(导出符号的信息是保存在模块单独的段中的,参考4)

 6) 通过depends字段记录模块的依赖关系
   modpost生成.mod.c的过程中会记录当前模块引用了哪些其他非vmlilnux中的符号,并将这些符号所在的模块名记录到.modinfo段的depends数组中(main => add_depends):
static const char __module_depends[] = "depends=module1,module2,..."

  注意这里的字符串名为 __module_depends,但.modinfo中的字段为depends


3. CRC的生成

  在内核中,并不是所有的符号都是有CRC的,只有显示通过EXPORT_SYMBOL[_GPL|_GPL_FUTURE]定义的符号在开启 CONFIG_MODVERSIONS的情况下才会生成CRC.
  这里以EXPORT_SYMBOL为例,其定义如下:
#define EXPORT_SYMBOL(sym)  __EXPORT_SYMBOL(sym, "")

#define ___EXPORT_SYMBOL(sym, sec)   \
    extern typeof(sym) sym;                        \
    __CRC_SYMBOL(sym, sec)                        \
    static const char __kstrtab_##sym[] __attribute__((section("__ksymtab_strings"), used, aligned(1)))  = #sym;\
    __KSYMTAB_ENTRY(sym, sec)

#if defined(CONFIG_MODULE_REL_CRCS)
......
#else
#define __CRC_SYMBOL(sym, sec)                        \
    asm("    .section \"___kcrctab" sec "+" #sym "\", \"a\"    \n"    \
        "    .weak    __crc_" #sym "                \n"    \
        "    .long    __crc_" #sym "                \n"    \
        "    .previous                    \n");
#endif

#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
......
#else
#define __KSYMTAB_ENTRY(sym, sec)                    \
    static const struct kernel_symbol __ksymtab_##sym        \
    __attribute__((section("___ksymtab" sec "+" #sym), used))    \
    = { (unsigned long)&sym, __kstrtab_##sym }

struct kernel_symbol {
    unsigned long value;
    const char *name;
};
#endif

   EXPORT_SYMBOL(x)中,x是内核的一个函数或变量,此宏实际上做了三件事情:

  1. 在目标文件中添加了一个名为 ____kcrctab+x 的段, 此段中用一个.long长度记录一个弱符号 __crc_x的地址,但这个弱符号并没有定义.
  2. 在目标文件中添加(若不存在)一个名为__ksymtab_string 的段(大多数情况下此段已存在), 并在此段中添加一个字符串变量 __kstrtab_x="x" 的定义.
  3. 在目标文件中生成了一个名为 ____ksymtab+x 的段,并在此段中定义了一个struct kernel_symbol类型的变量 __ksymtab_x = {&x, __kstrtab_x};此变量中记录了x的地址,和x对应的字符串__kstrtab_x的地址.
   也就是说,对于任何一个EXPORT_SYMBOL定义的符号x,最终编译出的目标文件(*.o)中都能看到三个段(这里x以inet6_getname为例):
   * __kcrctab+x
   * __ksymtab_string(公用段)
   * __kstrtab+x
tangyuan@ubuntu:~/qemu_aarch64_device/linux-4.19.109/net/ipv6$ readelf -S --wide ipv6.o |grep -E "inet6_getname|ksymtab_string"|grep -v rela
  [27] ___kcrctab+inet6_getname PROGBITS        0000000000000000 042498 000004 00   A  0   0  1
  [29] ___ksymtab+inet6_getname PROGBITS        0000000000000000 0424a0 000008 00   A  0   0  8
  [57] __ksymtab_strings PROGBITS               0000000000000000 047094 0005da 00   A  0   0  1
   同时静态链接符号表也能看到三个符号信息:
   * __crc_x
   * __kstrtab_x
   * __ksymtab_x
tangyuan@ubuntu:~/qemu_aarch64_device/linux-4.19.109/net/ipv6$ readelf -s --wide ipv6.o |grep -E "inet6_getname"
   209: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT   29 __ksymtab_inet6_getname
   210: 0000000000000066    14 OBJECT  LOCAL  DEFAULT   57 __kstrtab_inet6_getname
  1835: 00000000ae0b6bd1     0 NOTYPE  GLOBAL DEFAULT  ABS __crc_inet6_getname
  2292: 00000000000000c0   296 FUNC    GLOBAL DEFAULT    1 inet6_getname            //原始符号
  前面说过了__crc_x是一个未定义符号,这里ipv6.o中对应的__crc_inet6_getname符号类型应该为UND,但实际上其符号类型却为ABS,这是因为当前看到的ipv6.o并不是刚编译完的ipv6.o(这里称为ipv6_old.o),而是通过链接脚本.tmp_ipv6.ver再次处理的ipv6.o(这里称为ipv6_new.o),这里的逻辑总结如下:
  1) CC编译ipv6.c => ipv6_old.o //其中__crc_x是未定义的(UND)
  2) CC -E ipv6.c |scripts/genksyms/genksyms -r > .tmp_ipv6.ver
    genksyms工具会对源码中所有EXPORT_SYMBOL定义的符号计算CRC,并输出,其输出结果类似:
__crc_blk_queue_flag_set = 0x564adbd5;  
__crc_blk_queue_flag_clear = 0x52d55223;
__crc_blk_queue_flag_test_and_set = 0x5474d529;
__crc_blk_queue_flag_test_and_clear = 0xb029eaf1;

  3) LD -r ipv6_old.o -o ipv6_new.o -T .tmp_ipv6.ver

    这里将 .tmp_ipv6.ver当做链接脚本重新链接ipv6.o,因为链接脚本中有符号定义,所以导致最终生成的ipv6_new.o中__crc_x的定义为ABS
  所以说源码中的导出符号信息是在源码编译为目标文件时就已经记录到对应的% .o中了,且导出符号的CRC值也是在此阶段生成并记录在%.o文件中的 .

4.内核和模块的导出符号表

  在3中可知,在目标文件中实际上已经包含了源码中所有EXPORT_SYMBOL_XXX定义的导出符号的信息,以及对应的CRC值了,而最终链接的内核镜像(vmlinux)或模块(ko)中,导出符号存在的形式和目标文件有所不同:
  * 内核(vmlinux[.o]):
    内核编译时,最终使用的链接脚本是 vmlinux.lds, 其是根据不同平台的vmlinux.lds.S生成的,内容如下:
./include/asm-generic/vmlinux.lds.h
#define RO_DATA(align)  RO_DATA_SECTION(align)

#define RO_DATA_SECTION(align)                        \
......
    __ksymtab         : AT(ADDR(__ksymtab) - LOAD_OFFSET) {        \
        __start___ksymtab = .;                    \
        KEEP(*(SORT(___ksymtab+*)))                \
        __stop___ksymtab = .;                    \
    }    
......
    __kcrctab         : AT(ADDR(__kcrctab) - LOAD_OFFSET) {        \
        __start___kcrctab = .;                    \
        KEEP(*(SORT(___kcrctab+*)))                \
        __stop___kcrctab = .;                    \
    }            
......
     __ksymtab_strings : AT(ADDR(__ksymtab_strings) - LOAD_OFFSET) {    \
        *(__ksymtab_strings)                    \
    }    

./arch/arm64/kernel/vmlinux.lds.S
SECTIONS
{
......
RO_DATA(PAGE_SIZE)
......
}
  * 模块(ko):
    模块最终是通过Stage2的Makefile.modpost中的 cmd_ld_ko_o命令链接出来的,其指定了两个-T脚本(可累加),分别是module-common.lds和module.lds:
./scripts/module-common.lds
SECTIONS {
    /DISCARD/ : {
        *(.discard)
        *(.discard.*)
    }

    __ksymtab        0 : { *(SORT(___ksymtab+*)) }
    __ksymtab_gpl        0 : { *(SORT(___ksymtab_gpl+*)) }
    __ksymtab_unused    0 : { *(SORT(___ksymtab_unused+*)) }
    __ksymtab_unused_gpl    0 : { *(SORT(___ksymtab_unused_gpl+*)) }
    __ksymtab_gpl_future    0 : { *(SORT(___ksymtab_gpl_future+*)) }
    __kcrctab        0 : { *(SORT(___kcrctab+*)) }
    __kcrctab_gpl        0 : { *(SORT(___kcrctab_gpl+*)) }
    __kcrctab_unused    0 : { *(SORT(___kcrctab_unused+*)) }
    __kcrctab_unused_gpl    0 : { *(SORT(___kcrctab_unused_gpl+*)) }
    __kcrctab_gpl_future    0 : { *(SORT(___kcrctab_gpl_future+*)) }
    .init_array        0 : ALIGN(8) { *(SORT(.init_array.*)) *(.init_array) }
    __jump_table        0 : ALIGN(8) { KEEP(*(__jump_table)) }
}

   可以看出,二者实际上都将所有的导出符号表段(___ksymtab+x)合并为一个__ksymtab段,所有的crc段合并为一个__kcrctab段,输出到最终的vmlinux[.o]或ko中了(gpl等其他段同理,这里先忽略)。

   这些都是内存段,最终会加载到内存,唯一的区别就是对于内核来说,其会专门 __start___ksymtab等符号来记录这些段的起始结束位置这是因为模块加载时,内核解析模块后可以在struct modules中记录模块的段信息,而内核启动时没人记录自身信息,故其需要特殊标记来找到段首地址
   故和前面对比(*.o),在.ko和内核(vmlinux[.o])中实际上只是段合并了,但符号信息并未减少:
//模块
tangyuan@ubuntu:~/qemu_aarch64_device/linux-4.19.109/net/ipv6$ readelf -S --wide ipv6.ko |grep -E "inet6_getname|ksymtab_string"|grep -v rela
  [26] __ksymtab_strings PROGBITS        0000000000000000 04745c 0005da 00   A  0   0  1
tangyuan@ubuntu:~/qemu_aarch64_device/linux-4.19.109/net/ipv6$ readelf -S --wide ipv6.ko |grep -E "ksymtab|crc"|grep -v rela
  [10] __ksymtab         PROGBITS        0000000000000000 03f2d0 000128 00   A  0   0  8
  [12] __ksymtab_gpl     PROGBITS        0000000000000000 03f3f8 000160 00   A  0   0  8
  [14] __kcrctab         PROGBITS        0000000000000000 03f558 000094 00   A  0   0  1
  [16] __kcrctab_gpl     PROGBITS        0000000000000000 03f5ec 0000b0 00   A  0   0  1
  [26] __ksymtab_strings PROGBITS        0000000000000000 04745c 0005da 00   A  0   0  1
tangyuan@ubuntu:~/qemu_aarch64_device/linux-4.19.109/net/ipv6$ readelf -s --wide ipv6.ko |grep -E "inet6_getname|ksymtab_string"|grep -v rela
    57: 0000000000000028     0 NOTYPE  LOCAL  DEFAULT   10 __ksymtab_inet6_getname
    58: 0000000000000066    14 OBJECT  LOCAL  DEFAULT   26 __kstrtab_inet6_getname
  1592: 00000000ae0b6bd1     0 NOTYPE  GLOBAL DEFAULT  ABS __crc_inet6_getname
  2049: 00000000000000c0   296 FUNC    GLOBAL DEFAULT    2 inet6_getname

//内核
tangyuan@ubuntu:~/qemu_aarch64_device/linux-4.19.109$ readelf -S vmlinux|grep -E "ksymtab|crc"
  [ 5] __ksymtab         PROGBITS         ffff000008f90550  00f20550
  [ 6] __ksymtab_gpl     PROGBITS         ffff000008f99d88  00f29d88
  [ 7] __kcrctab         PROGBITS         ffff000008fa4e28  00f34e28
  [ 8] __kcrctab_gpl     PROGBITS         ffff000008fa9a44  00f39a44
  [ 9] __ksymtab_strings PROGBITS         ffff000008faf294  00f3f294

5.模块的导入符号表

  从ELF文件来看,内核和模块是没有导入导出表的,因为其本身就没有.dynsym段(目标文件本身就没有,vmlinux静态链接本来也没有,在脚本中又做了dynsym忽略),但内核和模块都统一通过EXPORT_SYMBOL的方式在其二进制的数据段构造了自己的导出表。
  而对于导入表来说,由于内核(vmlinux)中本身是不可以出现未定义符号的,所以其不需要导入表;而对于模块来说,由于其中会使用内核或其他模块中定义的符号,故是需要导入表的。
  实际上在模块加载时,是使用其静态链接重定位表当做导入表的,静态链接符号表中的UND符号实际上就应该是动态链接符号表中的UND符号,也就是所谓的导出表; 在模块加载时,内核会先解决模块静态链接符号表中所有的未决符号,之后处理模块的静态链接重定位表,将模块动态的静态链接到当前内核地址空间
  而如果 内核开启了 CONFIG_MODVERSIONS流程,那么在Stage2的modpost批处理阶段,会在*.mod.c文件中生成____versions数组,此数组中记录了模块%.o的静态链接符号表中所有未定义的符号(也就是模块的导出符号)的CRC(若在modpost阶段没找到此符号定义,则说明内核和模块中都没有此符号定义,报错)

6.关于3-5的总结:

  1) 首先对于ELF文件来说,目标文件(*.o)和静态链接可执行文件(static exe)是默认不生成.dynsym段的
  2) 在ELF文件中,本来是没有导入导出表这一概念定义的(此概念应该来自PE文件),但可以套用PE文件中对应的概念
  3) ELF文件运行时的符号信息都来自.dynsym(动态链接符号表),而静态链接符号表(.symtab)本身是不导入内存的,故导入导出表的概念正常只来自
    .dynsym中的符号
  4) 但正常情况下在静态链接符号表中的UND符号和动态链接符号表中的UND应该是一样的,也就是说只看导入表的话,看静态链接符号表中的UND应该也可以.
  5) 内核和模块的导出表实际上有多个,不同EXPORT_SYMBOL_XXX导出的函数会放到不同的导出表,内核和模块中的导出表全部都是手动由EXPORT_SYMBOL_XXX系列函数指定的.
  6) 下表为ELF文件中各个表的位置,正常对于目标文件来说,尚未链接应该是没有导入导出表的概念的,但内核和模块自己重新定义了导入导出表的概念,如下:
文件格式
DLL/PIE/PDE
static PDE
relocatable
内核vmlinux[.o](relocatable)
模块*.ko(relocatable)
静态链接符号表
.symtab
.symtab
.symtab
.symtab
.symtab
动态链接符号表
.dynsym
导入表
.dynsym中非UND符号
正常无UND符号,为空
未链接,无此概念
正常无UND符号,为空
.symtab中的UND符号
导出表
.dynsym中UND符号
未链接,无此概念
__ksymtab
__ksymtab
内存符号表
kallsyms_names
......
struct module.core_kallsyms
struct module.init_kallsyms
内存导出表
[__start___ksymtab,__stop___ksymtab]
......
struct module.syms
struct module.gpl_syms
导出表访问函数
find_symbol
find_symbol
内存符号表访问函数
kallsyms_lookup_name
kallsyms_lookup_name

你可能感兴趣的:(linux-kernel,内核模块编译,内核模块加载,linux模块加载)