内核模块的文件格式与EXPORT_SYMBOL的实现

简介: 内核模块的文件格式与EXPORT_SYMBOL的实现

模块的文件格式

以内核模块形式存在的驱动程序,比如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[];

如此,内核代码便可以直接使用这些变量而不会引起编译错误。

内核模块的加载器在处理模块中“未解决的引用”的符号时,会使用到这里定义的这些变量。


目录
相关文章
|
2月前
ES6之Symbol
ES6之Symbol
|
4月前
|
JavaScript 前端开发
|
4月前
|
JavaScript 前端开发 开发者
|
2月前
|
JavaScript
js开发:请解释什么是ES6的Symbol,以及它的用途。
ES6的Symbol数据类型创建唯一值,常用于对象属性键(防冲突)和私有属性。示例展示了如何创建及使用Symbol:即使描述相同,两个Symbol也不等;作为对象属性如`obj[symbol1] = 'value1'`;也可作枚举值,如`Color.RED = Symbol('red')`。
|
4月前
|
JavaScript 前端开发
ES6之原始数据类型Symbol
ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它属于 JavaScript 语言的原生数据类型之一,其他数据类型是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、大整数(BigInt)、对象(Object)。Symbol的作用是创建一个不可变且唯一的标识符,可以用作对象属性的键。它可以用来解决属性名冲突的问题,避免命名冲突。
37 0
|
8月前
ES6: Symbol概念与用法举例
ES6: Symbol概念与用法举例
28 0
|
9月前
|
JavaScript
es6数据类型Symbol以及es6操作数组常用的方法
es6数据类型Symbol以及es6操作数组常用的方法
63 0