在学习Linux内核驱动的时候,一开始就会碰到copy_from_user和copy_to_user这两个常用的函数。这两个函数在内核使用的非常频繁,负责将数据从用户空间拷贝到内核空间以及将数据从内核空间拷贝到用户空间。在4年半前初学Linux内核驱动程序的时候,我只是知道这个怎么用,并没有很深入的分析这两个函数。这次研究内核模块挂载的时候,又碰到了它们。决定还是认真跟踪一下函数。
首先这两个函数的原型在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;
- }
- static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)
- {
- if (access_ok(VERIFY_WRITE, to, n))
- n = __copy_to_user(to, from, n);
- return n;
- }
这两个函数从结构上来分析,其实都可以分为两个部分:
1、首先检查用户空间的地址指针是否有效(难点)
2、调用__copy_from_user和__copy_to_user函数
在这个分析中,我们先易后难。首先看看具体数据拷贝功能的__copy_from_user和__copy_to_user函数
对于ARM构架,没有单独实现这两个函数,所以他们的代码位于include/asm-generic/uaccess.h
- /*
- * 带有MMU的构架应该覆盖这两个函数
- */
- #ifndef __copy_from_user
- static inline __must_check long __copy_from_user(void *to,
- const void __user * from, unsigned long n)
- {
- if (__builtin_constant_p(n)) {
- switch(n) {
- case 1:
- *(u8 *)to = *(u8 __force *)from;
- return 0;
- case 2:
- *(u16 *)to = *(u16 __force *)from;
- return 0;
- case 4:
- *(u32 *)to = *(u32 __force *)from;
- return 0;
- #ifdef CONFIG_64BIT
- case 8:
- *(u64 *)to = *(u64 __force *)from;
- return 0;
- #endif
- default:
- break;
- }
- }
- memcpy(to, (const void __force *)from, n);
- return 0;
- }
- #endif
- #ifndef __copy_to_user
- static inline __must_check long __copy_to_user(void __user *to,
- const void *from, unsigned long n)
- {
- if (__builtin_constant_p(n)) {
- switch(n) {
- case 1:
- *(u8 __force *)to = *(u8 *)from;
- return 0;
- case 2:
- *(u16 __force *)to = *(u16 *)from;
- return 0;
- case 4:
- *(u32 __force *)to = *(u32 *)from;
- return 0;
- #ifdef CONFIG_64BIT
- case 8:
- *(u64 __force *)to = *(u64 *)from;
- return 0;
- #endif
- default:
- break;
- }
- }
- memcpy((void __force *)to, from, n);
- return 0;
- }
- #endif
- GCC的内建函数 __builtin_constant_p 用于判断一个值是否为编译时常数,如果参数值是常数,函数返回 1,否则返回 0。
从这两个函数中可以看出其实结构是一样的,首先看看n是不是常数,如果是并为1、2、4、8(64bit)则直接就用一个赋值语句拷贝数据。如果不是常数或n过大,则使用memcpy函数。而这个memcpy函数位于lib/string.c:
- #ifndef __HAVE_ARCH_MEMCPY
- /**
- * memcpy - Copy one area of memory to another
- * @dest: Where to copy to
- * @src: Where to copy from
- * @count: The size of the area.
- *
- * You should not use this function to access IO space, use memcpy_toio()
- * or memcpy_fromio() instead.
- */
- void *memcpy(void *dest, const void *src, size_t count)
- {
- char *tmp = dest;
- const char *s = src;
- while (count--)
- *tmp++ = *s++;
- return dest;
- }
- EXPORT_SYMBOL(memcpy);
- #endif
这个函数其实就是一个简单的利用循环来数据拷贝,非常简单。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
好了如何拷贝数据我们已经了解了,现在我们来看看前面的用户空间指针检测函数access_ok,这其实是一个宏定义,位于arch/arm/include/asm/uaccess.h文件中:
- /* We use 33-bit arithmetic here... */
- #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; })
- ......
- #define access_ok(type,addr,size) (__range_ok(addr,size) == 0)
- ......
这个就比较麻烦了,涉及到了C语言中内联汇编,如果还不熟悉的朋友可以看看《ARM GCC 内嵌汇编手册》,我也不是很熟。
现在我们来仔细分析__range_ok这个宏:
(1)unsigned long flag, roksum;\\定义两个变量
- flag:保存结果的变量:非零代表地址无效,零代表地址可以访问。初始存放非零值(current_thread_info()->addr_limit),也就是当前进程的地址上限值。
- roksum:保存要访问的地址范围末端,用于和当前进程地址空间限制数据做比较
(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__,这就是一个空函数。
(3)接下来的汇编,我适当地翻译如下:
adds %1, %2, %3
roksum = addr + size 这个操作影响状态位(目的是影响是进位标志C)
以下的两个指令都带有条件CC,也就是当C=0的时候才执行。
如果上面的加法指令进位了(C=1),则以下的指令都不执行,flag就为初始值current_thread_info()->addr_limit(非零值),并返回。
如果没有进位(C=0),就执行下面的指令
sbcccs %1, %1, %0
roksum = roksum - flag,也就是(addr + size)- (current_thread_info()->addr_limit),操作影响符号位。
如果(addr + size)>=(current_thread_info()->addr_limit),则C=1
如果(addr + size)<(current_thread_info()->addr_limit),则C=0
当C=0的时候执行以下指令,否则跳过(flag非零)。
movcc %0, #0
flag = 0,给flag赋值0
(4)flag;
返回flag值
综上所诉:__range_ok宏其实等价于:
如果(addr + size)>=(current_thread_info()->addr_limit),返回非零值
如果(addr + size)<(current_thread_info()->addr_limit),返回零
而access_ok就是检验将要操作的用户空间的地址范围是否在当前进程的用户地址空间限制中。这个宏的功能很简单,完全可以用C实现,不是必须使用汇编。个人理解:由于这两个函数使用频繁,就使用汇编来实现部分功能来增加效率。
从这里再次可以认识到,copy_from_user与copy_to_user的使用是结合进程上下文的,因为他们要访问“user”的内存空间,这个“user”必须是某个特定的进程。通过上面的源码就知道,其中使用了current_thread_info()来检查空间是否可以访问。如果在驱动中使用这两个函数,必须是在实现系统调用的函数中使用,不可在实现中断处理的函数中使用。如果在中断上下文中使用了,那代码就很可能操作了根本不相关的进程地址空间。
其次由于操作的页面可能被换出,这两个函数可能会休眠,所以同样不可在中断上下文中使用。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
上面的分析是根据个人经验跟踪的,恐有疏漏,如果有朋友发现了,欢迎批评指正!!
阅读(464) | 评论(0) | 转发(0) |