内核程序访问用户地址空间

Access User Address Space in Kernel Mode

作者 Hailiang Dong 日期 2017-05-27
内核程序访问用户地址空间

背景

由于做实验需要在Linux内核中添加一些系统调用,我们在一台机器上完成了系统调用的编写,并顺利通过了所有的测试,用户程序的运行也完全正常。不够由于实验数据的增大,我们需要一台内存更大的电脑。为了方便,我们直接把那台机器的硬盘拆下来,放到别的机器上,然后从那块硬盘启动进入系统做实验。

然后神奇的事情就发生了,在新的机器上,程序一进入系统调用就直接挂掉,并且在dmesg中看到了cannot handle paging request的错误。这个就让我很纳闷了,因为软件的环境完全没变(硬盘直接拆过来的),只有硬件不一样,为什么同样的程序就无法运行了?再说了,Linux系统(Ubuntu)也不是说换了硬件后要装驱动什么的,怎么会出现这个问题?

由于程序是进入系统调用后才出错的,也就说说明程序本身是没有问题的,有问题的是这个系统调用,貌似在新的机器上无法运行,出现了不兼容的问题。

内核程序访问用户地址空间

经过DEBUG,我们发现这个BUG背后的原因和内核态程序访问用户进程地址空间有关,为了将这个问题解释清楚,我们先大致讲讲操作系统的内存管理。

虚拟地址空间

事实上,对于现代操作系统来说,拿32位Linux举例,每一个进程都拥有4GB的虚拟地址空间,大概如下图所示

linux virtual address space

可以明显的从图中看到,虚拟地址的空间被分为了两个部分,内核地址空间和用户地址空间。进程对于虚拟地址的访问有硬件MMU进行地址翻译,转变为物理地址,再从内存中获取数据,从而达到多个进程共享一个内存物理空间的效果。

为了系统的安全,操作系统中的进程一般也会分为内核态和用户态,用户态的进程是无法访问其内核地址空间的,这主要是为了系统的安全。但现在就有一个问题,内核态的进程可以访问用户地址空间么

问题原因

在我们的系统调用中,我们的函数大致如下

1
2
3
4
5
6
7
asmlinkage long my_sys_call( ..., unsigned long* ret_info)
{
// .... created info
*ret_info = info;
// ....
return 0;
}

用户进程将一个变量的地址(位于用户虚拟地址空间传入了system call),在system call里面,我们创建了一些东西,并希望将相应的信息通过变量ret_info返回到用户空间。于是乎,我们在第4行代码处,将整数信息info赋值给了指针ret_info所指向的空间。

这行代码在一开始的机器中是正确的,但是在新的机器中却无法运行,也就是说内核程序也不一定能访问用户地址空间。事实上,这个问题和机器的硬件很有关系,为了更安全的进行数据访问和传输(在内核地址空间和用户地址空间中),内核是不能直接访问用户空间数据(例如memcpy或者上面的赋值),但它可以通过特殊的函数来访问用户空间数据,copy_to_user, copy_from_user这两个函数就是内核代码访问用户空间数据的函数。

通过查阅相关资料,主要可能有以下三个原因:

  1. 驱动程序架构不同或者内核的配置不同,用户空间数据指针可能运行在内核模式下根本就是无效的,可能没有那个虚拟地址映射到的物理地址,也有可能直接指向一些随机数。
  2. 用户空间的内存数据是分页的,运行在内核模式下的用户空间指针可能直接就不在内存上,而是在swap交换的其他存储设备上,这样就会发生页面错误。页面错误是内核所不允许的,会导致该进程死亡。
  3. 内核代码访问用户内存指针,就给内核开了后门,用户程序可以利用这一点来任意的访问操作全部地址空间,这样内核就没有安全性可言了。

从我们的这个例子中来看,最有可能的原因应该是第二种。由于页面交换的问题,当我们将指针ret_info传给内核了之后,在第4行的修改操作中,ret_info指向的内存空间可能已经被换出了,那么执行第四行的代码时就会触发page falut。然而,在内核态中,page falut是不被允许的,因为理论上内核态的数据是不会被换出的,因此,当内核态的程序需要进行paging的时候,整个程序就直接被killed掉了。