性能文章>如何使用Linux内核中没有被导出的变量或函数?>

如何使用Linux内核中没有被导出的变量或函数?原创

8566514

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 的字段,指向链接该超级块中所有的 inode 的链表 i_sb_list。我们也使用了自旋锁 spin_lock 对链表的相关操作进行了加锁,保护共享变量,现在看内核模块:

  • #include <linux/module.h>
  • #include <linux/fs.h>
  • #include <linux/init.h>
  • #include <linux/list.h>
  • #include <linux/spinlock.h>
  • #include <linux/kdev_t.h>
  • #include <linux/kallsyms.h> //方法二
  •  
  • /*
  • *方法二,使用kallsyms_lookup_name()查找函数或变量的虚拟地址
  • *使用时,需要注释其它方法的代码,取消此处及下面方法二的注释
  • spinlock_t * sb_lock_address;
  • struct list_head * super_blocks_address;
  • */
  •  
  • /*
  • *方法三,内核模块中直接使用内核函数的虚拟地址
  • *使用时,需要注释其他方法的代码,取消此处及下面方法三的注释
  • #define SUPER_BLOCKS_ADDRESS 0xffffffff91d2efe0
  • #define SB_LOCK_ADDRESS 0xffffffff922f35d4
  • */
  •  
  • static int __init my_init(void)
  • {
  • struct super_block *sb;
  • struct list_head *pos;
  • struct list_head *linode;
  • struct inode *pinode;
  • unsigned long long count = 0;
  •  
  • printk("\nPrint some fields of super_blocks:\n");
  •  
  • /*
  • *方法二
  • sb_lock_address = (spinlock_t *)kallsyms_lookup_name("sb_lock");
  • super_blocks_address = (struct list_head *)kallsyms_lookup_name("super_blocks");
  • spin_lock(sb_lock_address);
  • list_for_each(pos, super_blocks_address) {
  • */
  •  
  • /*
  • *方法三
  • spin_lock((spinlock_t *)SB_LOCK_ADDRESS);
  • list_for_each(pos, (struct list_head *)SUPER_BLOCKS_ADDRESS) {
  • */
  •  
  • /*此处使用了未导出变量,若使用方法二或方法三时需要注释以下两行*/
  • spin_lock(&sb_lock); //加锁,此处使用了未导出的变量
  • list_for_each(pos, &super_blocks) {
  •  
  • sb = list_entry(pos, struct super_block, s_list);
  • printk("dev_t:%d:%d", MAJOR(sb->s_dev),MINOR(sb->s_dev));
  • //打印文件系统所在设备的主设备号和次设备号
  • printk("file_type name:%s\n", sb->s_type->name);
  • //打印文件系统名
  •  
  • list_for_each(linode, &sb->s_inodes) {
  • pinode=list_entry(linode, struct inode, i_sb_list);
  • count++;
  • printk("%lu\t", pinode->i_ino); //打印索引节点号
  • }
  • }
  • //spin_unlock(sb_lock_address); //方法二
  • //spin_unlock(SB_LOCK_ADDRESS); //方法三
  • spin_unlock(&sb_lock); //解锁,此处使用了未导出的变量
  • printk("The number of inodes:%llu\n", sizeof(struct inode)*count);
  • return 0;
  • }
  •  
  • static void __exit my_exit(void)
  • {
  • printk("unloading…\n");
  • }
  • module_init(my_init);
  • module_exit(my_exit);
  • MODULE_LICENSE("GPL");
  •  

上面的内核模块中,我们使用 list_for_each 函数来遍历系统中的链接所有 super_block 的双向循环链表 s_list,此宏有两个参数,第一个参数是 pos, 是一个输出型参数, 用于保存每次遍历得到的 list_head 类型的结点的地址,第二个参数是 head ,是一个输入型参数,用于向要遍历的链表传递头结点,list_for_each 函数在4.19内核中定义如下:

  • #define list_for_each(pos, head) \
  • for (pos = (head)->next; pos != (head); pos = pos->next)
  •  

list_for_each 函数只能遍历超级块中的双向循环链表 s_list,不能得到正在被遍历的超级块的地址,此时无法访问超级块的其它字段,我们再使用内核中的 list_entry 函数,通过当前超级块中的成员 s_list 的地址,获得当前超级块的地址。该函数有三个参数,第一个参数是指向结构体成员的指针,第二个参数是结构体的类型, 第三个参数是结构体成员的名称 s_list ,该函数最后返回结构体的首地址。list_for_each 在4.19内核中定义如下:

  • #define hlist_entry(ptr, type, member) container_of(ptr,type,member)
  •  

