Linux地址空间
熟悉Linux内核的开发人员都知道,Linux下的进程地址空间分为内核空间和用户空间,对于32bit系统来说,典型的空间划分为:1G(内核空间)+3G(用户空间),对于这种划分来说,内核空间地址范围:0xC000 0000 ~0xFFFF FFFF,用户空间地址范围为:0x0000 0000 ~ 0xBFFF FFFF。当然,为了需要,我们可以将地址空间配置成其他方式,比如2G:2G等等。
# Linux虚拟地址机制
大家知道,Linux进程中使用的地址是虚拟地址,进程在操作这些地址时,MMU通过页表完成虚拟地址和物理地址之间的映射,如果页表miss,就触发缺页中断,然后,内核通过缺页异常处理机制分配可用的物理页,更新关于虚拟地址和物理地址页表项,从而完成整个虚拟地址的操作过程。
简单明确了Linux系统关于地址空间的管理机制之后,下面,就来具体讨论一下今天的主题:在内核中,如何实现内核空间和用户空间的数据交互。
内核APIs
内核提供了用于内核空间和用户空间数据相互拷贝的API:
- access_ok:用于检查用户空间指针的有效性;
- get_user:用于从用户空间拷贝一个简单的变量到内核空间,比如,1、2、4、8字节的变量。
- put_user:用于从内核空间拷贝一个简单的变量到用户空间。
- copy_to_user:用于从内核空间拷贝一个块数据到用户空间。
- copy_from_user:用于从用户空间拷贝一块数据到内核空间。
上面这些API一般都和具体的CPU架构相关,比如,X86架构相关的实现都在/linux/arch/x86/include/asm/uaccess.h和/linux/arch/x86/lib/usercopy_32.c、usercopy_64.c这些文件中。
get/put_user与copy_(to,from}_user的区别是所拷贝的数据类型不同,前者用于简单的数据,比如,int、long、char等,而后者用于一整块数据的拷贝。
下面详细的讲解一下上述各个API的具体作用。
access_ok
函数的原型如下:
access_ok(type, addr, size)
- type: VERIFY_WRITE、VERIFY_READ检查一段地址区域是否可读或者可写。
- 用于指定检查以addr为起始地址,长度为size为的用户空间是否可读或者可写。
- 如果可正常访问,函数返回0,否则返回-EFAULT。
- 该函数仅用于检测用户空间,不用于内核空间。
get/put_user
函数原型如下: get/_user( x, ptr ); put/_user( x, ptr );
get_user和put_user用于在内核空间和用户空间拷贝一个简单的数据,对于像结构体这样的数据需要使用copy_{to,from}_user。access_ok函数会检测ptr指针的有效性,之后,通过get_user_x或者 put_user_x完成数据拷贝。相对于copy_{to,from}_user,对于小型数据的拷贝,这两个函数的性能更好。两个函数都是执行成功之后返回0。
copy_{to, from}_user
函数的原型如下:
copy_to_user(to, from, n) copy_from_user(to, from, n)
两个函数用于在内核空间和用户空间之间拷贝数据,这个函数都是成功时返回0,失败时返回大于0的数据,表明未能拷贝成功的字节个数。两个函数都是通过access_ok来检测用户空间的地址的有效性,之后的数据拷贝实现,与CPU的体系结构有关,比如,ARM平台下,
_copy_from_user(void *to, const void __user *from, unsigned long n) { unsigned long res = n; might_fault(); if (likely(access_ok(VERIFY_READ, from, n))) { kasan_check_write(to, n); res = raw_copy_from_user(to, from, n); } if (unlikely(res)) memset(to + (n - res), 0, res); return res; }
最终会调用arm平台下的raw_copy_from_user:
raw_copy_to_user(void __user *to, const void *from, unsigned long n) { #ifndef CONFIG_UACCESS_WITH_MEMCPY unsigned int __ua_flags; __ua_flags = uaccess_save_and_enable(); n = arm_copy_to_user(to, from, n); uaccess_restore(__ua_flags); return n; #else return arm_copy_to_user(to, from, n); #endif }
copy_{to,from}_user Vs memcpy
还有一个经常被误用的API,那就是,memcpy,大家可能听过,或者看过memcpy不能用于用户空间和内核空间之间的数据交互,那原因是什么呢?本节将会详细的讲解copy_{to,from}_user与memcpy的区别和联系。
之所以在进行内核空间与用户空间拷贝时,使用copy_{to,from}_user,主要有两个原因:
- copy*函数会通过access_ok检测用户空间地址有效性。这样可以防止内核操作非法的地址,比如,用户传过来的是内核空间的地址,如果内核检测该地址,那么就会写坏该地址指向的内容,造成系统崩溃。同时,也会系统安全问题,比如,黑客会故意传入一个内核地址,并且这个地址保存着关键信息,比如用户id,内核在没有检测的情况下,会修改地址中的内容,比如,将用户id写为0,那么黑客就可以获取系统的最高权限。参考CVE-2017-5123内核漏洞。
- copy*函数可以在发生page fault时,进行自我修复。所谓的page fault时,是说用户空间的地址是一个非法的地址(非栈,非堆地址),这是内核在访问该地址时,MMU会检测到该地址的非法性,从而产生一个异常。对于该异常,内核有两种处理方式:
- 向当前进行发送SIGSEGV信号,并抛出Oops信息,如果内核开启了/proc/sys/kernel/panic_on_oops,那么内核直接panic。
- 处理该异常,正常返回。
- 如果这时使用的是copy_{to,from}_user函数的话,其会捕获该异常,并正常返回到用户空间。但是如果使用的是memcpy的话,对不起,内核直接Oops或者panic。
- 为了更加安全,硬件加入了PAN功能(Privileged Access Never),其可以限制内核访问用户空间的能力,所以,访问之前必须通过硬件指令关闭PAN,访问完之后,开启PAN,ARM V8架构增加了这项功能。只有copy_{to,from}_user函数具有打开和关闭PAN的能力,所以,对于开启了PAN功能的平台可,copy_{to,from}_user是唯一的选择。
所以,对于一个合格的内核开发人员,在涉及到用户空间和内核空间数据拷贝的场景时,杜绝使用memcpy是避免出现bugs的首要注意事项。