欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-05-09 21:23:59
...

如何使用Linux内核中没有被EXPORT_SYMBOL宏导出的变量或函数? 我们拿代码举例,给出三种方法,给出验证,不妨先看如下一段内核模块,功能为打印超级块super_block结构中一些域的值:

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

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");
    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);		
printk("The number of inodes:%llu\n", sizeof(struct inode)*count);  
return 0;
}

static void __exit my_exit(void)  
{  
        printk("unloading print_sb\n");  
}  
module_init(my_init);  
module_exit(my_exit);  
MODULE_LICENSE("GPL");  

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

但是在编译过程中会发生如下错误:
如何使用Linux内核中没有被导出的变量或函数
编译报错,‘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锁变量对超级块的相关操作进行加锁,如下图:
如何使用Linux内核中没有被导出的变量或函数
编译报错的原因就是在内核中并没有导出sb_lock和super_blocks变量,那么问题来了:

1、我们为什么不导出更多的变量或者函数来供我们使用呢?
感兴趣的同学可以和我讨论一下。

2、那么我们可以使用内核中没有导出的函数或变量吗?
答案是肯定的。

下面给出一些方法来使用内核中未被导出的函数或变量:

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

Linux内核提供了一个方便的方法用来管理符号的对模块外部的可见性,即内核符号表。在4.19版内核include\linux\export.h中,定义了EXPORT_SYMBOL宏,如下图:
如何使用Linux内核中没有被导出的变量或函数
如果我们要使用内核中的变量或函数,可以使用上图中的宏,在函数或变量定义后使用如下宏,然后编译内核:

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权限下是可以直接看到内核函数的虚拟地址,如下图所示:
如何使用Linux内核中没有被导出的变量或函数
使用kallsyms_lookup_name()函数可以找到对应符号在内核中的虚拟地址,包含在头文件linux/kallsyms.h中,它接受一个字符串格式内核函数,返回那个内核函数的地址,如果没找到指定的内核函数,它会返回0,要使用它必须启用CONFIG_KALLSYMS配置编译内核。 在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结构中一些域值的例子中,我们可以这样使用:

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

spinlock_t * sb_lock_address;
struct list_head * super_blocks_address;

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;

    sb_lock_address = (spinlock_t *)kallsyms_lookup_name("sb_lock"); 
    super_blocks_address = (struct list_head *)kallsyms_lookup_name("super_blocks"); 

    printk("\nPrint some fields of super_blocks:\n");

    spin_lock(sb_lock_address); 
    
    list_for_each(pos, super_blocks_address) {
        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);		
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");  

编译过程如下图所示,可以看到,并没有编译报错,我们已经成功地使用了内核中未被导出的的变量 sb_lock 和 super_blocks:
如何使用Linux内核中没有被导出的变量或函数
加载模块后使用dmesg查看结果:
如何使用Linux内核中没有被导出的变量或函数
反之,内核中也有通过虚拟地址查找内核中的函数或变量的函数 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;
}

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

首先介绍三种获取内核函数或变量的方法

在 /proc/kallsyms 文件获得内核函数或变量的虚拟地址

此方法同样用到kallsyms,我们可以使用如下命令直接找到内核中 sb_lock 和 super_block 变量的虚拟地址,命令如下,图如下:

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

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

在 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

如何使用Linux内核中没有被导出的变量或函数
还可以通过给定一个虚拟地址来查看地址对应哪个内核函数,命令如下图如下:

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

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

可以看到,不管用哪种方法,内核中 sb_lock 变量的虚拟地址为ffffffff922f35d4,super_blocks 变量的虚拟地址为ffffffff91d2efe0,现在我们修改内核模块代码为如下:

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

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

    spin_lock((spinlock_t *)SB_LOCK_ADDRESS); //加锁
    
    list_for_each(pos, (struct list_head *)SUPER_BLOCKS_ADDRESS) { 

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

结果显示,这种方法也可以使用内核中未被导出的函数或变量,但是这仅仅可以临时使用,并非长久之计,每次重启系统,这个变量的虚拟地址会发生变化,若要继续使用,还得再查看地址,再修改宏定义,至于地址发生变化的原因,这与内核符号表有关。
/proc/kallsyms文件是在内核启动后生成的,是动态的符号表,位于文件系统的/proc目录下,实现代码在kernel/kallsyms.c,使用前提是内核必须打开CONFIG_KALLSYMS编译选项。
如何使用Linux内核中没有被导出的变量或函数


静而后定,定能生慧

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