linux 之copy_to_user/copy_from_user

  • 了解linux内核中的copy_to_user和copy_from_user。
  • 内核 2.6.12

1.copy_from_user

  copy_from_user函数的目的是从用户空间拷贝数据到内核空间,失败返回没有被拷贝的字节数,成功返回0。它内部的实现当然不仅仅拷贝数据,还需要考虑到传入的用户空间地址是否有效,比如地址是不是超出用户空间范围啊,地址是不是没有对应的物理页面啊,否则内核就会oops的。不同的架构,该函数的实现不一样。下面主要以arm例进行说明。

  • copy_from_user:(linux/include/asm-arm/uaccess.h)
static inline unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
{
	if (access_ok(VERIFY_READ, from, n))
		n = __arch_copy_from_user(to, from, n);
	else /* security hole - plug it */
		memzero(to, n);
	return n;
}

  该函数先通过access_ok做第一层的地址范围有效性检查,然后通过__copy_from_user进行正式的拷贝。之所以只做第一层的检查,是因为第二层的检查(地址是不是没有对应的物理页面)只能通过异常处理来解决。

1.1.三个参数:

  • *to是内核空间的指针

  • *from是用户空间指针

  • n表示从用户空间想内核空间拷贝数据的字节数。

    如果成功执行拷贝操作,则返回0,否则返回还没有完成拷贝的字节数。

1.2.分为两个部分:

  • 首先检查用户空间的地址指针是否有效;
  • 调用__arch_copy_from_user函数。

1.2.1.access_ok

  • access_ok用来对用户空间的地址指针from作某种有效性检验,这个宏和体系结构相关,在arm平台上为(linux/include/asm-arm/uaccess.h):
#define __range_ok(addr,size) ({ \
	unsigned long flag, sum; \
	__chk_user_ptr(addr);	\
	__asm__("adds %1, %2, %3; sbcccs %1, %1, %0; movcc %0, #0" \
		: "=&r" (flag), "=&r" (sum) \
		: "r" (addr), "Ir" (size), "0" (current_thread_info()->addr_limit) \
		: "cc"); \
	flag; })

#define access_ok(type,addr,size)	(__range_ok(addr,size) == 0)

//检查用户地址空间指针
extern void __chk_user_ptr(void __user *);
# define __chk_user_ptr(x) (void)0

  对于有mmu的,会先__chk_user_ptr检查addr,该函数一般为空!(它的实现涉及到__CHECKER__宏的判断,CHECKER__宏在通过Sparse(Semantic Parser for C)工具对内核代码进行检查时会定义的。在使用make C=1或C=2时便会调用该工具,这个工具可以检查在代码中声明了sparse所能检查到的相关属性的内核函数和变量。如果定义了__CHECKERchk_user_ptr和__chk_io_ptr在这里只声明函数,没有函数体,目的就是在编译过程中Sparse能够捕捉到编译错误,检查参数的类型。如果没有定义__CHECKER,这就是一个空语句)。核心的内容:

unsigned long flag, roksum; \
__asm__("adds %1, %2, %3; sbcccs %1, %1, %0; movcc %0, #0" \
		: "=&r" (flag), "=&r" (roksum) \
		: "r" (addr), "Ir" (size), "0" (current_thread_info()->addr_limit) \
		: "cc"); \
	flag; })

  核心思想:判断源地址+要拷贝的size是否超出了进程所限制的地址limit范围。下面一行行分析,先看输入输出设置部分:

		: "=&r" (flag), "=&r" (roksum) \
		: "r" (addr), "Ir" (size), "0" (current_thread_info()->addr_limit) \
		: "cc"); \

  &表示输出数据不会被覆盖,"=&r" (flag), “=&r” (roksum)表示输出用通用寄存器来存放,同时指向flag和roksum中,输入用通用寄存器存放addr,以及32为整形size,同时,flag的初始值设置为current_thread_info()->addr_limit,"cc"表示该内嵌__asm__汇编指令将会改变CPU的条件状态寄存器cc。

  下面继续看命令部分:

adds %1, %2, %3; sbcccs %1, %1, %0; movcc %0, #0

  先将addr与size相加,存入到roksum中(计算结果会设置cpsr),如果前面的计算没有进位,那么说明add与size的相加没有超出unsigned int范围,于是用sbc来实现addr+size-flag-!C,也就是addr+size-current_thread_info()->addr_limit-1,最后如果前面的命令执行没有导致C位为1,那么执行mov %0, #0,也就是说将flag设置为0。如果C位为1了,那么说明(addr + size)>=(current_thread_info()->addr_limit)。这里要注意减法指令是没有借位时,C为0;有借位时,C为1。

  最后要说明一下,__range_ok定义的最后有一个flag;这个是gnu支持的扩展,在({})包围的代码里面,最后一个表达式或值会作为整个({})的返回值。也就是说flag就是__range_ok的返回值。__range_ok如果一切顺利,那么返回就是0,如果其中任何一个指令有问题,那么就不会是0了(最开始flag的初始值为current_thread_info()->addr_limit,非0)。

refer to

  • https://www.lenzhao.com/topic/5a28f4b52e95f0fd0a9818a8

你可能感兴趣的:(linux,copy_to_user)