深入Linux设备驱动程序内核机制
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.2 EXPORT_SYMBOL的内核实现

看过Linux内核源码的读者应该知道,源码中充斥着像EXPORT_SYMBOL这样的宏,在我们自己的设备驱动程序中也经常会发现它的身影。大部分时间里,我们只知道它用来向外界导出一个符号,仅此而已。我们对这些宏是如此习惯,以至于常常忽略其存在的意义,更不用说去仔细探究其背后的实现原理了。然而这些不起眼的宏却有着大用场,如果没有它们,我们的驱动程序甚至连printk这样常见的内核函数都不能使用。

本节描述EXPORT_SYMBOL、EXPORT_SYMBOL_GPL和EXPORT_SYMBOL_GPL_ FUTURE宏定义导出符号的内核机制。之所以把导出符号作为独立的一节,是因为在模块加载的过程中会使用本节描述到的机制,而且导出符号这一特性在Linux系统中对模块的存在具有重要意义。读者应该结合本节和模块加载部分的相关内容来理解如何导出符号和使用导出的符号这一重要的内核机制。

如果没有独立存在的内核模块,作为单一的Linux内核映像,导出符号就失去了意义。对于静态编译链接而成的内核映像而言,所有的符号引用都将在静态链接阶段完成。然而,内核模块的出现让事情发生了变化:内核模块不可避免地要使用到内核提供的基础设施(以调用内核函数的形式发生),作为独立编译链接的内核模块,必须要解决这种静态链接无法完成的符号引用问题(在内核模块所在的ELF文件中,这种引用被称为“未解决的引用”)。处理“未解决引用”问题的本质是在模块加载期间找到当前“未解决的引用”符号在内存中的实际目标地址。内核和内核模块通过符号表的形式向外部世界导出符号的相关信息,这种导出符号的方式在代码层面以EXPORT_SYMBOL宏定义的形式存在。从全局来看,EXPORT_SYMBOL这类宏功能的完整实现需要经过三个部分来达成:EXPORT_SYMBOL宏定义部分,链接脚本链接器部分和使用导出符号部分。本节讲述前两个部分,第三部分的描述将延后到模块加载的相关段落。

下面通过这些宏定义来仔细考量代码背后的技术细节。

