问题
这几天在arm上做蓝牙耳机驱动的时候,编译好了驱动但是在板子上insmod时候。
怎么会出现这种情况,不对呀,仔细查我们会发现,其实编译驱动的时候,就出现了一些警告,只是当时没有在意而已,而恰恰是这些警告导致的这些问题。
硬件设备
板子用的是realarm
内核linux-2.6.35
交叉编译器arm-linux-gcc 4.4.3
问题解析
究其原因,其原因就是我们的驱动找不到内核的几个函数,我们可以看到我们找不到的函数有两个,一个是kill_proc_info
一个是snd_hwdep_new
。
问题来了,我们内核编的好好的怎么会找不到这两个函数呢
System.map与内核符号表
亲自编译过linux内核的可能编译完内核都会发现在生成vmlinuz的同时,生成一个System.map文件。
nm /boot/vmlinux > System.map
通常我们会把发送到标准输出设备的链接映象信息重定向到一个文件中(如System.map)。编译内核时,System.map文件用于存放内核符号表信息。符号表是所有内核符号及其对应地址的一个列表,随着每次内核的编译,就会产生一个新的对应System.map文件,当内核运行出错时,通过System.map中的符号表解析,就可以查到一个地值应的变量名,或反之。
利用System.map,在内核或相关程序出错时,就可以获得我们比较容易识别的信息。
System.map位于使用它的软件(例如内核日志记录后台程序klogd)能够找到的地方。在系统启动时,如果没有以一个参数的形式为klogd给出System.map的位置,则klogd会在三个地方寻找System.map:
/boot/System.map
/System.map
/usr/src/linux/System.map
尽管内核本身实际上不使用System.map,但其它程序,像klogd、lsof、ps以及其它像dosemu等许多软件
都需要有一个正确的System.map文件。利用该文件,这些程序就可以根据已知的内存地址查找出对应的内核变量名称,便于对内核的调试工作。
首先我们可以查看下我们需要的函数是否导入到符号表中System.map
使用如下命令查看cat -n System.map | grep kill_proc_info
和 cat -n System.map | grep snd_hwdep_new
我们发现函数snd_hwdep_new这个函数不在内核中,但是kill_proc_info已经在符号标志中,也就是已经编译在内核里面了,但是为什么内核还是找不到它呢,没事我们逐个解决。
EXPORT_SYMBOL
EXPORT_SYMBOL只出现在2.6内核中,在2.4内核默认的非static 函数和变量都会自动导入到kernel 空间的, 都不用EXPORT_SYMBOL() 做标记的。
2.6以后的内核就必须用EXPORT_SYMBOL() 来导出来(因为2.6默认不到处所有的符号)。
EXPORT_SYMBOL的作用是什么?
EXPORT_SYMBOL标签内定义的函数或者符号对全部内核代码公开,不用修改内核代码就可以在您的内核模块中直接调用,即使用EXPORT_SYMBOL可以将一个函数以符号的方式导出给其他模块使用。
这里要和System.map做一下对比:
System.map 中的是链接时的函数地址。链接完成以后,在2.6内核运行过程中,是不知道哪个符号在哪个地址的。
EXPORT_SYMBOL 的符号, 是把这些符号和对应的地址保存起来,在内核运行的过程中,可以找到这些符号对应的地址。而模块在加载过程中,其本质就是能动态连接到内核,如果在模块中引用了内核或其它模块的符号,就要EXPORT_SYMBOL这些符号,这样才能找到对应的地址连接。
使用需要三个步骤
第一、在模块函数定义之后使用EXPORT_SYMBOL(函数名)
第二、在掉用该函数的模块中使用extern对之声明
第三、首先加载定义该函数的模块,再加载调用该函数的模块
另外,在编译调用某导出函数的模块时,往往会有
WARNING: "****" [**********] undefined!
这个正好就是我们编译驱动时出现的那个警告
解决kill_proc_info
问题解析
kill_proc_info
这个函数已经编译进了内核(在符号表中有这个函数),但是模块仍然找不到地址,这个说明一个问题,就是我们的函数kill_proc_info
并没有被导出,也就是加载模块时候,模块驱动找不到地址。
我们查找一下内核源码中,所有出现kill_proc_info
的地方
我们会发现没有EXPORT_SYMBOL
的记录,也就是说**该函数的确没有导出, 即外界不可访问。
那么我们就按照那三个步骤我们以此查找
在模块函数定义之后使用EXPORT_SYMBOL(函数名)
首先我们找到定义函数的地方,前我们已经用grep -r kill_proc_info *
查找内核源码中所有出现kill_proc_info的地方。
我们可以看到其声明在include/linux/sched.h
定义在kernel/signal.c
中
其声明的函数原型为
extern int kill_proc_info(int, struct siginfo *, pid_t);
那么我们就在kernel/signal.c
文件中函数定义之后添加如下代码
EXPORT_SYMBOL(kill_proc_info);
在掉用该函数的模块中使用extern对之声明
然后我们在自己的模块中,调用该函数之前,声明此函数,声明如下
extern int kill_proc_info(int, struct siginfo *, pid_t);
重新编译内核和我们的驱动模块
最后我们重新编译内核和我们的驱动模块,我们可以发现我们的
WARNING: "****" [kill_proc_info] undefined!
这个警告消失了,同时我们再次查找kill_proc_info
的信息,我们可以看到,除了EXPORT_SYMBOL的信息,System.map中也多了几项关于kill_proc_info的信息
解决snd_hwdep_new
问题解析
对于snd_hwdep_new
我们采用同kill_proc_info
同样的方法逐个排除其原因
我们发现该函数
声明在
include\sound/hwdep.c
定义在sound/core/hwdep.c
已经被EXPORT_SYMBOL导出
符号表System.map中没有这个函数
这个说明我们的函数所属的模块在内核编译的过程中,没有被编译进内核中。 这样我们的驱动模块使用它的函数同样找不到地址。
查找所属模块
那么我们来确认一下我们推断的正确性。
我们需要查找到该函数所属的模块,然后到该模块sound/core
下,查看Kconfig和Makefile的信息。
首先进入该模块sound\core
,源代码文件为hwdep.c,那么目标代码很有可能就叫做hwdep.o
,我们看目录下有没有这个目标文件
很明显没有,但是我们并不能保证它就没有参与编译,因为它编译的目标文件也有可能不是hwdep.o
, 这个依赖关系我们可以到Makefile中查找。
很明显编译后,的确会生成hwdep.o
而且我们也发现,这个配置信息是CONFIG_HW_DEP
那么我们就去Kconfig中查找这个变量的配置
我们可以发现这个模块属于ALSA SOUND
,那么我们就好解决了,可以有多种方案实现
,其实解决方案的本质是一样的,
一个是直接修改配置文件Kconfig,使其编译进去内核
一个是使用make menuconfig或者其他配置工具,选择编译所属模块
我选择了第一种,因为我们现在已经找到了其Kconfig配置所在的位置,直接修改即可,选择第二种,方案的话往往不好取舍。
配置编译
我们在模块配置的地方加入default y
意为在编译内核的时候,直接将模块编译进内核,
这就相当于在make menuconfig的时候在该模块的地方选择y
(或者*
)
重新编译内核和我们的驱动模块
最后我们重新编译内核和我们的驱动模块,我们可以发现我们的
WARNING: "****" [snd_hwdep_new] undefined!
这个警告消失了,同时我们再次查找snd_hwdep_new
的信息,我们可以看到,除了EXPORT_SYMBOL的信息,System.map中出现关于snd_hwdep_new的信息
总结
出现Unknown symbol in module,其本质就是模块在加载过程中,找不到函数的地址
那么我们就查找System.map
和函数声明和定义的地方时候用EXPORT_SYMBOL
然后确认其具体出现原因,一般有两个
①函数未被EXPORT_SYMBOL,导致加载时找不到链接地址
②函数所在模块未被编译到内核中,导致加载时找不到链接地址
我们逐个排查,找出原因所在后,逐个解决即可,完成后重新编译内核已经驱动模块。
但是要注意如果您依赖的函数也是一个驱动模块,则应该首先加载定义该函数的模块,再加载调用该函数的模块