• copy_from_user的详细用法!


    copy_from_user函数的目的是从用户空间拷贝数据到内核空间,失败返回没有被拷贝的字节数,成功返回0.
     
    copy_from_user(void *to, const void __user *from, unsigned long n)
    1. @*to         将数据拷贝到内核的地址
    2. @*from    需要拷贝数据的地址
    3. @n     拷贝数据的长度(字节)
    3. 也就是将@form地址中的数据拷贝到@to地址中去,拷贝长度是n
     
    这么简单的一个函数却含盖了许多关于内核方面的知识,比如内核关于异常出错的处理.从用户空间拷贝数据到内核中时必须非常小心,如果用户空间的数据地址是个非法的地址,或是超出用户空间的范围,或是那些地址还没有被映射到,都可能对内核产生很大的影响,如oops,或者被造成系统安全的影响.所以copy_from_user函数的功能就不只是从用户空间拷贝数据那样简单了,它还要做一些指针检查以及处理这些问题的方法.
        下面我们来仔细分析下这个函数.函数原型在[arch/i386/lib/usercopy.c]中
     
    unsigned long
    copy_from_user(void *to, const void __user *from, unsigned long n)
    {
      might_sleep();
      if (access_ok(VERIFY_READ, from, n))
         n = __copy_from_user(to, from, n);
      else
         memset(to, 0, n);
      return n;
    }
     
    首先这个函数是可以睡眠的,它调用might_sleep()来处理,它在include/linux/kernel.h中定义,本质也就是调用schedule(),转到其他进程.接下来就要验证用户空间地址的有效性.它在
    /include/asm-i386/uaccess.h中定义.
    #define access_ok(type,addr,size) (likely(__range_ok(addr,size) == 0)),进一步调用__rang_ok函数来处理,它所做的测试很简单,就是比较addr+size这个地址的大小是否超出了用户进程空间的大小,也就是0xbfffffff.可能有读者会问,只做地址范围检查,怎么不做指针合法性的检查呢,如果出现前面提到过的问题怎么办?这个会在下面的函数中处理,我们慢慢看.在做完地址范围检查后,如果成功则调用__copy_from_user函数开始拷贝数据了,如果失败的话,就把从to指针指向的内核空间地址到to+size范围填充为0.
     
    __copy_from_user也在uaceess.h中定义,

    static inline unsigned long
    __copy_from_user(void *to, const void __user *from, unsigned long n)
    {
       might_sleep();
       return __copy_from_user_inatomic(to, from, n);
    }

    这里继续调用__copy_from_user_inatomic.

    static inline unsigned long
    __copy_from_user_inatomic(void *to, const void __user *from, unsigned long n)
    {
       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);
    }

    这里先判断要拷贝的字节大小,如果是8,16,32大小的话,则调用__get_user_size来拷贝数据.这样做是一种程序设计上的优化了。
    #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","","=r",errret);break;
    default: (x) = __get_user_bad();
    }
    } while (0)
    #define __get_user_asm(x, addr, err, itype, rtype, ltype, errret)
    __asm__ __volatile__(
    "1: mov"itype" %2,%"rtype"1 "
    "2: "
    ".section .fixup,"ax" "
    "3: movl %3,%0 "
    " xor"itype" %"rtype"1,%"rtype"1 "
    " jmp 2b "
    ".previous "
    ".section __ex_table,"a" "
    " .align 4 "
    " .long 1b,3b "
    ".previous"
    : "=r"(err), ltype (x)
    : "m"(__m(addr)), "i"(errret), "0"(err))
    实际上在完成一些宏的转换后,也就是利用movb,movw,movl指令传输数据了,对于
    内嵌汇编中的.section .fixup, .section __ex_table,我们呆会要仔细讲。
    如果不是那些特殊大小时,则调用__copy_from_user_ll处理。
     
    unsigned long
    __copy_from_user_ll(void *to, const void __user *from, unsigned long n)
    {
        if (movsl_is_ok(to, from, n))
            __copy_user_zeroing(to, from, n);
        else
           n = __copy_user_zeroing_intel(to, from, n);
        return n;
    }
    直接调用__copy_user_zeroing开始真正的拷贝数据了,绕了那么多弯,总算快看到出路了。copy_from_user函数的精华部分也就都在这了。
     
    #define __copy_user_zeroing(to,from,size)
    do {
    int __d0, __d1, __d2;
    __asm__ __volatile__(
    " cmp $7,%0 "
    " jbe 1f "
    " movl %1,%0 "
    " negl %0 "
    " andl $7,%0 "
    " subl %0,%3 "
    "4: rep; movsb "
    " movl %3,%0 "
    " shrl $2,%0 "
    " andl $3,%3 "
    " .align 2,0x90 "
    "0: rep; movsl "
    " movl %3,%0 "
    "1: rep; movsb "
    "2: "
    ".section .fixup,"ax" "
    "5: addl %3,%0 "
    " jmp 6f "
    "3: lea 0(%3,%0,4),%0 "
    "6: pushl %0 "
    " pushl %%eax "
    " xorl %%eax,%%eax "
    " rep; stosb "
    " popl %%eax "
    " popl %0 "
    " jmp 2b "
    ".previous "
    ".section __ex_table,"a" "
    " .align 4 "
    " .long 4b,5b "
    " .long 0b,3b "
    " .long 1b,6b "
    ".previous"
    : "=&c"(size), "=&D" (__d0), "=&S" (__d1), "=r"(__d2)
    : "3"(size), "0"(size), "1"(to), "2"(from)
    : "memory");
    } while (0)
     
        这个函数的前一部分比较简单,也就是拷贝数据.关于后一部分就会涉及到我们前面提到过的那些情况了,如果用户空间的地址没被映射怎么办呢?在一些老的内核版本中是用verify_area()来验证地址地址合法性的,比如在早期的linux 0.11内核.
    [linux0.11/kenrel/fork.c]
    // 进程空间写前验证函数。在现代CPU中,其控制寄存器CR0有个写保护标志位(wp:16),内核可以通过设置
    // 该位来禁止特权级0的代码向用户空间只读页面执行写数据,否则将导致写保护异常。
    // addr为内存物理地址
    void verify_area(void * addr,int size)
    {
    unsigned long start;
    start = (unsigned long) addr;
    size += start & 0xfff; // start & 0xfff为起始地址addr在页面中的偏移,2^12=4096
    start &= 0xfffff000; // start为页开始地址,即页面边界值。此时start为当前进程空间中的逻辑地址start += get_base(current->ldt[2]); // get_base(current->ldt[2])为进程数据段在线性地址空间中的开始地址,在加上start,变为系统这个线性空间中的地址
    页边界 addr ----size----- 页边界
    +--------------------------------------------------------+
    | ... | start&0xfff | | | ... |
    +--------------------------------------------------------+
    | start |
    start-----------size-------------
    while (size>0) {
    size -= 4096;
    write_verify(start); // 以页为单位,进行写保护验证,如果页为只读,则将其变为可写
    start += 4096;
    }
    }
    [linux0.11/mm/memory.c]
    // 验证线性地址是否可写
    void write_verify(unsigned long address)
    {
    unsigned long page;
    // 如果对应页表为空的话,直接返回
    if (!( (page = *((unsigned long *) ((address>>20) & 0xffc)) )&1))
    return;
    page &= 0xfffff000;
    page += ((address>>10) & 0xffc);
    // 经过运算后page为页表项的内容,指向实际的一页物理地址
    if ((3 & *(unsigned long *) page) == 1) // 验证页面是否可写,不可写则执行un_wp_page,取消写保护.
    un_wp_page((unsigned long *) page);
    return;
    }
     
       但是如果每次在用户空间复制数据时,都要做这种检查是很浪费时间的,毕竟坏指针是很少存在的,在新内核中的做法是,在从用户空间复制数据时,取消验证指针合法性的检查,只多地址范围的检查,就象access_ok()所做的那样,一但碰上了坏指针,就要页异常出错处理程序去处理它了.我们去看看do_page_fault函数.
    [arch/asm-i386/mm/fault.c/do_page_falut()]
    fastcall void do_page_fault(struct pt_regs *regs, unsigned long error_code)
    {
    ...
    ...
    if (!down_read_trylock(&mm->mmap_sem)) {
    if ((error_code & 4) == 0 &&
    !search_exception_tables(regs->eip))
    goto bad_area_nosemaphore;
    down_read(&mm->mmap_sem);
    }
    ...
    ...
    bad_area_nosemaphore:
    ...
    no_context:
    if (fixup_exception(regs))
    return;
    ...
    ...
    }
     
    error_code保存的是出错码,(error_code & 4) == 0代表产生异常的原因是在内核中.它调用fixup_exception(regs)来处理这个问题.既然出错了,那么如何来修复它呢?先看下fixup_exception()函数的实现:
     
    [arch/asm-i386/mm/extable.c]
    int fixup_exception(struct pt_regs *regs)
    {
    const struct exception_table_entry *fixup;
    ...
    fixup = search_exception_tables(regs->eip);
    if (fixup) {
    regs->eip = fixup->fixup;
    return 1;
    }
    ...
    }
    [kernel/extable.c]
    const struct exception_table_entry *search_exception_tables(unsigned long addr)
    {
    const struct exception_table_entry *e;
    e = search_extable(__start___ex_table, __stop___ex_table-1, addr);
    if (!e)
    e = search_module_extables(addr);
    return e;
    }
    [/lib/extable.c]
    const struct exception_table_entry *
    search_extable(const struct exception_table_entry *first,
    const struct exception_table_entry *last,
    unsigned long value)
    {
    while (first <= last) {
    const struct exception_table_entry *mid;
    mid = (last - first) / 2 + first;
    if (mid->insn < value)
    first = mid + 1;
    else if (mid->insn > value)
    last = mid - 1;
    else
    return mid;
    }
    return NULL;
    }
     
    在内核中有个异常出错地址表,在地址表中有个出错地址的修复地址也气对应,它结构如下:

    [/include/asm-i386/uaccess.h]
    struct exception_table_entry
    {
        unsigned long insn, fixup;
    };

    insn是产生异常指令的地址,fixup用来修复出错地址的地址,也就是当异常发生后,用它的地址来替换异常指令发生的地址。__copy_user_zeroing中的.section __ex_table代表异常出错地址表的地址,.section .fixup代表修复的地址。他们都是elf文件格式中的2个特殊节。

    ".section __ex_table,"a" "
    " .align 4 "
    " .long 4b,5b "
    " .long 0b,3b "
    " .long 1b,6b "

    4b,5b的意思是当出错地址在4b标号对应的地址上时,就转入5b标号对应的地址去接着运行,也就是修复的地址。依次类推。所以理解这一点后,fixup_exception()函数就很容易看明白了就是根据出错地址搜索异常地址表,找到对应的修复地址,跳转到那里去执行就ok了。
       ok,到这里copy_from_user函数也就分析完了,如果有什么不明白的话,可以通过阅读/usr/src/linux/Documentation/exception.txt来得到更多关于异常处理方面的知识。
    懒惰不会让你一下子跌到 但会在不知不觉中减少你的收获; 勤奋也不会让你一夜成功 但会在不知不觉中积累你的成果 越努力,越幸运。
  • 相关阅读:
    别了,DjVu!
    DjVu转PDF
    我的电子书历程
    连续翻页浏览器面临的共同问题
    对超过2TB的硬盘进行分区需要使用parted
    DB2常用命令
    CentOS增加网卡
    mysql相关参数解读
    max_user_connections参数设置试验
    mysql最大连接数试验
  • 原文地址:https://www.cnblogs.com/Rainingday/p/12618715.html
Copyright © 2020-2023  润新知