第三十章 linux-模块的文件格式与EXPORT_SYMBOL的实现
文章目录
- 第三十章 linux-模块的文件格式与EXPORT_SYMBOL的实现
- 模块的文件格式
- EXPORT_SYMBOL的实现
模块的文件格式
以内核模块形式存在的驱动程序,比如demodev.ko,其在文件的数据组织形式上是ELF(Executable and Linkable Format)格式,更具体地,内核模块是一种普通的可重定位目标文件。
ELF是Linux下非常重要的一种文件格式,常见的可执行程序都是以ELF的形式存在。在这里我们结合Linux源代码中定义的ELF相关数据结构(基于32位体系架构),给出ELF格式的一个比较详细的结构图:
图中忽略了驱动程序模块ELF文件中不会用到的Program header table从图中可以看到,静态的ELF文件视图3总体上可分为三大部分:头部的ELF header,中间的section和尾部的Section header table。
- ELF header
大小是52字节,位于文件头部。
typedef struct elf32_hdr {
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;/*表明文件类型,对于驱动模块,这个值是1,也就是说驱动模块是一个可定位的ELF文件〈relocatable file)。*/
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry; /* Entry point */
Elf32_Off e_phoff;
Elf32_Off e_shoff;/*表明Section heade rtable部分在文件中的偏移量。*/
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;/*表明Section header table部分中每一个entry的大小(以字节计)。*/
Elf32_Half e_shnum;/*表明Section header table中有多少个entry。因此,Section header table的大小便为e_shentsize x e_shnum个字节。*/
Elf32_Half e_shstrndx;/*与section header entry中的sh_name一起用来指明对应的section的name。*/
} Elf32_Ehdr;
-
Section
ELF文件的主体,位于文件视图中间部分的一个连续区域中。但是当模块被内核加载时,会根据各自属性被重新分配到新的内存区域(有些section也可能只是起辅助作用,因而在运行时并不占用实际的内存空间)。 -
Section header table
该部分位于文件视图的末尾,由若干个(具体个数由ELF header中的e_shnum变量指定)Section header entry组成,每个entry具有同样的数据结构类型。
typedef struct elf32_shdr {
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;/*这个值用来表示该entry所对应的section在内存中的实际地址。在静态的文件视图中,这个值为0,当模块被内核加载时,加载器会用该在内存中的实际地址来改写sh_addr〈如果section不占用内存空间,该值为0)。*/
Elf32_Off sh_offset;/*表明对应的section在文件视图中的偏移量。*/
Elf32_Word sh_size;/*表明对应的section在文件视图中的大小〈以字节计)。类型为SHT_NOBITS的section例外,这种n在文件视图中不占有空间。*/
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;/*主要用于由固定数量entry组成的表所构成的section,如符号表,此种情况下用来表示表中entry的大小。*/
} Elf32_Shdr;
EXPORT_SYMBOL的实现
看过Linux内核源码的读者应该知道,源码中充斥着像EXPORT_SYMBOL这样的宏,在我们自己的设备驱动程序中也经常会发现它的身影。大部分时间里,我们只知道它用来向外界导出一个符号,仅此而己。我们对这些宏是如此习惯,以至于常常忽略其存在的意义,更不用说去仔细探究其背后的实现原理了。然而这些不起眼的宏却有着大用场,如果没有它们,我们的驱动程序甚至连printk这样常见的内核函数都不能使用。
内核和内核模块通过符号表的形式向外部世界导出符号的相关信息,这种导出符号的方式在代码层面以EXPORT_SYMBOL宏定义的形式存在。从全局来看,EXPORT_SYMBOL这类宏功能的完整实现需要经过三个部分来达成:EXPORT_SYMBOL宏定义部分,链接脚本链接器部分和使用导出符号部分。本节讲述前两个部分,第二部分的描述将延后到模块加载的相关段落。
//导出符号
/*
*__CRC_SYMBOL用来作为版本控制信息使用,
*每个EXPORT_SYMBOL宏实际上定义了两个变量:
*第一个变量是个简单的char型指针,用来表示导出的符号名称:
*第二个变量类型是struct_kernel_symbol数据结构,用来表示一个内核符号的实例,
*/
#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); \
extern const struct kernel_symbol __ksymtab_##sym; \
__visible const struct kernel_symbol __ksymtab_##sym \
__used \
__attribute__((section("___ksymtab" sec "+" #sym), unused)) \
= { (unsigned long)&sym, __kstrtab_##sym }
kernel_symbol 定义为:
struct kernel_symbol
{
unsigned long value;//该符号在内存中的地址
const char *name;//符号名。
};
通过struct kernel_symbol的一个对象告诉外部世界关于这个符号的两点信息:符号名称和地址。可见,由EXPORT_SYMBOL等宏导出的符号,与一般的变量宁并没有实质性的差异,唯一的不同点在于它们被放在了特定的section中。
以EXPORT_SYMBOL(my_exp_function)为具体例子,即向外部导出一个名为my_exp_function的函数。
__CRC_SYMBOL(sym, sec) \
static const char __kstrtab_my_exp_function[] \
__attribute__((section("__ksymtab_strings"), aligned(1))) \
= VMLINUX_SYMBOL_STR(my_exp_function); \
extern const struct kernel_symbol __ksymtab_my_exp_function; \
上面的__kstrtabmy_exp_function会被放置在一个名为“__ksymtab_strings”的section中,__ksymtab_my_exp_function会放置在一个名为“ksymtab”的section中(对于EXPORT_SYMBOLGPL和EXPORT_SYMBOL_GPLFUTURE而言,其struct kernel_symbol实例所在的section名称则分别为“ksymtab_gpl”和“ksymtab_gpl_future”)。
对这些section的使用需要经过一个中间环节,即链接脚本与链接器部分。链接脚本告诉链接器把所有目标文件中的名为“__ksymtab”的section放置在最终内核〈或者是内核模块)映像文件的名为“__ksymtab”的section中(对于目标文件中的名为"__ksymtab_gpl”、“__ksymtab_gpl_future”、”__kcrctab"、“__kcrctab_gpl_future”、“__kcrctab”、“kcrctab_gpl”和“kcrctab_gpl_future”的section都同样处理),看看下面的这个具体的链接脚本的例子就很清楚了。
/* Kernel symbol table: Normal symbols */ \
__ksymtab : AT(ADDR(__ksymtab) - LOAD_OFFSET) { \
VMLINUX_SYMBOL(__start___ksymtab) = .; \
*(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) = .; \
*(SORT(___ksymtab_gpl+*)) \
VMLINUX_SYMBOL(__stop___ksymtab_gpl) = .; \
} \
\
/* Kernel symbol table: Normal unused symbols */ \
__ksymtab_unused : AT(ADDR(__ksymtab_unused) - LOAD_OFFSET) { \
VMLINUX_SYMBOL(__start___ksymtab_unused) = .; \
*(SORT(___ksymtab_unused+*)) \
VMLINUX_SYMBOL(__stop___ksymtab_unused) = .; \
} \
\
/* Kernel symbol table: GPL-only unused symbols */ \
__ksymtab_unused_gpl : AT(ADDR(__ksymtab_unused_gpl) - LOAD_OFFSET) { \
VMLINUX_SYMBOL(__start___ksymtab_unused_gpl) = .; \
*(SORT(___ksymtab_unused_gpl+*)) \
VMLINUX_SYMBOL(__stop___ksymtab_unused_gpl) = .; \
} \
\
/* Kernel symbol table: GPL-future-only symbols */ \
__ksymtab_gpl_future : AT(ADDR(__ksymtab_gpl_future) - LOAD_OFFSET) { \
VMLINUX_SYMBOL(__start___ksymtab_gpl_future) = .; \
*(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) = .; \
*(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) = .; \
*(SORT(___kcrctab_gpl+*)) \
VMLINUX_SYMBOL(__stop___kcrctab_gpl) = .; \
} \
\
/* Kernel symbol table: Normal unused symbols */ \
__kcrctab_unused : AT(ADDR(__kcrctab_unused) - LOAD_OFFSET) { \
VMLINUX_SYMBOL(__start___kcrctab_unused) = .; \
*(SORT(___kcrctab_unused+*)) \
VMLINUX_SYMBOL(__stop___kcrctab_unused) = .; \
} \
\
/* Kernel symbol table: GPL-only unused symbols */ \
__kcrctab_unused_gpl : AT(ADDR(__kcrctab_unused_gpl) - LOAD_OFFSET) { \
VMLINUX_SYMBOL(__start___kcrctab_unused_gpl) = .; \
*(SORT(___kcrctab_unused_gpl+*)) \
VMLINUX_SYMBOL(__stop___kcrctab_unused_gpl) = .; \
} \
\
/* Kernel symbol table: GPL-future-only symbols */ \
__kcrctab_gpl_future : AT(ADDR(__kcrctab_gpl_future) - LOAD_OFFSET) { \
VMLINUX_SYMBOL(__start___kcrctab_gpl_future) = .; \
*(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,它们会在对内核或者是某一内核模块的导出符号表进行查找时用到。
/* 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 unsigned long __start___kcrctab[];
extern const unsigned long __start___kcrctab_gpl[];
extern const unsigned long __start___kcrctab_gpl_future[];
#ifdef CONFIG_UNUSED_SYMBOLS
extern const struct kernel_symbol __start___ksymtab_unused[];
extern const struct kernel_symbol __stop___ksymtab_unused[];
extern const struct kernel_symbol __start___ksymtab_unused_gpl[];
extern const struct kernel_symbol __stop___ksymtab_unused_gpl[];
extern const unsigned long __start___kcrctab_unused[];
extern const unsigned long __start___kcrctab_unused_gpl[];
如此,内核代码便可以直接使用这些变量而不会引起编译错误。
内核模块的加载器在处理模块中“未解决的引用”的符号时,会使用到这里定义的这些变量。