模块的文件格式
以内核模块形式存在的驱动程序,比如demodev.ko,其在文件的数据组织形式上是ELF(Executable and Linkable Format)格式,更具体地,内核模块是一种普通的可重定位目标文件。file一个hello.ko得到:
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[];
如此,内核代码便可以直接使用这些变量而不会引起编译错误。
内核模块的加载器在处理模块中“未解决的引用”的符号时,会使用到这里定义的这些变量。