前言
内核模块被载入后,就会被动态地连接到内核(与用户空间的动态库类似,只有函数被显式的导出为外部函数后,才可以被动态库调用)。在内核中,导出内核函数需要特殊的指令: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);
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);
一、EXPORT_SYMBOL简介
我们都知道Linux 是单内核,作为一个不可分割的静态执行库,内核通常以单个静态的二进制文件形式存放在磁盘中,在一个单独的内核地址空间上运行,由于内核都处在同一内核地址空间,因此内核可以直接调用函数。如果只是单一的内核映像存在,而没有内核模块存在,EXPORT_SYMBOL通常是没有啥意义的,因为对于静态编译链接而成的单一的内核映像而言,所有的符号引用都将在静态链接阶段完成。
由于Linux支持动态地加载内核模块,运行内核在运行时根据需要动态地加载模块,由于有内核模块的存在,内核模块通常会使用内核映像中的符号,因此使用EXPORT_SYMBOL将内核映像的符号导出,这样在内核模块中就能使用该符号了。
内核模块是独立编译的,然后加载到正在运行的内核中,当编译内核模块时,编译器引用到内核中的符号时,会产生未解决的引用,处理“未解决引用”问题的就要在模块加载期间找到当前“未解决的引用”符号在内存中的实际目标地址。内核和内核模块通过符号表的形式向外部导出符号的相关信息,这种导出符号的方式在代码层面以EXPORT_SYMBOL宏定义的形式存在。
解决模块中的未解决引用是在模块的加载阶段,用nm命名查看模块未解决的引用:
nm nf_conntrack.ko
输出中U代表未解决的引用,对于未解决的引用符号,不显示该符号相对于模块起始地址的相对偏移地址,即没有符号值。
查看模块所有的未定义引用:
nm nf_conntrack.ko | grep '\<U\>'
二、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")
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 };
第一个变量是一个字符串静态变量,用来表示导出的符号名称"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; };
其中,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
对这些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) \ }
这里之所以要把所有向外界导出的符号统一放到一个特殊的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[];
内核代码便可以直接使用这些变量而不会引起编译错误。内核模块的加载器在处理模块中“未解决的引用”的符号时,会使用到这里定义的这些变量。
三、模块导出符号
由前面我们可以知道模块不仅可以使用内核或者其他模块导出的符号,而且可以向外部导出自己的符号,模块导出符号使用的宏和内核导出符号所使用的完全一样: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; ...... } **先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前在阿里** **深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!** **因此收集整理了一份《2024年最新Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。** ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/efd1bb9cdfd10d8d28adf20d209799fa.png) ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/6803ce1fffb74643400db3899b9a2700.png) ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/fea31e58126a1684b8dd5b99fb0c4f61.png) ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/91087b445b3f453ae6fe30c0101d9632.png) ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/c7b2e33aa1f6192d563fa162f7badbfc.png) **既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!** **由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新** **[需要这份系统化的资料的朋友,可以点击这里获取!](https://bbs.csdn.net/topics/618635766)** 外链图片转存中...(img-18QJR7hj-1715750941866)] [外链图片转存中...(img-vkAEzseu-1715750941867)] [外链图片转存中...(img-3cTFcIhi-1715750941867)] **既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!** **由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新** **[需要这份系统化的资料的朋友,可以点击这里获取!](https://bbs.csdn.net/topics/618635766)**