必威体育Betway必威体育官网
当前位置:首页 > IT技术

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

时间:2019-08-10 19:12:08来源:IT技术作者:seo实验室小编阅读:54次「手机版」
 

export_symbol

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也可以使用了就。

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

使用readelf工具,-h参数查看.ko文件的,文件头

arm-none-linux-gnueabi-readelf -h  export_2.ko | less

我们发现文件类型为可重定位目标文件,这和一般的目标文件格式没有任何区别。我们知道,目标文件是不能直接执行的,它需要经过链接器的地址空间分配、符号解析和重定位的过程,转化为可执行文件才能执行。

记得前面在uboot阶段uboot的自举就是一段重定位代码,内核中就在本节不再继续升入分析,有时间我再重开一节,对linux中.ko文件安装重定位再分析一下。

相关阅读

使用Eclipse导出向导生成jar包

使用Eclipse导出向导生成jar包可以用jar命令对java项目(project)进行打包(Java自带的命令行式打包软件jar.exe),也可以使用Eclipse导出

淘宝助理导出的数据包没有图片是怎么回事?

淘宝助理的功能很强大,不仅可以帮助卖家进行批量上传、复制宝贝,还可以进行店铺数据的转移和更新,但是有朋友在进行数据包导出的时候

淘宝如何做数据包?如何导出?

任何一个淘宝店铺都要学会去做淘宝数据包,作为淘宝新手,您是不是还不知道淘宝如何做数据包吗??今天小编我就来教你具体的方法,大家还

唱吧免费导出歌曲mp3

1. 复制歌曲链接到地址栏 2. 右键查看网页源代码,ctrl + F 找“mp3” 3. 新打开一个标签栏,把红框圈中到地址复制到新标签栏,即可

MSM8998(高通835处理器)外接指纹识别传感器linux驱动

/* * FPC1020 Fingerprint sensor device driver * * This driver will control the platform resources that the FPC finger

分享到:

栏目导航

推荐阅读

热门阅读