<include/linux/module.h>
#define__EXPORT_SYMBOL(sym,sec)              \
    extern typeof(sym)sym;                 \
    __CRC_SYMBOL(sym,sec)                  \
    static const char__kstrtab_##sym[]          \
    __attribute__((section("__ksymtab_strings"),aligned(1)))\
    =MODULE_SYMBOL_PREFIX#sym;                   \
    static const struct kernel_symbol__ksymtab_##sym  \
    __used                             \
    __attribute__((section("__ksymtab"sec),unused))   \
    ={(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")

以上为来自Linux源码树中的EXPORT_SYMBOL等相关宏的定义细节。其中的__CRC_SYMBOL用来作为版本控制信息使用,在本章后续的“模块的版本控制”一节中将予以讨论。在接下来的分析中,为了使读者更清楚其中的实现细节,笔者会对内核中的源码稍作改写,这种改写并不会改变原来代码的本质,而只是为了让读者看起来更加方便。此外,为叙述简单起见,将用EXPORT_SYMBOL(my_exp_function)作为具体的例子,即向外部导出一个名为my_exp_function的函数,这个导出函数的例子同样也用在EXPORT_SYMBOL_GPL和EXPORT_SYMBOL_GPL_FUTURE中。

从源代码可以看出,每个EXPORT_SYMBOL宏实际上定义了两个变量:

static const char *__kstrtab_my_exp_function = "my_exp_function";
static const struct kernel_symbol __ksymtab_my_exp_function =
{ (unsigned long)& my_exp_function, __kstrtab_my_exp_function };

第一个变量是个简单的char型指针,用来表示导出的符号名称;第二个变量类型是struct kernel_symbol数据结构,用来表示一个内核符号的实例,struct kernel_symbol的定义为:

<include/linux/module.h>
struct kernel_symbol
{
    unsigned long value;
    const char*name;
};

其中,value是该符号在内存中的地址,name是符号名。所以,单由该数据结构可以知道,用EXPORT_SYMBOL(my_exp_function)来导出符号“my_exp_function”,实际上是要通过struct kernel_symbol的一个对象告诉外部世界关于这个符号的两点信息:符号名称和地址对于由内核模块导出的符号而言,由于在静态链接时无法确定该符号在内存中的最终地址,因此这个地址信息要一直等到模块被成功加载进系统后才有效。在模块加载的过程中,由内核模块加载器负责修改该成员以反映出符号在内存中的最终目标地址,这也就是所谓的“重定位”过程。

可见,由EXPORT_SYMBOL等宏导出的符号,与一般的变量定义并没有实质性的差异,唯一的不同点在于它们被放在了特定的section中。

上面的__kstrtab_my_exp_function会被放置在一个名为“__ksymtab_strings”的section中,__ksymtab_my_exp_function会放置在一个名为“__ksymtab”的section中(对于EXPORT_SYMBOL_GPL和EXPORT_SYMBOL_GPL_FUTURE而言,其struct kernel_symbol实例所在的section名称则分别为“__ksymtab_gpl”和“__ksymtab_gpl_future”)。

对这些section的使用需要经过一个中间环节,即链接脚本与链接器部分。链接脚本告诉链接器把所有目标文件中的名为“__ksymtab”的section放置在最终内核(或者是内核模块)映像文件的名为“__ksymtab”的section中(对于目标文件中的名为“__ksymtab_gpl”、“__ksymtab_gpl_future”、“__kcrctab”、“__kcrctab_gpl”和“__kcrctab_gpl_future”的section都同样处理),看看下面的这个具体的链接脚本的例子就很清楚了。

<arch/x86/kernel/vmlinux.lds>
__ksymtab : AT(ADDR(__ksymtab) - 0xC0000000)
{ __start___ksymtab = .; *(__ksymtab) __stop___ksymtab = .; }
__ksymtab_gpl : AT(ADDR(__ksymtab_gpl) - 0xC0000000)
{ __start___ksymtab_gpl = .; *(__ksymtab_gpl) __stop___ksymtab_gpl = .; }
__ksymtab_gpl_future : AT(ADDR(__ksymtab_gpl_future) - 0xC0000000)
{
__start___ksymtab_gpl_future = .;
*(__ksymtab_gpl_future) __stop___ksymtab_gpl_future = .;
}
__kcrctab : AT(ADDR(__kcrctab) - 0xC0000000)
{ __start___kcrctab = .; *(__kcrctab) __stop___kcrctab = .; }
__kcrctab_gpl : AT(ADDR(__kcrctab_gpl) - 0xC0000000)
{ __start___kcrctab_gpl = .; *(__kcrctab_gpl) __stop___kcrctab_gpl = .; }
__kcrctab_gpl_future : AT(ADDR(__kcrctab_gpl_future) - 0xC0000000)
{ __start___kcrctab_gpl_future = .; *(__kcrctab_gpl_future) __stop___kcrctab_gpl_future = .; }
__ksymtab_strings : AT(ADDR(__ksymtab_strings) - 0xC0000000)
{ *(__ksymtab_strings) }

这里之所以要把所有向外界导出的符号统一放到一个特殊的section里面,是为了在加载其他模块时用来处理那些“未解决的引用”符号,在稍后的“模块的加载过程”一节中可看到这种用途。注意这里由链接脚本定义的几个变量__start___ksymtab、__stop___ksymtab、__start___ksymtab_gpl、__stop___ksymtab_gpl、__start___ksymtab_gpl_future、__stop___ksymtab_gpl_future,它们会在对内核或者是某一内核模块的导出符号表进行查找时用到。

内核源码中为使用这些链接器产生的变量作了如下的声明:

<kernel/module.c>
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[];

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

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