Linux内核模块编程问题

来源:互联网 发布:网址翻译软件 ios 编辑:程序博客网 时间:2024/06/09 08:39

最近在做的实验,以内核模块方式实现,现在就内核模块编程遇到的的一些问题,做以记录如下:

1.内核函数访问用户态空间

在内核模块中若想要打开或者写一个文件时,可以调用内核中的open/write函数,但是内核中的这些函数其中的一些参数是需要从用户态传过来的,也就是有“__user”修饰的函数(该类函数需要进行内存地址的检查变换)。如果在内核中直接指定一个文件或者缓冲区(内核空间中的),则会报错“Bad address”(返回-EFAULT),因为现在这些函数需要的是从用户空间地址,这些内核函数会对参数进行检查,判断其地址是不是用户空间的。 
为了解决用户空间和内核空间数据交换的问题,有两种方式可以解决:

1.使用brk内核函数

用户态可以使用malloc函数申请内存,其在内核中是通过brk内核函数实现的,可以改变数据段的大小,为了实现用户态和内核态数据的交换,就可以依靠brk来实现。在内核模块中通过current可以定位到当前进程数据段大小,然后通过内核函数brk增加当前进程的内存空间,最后就可以通过copy_to_user/copy_from_user进行最终数据的交换。比如在内核中使用open打开文件,可以如下进行:

unsigned long getUserMem(int length)
{
unsigned long mmm = 0;
int ret;
 
mmm = current->mm->brk; //获取当前进程数据段的大小
ret = brk(mmm+length); //利用brk为当前进程增加length大小的内存空间
if(ret <0)
{
printk(KERN_ERR "Can't allocate userspace mem\n");
return (-ENOMEM);
}
return mmm;
}
 
char kernelBuf[256] = "test"; //内核模块中指定要打开的文件名
int len = strlen(kernelBuf);
//将filename指向getUserMem中返回的地址,此时filename指向的就是用户空间的缓冲区
char *filename = (void*)getUserMem(len) + 2;
 
copy_to_user(filename, kernelBuf, len); //将内核缓冲区中的kernelBuf通过copy_to_user复制到刚申请的用户缓冲区中
open(filename, O_RDWR, 0); //调用内核函数open打开,若是第一个参数使用kernelBuf则报错“Bad address”

方法二:使用set_fs和get_fs

使用set_fs()函数或宏(set_fs()可能是宏定义),如果为函数,其原形如下: 
void set_fs(mm_segment_t fs); 
  该函数的作用是改变kernel对内存地址检查的处理方式,其实该函数的参数fs只有两个取值:USER_DS,KERNEL_DS,分别代表用户空间和内核空间,默认情况下,kernel取值为USER_DS,即对用户空间地址检查并做变换。那么要在这种对内存地址做检查变换的函数中使用内核空间地址,就需要使用set_fs(KERNEL_DS)进行设置。get_fs()一般也可能是宏定义,它的作用是取得当前的设置,这两个函数的一般用法为:

mm_segment_t old_fs;
old_fs = get_fs();
set_fs(KERNEL_DS); //或者set_fs(get_ds());
...... //与内存有关的操作 ,open函数传内核空间的地址
set_fs(old_fs);

还有一些其它的内核函数也有用__user修饰的参数,在kernel中需要用kernel空间的内存代替时,都可以使用类似办法。 
会根据get_fs()的值决定是否执行函数参数有效性检查,若get_fs() ==USER_DS则执行检查,等于KERNEL_DS则跳过检查。 
相关宏定义如下:

/*
* The fs value determines whether argument validity checking should be
* performed or not. If get_fs() == USER_DS, checking is performed, with
* get_fs() == KERNEL_DS, checking is bypassed.
*
* For historical reasons, these macros are grossly misnamed.
*/
 
#define MAKE_MM_SEG(s) ((mm_segment_t) { (s) })
 
#define KERNEL_DS MAKE_MM_SEG(-1UL)
#define USER_DS MAKE_MM_SEG(TASK_SIZE_MAX)
 
#define get_ds() (KERNEL_DS)
#define get_fs() (current_thread_info()->addr_limit)
#define set_fs(x) (current_thread_info()->addr_limit = (x))

2.内核模块中使用未导出的函数

一般我们在编写内核模块时,可以直接使用内核中使用EXPORT_SYMBOL或者EXPORT_SYMBOL_GPL导出的函数,没有导出的内核函数不能直接使用。否则会报错未定义:

WARNING: "do_sys_open" [/home/tiany/paper/mod/mySdelNotEcrypt_success/hello.ko] undefined!

那么我们到底能不能使用内核中没有导出的函数呢?答案肯定是可以的,那就是内核符号表。在2.6内核中,为了更好地调试内核,引入了kallsyms。kallsyms抽取了内核用到的所有函数地址(全局的、静态的)和非栈数据变量地址,生成了一个数据块,作为只读数据链接进kernel image。使用root权限可以/proc/kallsyms查看。 
使用kallsyms_lookup_name()(在kernel/kallsyms.c文件中定义的)函数可以找到对应符号在内核中的虚拟地址,要使用它必须启用CONFIG_KALLSYMS编译内核。 包含在头文件linux/kallsyms.h中. 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_addresses[i];
}
return module_kallsyms_lookup_name(name);
}
EXPORT_SYMBOL_GPL(kallsyms_lookup_name);

可以看到该函数已经使用EXPORT_SYMBOL_GPL,可以直接在内核模块中使用。例如,do_sys_open函数原型为long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode) 
为了在内核模块中使用它,可以采用如下方法:

//定义钩子函数,返回值和参数都需与do_sys_open函数原型一致
long (*orig_do_sys_open)(int dfd, const char __user *filename, int flags, umode_t mode);
//使用kallsyms_lookup_name函数获取符号do_sys_open的地址
orig_do_sys_open = (void*)kallsyms_lookup_name("do_sys_open");

如上处理后,在后边就可以直接使用orig_do_sys_open(…)方式使用do_sys_open函数了。

其他问题

1.在编译内核模块时有如下警告信息:

warning: the frame size of 4248 bytes is larger than 1024 bytes [-Wframe-larger-than=]

主要是因为内核中设置了堆栈报警大小,其默认值为1024.

  1. 编译内核模块时报错
error: function declaration isnt a prototype [-Werror=strict-prototypes]

主要问题是因为定义的函数没有任何参数,需要添加void解决。

原创粉丝点击