• Linux EXPORT_SYMBOL宏详解


    前言

    内核模块被载入后,就会被动态地连接到内核(与用户空间的动态库类似,只有函数被显式的导出为外部函数后,才可以被动态库调用)。在内核中,导出内核函数需要特殊的指令:EXPORT_SYMBOL宏和EXPORT_SYMBOL_GPL宏。
    在内核代码中我们经常看到EXPORT_SYMBOL宏,用来导出一个内核符号。比如:
    EXPORT_SYMBOL宏:

    // linux-4.10.1/kernel/module.c
    
    int register_module_notifier(struct notifier_block *nb)
    {
    	return blocking_notifier_chain_register(&module_notify_list, nb);
    }
    EXPORT_SYMBOL(register_module_notifier);
    
    int unregister_module_notifier(struct notifier_block *nb)
    {
    	return blocking_notifier_chain_unregister(&module_notify_list, nb);
    }
    EXPORT_SYMBOL(unregister_module_notifier);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    EXPORT_SYMBOL_GPL宏:

    // linux-4.10.1/kernel/module.c
    
    /*
     * Mutex protects:
     * 1) List of modules (also safely readable with preempt_disable),
     * 2) module_use links,
     * 3) module_addr_min/module_addr_max.
     * (delete and add uses RCU list operations). */
    DEFINE_MUTEX(module_mutex);
    EXPORT_SYMBOL_GPL(module_mutex);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    一、EXPORT_SYMBOL简介

    我们都知道Linux 是单内核,作为一个不可分割的静态执行库,内核通常以单个静态的二进制文件形式存放在磁盘中,在一个单独的内核地址空间上运行,由于内核都处在同一内核地址空间,因此内核可以直接调用函数。如果只是单一的内核映像存在,而没有内核模块存在,EXPORT_SYMBOL通常是没有啥意义的,因为对于静态编译链接而成的单一的内核映像而言,所有的符号引用都将在静态链接阶段完成。

    由于Linux支持动态地加载内核模块,运行内核在运行时根据需要动态地加载模块,由于有内核模块的存在,内核模块通常会使用内核映像中的符号,因此使用EXPORT_SYMBOL将内核映像的符号导出,这样在内核模块中就能使用该符号了。

    内核模块是独立编译的,然后加载到正在运行的内核中,当编译内核模块时,编译器引用到内核中的符号时,会产生未解决的引用,处理“未解决引用”问题的就要在模块加载期间找到当前“未解决的引用”符号在内存中的实际目标地址。内核和内核模块通过符号表的形式向外部导出符号的相关信息,这种导出符号的方式在代码层面以EXPORT_SYMBOL宏定义的形式存在。

    解决模块中的未解决引用是在模块的加载阶段,用nm命名查看模块未解决的引用:

     nm nf_conntrack.ko 
    
    • 1

    输出中U代表未解决的引用,对于未解决的引用符号,不显示该符号相对于模块起始地址的相对偏移地址,即没有符号值。
    在这里插入图片描述

    查看模块所有的未定义引用:

     nm nf_conntrack.ko | grep '\'
    
    • 1

    在这里插入图片描述

    二、EXPORT_SYMBOL源码详解

    // linux-4.10.1/include/linux/export.h
    
    struct kernel_symbol
    {
    	unsigned long value;
    	const char *name;
    };
    
    /* 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)))	\
    	= VMLINUX_SYMBOL_STR(sym);					\
    	static const struct kernel_symbol __ksymtab_##sym		\
    	__used								\
    	__attribute__((section("___ksymtab" sec "+" #sym), used))	\
    	= { (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")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    C语言的宏定义中 # 和 ## 运算符:
    #运算符是将其后面的宏参数转化为字符串,用来创建字符串,例如#sym,表示"sym"。
    ##运算符用来替换粘合两个不同的符号,例如__ksymtab_##sym,就表示"__ksymtab_sym"。

    attribute:__attribute__实际上是GCC的一种编译器命令,用来指示编译器执行实现某些高级操作。__attribute__可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute),函数属性可以帮助开发人员向函数声明中添加一些特性,这可以使编译器在错误检查方面增强。

    使用EXPORT_SYMBOL(export_function)导出export_function函数,上述宏可以扩展为:

    static const char  __kstrtab_export_function[] = "export_function";
    static const struct kernel_symbol  __ksymtab_export_function = {(unsigned long)&export_function, __kstrtab_export_function };
    
    • 1
    • 2

    第一个变量是一个字符串静态变量,用来表示导出的符号名称"export_function"。
    第二个变量类型是struct kernel_symbol数据结构,用来表示一个内核符号的实例,struct kernel_symbol的定义为:

    // linux-4.10.1/include/linux/export.h
    
    struct kernel_symbol
    {
    	unsigned long value;
    	const char *name;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    其中,value是该符号在内存中的地址,name是符号名。所以,由该数据结构可以知道,用EXPORT_SYMBOL(export_function)来导出符号"export_function",实际上是要通过struct kernel_symbol的一个对象告诉外部关于这个符号的两点信息:符号名称和地址。这样使得内核根据函数的字符串名称,即可找到匹配的代码地址,在解决未定义的引用时需要这要做。

    因此,由EXPORT_SYMBOL等宏导出的符号,与一般的变量定义并没有实质性的差异,唯一的不同点在于它们被放在了特定的section中。

    上面的符号"export_function"会放在"__ksymtab_strings"的section中,struct kernel_symbol __ksymtab_export_function会放在
    “__ksymtab"的section中。对于EXPORT_SYMBOL_GPL和EXPORT_SYMBOL_GPL_FUTURE而言,其struct kernel_symbol实例所在的section名称则分别为”__ksymtab_gpl"和"__ksymtab_gpl_future")。

    我已 nf_conntrack.ko 模块为例子:

    readelf -S nf_conntrack.ko
    
    • 1

    在这里插入图片描述
    对这些section的使用需要经过一个中间环节,即链接脚本与链接器部分。链接脚本告诉链接器把所有目标文件中的名为“__ksymtab”的section放置在最终内核(或者是内核模块)映像文件的名为“__ksymtab”的section中(对于目标文件中的名为“__ksymtab_gpl”、“__ksymtab_gpl_future”、“__kcrctab”、“__kcrctab_gpl”和“__kcrctab_gpl_future”的section都同样处理)。

    如下所示:

    // linux-4.10.1/include/asm-generic/vmlinux.lds.h
    
    /* Kernel symbol table: Normal symbols */			\
    	__ksymtab         : AT(ADDR(__ksymtab) - LOAD_OFFSET) {		\
    		VMLINUX_SYMBOL(__start___ksymtab) = .;			\
    		KEEP(*(SORT(___ksymtab+*)))				\
    		VMLINUX_SYMBOL(__stop___ksymtab) = .;			\
    	}								\
    									\
    	/* Kernel symbol table: GPL-only symbols */			\
    	__ksymtab_gpl     : AT(ADDR(__ksymtab_gpl) - LOAD_OFFSET) {	\
    		VMLINUX_SYMBOL(__start___ksymtab_gpl) = .;		\
    		KEEP(*(SORT(___ksymtab_gpl+*)))				\
    		VMLINUX_SYMBOL(__stop___ksymtab_gpl) = .;		\
    	}								\
    	
    	......
    									\
    	/* Kernel symbol table: GPL-future-only symbols */		\
    	__ksymtab_gpl_future : AT(ADDR(__ksymtab_gpl_future) - LOAD_OFFSET) { \
    		VMLINUX_SYMBOL(__start___ksymtab_gpl_future) = .;	\
    		KEEP(*(SORT(___ksymtab_gpl_future+*)))			\
    		VMLINUX_SYMBOL(__stop___ksymtab_gpl_future) = .;	\
    	}								\
    									\
    	/* Kernel symbol table: Normal symbols */			\
    	__kcrctab         : AT(ADDR(__kcrctab) - LOAD_OFFSET) {		\
    		VMLINUX_SYMBOL(__start___kcrctab) = .;			\
    		KEEP(*(SORT(___kcrctab+*)))				\
    		VMLINUX_SYMBOL(__stop___kcrctab) = .;			\
    	}								\
    									\
    	/* Kernel symbol table: GPL-only symbols */			\
    	__kcrctab_gpl     : AT(ADDR(__kcrctab_gpl) - LOAD_OFFSET) {	\
    		VMLINUX_SYMBOL(__start___kcrctab_gpl) = .;		\
    		KEEP(*(SORT(___kcrctab_gpl+*)))				\
    		VMLINUX_SYMBOL(__stop___kcrctab_gpl) = .;		\
    	}								\
    									\
    	......
    									\
    	/* Kernel symbol table: GPL-future-only symbols */		\
    	__kcrctab_gpl_future : AT(ADDR(__kcrctab_gpl_future) - LOAD_OFFSET) { \
    		VMLINUX_SYMBOL(__start___kcrctab_gpl_future) = .;	\
    		KEEP(*(SORT(___kcrctab_gpl_future+*)))			\
    		VMLINUX_SYMBOL(__stop___kcrctab_gpl_future) = .;	\
    	}								\
    									\
    	/* Kernel symbol table: strings */				\
            __ksymtab_strings : AT(ADDR(__ksymtab_strings) - LOAD_OFFSET) {	\
    		*(__ksymtab_strings)					\
    	}								\
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    这里之所以要把所有向外界导出的符号统一放到一个特殊的section里面,是为了在加载其他模块时用来处理那些“未解决的引用”符号。上述由链接脚本定义的几个变量__start___ksymtab、__stop___ksymtab、__start___ksymtab_gpl、__stop___ksymtab_gpl、__start___ksymtab_gpl_future、__stop___ksymtab_gpl_future等,它们会在对内核或者是某一内核模块的导出符号表进行查找时用到。

    内核源码中为使用这些链接器产生的变量作了如下的声明:

    // linux-4.10.1/kernel/module.c
    
    /* Provided by the linker */
    extern const struct kernel_symbol __start___ksymtab[];
    extern const struct kernel_symbol __stop___ksymtab[];
    extern const struct kernel_symbol __start___ksymtab_gpl[];
    extern const struct kernel_symbol __stop___ksymtab_gpl[];
    extern const struct kernel_symbol __start___ksymtab_gpl_future[];
    extern const struct kernel_symbol __stop___ksymtab_gpl_future[];
    extern const s32 __start___kcrctab[];
    extern const s32 __start___kcrctab_gpl[];
    extern const s32 __start___kcrctab_gpl_future[];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    内核代码便可以直接使用这些变量而不会引起编译错误。内核模块的加载器在处理模块中“未解决的引用”的符号时,会使用到这里定义的这些变量。

    三、模块导出符号

    由前面我们可以知道模块不仅可以使用内核或者其他模块导出的符号,而且可以向外部导出自己的符号,模块导出符号使用的宏和内核导出符号所使用的完全一样:EXPORT_SYMBOL、EXPORT_SYMBOL_GPL和EXPORT_SYMBOL_FUTURE。

    内核模块会把导出的符号分别放到“__ksymtab”、“__ksymtab_gpl”和“__ksymtab_gpl_future”section中。如果一个内核模块向外界导出了自己的符号,那么将由模块的编译工具链负责生成这些导出符号section,而且这些section都带有A标志,所以在模块加载过程中会被搬移到CORE section区域中。如果模块没有向外界导出任何符号,那么在模块的ELF文件中,将不会产生这些section。
    在这里插入图片描述
    备注:A (alloc),表示 Section 的属性,alloc(SHF_ALLOC) 表示 Section 在模块运行期间需要占据内存。
    没有SHF_ALLOC标志的section,这样的section最终不占有实际内存地址。

    显然,内核需要对模块导出的符号进行管理,以便在处理其他模块中那些“未解决的引用”符号时能够找到这些符号。内核对模块导出的符号的管理使用到了struct module结构中如下的成员变量。

    内核中为每一个内核模块都分配了一个struct module实例:

    // linux-4.10.1/include/linux/module.h
    
    struct module {
    	......
    	/* Exported symbols */
    	const struct kernel_symbol *syms; //内核模块导出的符号所在起始地址
    	const s32 *crcs;				  //内核模块导出符号的校验码所在起始地址
    	unsigned int num_syms;			  //核模块导出的符号的数目	
    	......
    	/* GPL-only exported symbols. */
    	unsigned int num_gpl_syms;
    	const struct kernel_symbol *gpl_syms;
    	const s32 *gpl_crcs;
    	......
    	/* symbols that will be GPL-only in the near future. */
    	const struct kernel_symbol *gpl_future_syms;
    	const s32 *gpl_future_crcs;
    	unsigned int num_gpl_future_syms;
    	......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    以struct module结构体的 Exported symbols 为例:
    const struct kernel_symbol *syms表示:内核模块导出的符号所在起始地址。
    const s32 *crcs表示:内核模块导出符号的校验码所在起始地址。
    unsigned int num_syms:表示内核模块导出的符号的数目。

    syms是一个数组,有num_syms个数组项,数组的类型是struct kernel_symbol,负责将表示符name分配到内存地址value。
    crcs也是一个有num_syms个数组项的数组,存储了导出符号的校验和,用于实现版本控制。

    // linux-4.10.1/include/linux/export.h
    
    struct kernel_symbol
    {
    	unsigned long value;
    	const char *name;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    内核通过调用 find_module_sections函数在Section header table的查找,获得“__ksymtab”、“__ksymtab_gpl”和“__ksymtab_gpl_future”section在CORE section中的地址,将其记录在mod->syms、mod->crcs、mod->gpl_syms、mod->gpl_crcs、mod->gpl_future_syms、mod->gpl_future_crcs中,其中 syms表示:内核模块导出的符号所在起始地址,crcs表示:内核模块导出符号的校验码所在起始地址。如下所示:

    // linux-4.10.1/kernel/module.c
    
    /* Find a module section: 0 means not found. */
    static unsigned int find_sec(const struct load_info *info, const char *name)
    {
    	unsigned int i;
    
    	for (i = 1; i < info->hdr->e_shnum; i++) {
    		Elf_Shdr *shdr = &info->sechdrs[i];
    		/* Alloc bit cleared means "ignore it." */
    		if ((shdr->sh_flags & SHF_ALLOC)
    		    && strcmp(info->secstrings + shdr->sh_name, name) == 0)
    			return i;
    	}
    	return 0;
    }
    
    /* Find a module section, or NULL.  Fill in number of "objects" in section. */
    static void *section_objs(const struct load_info *info,
    			  const char *name,
    			  size_t object_size,
    			  unsigned int *num)
    {
    	unsigned int sec = find_sec(info, name);
    
    	/* Section 0 has sh_addr 0 and sh_size 0. */
    	*num = info->sechdrs[sec].sh_size / object_size;
    	return (void *)info->sechdrs[sec].sh_addr;
    }
    
    static int find_module_sections(struct module *mod, struct load_info *info)
    {
    	......
    	mod->syms = section_objs(info, "__ksymtab",
    				 sizeof(*mod->syms), &mod->num_syms);
    	mod->crcs = section_addr(info, "__kcrctab");
    	
    	mod->gpl_syms = section_objs(info, "__ksymtab_gpl",
    				     sizeof(*mod->gpl_syms),
    				     &mod->num_gpl_syms);
    	mod->gpl_crcs = section_addr(info, "__kcrctab_gpl");
    	
    	mod->gpl_future_syms = section_objs(info,
    					    "__ksymtab_gpl_future",
    					    sizeof(*mod->gpl_future_syms),
    					    &mod->num_gpl_future_syms);
    	mod->gpl_future_crcs = section_addr(info, "__kcrctab_gpl_future");
    	......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    这样内核通过这些变量将可得到模块导出的符号的所有信息,如图所示:
    在这里插入图片描述

    总结

    EXPORT_SYMBOL 是将内核或模块内定义的函数或变量(即 symbol)放到 symbol table, 内核或模块elf 文件中的一个section,导出内核符号表中的给定函数或变量,允许模块动态链接并使用符号。这个功能类似于动态链接库,之所以在 Linux 内核中需要手动(显示地)地这样做,比如通过EXPORT_SYMBOL,是因为内核本身不是一个 shared library,内核是单内核,作为一个不可分割的静态执行库。

    以上就是Linux 内核中 EXPORT_SYMBOL宏 详解,有一部分相关内容在模块的加载过程才能得到体现,这里没有描述其过程。

    参考资料

    深入Linux设备驱动程序内核机制
    深入Linux内核架构

    https://zhuanlan.zhihu.com/p/605670093

  • 相关阅读:
    如何将高效设计应用于 DAO?
    中国再生纤维行业发展前景战略及投资风险预测分析报告
    Qt学习11 Qt 中的字符串类
    编码踩坑——记一次fastjson引发的空指针问题、引用标识$ref
    ROS MoveIT2(humble)安装总结
    算法训练营day31,贪心算法5
    java多线程
    9. React 高阶组件是什么, 和普通组件有什么区别, 适用什么场景?
    【kotlin】利用by关键字更加方便地实现装饰器模式
    ruoyi实现富文本生成网页功能,vue实现wangEditro功能,网页静态化,二维码预览网页
  • 原文地址:https://blog.csdn.net/weixin_45030965/article/details/126658984