Linux内核为了减轻命名空间的污染,并做到正确的信息隐藏,内核提供了管理内核符号可见性的方式,没有被EXPORT_SYMBOL相关的宏导入的变量或函数是不能直接使用的,为了说明并解决这个问题,我们不妨先看如下一段内核模块,功能为复印超级块super_block结构中一些域的值。
我们晓得vfs(虚拟文件系统)是用super_block(超级块)来描述整个文件系统的信息,内核在对一个文件系统进行初始化和注册时,就为其分配了一个super_block,该文件系统卸载时,其对应的super_block也会被手动删掉。super_block结构中有一个list_head类型的数组s_list拿来把系统中的super_block组成一个单向循环数组,并使用一个称作super_blocks的全局变量来指向该单向循环数组中的第一个元素。super_block中还有一个称作s_inodes的数组linux内核打印调用栈,指向链接该超级块中所有的inode的数组i_sb_list。我们也使用了载流子锁spin_lock对数组的相关操作进行了加锁,保护共享变量,如今看内核模块:
里面的内核模块中,我们使用list_for_each函数来遍历系统中的链接所有super_block的单向循环数组s_list,此宏有两个参数,第一个参数是pos,是一个输出型参数linux内核打印调用栈,用于保存每次遍历得到的list_head类型的结点的地址,第二个参数是head,是一个输入型参数,用于向要遍历的数组传递头结点,list_for_each函数在4.19内核中定义如下:
list_for_each函数只能遍历超级块中的单向循环数组s_list,不能得到正在被遍历的超级块的地址,此时难以访问超级块的其它数组,我们再使用内核中的list_entry函数,通过当前超级块中的成员s_list的地址,获得当前超级块的地址。该函数有三个参数,第一个参数是指向结构体成员的表针,第二个参数是结构体的类型,第三个参数是结构体成员的名称s_list,该函数最后返回结构体的首地址。list_for_each在4.19内核中定义如下:
可以看见,它是container_of宏的一个封装,我们再看内核中的container_of宏,定义如下:
本内核模块对应的Makefile文件如下:
在使用内核中未被导入的变量时中标麒麟linux,执行make命令进行编译,发生如下错误:
编译报错,‘sb_lock’undeclared(firstuseinthisfunction);,‘super_blocks’undeclared(firstuseinthisfunction);,即sb_lock和super_blocks变量没有定义,实际上在内核中早已定义了这两个变量,包含在头文件fs.h和spinlock.h中,在内核源码中如下:
在4.19版内核fssuper.c中,定义了super_blocks变量来指向super_block结构中的s_list双数组的数组头,其中s_list是拿来链接系统中已安装文件系统超级块的单向循环数组,也定义了sb_lock锁变量对超级块的相关操作进行加锁,如右图:
编译报错的诱因就是在内核中并没有导入sb_lock和super_blocks变量,这么问题来了:
1、我们为何不导入更多的变量或则函数来供我们使用呢?
2、我们可以使用内核中没有导入的函数或变量吗?假如可以使用,怎么使用?
下边给出一些技巧来使用内核中未被导入的变量或函数,并进行验证。
方式一:使用EXPORT_SYMBOL宏导入函数或变量
Linux内核提供了一个便捷的方式拿来管理符号的对模块外部的可见性,即内核符号表。在4.19版内核includelinuxexport.h中,定义了EXPORT_SYMBOL宏,如右图:
假如我们要使用内核中的变量或函数,可以使用上图中的宏,在函数或变量定义后使用如下宏,之后编译内核:
或则
此时EXPORT_SYMBOL定义的函数或则变量对全部内核代码公开,不用更改内核代码就可以在内核模块中直接调用,即使用EXPORT_SYMBOL可以将一个函数或变量以符号的形式导入给其他模块使用。EXPORT_SYMBOL导入的符号,是把这种符号和对应的地址保存上去,在内核运行的过程中,可以找到这种符号对应的地址。而模块在加载过程中,其本质就是能动态联接到内核,假如在模块中引用了内核或其它模块的符号linux服务器维护,就要EXPORT_SYMBOL这种符号,这样就能找到对应的地址联接。
里面的两个宏均可把给定的符号导入到模块外,EXPORT_SYMBOL_GPL宏只能使符号对GPL许可的模块可用。符号必须在模块文件的全局部份导入,不能在函数中导入,这是由于上述这两个宏将被扩充成一个特殊用途的申明,而该变量必须是全局的。这个变量储存于模块的一个特殊的可执行部份(一个”ELF段”),在装载时,内核通过这个段来找寻模块导入的变量。
上述方式须要更改内核代码,编译内核。
方式二:使用kallsyms_lookup_name()查找函数或变量的虚拟地址
kallsyms抽取了内核用到的所有函数地址(全局的、静态的)和非栈数据变量地址,生成了一个数据块,作为只读数据链接进kernelimage,使用root权限可以在/proc/kallsyms中查看,没错,root权限下是可以直接看见内核函数的虚拟地址,如右图所示:
使用kallsyms_lookup_name()函数可以找到对应符号在内核中的虚拟地址,包含在头文件linux/kallsyms.h中,它接受一个字符串格式内核函数,返回那种内核函数的地址,倘若没找到指定的内核函数,它会返回0,要使用它必须启用CONFIG_KALLSYMS配置编译内核。
在4.19版内核kernelkallsyms.c中kallsyms_lookup_name()函数定义如下:
可以看见该函数早已使用EXPORT_SYMBOL_GPL,可以直接在内核模块中使用。假如要使用内核中未被导入的函数,我们可以定义钩子函数,返回值和参数都要与我们要导入的函数原型一致。在本文复印超级块super_block结构中一些域值的内核模块代码中,我们把使用了内核未导入的变量和技巧单相关代码进行注释,取消方式二相关代码的注释,再执行make命令进行编译,可以看见,并没有编译报错,我们早已成功地使用了内核中未被导入的的变量sb_lock和super_blocks,编译结果如右图所示。
加载模块后使用dmesg查看结果:
反之,内核中也有通过虚拟地址查找内核中的函数或变量的函数sprint_symbol,在内核中被定义如下:
可以看见,sprint_symbol函数是__sprint_symbol函数的封装,该函数早已使用EXPORT_SYMBOL_GPL导入,可以直接在内核模块中使用。该函数有两个参数,第一个参数是buffer,字符型文本缓冲区,它拿来记录内核符号的信息,是一个输出型参数,第二个参数是address,无符号长整型的内核符号中的某一地址,是一个输入型参数。该函数中调用了__sprint_symbol内核函数,定义如下:
__sprint_symbol函数的功能是按照一个显存中的地址address查找一个内核符号,并将该符号的基本信息,如符号名name在内核符号表中的偏斜offset和大小size,所属的模块名(假如有的话)等信息联接成字符串形参给文本缓冲区buffer,所查找的内核符号可以是先前就存在于内核中的符号,也可以是坐落动态插入的模块中的符号,其中使用了kallsyms_lookup函数,定义如下:
方式三:内核模块中直接使用内核函数的虚拟地址
首先介绍两种获取内核函数或变量虚拟地址的方式:
1、在/proc/kallsyms文件获得内核函数或变量的虚拟地址
此方式同样用到kallsyms,我们可以使用如下命令直接找到内核中sb_lock和super_block变量的虚拟地址,命令如下,图如下:
2、在System.map文件获得内核函数或变量的虚拟地址
内核镜像的System.map文件储存了内核符号表的信息,可以通过此文件获取到具体内核函数或变量的虚拟地址,命令如下,图如下:
还可以通过给定一个虚拟地址来查看地址对应那个内核函数,命令如右图如下:
可以看见,不管用哪种方式,此时内核中sb_lock变量的虚拟地址为ffffffff922922ff353535dd4,super_blocks变量的虚拟地址为ffffffff9191dd22efeefeefe0。现今更改内核模块代码,我们把使用了内核未导入的变量和技巧二相关的代码进行注释,取消方式单相关代码的注释,再执行make命令进行编译。
结果显示,这些方式也可以使用内核中未被导入的变量或函数,并且这仅仅可以临时使用,并非长久之计,每次重启系统,这个变量的虚拟地址会发生变化,若要继续使用,还得再查看地址,再更改宏定义,至于地址发生变化的缘由,这与内核符号表有关。/proc/kallsyms文件是在内核启动后生成的,是动态的符号表,坐落文件系统的/proc目录下,实现代码在kernel/kallsyms.c,使用前提是内核必须打开CONFIG_KALLSYMS编译选项。通常情况下,还是推荐使用第二种方式。
本文来自:Linux内核之旅,作者:梁金荣,Linux内核之旅社区负责人。