copy_{to,from}_user Vs memcpy

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的首要注意事项。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值