从零开始之驱动发开、linux驱动(三十五、利用EXPORT_SYMBOL导出符表原理)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_16777851/article/details/83931530

Linux内核头文件提供了一个方便的方法用来管理符号的对模块外部的可见性,因此减少了命名空间的污染(命名空间的名称可能会与内核其他地方定义的名称冲突),并且适当信息隐藏。 如果你的模块需要输出符号给其他模块使用,应当使用下面的宏定义:

EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);

这两个宏均用于将给定的符号导出到模块外. GPL版本的宏定义只能使符号对GPL许可的模块可用。 符号必须在模块文件的全局部分导出,不能在函数中导出,这是因为上述这两个宏将被扩展成一个特殊用途的声明,而该变量必须是全局的。

其中模块的导出在下面路径

linux/export.h

其中目前linux做了三种,第三种future目前还没使用。

#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")

EXPORT_SYMBOL和EXPORT_SYMBOL_GPL也是一些小的差异,下面我就以GPL为例,对宏进行展开看一下具体导出是做了哪些事。

__EXPORT_SYMBOL传入的参数第一个是符号,第二个是一个字符串


/* 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);				\                /* 把符号转换成字符串 */
	extern const struct kernel_symbol __ksymtab_##sym;	\    /* 以外部形式导出下面定义的kernel_symbol  */
	__visible const struct kernel_symbol __ksymtab_##sym	\    /* 定义一个kernel_symbol   */
	__used							\
	__attribute__((section("___ksymtab" sec "+" #sym), unused))	\
	= { (unsigned long)&sym, __kstrtab_##sym }        /* 里面放了两个参数,sym地址,一个是sym字符串 */



/* Indirect, so macros are expanded before pasting. */
#define VMLINUX_SYMBOL(x) __VMLINUX_SYMBOL(x)
#define VMLINUX_SYMBOL_STR(x) __VMLINUX_SYMBOL_STR(x)


#define __VMLINUX_SYMBOL(x) x
#define __VMLINUX_SYMBOL_STR(x) #x            /* 斧好转字符串 */

struct kernel_symbol
{
	unsigned long value;            /* 地址 */
	const char *name;               /* 符号转出来的字符串 */
};

可见__EXPORT_SYMBOL就是把要导出的符号以kernel_symbol的形式组织,单独编译成一个段。

下面是符号导出的两种用法,一种是导出全局变量,一个是导出函数。

EXPORT_SYMBOL_GPL(nr_irqs);
EXPORT_SYMBOL_GPL(kdb_register);
#define __EXPORT_SYMBOL(nr_irqs, _gpl)				\
	extern typeof(nr_irqs) nr_irqs;			\      //申明nr_irqs是全局变量
	__CRC_SYMBOL(nr_irqs, _gpl)					\         //CRC校验用,一般是空的
	static const char __kstrtab_nr_irqs[]			\                    //定义一个字符串
	__attribute__((section("__ksymtab_strings"), aligned(1))) \          //属性修饰,把这个字符数组编译进__ksymtab_strings段
	= "nr_irqs";				\                                        //初始化字符数组
	extern const struct kernel_symbol __ksymtab_nr_irqs;	\            //申明__ksymtab_nr_irqs是全局变量
	__visible const struct kernel_symbol __ksymtab_nr_irqs	\            //定义__ksymtab_nr_irqs
	__used							\
	__attribute__((section("___ksymtab_gpl+nr_irqs"), unused))	\        //把kernel_symbol 单独编译成一个段
	= { (unsigned long)&nr_irqs, __kstrtab_nr_irqs}                      //用符号地址和符号转换后的字符串初始化内核符号结构




#define __EXPORT_SYMBOL(kdb_register, _gpl)				\
	extern typeof(nrkdb_registerirqs) kdb_register;		\                    //申明nr_irqs是全局变量
	__CRC_SYMBOL(kdb_register, _gpl)					\     //CRC校验用,一般是空的
	static const char __kstrtab_kdb_register[]			\      //定义一个字符串
	__attribute__((section("__ksymtab_strings"), aligned(1))) \          //属性修饰,把这个字符数组编译进__ksymtab_strings段
	= "kdb_register";				\                         //初始化字符数组
	extern const struct kernel_symbol __ksymtab_kdb_register;	\            //申明__ksymtab_kdb_register是全局变量
	__visible const struct kernel_symbol __ksymtab_kdb_register	\            //定义__ksymtab_kdb_register
	__used							\
	__attribute__((section("___ksymtab_gpl+kdb_register"), unused))	\        //把kernel_symbol 单独编译成一个段
	= { (unsigned long)&kdb_register, __kstrtab_kdb_register}                      //用符号地址和符号转换后的字符串初始化内核符号结构


其中__ksymtab_strings段,__ksymtab_unused段,__kcrctab_gpl段等都是放在rodata段中的。

下面列出链接脚本中这些段的组织形式

 __ksymtab : AT(ADDR(__ksymtab) - 0) 
 { __start___ksymtab = .; 
 *(SORT(___ksymtab+*)) __stop___ksymtab = .; } 
 __ksymtab_gpl : AT(ADDR(__ksymtab_gpl) - 0) 
 { __start___ksymtab_gpl = .; *(SORT(___ksymtab_gpl+*)) 
 __stop___ksymtab_gpl = .; } 
 __ksymtab_unused : AT(ADDR(__ksymtab_unused) - 0) 
 { __start___ksymtab_unused = .; *(SORT(___ksymtab_unused+*)) 
 __stop___ksymtab_unused = .; } 
 __ksymtab_unused_gpl : AT(ADDR(__ksymtab_unused_gpl) - 0) 
 { __start___ksymtab_unused_gpl = .; *(SORT(___ksymtab_unused_gpl+*)) 
 __stop___ksymtab_unused_gpl = .; } 
 __ksymtab_gpl_future : AT(ADDR(__ksymtab_gpl_future) - 0) 
 { __start___ksymtab_gpl_future = .; *(SORT(___ksymtab_gpl_future+*)) _
 _stop___ksymtab_gpl_future = .; } 
 __kcrctab : AT(ADDR(__kcrctab) - 0) 
 { __start___kcrctab = .; *(SORT(___kcrctab+*)) 
 __stop___kcrctab = .; } 
 __kcrctab_gpl : AT(ADDR(__kcrctab_gpl) - 0) 
 { __start___kcrctab_gpl = .; *(SORT(___kcrctab_gpl+*)) 
 __stop___kcrctab_gpl = .; } 
 __kcrctab_unused : AT(ADDR(__kcrctab_unused) - 0) 
 { __start___kcrctab_unused = .; *(SORT(___kcrctab_unused+*)) 
 __stop___kcrctab_unused = .; } 
 __kcrctab_unused_gpl : AT(ADDR(__kcrctab_unused_gpl) - 0) 
 { __start___kcrctab_unused_gpl = .; *(SORT(___kcrctab_unused_gpl+*)) 
 __stop___kcrctab_unused_gpl = .; } 
 __kcrctab_gpl_future : AT(ADDR(__kcrctab_gpl_future) - 0) 
 { __start___kcrctab_gpl_future = .; *(SORT(___kcrctab_gpl_future+*)) 
 __stop___kcrctab_gpl_future = .; } 
 __ksymtab_strings : AT(ADDR(__ksymtab_strings) - 0) 
 { *(__ksymtab_strings) } 

可以看到kernel_symbol段中后面的sym都是以*通配符来表示的。

CRC的在打开相应的宏之后,某些符号也是也已生产CRC校验和的。

#ifndef __GENKSYMS__
#ifdef CONFIG_MODVERSIONS
/* Mark the CRC weak since genksyms apparently decides not to
 * generate a checksums for some symbols */
#define __CRC_SYMBOL(sym, sec)					\
	extern __visible void *__crc_##sym __attribute__((weak));		\
	static const unsigned long __kcrctab_##sym		\
	__used							\
	__attribute__((section("___kcrctab" sec "+" #sym), unused))	\
	= (unsigned long) &__crc_##sym;
#else
#define __CRC_SYMBOL(sym, sec)
#endif

总结:在内核符号导出中,调用了EXPORT_SYMBOL(sym),则会完成以下操作:

(1)   定义一个字符数组存放内核导出符号的名称,并放置到“__ksymtab_strings”的section中。

(2)   定义一个内核符号结构用于存放导出符号的内存地址和名称,并放置到”__ksymatab”中。

即通过EXPORT_SYMBOL(sym)告诉了内核以外的世界关于这个符号的两点信息:内核符号的名称和其内存地址。
 

在2.6及以后内核中,为了更好地调试内核,引入了kallsyms。kallsyms抽取了内核用到的所有函数地址(全局的、静态的)和非栈数据变量地址,生成一个数据块,作为只读数据链接进kernel image,相当于内核中存了一个System.map。需要配置CONFIG_KALLSYMS

CONFIG_KALLSYMS=y   符号表中包含所有的函数
CONFIG_KALLSYMS_ALL=y 符号表中包括所有的变量(包括没有用EXPORT_SYMBOL导出的变量)
 

这里说一下kallsyms是什么东西?

在2.6即以后的内核中,为了更方便的调试内核代码,开发者考虑将内核代码中所有函数以及所有非栈变量的地址抽取出来,形成是一个简单的数据块(data blob:符号和地址对应),并将此链接进 vmlinux 中去。如此,在需要的时候,内核就可以将符号地址信息以及符号名称都显示出来,方便开发者对内核代码的调试。完成这一地址抽取+数据快组织封装功能的相关子系统就称之为 kallsyms。反之,如果没有 kallsyms 的帮助,内核只能将十六进制的符号地址呈现给外界,因为它能理解的只有符号地址,而并不包括人类可读的符号名称。

可以通过查看/proc/kallsyms来查看导出的所有符号。​

打开System.map 可以看到对于C函数或变量无论哪种形式导出的都是一样的。

其中中间的符号表示,符号的类型,分别如下

T   The symbol is in the text(code) section
D   The symbol is in the initialized data section
R   The sysbol is in a read only data section
t   static 
d   static
R   const
r   static const

下面我用一个例子来说明符号如何在不同文件使用和如何让调试函数。

内核函数1

 #include <linux/module.h>
 #include <linux/kernel.h>


typedef int (*func_t)(const char *,...);



void export_1_func(void)
{
    func_t func = (void *)(0x802ce1fc);
    
    func(KERN_INFO"export_1_func\n");

}

EXPORT_SYMBOL(export_1_func);


int export_1_init(void)
{
    printk(KERN_INFO"export_1_init\n");
    return 0;
}


void export_1_exit(void)
{
    printk(KERN_INFO"export_1_exit\n");
}

MODULE_LICENSE("GPL");
module_init(export_1_init);
module_exit(export_1_exit);

内核函数2

#include <linux/init.h>  
#include <linux/module.h>  


int export_2_init(void)
{
    extern void export_1_func(void);

    export_1_func();
    
    printk(KERN_INFO"export_2_init\n");
    
    return 0;
}

module_init(export_2_init);
MODULE_LICENSE("Dual BSD/GPL");                                                                                                                            

其中内核函数中调用的地址是printk函数的地址,如下(可以在System.map中搜索快速定位找到)

Makefile文件如下

KERN_DIR = /home/run/work/kernel/linux-3.16.57

.PHONY:all
all:
    make -C $(KERN_DIR) M=`pwd` modules
    
.PHONY:clean
clean:
    make -C $(KERN_DIR) M=`pwd` modules clean

   
obj-m += export_1.o
obj-m += export_2.o

安装驱动后的打印结果如下:

可见在有了符号表以后,通过地址可以直接调用函数用于调试。

如果驱动1中没有导出函数,则2中调用对应函数时则会失败。

当然因为我上面的两个模块是在同一个编译时是先编译export_1后编译export_2的,所以编译时没问题

如果我先编译2,因为他使用了模块1里面的函数,而模块1又还没编译(或者和模块1不再同一路径下),就会出现找不到的情况而产生告警信息

解决方法是:

1.在模块2编译前先编译模块1,模块1编译产生Module.symvers文件

2.模块2的Makefile中添加模块1的Module.symvers文件的路径

Module.symvers文件的内容在我的这个例子中如下

还有一个方法就是不在模块2的Makefile文件中添加导出,而是把模块1的Module.symvers文件在模块2中拷贝一份。这样模块2也可以使用了就。

最后说一下核心的东西,多个模块之间有符号(函数,变量)依赖安装时,应该按依赖顺序依次安装。

下面是模块2的.ko文件的反汇编

可以看到无论是printk还是我们自定义的export_1_func函数的跳转编译地址都是0.这句跳转地址在.ko文件安装时肯定是要被替换掉,跳转到真正的内核函数地址,如何找到这两个对应地址,这就依赖于前面所说的___ksymtab段了,只需要在这个段中根据调用文件的函数名字符串,查找到具体的某个kernel_symbol,即找到了对应的函数地址。

最后,模块1安装时也是根据内核空间未使用的代码段,紧接着放置,一次安排好各函数的地址。

所以必须模块1先安装,才能安装依赖于模块1的模块2.

猜你喜欢

转载自blog.csdn.net/qq_16777851/article/details/83931530
今日推荐