copy 您所在的位置:网站首页 zerbing copy

copy

2023-04-18 17:24| 来源: 网络整理| 查看: 265

前言

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_intel。movsl_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月

    毕业那两年在做嵌入式应用开发,主要是单片机和arm linux上的应用开发,后来又做了两年arm linux驱动开发,15年到现在在做pc端及嵌入式端开发,包括服务器系统裁剪、底层框架实现、硬件加速等。喜欢技术分享、交流!联系方式: [email protected][email protected]



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有