copy_from_user分析

前言

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

arm copy_from_user

arm架构下,copy_from_user相关的文件主要有arch/arm/include/asm/uaccess.h  arch/arm/lib/copy_from_user.S  arch/arm/lib/copy_template.S。下面先来看copy_from_user,它的实现在arch/arm/include/asm/uaccess.h中:

static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)
{
	if (access_ok(VERIFY_READ, from, n))
		n = __copy_from_user(to, from, n);
	else /* security hole - plug it */
		memset(to, 0, n);
	return n;
}

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

下面看access_ok的实现吧!(代码实现还是在同一个文件里)同样,不同的架构,实现方式不同。甚至有mmu和无mmu也不同。

#ifdef CONFIG_MMU
...
...
...
#define __range_ok(addr,size) ({ \
	unsigned long flag, roksum; \
	__chk_user_ptr(addr);	\
	__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; })
#else /* CONFIG_MMU */
...
...
...
#define __range_ok(addr,size)	((void)(addr),0)
#endif

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

对于无mmu的,检查就是不检查,因为无mmu也就是意味着没有虚拟地址映射,用的都是物理地址(出了问题,也无法解决)。

对于有mmu的,会先__chk_user_ptr检查addr,该函数一般为空!(它的实现涉及到__CHECKER__宏的判断,__CHECKER__宏在通过Sparse(Semantic Parser for C)工具对内核代码进行检查时会定义的。在使用make C=1或C=2时便会调用该工具,这个工具可以检查在代码中声明了sparse所能检查到的相关属性的内核函数和变量。如果定义了__CHECKER____chk_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; })

这是一段c内嵌汇编(linux采用AT&T编码方式,左边值为原操作数,右边值为目的操作数,与intel编码方式不同,可参考GNU C内嵌汇编语言 )!核心思想就是判断源地址+要拷贝的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)

好了,分析完__range_ok的实现,现在继续看__copy_from_user,还是在相同的文件里(同样有mmu和非mmu之分):

#ifdef CONFIG_MMU
extern unsigned long __must_check __copy_from_user(void *to, const void __user *from, unsigned long n);
...
...
...
#else
#define __copy_from_user(to,from,n)	(memcpy(to, (void __force *)from, n), 0)
...
...
...
#endif

有mmu的时候,它对应的实现在arch/arm/lib/copy_from_user.S里面:

...
...
...
ENTRY(__copy_from_user)

#include "copy_template.S"

ENDPROC(__copy_from_user)

	.pushsection .fixup,"ax"
	.align 0
	copy_abort_preamble
	ldmfd	sp!, {r1, r2}
	sub	r3, r0, r1
	rsb	r1, r3, r2
	str	r1, [sp]
	bl	__memzero
	ldr	r0, [sp], #4
	copy_abort_end
	.popsection

核心的实现在arch/arm/lib/copy_template.S中,arch/arm/lib/copy_template.S里面的具体逻辑会因为arch/arm/lib/copy_from_user.S之前所定义的宏而不同。这里就不再跟进去分析了,异常表的处理我打算通过分析x86实现的时候来完成。

x86 copy_from_user

x86架构下,copy_from_user相关的文件主要有arch/x86/include/asm/uaccess.h  arch/x86/lib/usercopy_32.S  arch/x86/include/asm/uaccess_32.h arch/x86/include/asm/uaccess_64.h。下面先来看copy_from_user,它的实现在arch/x86/include/asm/uaccess.h中:

