Linux内核中的copy_to_user和copy_from_user(一)

2014-11-24 08:47:15 来源: 作者: 浏览: 1

Kernel version:2.6.14


CPU architecture:ARM920T


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,否则返回还没有完成拷贝的字节数。


这个函数从结构上来分析,其实都可以分为两个部分:


首先检查用户空间的地址指针是否有效;


调用__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):


(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_p

-->

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: