Linux内核模块编程问题

Posted ty_laurel

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux内核模块编程问题相关的知识,希望对你有一定的参考价值。

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

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解决。

以上是关于Linux内核模块编程问题的主要内容,如果未能解决你的问题,请参考以下文章

Linux内核模块编程问题

Linux模块编程框架

《linux内核设计与分析》内核模块编程

Linux内核模块编程与内核模块LICENSE -《具体解释(第3版)》预读

《linux内核设计与分析》内核模块编程

inux内核模块编程入门