static inline unsigned long __must_check
copy_from_user(void *to, const void __user *from, unsigned long n)
{
	int sz = __compiletime_object_size(to);

	might_fault();

	/*
	 * While we would like to have the compiler do the checking for us
	 * even in the non-constant size case, any false positives there are
	 * a problem (especially when DEBUG_STRICT_USER_COPY_CHECKS, but even
	 * without - the [hopefully] dangerous looking nature of the warning
	 * would make people go look at the respecitive call sites over and
	 * over again just to find that there's no problem).
	 *
	 * And there are cases where it's just not realistic for the compiler
	 * to prove the count to be in range. For example when multiple call
	 * sites of a helper function - perhaps in different source files -
	 * all doing proper range checking, yet the helper function not doing
	 * so again.
	 *
	 * Therefore limit the compile time checking to the constant size
	 * case, and do only runtime checking for non-constant sizes.
	 */

	if (likely(sz < 0 || sz >= n))
		n = _copy_from_user(to, from, n);
	else if(__builtin_constant_p(n))
		copy_from_user_overflow();
	else
		__copy_from_user_overflow(sz, n);

	return n;
}

GCC的内建函数__builtin_constant_p用于判断一个值是否为编译时常数,如果参数值是常数,函数返回 1,否则返回 0。copy_from_user核心的实现在_copy_from_user中:

unsigned long _copy_from_user(void *to, const void __user *from, unsigned n)
{
	if (access_ok(VERIFY_READ, from, n))
		n = __copy_from_user(to, from, n);
	else
		memset(to, 0, n);
	return n;
}

其中,access_ok相关代码(代码比较简单,不再分析):

static inline bool __chk_range_not_ok(unsigned long addr, unsigned long size, unsigned long limit)
{
	/*
	 * If we have used "sizeof()" for the size,
	 * we know it won't overflow the limit (but
	 * it might overflow the 'addr', so it's
	 * important to subtract the size from the
	 * limit, not add it to the address).
	 */
	if (__builtin_constant_p(size))
		return addr > limit - size;

	/* Arbitrary sizes? Be careful about overflow */
	addr += size;
	if (addr < size)
		return true;
	return addr > limit;
}

#define __range_not_ok(addr, size, limit)				\
({									\
	__chk_user_ptr(addr);						\
	__chk_range_not_ok((unsigned long __force)(addr), size, limit); \
})

#define access_ok(type, addr, size) \
	likely(!__range_not_ok(addr, size, user_addr_max()))

下面看__copy_from_user相关的代码实现(以32位系统为例),注释直接添加到代码中:

static __always_inline unsigned long
__copy_from_user(void *to, const void __user *from, unsigned long n)
{
	might_fault();
	if (__builtin_constant_p(n)) {//如果能够识别为常量,就进入
		unsigned long ret;

		switch (n) {
		case 1:
			__get_user_size(*(u8 *)to, from, 1, ret, 1);
			return ret;
		case 2:
			__get_user_size(*(u16 *)to, from, 2, ret, 2);
			return ret;
		case 4:
			__get_user_size(*(u32 *)to, from, 4, ret, 4);
			return ret;
		}
	}
	return __copy_from_user_ll(to, from, n);//如果不能识别n是一个常量,就调用
}

先看__get_user_size实现,__chk_user_ptr之前已经说过,不再重复。主要看__get_user_asm

#define __get_user_size(x, ptr, size, retval, errret)			\
do {									\
	retval = 0;							\
	__chk_user_ptr(ptr);						\
	switch (size) {							\
	case 1:								\
		__get_user_asm(x, ptr, retval, "b", "b", "=q", errret);	\
		break;							\
	case 2:								\
		__get_user_asm(x, ptr, retval, "w", "w", "=r", errret);	\
		break;							\
	case 4:								\
		__get_user_asm(x, ptr, retval, "l", "k", "=r", errret);	\
		break;							\
	case 8:								\
		__get_user_asm_u64(x, ptr, retval, errret);		\
		break;							\
	default:							\
		(x) = __get_user_bad();					\
	}								\
} while (0)

__get_user_size根据要copy的size传入不同的参数,最终会使用movb或者movw或者movl来实现1、2\4字节的拷贝。主要需要注意的就是.section .fixup_ASM_EXTABLE.section .fixup指定了.fixup section,且该段为可重定位的代码段,_ASM_EXTABLE定义了__ex_table段,且该段为可重定位的数据段,实际上它指定了3b处异常时的跳转地址,即3b,3b刚好就是.fixup段处。

