
基于arm 构架(带mmu)的copy_from_user与copy_to_user详细分析.doc
11页基于 ARM 构架(带 MMU)的 copy_from_user 与copy_to_user 详细分析在学习 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, %3roksum = 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)addr_limit) ,则 C=0当 C=0的时候执行以下指令,否则跳过( flag 非零) movcc %0, #0flag = 0,给 flag 赋值 0(4)flag; 返回 flag 值综上所诉:__range_ok 宏其实等价于:如果(addr + size)>=(curr。