可以看到,它是 container_of 宏的一个封装,我们再看内核中的 container_of 宏,定义如下:

  • #define container_of(ptr, type, member) ({ \
  • void *__mptr = (void *)(ptr); \
  • BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) && \
  • !__same_type(*(ptr), void), \
  • "pointer type mismatch in container_of()"); \
  • ((type *)(__mptr - offsetof(type, member))); })
  •  

本内核模块对应的Makefile文件如下:

  • obj-m:=print_sb.o
  • CURRENT_PATH:=$(shell pwd)
  • LINUX_KERNEL:=$(shell uname -r)
  • LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
  • all:
  • make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
  • clean:
  • make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
  •  

在使用内核中未被导出的变量时,执行make命令进行编译,发生如下错误:

执行make命令进行编译发生错误 - 使用Linux内核中没有被导出的变量或函数 - HeapDump性能社区

编译报错,‘sb_lock’ undeclared (first use in this function);,‘super_blocks’ undeclared (first use in this function);,即sb_lock和super_blocks变量没有定义,实际上在内核中已经定义了这两个变量,包含在头文件fs.h和spinlock.h中,在内核源码中如下:

在4.19版内核fs\super.c中,定义了 super_blocks 变量来指向 super_block 结构中的 s_list 双链表的链表头,其中 s_list 是用来链接系统中已安装文件系统超级块的双向循环链表,也定义了 sb_lock 锁变量对超级块的相关操作进行加锁,如下图:

内核源码头文件fs.h和spinlock.h中定义sb_lock和super_blocks的源代码 - 使用Linux内核中没有被导出的变量或函数 - HeapDump性能社区

编译报错的原因就是在内核中并没有导出sb_lock和super_blocks变量,那么问题来了:

1、我们为什么不导出更多的变量或者函数来供我们使用呢?

2、我们可以使用内核中没有导出的函数或变量吗?如果可以使用,如何使用?

下面给出一些方法来使用内核中未被导出的变量或函数,并进行验证。

方法一:使用EXPORT_SYMBOL宏导出函数或变量

Linux内核提供了一个方便的方法用来管理符号的对模块外部的可见性,即内核符号表。在4.19版内核include\linux\export.h中,定义了EXPORT_SYMBOL宏,如下图:

4.19版内核include\linux\export.h 定义了EXPORT_SYMBOL宏 - 使用Linux内核中没有被导出的变量或函数 - HeapDump性能社区

如果我们要使用内核中的变量或函数,可以使用上图中的宏,在函数或变量定义后使用如下宏,然后编译内核:

  • EXPORT_SYMBOL(sb_lock);
  •  

或者

  • EXPORT_SYMBOL_GPL(sb_lock);
  •  

此时EXPORT_SYMBOL定义的函数或者变量对全部内核代码公开,不用修改内核代码就可以在内核模块中直接调用,即使用EXPORT_SYMBOL可以将一个函数或变量以符号的方式导出给其他模块使用。EXPORT_SYMBOL导出的符号,是把这些符号和对应的地址保存起来,在内核运行的过程中,可以找到这些符号对应的地址。而模块在加载过程中,其本质就是能动态连接到内核,如果在模块中引用了内核或其它模块的符号,就要EXPORT_SYMBOL这些符号,这样才能找到对应的地址连接。
上面的两个宏均可把给定的符号导出到模块外,EXPORT_SYMBOL_GPL宏只能使符号对GPL许可的模块可用。符号必须在模块文件的全局部分导出,不能在函数中导出,这是因为上述这两个宏将被扩展成一个特殊用途的声明,而该变量必须是全局的。这个变量存储于模块的一个特殊的可执行部分(一个"ELF段" ),在装载时,内核通过这个段来寻找模块导出的变量。
上述方法需要修改内核代码,编译内核

方法二:使用kallsyms_lookup_name()查找函数或变量的虚拟地址

kallsyms抽取了内核用到的所有函数地址(全局的、静态的)和非栈数据变量地址,生成了一个数据块,作为只读数据链接进kernel image,使用root权限可以在/proc/kallsyms中查看,没错,root权限下是可以直接看到内核函数的虚拟地址,如下图所示:

root权限下可以直接看到kallsyms抽取的内核函数的虚拟地址 - 使用Linux内核中没有被导出的变量或函数 - HeapDump性能社区

使用kallsyms_lookup_name()函数可以找到对应符号在内核中的虚拟地址,包含在头文件linux/kallsyms.h中,它接受一个字符串格式内核函数,返回那个内核函数的地址,如果没找到指定的内核函数,它会返回0,要使用它必须启用CONFIG_KALLSYMS配置编译内核。

使用kallsyms_lookup_name()函数可以找到对应符号在内核中的虚拟地址 - 使用Linux内核中没有被导出的变量或函数 - HeapDump性能社区