#define __get_user_asm(x, addr, err, itype, rtype, ltype, errret)	\
	asm volatile(ASM_STAC "\n"					\
		     "1:	mov"itype" %2,%"rtype"1\n"		\
		     "2: " ASM_CLAC "\n"				\
		     ".section .fixup,\"ax\"\n"				\
		     "3:	mov %3,%0\n"				\
		     "	xor"itype" %"rtype"1,%"rtype"1\n"		\
		     "	jmp 2b\n"					\
		     ".previous\n"					\
		     _ASM_EXTABLE(1b, 3b)				\
		     : "=r" (err), ltype(x)				\
		     : "m" (__m(addr)), "i" (errret), "0" (err))

分析完1、2、4字节的拷贝后,继续看非1、2、4字节的拷贝实现,现在继续看__copy_from_user_ll

unsigned long __copy_from_user_ll(void *to, const void __user *from,
					unsigned long n)
{
	stac();
	if (movsl_is_ok(to, from, n))
		__copy_user_zeroing(to, from, n);
	else
		n = __copy_user_zeroing_intel(to, from, n);
	clac();
	return n;
}

先通过movsl_is_ok判断下,然后分别调用__copy_user_zeroing或者__copy_user_zeroing_intelmovsl_is_ok的实现:

static inline int __movsl_is_ok(unsigned long a1, unsigned long a2, unsigned long n)
{
#ifdef CONFIG_X86_INTEL_USERCOPY
	if (n >= 64 && ((a1 ^ a2) & movsl_mask.mask))
		return 0;
#endif
	return 1;
}
#define movsl_is_ok(a1, a2, n) \
	__movsl_is_ok((unsigned long)(a1), (unsigned long)(a2), (n))

从这里可以知道,只有配置了CONFIG_X86_INTEL_USERCOPY,才有可能返回0,不然一般多事返回1。我们不考虑定义CONFIG_X86_INTEL_USERCOPY的情况,也就是该函数返回1时,继续转入到__copy_user_zeroing的调用,代码实现如下:

#define __copy_user_zeroing(to, from, size)				\
do {									\
	int __d0, __d1, __d2;						\
	__asm__ __volatile__(						\
		"	cmp  $7,%0\n"					\
		"	jbe  1f\n"					\
		"	movl %1,%0\n"					\
		"	negl %0\n"					\
		"	andl $7,%0\n"					\
		"	subl %0,%3\n"					\
		"4:	rep; movsb\n"					\
		"	movl %3,%0\n"					\
		"	shrl $2,%0\n"					\
		"	andl $3,%3\n"					\
		"	.align 2,0x90\n"				\
		"0:	rep; movsl\n"					\
		"	movl %3,%0\n"					\
		"1:	rep; movsb\n"					\
		"2:\n"							\
		".section .fixup,\"ax\"\n"				\
		"5:	addl %3,%0\n"					\
		"	jmp 6f\n"					\
		"3:	lea 0(%3,%0,4),%0\n"				\
		"6:	pushl %0\n"					\
		"	pushl %%eax\n"					\
		"	xorl %%eax,%%eax\n"				\
		"	rep; stosb\n"					\
		"	popl %%eax\n"					\
		"	popl %0\n"					\
		"	jmp 2b\n"					\
		".previous\n"						\
		_ASM_EXTABLE(4b,5b)					\
		_ASM_EXTABLE(0b,3b)					\
		_ASM_EXTABLE(1b,6b)					\
		: "=&c"(size), "=&D" (__d0), "=&S" (__d1), "=r"(__d2)	\
		: "3"(size), "0"(size), "1"(to), "2"(from)		\
		: "memory");						\
} while (0)

