linux内核中的copy_to_user和copy_from_user(一)

linux内核中的copy_to_user和copy_from_user(一)

Kernel version:2.6.14

CPU architecture:ARM920T

Author:ce123(http://blog.csdn.net/ce123)


1.copy_from_user

在学习Linux内核驱动的时候,经常会碰到copy_from_user和copy_to_user这两个函数,设备驱动程序中的ioctl函数就经常会用到。这两个函数负责在用户空间和内核空间传递数据。首先看看它们的定义(linux/include/asm-arm/uaccess.h),先看copy_from_user:

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;
}

先看函数的三个参数:*to是内核空间的指针,*from是用户空间指针,n表示从用户空间想内核空间拷贝数据的字节数。如果成功执行拷贝操作,则返回0,否则返回还没有完成拷贝的字节数。

这个函数从结构上来分析,其实都可以分为两个部分:
  1. 首先检查用户空间的地址指针是否有效;
  2. 调用__arch_copy_from_user函数。

1.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)
可以看到access_ok中第一个参数type并没有用到,__range_ok的作用在于判断addr+size之后是否还在进程的用户空间范围之内。下面我们具体看一下。这段代码涉及到GCC内联汇编,不懂的朋友可以先看看这篇博客(http://blog.csdn.net/ce123/article/details/8209702)。
(1)unsigned long flag, sum;\\定义两个变量
  • flag:保存结果的变量:非零代表地址无效,零代表地址可以访问。初始存放非零值(current_thread_info()->addr_limit),也就是当前进程的地址上限值。
  • sum:保存要访问的地址范围末端,用于和当前进程地址空间限制数据做比较。
(2)__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__,这就是一个空函数。

请看具体的定义(linux/compiler.h):

#ifdef __CHECKER__
...
extern void __chk_user_ptr(void __user *);
extern void __chk_io_ptr(void __iomem *);
#else
...
# define __chk_user_ptr(x) (void)0
# define __chk_io_ptr(x) (void)0
...
#endif
(3)接下来是汇编:
adds %1, %2, %3
sum = addr + size 这个操作影响状态位(目的是影响是进位标志C),以下的两个指令都带有条件CC,也就是当C=0的时候才执行。

如果上面的加法指令进位了(C=1),则以下的指令都不执行,flag就为初始值current_thread_info()->addr_limit(非0),并返回。
如果没有进位(C=0),就执行下面的指令:
sbcccs %1, %1, %0
sum = sum - flag - 1,也就是(addr + size) - (current_thread_info()->addr_limit) - 1,操作影响符号位。
如果(addr + size) >= (current_thread_info()->addr_limit) - 1,则C=1
如果(addr + size) < (current_thread_info()->addr_limit) - 1,则C=0
当C=0的时候执行以下指令,否则跳过(flag非零)。
movcc %0, #0
flag = 0,给flag赋值0。

综上所述:__range_ok宏其实等价于:

  • 如果(addr + size) >= (current_thread_info()->addr_limit) - 1,返回非零值
  • 如果(addr + size) < (current_thread_info()->addr_limit),返回零
而access_ok就是检验将要操作的用户空间的地址范围是否在当前进程的用户地址空间限制中。这个宏的功能很简单,完全可以用C实现,不是必须使用汇编。但于这两个函数使用频繁,就使用汇编来实现部分功能来增加效率。
从这里再次可以认识到,copy_from_user的使用是结合进程上下文的,因为他们要访问“user”的内存空间,这个“user”必须是某个特定的进程。通过上面的源码就知道,其中使用了current_thread_info()来检查空间是否可以访问。如果在驱动中使用这两个函数,必须是在实现系统调用的函数中使用,不可在实现中断处理的函数中使用。如果在中断上下文中使用了,那代码就很可能操作了根本不相关的进程地址空间。其次由于操作的页面可能被换出,这两个函数可能会休眠,所以同样不可在中断上下文中使用。

1.2.__arch_copy_from_user

在深入讲解之前,我们先想一个问题:为什么要使用copy_from_user函数???理论上,内核空间可以直接使用用户空间传过来的指针,即使要做数据拷贝的动作,也可以直接使用memcpy,事实上,在没有MMU的体系架构上,copy_form_user最终的实现就是利用了memcpy。但对于大多数有MMU的平台,情况就有了一些变化:用户空间传过来的指针是在虚拟地址空间上的,它指向的虚拟地址空间很可能还没有真正映射到实际的物理页面上。但这又能怎样呢?缺页导致的异常会透明的被内核予以修复(为缺页的地址空间提交新的物理页面),访问到缺页的指令会继续运行仿佛什么都没有发生一样。但这只是用户空间缺页异常的行为,在内核空间这样却因一场必须被显示的修复,这是由内核提供的缺页异常处理函数的设计模式决定的,其背后的思想后:在内核态中,如果程序试图访问一个尚未提交物理页面的用户空间地址,内核必须对此保持警惕而不能像用户空间那样毫无察觉。
如果内核访问一个尚未被提交物理页面的空间,将产生缺页异常,内核会调用do_page_fault,因为异常发生在内核空间,do_page_fault将调用search_exception_tables在“ __ex_table”中查找异常指令的修复指令,在__arch_copy_from_user函数中经常使用USER宏,这个宏中了定义了“__ex_table”section。

linux/include/asm-arm/assembler.h
#define USER(x...)				\
9999:	x;					\
	.section __ex_table,"a";		\
	.align	3;				\
	.long	9999b,9001f;			\
	.previous
该定义中有如下数据;
.long	9999b,9001f;
其中9999b对应标号9999处的指令,9001f是9001处的指令,是9999b处指令的修复指令。这样,当标号9999处发生缺页异常时,系统将调用do_page_fault提交物理页面,然后跳到9001继续执行。
如果在驱动程序中不使用copy_from_user而用memcpy来代替,对于上述的情形会产生什么结果呢?当标号9999出发生缺页异常时,系统在“__ex_table”section总将找不到修复地址,因为memcpy没有像copy_from_user那样定义一个“__ex_table”section,此时do_page_fault将通过no_context函数产生Oops。极有可能会看到类似如下信息:
Unable to handle kernel NULL pointer dereference at virtual address 00000fe0
所有为了确保设备驱动程序的安全,应该使用copy_from_user函数而不是memcpy。
下面我们深入分析__arch_copy_from_user函数的实现,该函数是用汇编实现的,定义在linux/arch/arm/lib/uaccess.S文件中。

/* Prototype: unsigned long __arch_copy_from_user(void *to,const void *from,unsigned long n);
 * Purpose  : copy a block from user memory to kernel memory
 * Params   : to   - kernel memory
 *          : from - user memory
 *          : n    - number of bytes to copy
 * Returns  : Number of bytes NOT copied.
 */
.cfu_dest_not_aligned:
		rsb	ip, ip, #4
		cmp	ip, #2
USER(		ldrbt	r3, [r1], #1)			@ May fault
		strb	r3, [r0], #1
USER(		ldrgebt	r3, [r1], #1)			@ May fault
		strgeb	r3, [r0], #1
USER(		ldrgtbt	r3, [r1], #1)			@ May fault
		strgtb	r3, [r0], #1
		sub	r2, r2, ip
		b	.cfu_dest_aligned

ENTRY(__arch_copy_from_user)
		stmfd	sp!, {r0, r2, r4 - r7, lr}
		cmp	r2, #4
		blt	.cfu_not_enough
	PLD(	pld	[r1, #0]		)
	PLD(	pld	[r0, #0]		)
		ands	ip, r0, #3
		bne	.cfu_dest_not_aligned
.cfu_dest_aligned:
		ands	ip, r1, #3
		bne	.cfu_src_not_aligned
/*
 * Seeing as there has to be at least 8 bytes to copy, we can
 * copy one word, and force a user-mode page fault...
 */

.cfu_0fupi:	subs	r2, r2, #4
		addmi	ip, r2, #4
		bmi	.cfu_0nowords
USER(		ldrt	r3, [r1], #4)
		str	r3, [r0], #4
		mov	ip, r1, lsl #32 - PAGE_SHIFT	@ On each page, use a ld/st??t instruction
		rsb	ip, ip, #0
		movs	ip, ip, lsr #32 - PAGE_SHIFT
		beq	.cfu_0fupi
/*
 * ip = max no. of bytes to copy before needing another "strt" insn
 */
		cmp	r2, ip
		movlt	ip, r2
		sub	r2, r2, ip
		subs	ip, ip, #32
		blt	.cfu_0rem8lp
	PLD(	pld	[r1, #28]		)
	PLD(	pld	[r0, #28]		)
	PLD(	subs	ip, ip, #64			)
	PLD(	blt	.cfu_0cpynopld		)
	PLD(	pld	[r1, #60]		)
	PLD(	pld	[r0, #60]		)

.cfu_0cpy8lp:
	PLD(	pld	[r1, #92]		)
	PLD(	pld	[r0, #92]		)
.cfu_0cpynopld:	ldmia	r1!, {r3 - r6}			@ Shouldnt fault
		stmia	r0!, {r3 - r6}
		ldmia	r1!, {r3 - r6}			@ Shouldnt fault
		subs	ip, ip, #32
		stmia	r0!, {r3 - r6}
		bpl	.cfu_0cpy8lp
	PLD(	cmn	ip, #64			)
	PLD(	bge	.cfu_0cpynopld		)
	PLD(	add	ip, ip, #64		)

.cfu_0rem8lp:	cmn	ip, #16
		ldmgeia	r1!, {r3 - r6}			@ Shouldnt fault
		stmgeia	r0!, {r3 - r6}
		tst	ip, #8
		ldmneia	r1!, {r3 - r4}			@ Shouldnt fault
		stmneia	r0!, {r3 - r4}
		tst	ip, #4
		ldrnet	r3, [r1], #4			@ Shouldnt fault
		strne	r3, [r0], #4
		ands	ip, ip, #3
		beq	.cfu_0fupi
.cfu_0nowords:	teq	ip, #0
		beq	.cfu_finished
.cfu_nowords:	cmp	ip, #2
USER(		ldrbt	r3, [r1], #1)			@ May fault
		strb	r3, [r0], #1
USER(		ldrgebt	r3, [r1], #1)			@ May fault
		strgeb	r3, [r0], #1
USER(		ldrgtbt	r3, [r1], #1)			@ May fault
		strgtb	r3, [r0], #1
		b	.cfu_finished

.cfu_not_enough:
		movs	ip, r2
		bne	.cfu_nowords
.cfu_finished:	mov	r0, #0
		add	sp, sp, #8
		LOADREGS(fd,sp!,{r4 - r7, pc})

.cfu_src_not_aligned:
		bic	r1, r1, #3
USER(		ldrt	r7, [r1], #4)			@ May fault
		cmp	ip, #2
		bgt	.cfu_3fupi
		beq	.cfu_2fupi
.cfu_1fupi:	subs	r2, r2, #4
		addmi	ip, r2, #4
		bmi	.cfu_1nowords
		mov	r3, r7, pull #8
USER(		ldrt	r7, [r1], #4)			@ May fault
		orr	r3, r3, r7, push #24
		str	r3, [r0], #4
		mov	ip, r1, lsl #32 - PAGE_SHIFT
		rsb	ip, ip, #0
		movs	ip, ip, lsr #32 - PAGE_SHIFT
		beq	.cfu_1fupi
		cmp	r2, ip
		movlt	ip, r2
		sub	r2, r2, ip
		subs	ip, ip, #16
		blt	.cfu_1rem8lp
	PLD(	pld	[r1, #12]		)
	PLD(	pld	[r0, #12]		)
	PLD(	subs	ip, ip, #32		)
	PLD(	blt	.cfu_1cpynopld		)
	PLD(	pld	[r1, #28]		)
	PLD(	pld	[r0, #28]		)

.cfu_1cpy8lp:
	PLD(	pld	[r1, #44]		)
	PLD(	pld	[r0, #44]		)
.cfu_1cpynopld:	mov	r3, r7, pull #8
		ldmia	r1!, {r4 - r7}			@ Shouldnt fault
		subs	ip, ip, #16
		orr	r3, r3, r4, push #24
		mov	r4, r4, pull #8
		orr	r4, r4, r5, push #24
		mov	r5, r5, pull #8
		orr	r5, r5, r6, push #24
		mov	r6, r6, pull #8
		orr	r6, r6, r7, push #24
		stmia	r0!, {r3 - r6}
		bpl	.cfu_1cpy8lp
	PLD(	cmn	ip, #32			)
	PLD(	bge	.cfu_1cpynopld		)
	PLD(	add	ip, ip, #32		)

.cfu_1rem8lp:	tst	ip, #8
		movne	r3, r7, pull #8
		ldmneia	r1!, {r4, r7}			@ Shouldnt fault
		orrne	r3, r3, r4, push #24
		movne	r4, r4, pull #8
		orrne	r4, r4, r7, push #24
		stmneia	r0!, {r3 - r4}
		tst	ip, #4
		movne	r3, r7, pull #8
USER(		ldrnet	r7, [r1], #4)			@ May fault
		orrne	r3, r3, r7, push #24
		strne	r3, [r0], #4
		ands	ip, ip, #3
		beq	.cfu_1fupi
.cfu_1nowords:	mov	r3, r7, get_byte_1
		teq	ip, #0
		beq	.cfu_finished
		cmp	ip, #2
		strb	r3, [r0], #1
		movge	r3, r7, get_byte_2
		strgeb	r3, [r0], #1
		movgt	r3, r7, get_byte_3
		strgtb	r3, [r0], #1
		b	.cfu_finished

.cfu_2fupi:	subs	r2, r2, #4
		addmi	ip, r2, #4
		bmi	.cfu_2nowords
		mov	r3, r7, pull #16
USER(		ldrt	r7, [r1], #4)			@ May fault
		orr	r3, r3, r7, push #16
		str	r3, [r0], #4
		mov	ip, r1, lsl #32 - PAGE_SHIFT
		rsb	ip, ip, #0
		movs	ip, ip, lsr #32 - PAGE_SHIFT
		beq	.cfu_2fupi
		cmp	r2, ip
		movlt	ip, r2
		sub	r2, r2, ip
		subs	ip, ip, #16
		blt	.cfu_2rem8lp
	PLD(	pld	[r1, #12]		)
	PLD(	pld	[r0, #12]		)
	PLD(	subs	ip, ip, #32		)
	PLD(	blt	.cfu_2cpynopld		)
	PLD(	pld	[r1, #28]		)
	PLD(	pld	[r0, #28]		)

.cfu_2cpy8lp:
	PLD(	pld	[r1, #44]		)
	PLD(	pld	[r0, #44]		)
.cfu_2cpynopld:	mov	r3, r7, pull #16
		ldmia	r1!, {r4 - r7}			@ Shouldnt fault
		subs	ip, ip, #16
		orr	r3, r3, r4, push #16
		mov	r4, r4, pull #16
		orr	r4, r4, r5, push #16
		mov	r5, r5, pull #16
		orr	r5, r5, r6, push #16
		mov	r6, r6, pull #16
		orr	r6, r6, r7, push #16
		stmia	r0!, {r3 - r6}
		bpl	.cfu_2cpy8lp
	PLD(	cmn	ip, #32			)
	PLD(	bge	.cfu_2cpynopld		)
	PLD(	add	ip, ip, #32		)

.cfu_2rem8lp:	tst	ip, #8
		movne	r3, r7, pull #16
		ldmneia	r1!, {r4, r7}			@ Shouldnt fault
		orrne	r3, r3, r4, push #16
		movne	r4, r4, pull #16
		orrne	r4, r4, r7, push #16
		stmneia	r0!, {r3 - r4}
		tst	ip, #4
		movne	r3, r7, pull #16
USER(		ldrnet	r7, [r1], #4)			@ May fault
		orrne	r3, r3, r7, push #16
		strne	r3, [r0], #4
		ands	ip, ip, #3
		beq	.cfu_2fupi
.cfu_2nowords:	mov	r3, r7, get_byte_2
		teq	ip, #0
		beq	.cfu_finished
		cmp	ip, #2
		strb	r3, [r0], #1
		movge	r3, r7, get_byte_3
		strgeb	r3, [r0], #1
USER(		ldrgtbt	r3, [r1], #0)			@ May fault
		strgtb	r3, [r0], #1
		b	.cfu_finished

.cfu_3fupi:	subs	r2, r2, #4
		addmi	ip, r2, #4
		bmi	.cfu_3nowords
		mov	r3, r7, pull #24
USER(		ldrt	r7, [r1], #4)			@ May fault
		orr	r3, r3, r7, push #8
		str	r3, [r0], #4
		mov	ip, r1, lsl #32 - PAGE_SHIFT
		rsb	ip, ip, #0
		movs	ip, ip, lsr #32 - PAGE_SHIFT
		beq	.cfu_3fupi
		cmp	r2, ip
		movlt	ip, r2
		sub	r2, r2, ip
		subs	ip, ip, #16
		blt	.cfu_3rem8lp
	PLD(	pld	[r1, #12]		)
	PLD(	pld	[r0, #12]		)
	PLD(	subs	ip, ip, #32		)
	PLD(	blt	.cfu_3cpynopld		)
	PLD(	pld	[r1, #28]		)
	PLD(	pld	[r0, #28]		)

.cfu_3cpy8lp:
	PLD(	pld	[r1, #44]		)
	PLD(	pld	[r0, #44]		)
.cfu_3cpynopld:	mov	r3, r7, pull #24
		ldmia	r1!, {r4 - r7}			@ Shouldnt fault
		orr	r3, r3, r4, push #8
		mov	r4, r4, pull #24
		orr	r4, r4, r5, push #8
		mov	r5, r5, pull #24
		orr	r5, r5, r6, push #8
		mov	r6, r6, pull #24
		orr	r6, r6, r7, push #8
		stmia	r0!, {r3 - r6}
		subs	ip, ip, #16
		bpl	.cfu_3cpy8lp
	PLD(	cmn	ip, #32			)
	PLD(	bge	.cfu_3cpynopld		)
	PLD(	add	ip, ip, #32		)

.cfu_3rem8lp:	tst	ip, #8
		movne	r3, r7, pull #24
		ldmneia	r1!, {r4, r7}			@ Shouldnt fault
		orrne	r3, r3, r4, push #8
		movne	r4, r4, pull #24
		orrne	r4, r4, r7, push #8
		stmneia	r0!, {r3 - r4}
		tst	ip, #4
		movne	r3, r7, pull #24
USER(		ldrnet	r7, [r1], #4)			@ May fault
		orrne	r3, r3, r7, push #8
		strne	r3, [r0], #4
		ands	ip, ip, #3
		beq	.cfu_3fupi
.cfu_3nowords:	mov	r3, r7, get_byte_3
		teq	ip, #0
		beq	.cfu_finished
		cmp	ip, #2
		strb	r3, [r0], #1
USER(		ldrgebt	r3, [r1], #1)			@ May fault
		strgeb	r3, [r0], #1
USER(		ldrgtbt	r3, [r1], #1)			@ May fault
		strgtb	r3, [r0], #1
		b	.cfu_finished

		.section .fixup,"ax"
		.align	0
		/*
		 * We took an exception.  r0 contains a pointer to
		 * the byte not copied.
		 */
9001:		ldr	r2, [sp], #4			@ void *to
		sub	r2, r0, r2			@ bytes copied
		ldr	r1, [sp], #4			@ unsigned long count
		subs	r4, r1, r2			@ bytes left to copy
		movne	r1, r4
		blne	__memzero
		mov	r0, r4
		LOADREGS(fd,sp!, {r4 - r7, pc})
		.previous
我们将在另一篇博文中详细分析该函数。

你可能感兴趣的:(linux内核中的copy_to_user和copy_from_user(一))