Linux内核态申请用户态内存空间的方法

目录

一、使用brk系统调用

二、使用set_fs、get_fs

三、do_mmap或者sys_mmap_pgoff


前面文章这里这里介绍过,内核空间跟用户地址空间是隔离的,正常情况下用户态访问不了内核地址空间;为了系统安全加入smap机制的内核也不允许内核随便访问用户态内存。那么如果有需求需要在内核态用到用户态地址空间,该怎么办呢,笔者前段时间就遇到这个问题,本文将分享下几种解决方案,也作为笔记记录之。

 

以下是维基百科中内核态跟用户态关系图:

可以看出用户态调用底层相关的接口都是通过glic封装过的接口,glic再去触发到内核态的系统调用。

 

在内核模块中也可以直接调用系统接口,如内核中的sys_open/sys_write函数,但是内核中的这些函数其中的一些参数是需要从用户态传过来的,也就是有“__user”修饰的参数。

如下(3.10.0-957版本内核,在内核源码/include/linux/syscalls.h中):

asmlinkage long sys_read(unsigned int fd, char __user *buf, size_t count);
asmlinkage long sys_readv(unsigned long fd,
                          const struct iovec __user *vec,
                          unsigned long vlen);
asmlinkage long sys_write(unsigned int fd, const char __user *buf,
                          size_t count);
asmlinkage long sys_writev(unsigned long fd,
                           const struct iovec __user *vec,
                           unsigned long vlen);
asmlinkage long sys_pread64(unsigned int fd, char __user *buf,
                            size_t count, loff_t pos);
asmlinkage long sys_pwrite64(unsigned int fd, const char __user *buf,
                             size_t count, loff_t pos);
asmlinkage long sys_preadv(unsigned long fd, const struct iovec __user *vec,
                           unsigned long vlen, unsigned long pos_l, unsigned long pos_h);
asmlinkage long sys_pwritev(unsigned long fd, const struct iovec __user *vec,
                            unsigned long vlen, unsigned long pos_l, unsigned long pos_h);

如果在内核中直接用kmalloc一段内存地址作为缓冲区传入,则会报错“Bad address”(返回-EFAULT),因为现在这些函数需要的是从用户空间地址,这些内核函数会对参数进行检查,判断其地址是不是用户空间的。 

有以下解决方案:

 

一、使用brk系统调用

用户态中是使用malloc函数申请内存,malloc在内核中是通过brk系统调用实现的,可以改变当前进程数据段的大小,所有可以用brk来实现内核态扩容进程的用户态地址空间。

前面文章中讲过,在内核代码中,current变量是对应当前正在执行的进程或线程的任务结构(task_struct),current里的mm成员则指向对应进程或线程的内存管理的数据结构,所以在内核模块中可以通过current可以定位到当前进程数据段大小,然后通过系统接口brk增加当前进程的内存地址空间,之后可以通过copy_to_user/copy_from_user进行内核态与用户态数据的传输。比如在内核模块中打开文件,代码示例:

char kbuf[256] = <span data-raw-text="" "="" data-textnode-index="173" data-index="1851" class="character">"test.log<span data-raw-text="" "="" data-textnode-index="173" data-index="1860" class="character">";   //the file name
int len = strlen(kbuf);

//get current process data segment size
unsigned long mmm = current->mm->brk;

//increase current process data segment size
int ret = brk(mmm+len); 
if(ret < 0)
  printk(<span data-raw-text="" "="" data-textnode-index="193" data-index="2067" class="character">"brk. increase data segment size occur error!\n<span data-raw-text="" "="" data-textnode-index="193" data-index="2114" class="character">");

char *filename = (void*)mmm + 2; //filename in userland

copy_to_user(filename, kbuf, len);
open(filename, O_RDWR, 0);

 

二、使用set_fs、get_fs

 如下为3.10.108内核中,set_fs、get_fs宏定义及说明:

/*
 * 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.
 *
 * Or at least it did once upon a time.  Nowadays it is 
 * a mask that defines which bits of the address space 
 * are off limits.  This is a wee bit faster than the above.
 *
 * For historical reasons, these macros are grossly misnamed.
 */
#define KERNEL_DS  ((mm_segment_t) { 0UL })
#define USER_DS    ((mm_segment_t) { -0x40000000000UL })

#define VERIFY_READ  0
#define VERIFY_WRITE  1

#define get_fs()  (current_thread_info()->addr_limit)
#define get_ds()  (KERNEL_DS)
#define set_fs(x) (current_thread_info()->addr_limit = (x))

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

mm_segment_t old_fs = get_fs(); 
set_fs(KERNEL_DS); 
/*
* 
* 这里可以用kmalloc申请到的内核地址,可以传入需要__user用户态参数的
*  */
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))

 

三、do_mmap或者sys_mmap_pgoff

有些情况下,通过第二种方法还是是无法绕过地址空间检测的,即之前文章介绍过的Direct IO下,所有IO调用(如read、write,pread、pwrite、preadv、pwritev、io_submit等),传入的缓冲区地址必须是用户态地址空,否则都会报错,那么这种情况下,如何在内核态申请到用户态内存呢?

可以使用do_mmap或者mmap_pgoff系统调用,这俩接口可以申请到某一进程的用户态的地址,该接口默认情况下是没有被内核导出的(就是说在内核模块中无法直接使用do_mmap、mmap_pgoff等接口),但是2.6.37及以上内核提供了kallsyms_lookup_name接口,可以通过kallsyms_lookup_name接口获取到系统调用的地址。我们知道centos6.x用的都是2.6.32.x的内核版本,centos7.x用的是3.10.0的内核,所有在centos7.x中可以轻松使用上述接口。

用户态的mmap,到内核其实就是调用了sys_mmap_pgoff,sys_mmap_pgoff又调用了do_mmap,编码时注意不同版本内核下,do_mmap接口签名的差异,如下是在3.10.0内核下代码示例:

#include <asm/mman.h> //do_mmap_pgoff

extern asmlinkage do_mmap_t sys_do_mmap;
extern asmlinkage do_munmap_t sys_do_munmap;

//malloc userland memory in kernelspace
void* kmalloc_user_memory(unsigned long size){

  void __user *mm_buf = NULL;
  if(sys_do_mmap != NULL){
    unsigned long populate = 0;
    down_write(&current->mm->mmap_sem);
    mm_buf = (char*)sys_do_mmap(NULL, 0, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, 0, 0, &populate, NULL);
    up_write(&current->mm->mmap_sem); 
  }
  if (mm_buf == NULL)
    printk("[err] kmalloc_user_memory failed!\n");
  return mm_buf;
}

/*
* free the userland memory malloced by kmalloc_user_memory func
*
* */
bool kfree_user_memory(void __user *buf, unsigned long size){
  int error = -1;
  if(sys_do_munmap != NULL){
    down_write(&current->mm->mmap_sem);
    error = sys_do_munmap(current->mm, buf, size, NULL);
    up_write(&current->mm->mmap_sem); 
  }

  if(error != 0)
    printk("[err] sys_do_munmap. buf:%p, size:%lu, ret:%d\n", buf, size, error);
  
  return (error==0);
}

感兴趣的话可以关注我的微信公众号【大胖聊编程】,我的公众号中有更多文章分享,也可以在公众号中联系到我,加好友一起交流学习。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值