同样是汇编实现,.section .fixup_ASM_EXTABLE部分前面已经说了,而指令部分就是我们通常的数据拷贝,因此也就不再分析了。

这里摘抄下网上的一段叙述,同时他对__copy_user_zeroing的指令部分有详细的注释,大家可以看看:

在cpu进行访址的时候,内核空间和用户空间使用的都是线性地址,cpu在访址的过程中会自动完成从线性地址到物理地址的转换[用户态、内核态都得依靠进程页表完成转换],而合理的线性地址意味着:该线性地址位于该进程task_struct->mm虚存空间的某一段vm_struct_mm中,而且建立线性地址到物理地址的映射,即线性地址对应内容在物理内存中。如果访存失败,有两种可能:该线性地址存在在进程虚存区间中,但是并未建立于物理内存的映射,有可能是交换出去,也有可能是刚申请到线性区间[内核是很会偷懒的],要依靠缺页异常去建立申请物理空间并建立映射;第2种可能是线性地址空间根本没有在进程虚存区间中,这样就会出现常见的坏指针,就会引发常见的段错误[也有可能由于访问了无权访问的空间造成保护异常]。如果坏指针问题发生在用户态,最严重的就是杀死进程[最常见的就是在打dota时候出现的大红X,然后dota程序结束],如果发生在内核态,整个系统可能崩溃[xp的蓝屏很可能就是这种原因形成的]。所以linux当然不会任由这种情况的发生,其措施如下:
linux内核对于可能发生问题的指令都会准备"修复地址",比如前面的fixup部分,而且遵循谁使用这些指令,谁负责修复工作的原则。比如前面的代码中,标号5即为标号4的修复指令,3为0,6为1的修复指令。在编译过程中,编译器会将5,4等的地址对应的存入struct exception_table_entry{unsigned long insn,fixup;}中。insn即可能为4的地址,而fixup可能为5的地址,如果4为坏地址[即该地址并未在虚存区间中],则在页面异常处理过程中,会转入bad_area处,如果发生在用户态直接杀死进程即可。如果发生在内核态,首先通过search_exception_table查找异常处理表exception_table。即找到某一个exception_table_entry,假设其insn=标号4地址,fixup=标号5地址.内核将发生:
regs->ip=fixup,即通过修改当前的内核地址,从而将内核从死亡的边缘拉回来,通过标号5地址处的修复工作从而全身而退。

总结

主要分析了copy_from_user接口的内部实现,copy_to_user实现类似,不再重复分析。总的来说,copy_from_user完成了数据的拷贝的同时,处理了可能发生了地址访问异常。理论上,内核空间可以直接使用用户空间传过来的指针,即使要做数据拷贝的动作,也可以直接使用memcpy,事实上,在没有MMU的体系架构上,copy_form_user最终的实现就是利用了memcpy。但对于大多数有MMU的平台,情况就有了一些变化:用户空间传过来的指针是在虚拟地址空间上的,它指向的虚拟地址空间很可能还没有真正映射到实际的物理页面上。用户空间的缺页导致的异常会透明的被内核予以修复(为缺页的地址空间提交新的物理页面),访问到缺页的指令会继续运行仿佛什么都没有发生一样。内核空间必须被显示的修复,这是由内核提供的缺页异常处理函数的设计模式决定的(其背后的思想后:在内核态中,如果程序试图访问一个尚未提交物理页面的用户空间地址,内核必须对此保持警惕而不能像用户空间那样毫无察觉。如果内核访问一个尚未被提交物理页面的空间,将产生缺页异常,这个时候内核会调用do_page_fault,因为异常发生在内核空间,do_page_fault的处理逻辑将调用search_exception_tables__ex_table中查找异常指令的修复指令),正因为这样,copy_from_user的实现才会看起来有些复杂,当然性能方面提升也是它的复杂度提升的一个原因。

完!
2015年7月

posted @ 2017-10-14 10:19  rongpmcu  阅读(14977)  评论(0编辑  收藏  举报