在4.19版内核kernel\kallsyms.c中kallsyms_lookup_name()函数定义如下:

  • /* Lookup the address for this symbol. Returns 0 if not found. */
  • unsigned long kallsyms_lookup_name(const char *name)
  • {
  • char namebuf[KSYM_NAME_LEN];
  • unsigned long i;
  • unsigned int off;
  •  
  • for (i = 0, off = 0; i < kallsyms_num_syms; i++) {
  • off = kallsyms_expand_symbol(off, namebuf, ARRAY_SIZE(namebuf));
  •  
  • if (strcmp(namebuf, name) == 0)
  • return kallsyms_sym_address(i);
  • }
  • return module_kallsyms_lookup_name(name);
  • }
  • EXPORT_SYMBOL_GPL(kallsyms_lookup_name);
  •  

可以看到该函数已经使用EXPORT_SYMBOL_GPL,可以直接在内核模块中使用。如果要使用内核中未被导出的函数,我们可以定义钩子函数,返回值和参数都要与我们要导出的函数原型一致。在本文打印超级块super_block结构中一些域值的内核模块代码中,我们把使用了内核未导出的变量和方法三相关代码进行注释,取消方法二相关代码的注释,再执行make命令进行编译,可以看到,并没有编译报错,我们已经成功地使用了内核中未被导出的的变量 sb_lock 和 super_blocks,编译结果如下图所示。

成功地使用了内核中未被导出的的变量 sb_lock 和 super_blocks 编译结果 - 使用Linux内核中没有被导出的变量或函数 - HeapDump性能社区

加载模块后使用dmesg查看结果:

使用dmesg查看成功地使用了内核中未被导出的的变量 sb_lock 和 super_blocks 编译结果 - 使用Linux内核中没有被导出的变量或函数 - HeapDump性能社区

反之,内核中也有通过虚拟地址查找内核中的函数或变量的函数 sprint_symbol,在内核中被定义如下:

  • int sprint_symbol(char *buffer, unsigned long address)
  • {
  • return __sprint_symbol(buffer, address, 0, 1);
  • }
  • EXPORT_SYMBOL_GPL(sprint_symbol);
  •  

可以看到,sprint_symbol 函数是__sprint_symbol函数的封装,该函数已经使用EXPORT_SYMBOL_GPL导出,可以直接在内核模块中使用。该函数有两个参数,第一个参数是buffer,字符型文本缓冲区, 它用来记录内核符号的信息, 是一个输出型参数,第二个参数是address,无符号长整型的内核符号中的某一地址, 是一个输入型参数。该函数中调用了__sprint_symbol内核函数,定义如下:

  • /* Look up a kernel symbol and return it in a text buffer. */
  • static int __sprint_symbol(char *buffer, unsigned long address,
  • int symbol_offset, int add_offset)
  • {
  • char *modname;
  • const char *name;
  • unsigned long offset, size;
  • int len;
  •  
  • address += symbol_offset;
  • name = kallsyms_lookup(address, &size, &offset, &modname, buffer);
  • if (!name)
  • return sprintf(buffer, "0x%lx", address - symbol_offset);
  •  
  • if (name != buffer)
  • strcpy(buffer, name);
  • len = strlen(buffer);
  • offset -= symbol_offset;
  •  
  • if (add_offset)
  • len += sprintf(buffer + len, "+%#lx/%#lx", offset, size);
  •  
  • if (modname)
  • len += sprintf(buffer + len, " [%s]", modname);
  •  
  • return len;
  • }
  •  

__sprint_symbol函数的功能是根据一个内存中的地址 address 查找一个内核符号,并将该符号的基本信息,如符号名 name在内核符号表中的偏移 offset 和大小 size,所属的模块名(如果有的话)等信息连接成字符串赋值给文本缓冲区 buffer,所查找的内核符号可以是原本就存在于内核中的符号,也可以是位于动态插入的模块中的符号,其中使用了kallsyms_lookup函数,定义如下:

  • static inline const char *kallsyms_lookup(unsigned long addr,
  • unsigned long *symbolsize,
  • unsigned long *offset,
  • char **modname, char *namebuf)
  • {
  • return NULL;
  • }
  •  

方法三:内核模块中直接使用内核函数的虚拟地址

首先介绍两种获取内核函数或变量虚拟地址的方法:
1、在 /proc/kallsyms 文件获得内核函数或变量的虚拟地址
此方法同样用到kallsyms,我们可以使用如下命令直接找到内核中 sb_lock 和 super_block 变量的虚拟地址,命令如下,图如下:

  • cat /proc/kallsyms | grep sb_lock
  •  
  • cat /proc/kallsyms | grep super_blocks
  •  

直接找到内核中 sb_lock 和 super_block 变量的虚拟地址 - 使用Linux内核中没有被导出的变量或函数 - HeapDump性能社区

2、在 System.map 文件获得内核函数或变量的虚拟地址
内核镜像的 System.map 文件存储了内核符号表的信息, 可以通过此文件获取到具体内核函数或变量的虚拟地址,命令如下,图如下:

  • grep sb_lock /boot/System.map-4.18.0-15-generic
  •  
  • grep super_blocks /boot/System.map-4.18.0-15-generic
  •  

内核镜像的 System.map 文件存储了内核符号表的信息 - 使用Linux内核中没有被导出的变量或函数 - HeapDump性能社区

还可以通过给定一个虚拟地址来查看地址对应哪个内核函数,命令如下图如下:

  • grep ffffffff82af35d4 /boot/System.map-4.18.0-15-generic
  •  
  • grep ffffffff8252efe0 /boot/System.map-4.18.0-15-generic
  •  

内核镜像的 System.map 文件存储了内核符号表的信息 - 使用Linux内核中没有被导出的变量或函数 - HeapDump性能社区

可以看到,不管用哪种方法,此时内核中 sb_lock 变量的虚拟地址为ffffffff922f35d4,super_blocks 变量的虚拟地址为ffffffff91d2efe0。现在修改内核模块代码,我们把使用了内核未导出的变量和方法二相关的代码进行注释,取消方法三相关代码的注释,再执行make命令进行编译。
结果显示,这种方法也可以使用内核中未被导出的变量或函数,但是这仅仅可以临时使用,并非长久之计,每次重启系统,这个变量的虚拟地址会发生变化,若要继续使用,还得再查看地址,再修改宏定义,至于地址发生变化的原因,这与内核符号表有关。/proc/kallsyms文件是在内核启动后生成的,是动态的符号表,位于文件系统的/proc目录下,实现代码在kernel/kallsyms.c,使用前提是内核必须打开CONFIG_KALLSYMS编译选项。一般情况下,还是推荐使用第二种方法。

本文来自:Linux内核之旅,作者: 梁金荣,Linux内核之旅社区负责人。

 

相关阅读

在调试器里看LINUX内核态栈溢出

深入理解Linux内核进程上下文切换

一张图看懂linux内核中percpu变量的实现

分类:标签:
请先登录,查看5条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

在调试器里看LINUX内核态栈溢出
图灵最先发明了栈,但没有给它取名字。德国人鲍尔也“发明”了栈,取名叫酒窖。澳大利亚人汉布林也“发明”了栈,取名叫弹夹。1959年,戴克斯特拉在度假时想到了Stack这个名字,后来被广泛使用。
LONG究竟有多长,从皇帝的新衣到海康SDK
转眼之间初中毕业30年了,但我仍清楚的记得初中英语的一篇课文,题目叫《皇帝的新装》(“The king’s new clothes”)。这篇课文的前两句话是:”Long long ago, there
雕刻在LINUX内核中的LINUS故事
因为LINUX操作系统的流行,Linus 已经成为地球人都知道的名人。虽然大家可能都听过钱钟书先生的名言:“假如你吃个鸡蛋觉得味道不错,又何必认识那个下蛋的母鸡呢?” 但是如果真是遇到一个“特别显赫”
从猫蛇之战再看内核戏CPU
连续写了几天的代码,有些疲倦,吃过晚饭,换个工作方式,继续和大家聊猫蛇之战。蛇不仅丑陋,而且可能伤人害命,是邪恶的象征。猫与蛇战,代表着讨伐奸邪,是正义之战。猫与蛇战,技艺娴熟,举重若轻,叫人拍手叫绝
如何使用Linux内核中没有被导出的变量或函数?
本文详细介绍了使用EXPORT_SYMBOL宏导出函数或变量、使用kallsyms_lookup_name()查找函数或变量的虚拟地址以及内核模块中直接使用内核函数的虚拟地址等3种方案解决没有被EXPORT_SYMBOL 相关的宏导出的变量或函数不能直接使用的问题
LINUX网络子系统中DMA机制的实现
我们先从计算机组成原理的层面介绍DMA,再简单介绍Linux网络子系统的DMA机制是如何的实现的。 计算机组成原理中的DMA 以往的I/O设备和主存交换信息都要经过CPU的操作。不论是最早的轮询方式,
内存泄漏(增长)火焰图
本文总结了在分析内存增长和内存泄漏问题用到的4种追踪方法得到有关内存使用情况的代码路径,使用栈追踪技术对代码路径进行检查,并且会以火焰图的形式把它们可视化输出,在Linux上演示分析过程,随后概述其它系统的情况。
为什么容器内存占用居高不下,频频 OOM(续)
在之前的文章《[为什么容器内存占用居高不下,频频 OOM](https://heapdump.cn/article/1589003)》 中,我根据现状进行了分析和说明,收到了很多读者的建议和疑