最后的话
最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!
资料预览
给大家整理的视频资料:
给大家整理的电子书资料:
如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
* 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;
…
最后的话
最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!
资料预览
给大家整理的视频资料:
给大家整理的电子书资料:
如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
]
如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!