为什么要用copy_from_user/copy_to_user?

在read/write/ioctl等系统调用里,经常需要从用户空间读取数据,或者向用户空间的地址写入数据。如果应用程序传入了一个参数user_arg,指向的是用户空间的地址。那么我们在内核态里能否直接从这个地址读取数据呢?答案是肯定的,因为内核能够看到进程的整个地址空间,属于这个进程的所有page在此进程的page table里,内核函数当然可以访问那个指针user_arg。那么为什么一定要用copy_from_user/copy_to_user,而不是直接用memcpy或者直接dereference那个地址?
  • 首先,直接使用那个地址很不安全,如果应用程序传过来一个非法地址,就有可能panic系统。或者应用程序可以给出一个指针指向kernel地址(>PAGE_OFFSET),那样就可以随意访问kernel page数据。
  • 所以,我们需要使用内核提供的方法对应用程序传过来的地址进行检查和验证:是不是用户空间地址;是否已经使用malloc/mmap/brk等分配(即是否落于有效的VMA内)。。。
    • access_ok可以粗略地检查user_arg是否位于用户空间,见 access_ok( arch/x86/include/asm/uaccess.h)。
    • 而检查user_arg是否属于已分配的用户空间地址则要推迟到page fault发生时。这就是fixup存在的原因。触发pagefault的指令(处于进程上下文)和处理pagefault的函数(中断上下文)处于不同的context里,pagefault handler没有办法直接返回错误,这是与函数调用最大的不同之处。

下面以__copy_user(copy_from_user/copy_to_user最终都会调到这个函数)为例具体分析这个copy过程是怎样执行的。

639 #define __copy_user(to, from, size)                                     \
640 do {                                                                    \
641         int __d0, __d1, __d2;                                           \
642         __asm__ __volatile__(                                           \
643                 "       cmp  $7,%0\n"                                   \
644                 "       jbe  1f\n"                                      \
645                 "       movl %1,%0\n"                                   \
646                 "       negl %0\n"                                      \
647                 "       andl $7,%0\n"                                   \
648                 "       subl %0,%3\n"                                   \
649                 "4:     rep; movsb\n"                                   \
650                 "       movl %3,%0\n"                                   \
651                 "       shrl $2,%0\n"                                   \
652                 "       andl $3,%3\n"                                   \
653                 "       .align 2,0x90\n"                                \
654                 "0:     rep; movsl\n"                                   \
655                 "       movl %3,%0\n"                                   \
656                 "1:     rep; movsb\n"                                   \
657                 "2:\n"                                                  \
658                 ".section .fixup,\"ax\"\n"                              \
659                 "5:     addl %3,%0\n"                                   \
660                 "       jmp 2b\n"                                       \
661                 "3:     lea 0(%3,%0,4),%0\n"                            \
662                 "       jmp 2b\n"                                       \
663                 ".previous\n"                                           \
664                 ".section __ex_table,\"a\"\n"                           \
665                 "       .align 4\n"                                     \
666                 "       .long 4b,5b\n"                                  \
667                 "       .long 0b,3b\n"                                  \
668                 "       .long 1b,2b\n"                                  \
669                 ".previous"                                             \
670                 : "=&c"(size), "=&D" (__d0), "=&S" (__d1), "=r"(__d2)   \
671                 : "3"(size), "0"(size), "1"(to), "2"(from)              \
672                 : "memory");                                            \
673 } while (0)

在上面的代码里定义了三个exception entry,即line 666, 667, 668。exception entry的格式是{instruction, fixup},如“ .long 4b, 5b”表示label 4处(line 649)的指令发生exception,就使用label 5处(line 659)的fixup代码。关于exception table entry的定义见"linux-4.4/arch/x86/include/asm/uaccess.h"。
Label 4处的代码在执行copy的动作,它需要访问内存,假如要访问的地址没有对应的物理页存在,此时就会发生page fault,相应的page fault handler -- do_page_fault就会被调用(见linux-4.4/arch/x86/mm/fault.c)。编译器会生成.fixup section,这个section里存放的是exception table。

在do_page_fault(实际上是__do_page_fault)函数里发生什么了呢?
  • 会检查发生fault的地址(CR2),如果是kernel address(vmalloc分配的)的fault,则特殊处理并返回;
  • 否则就是user address,
    • 如果当前是kernel context(CS != 3,对于*_user等函数,肯定是处于kernel context中),则调用 search_exception_tables查找触发此page fault的指令地址(RIP)是否存在相应的exception fixup 项(这个fixup就是上面的copy_user等函数提供的):
      • 如果不存在,就是一个bad area访问,产生oops -- 如果panic_on_oops=1,则会panic系统。
      • 如果存在fixup,就继续执行正常的page fault处理:
        • 检查此fault地址是否在process已分配的vma内,然后执行正常的物理内存分配动作(分配新的物理页面或者换入所需的物理页面)。在这一过程中,
        • 如果未发生错误,则do_page_fault返回,产生page fault的指令继续执行。在上面例子里,就是返回label 4处继续执行,完成数据copy的工作。
        • 否则,do_page_fault会 调用 fixup_exception 并返回,但返回地址发生改变,新的IP指向fixup指令处,所以会执行fixup指令 !在上面的例子里,就是返回label 5处继续执行:把未成功写的字节数写入size,跳到label 2处,函数结束(fixup指令在这里产生作用)。
        • 如果不是在此进程的VMAs内,则是一个bad_area访问,调用相应的fixup代码(过程同上)。

Reference:
linux-4.4/Documentation/x86/exception-tables.txt
http://lkml.iu.edu/hypermail/linux/kernel/0512.0/0580.html
http://lkml.iu.edu/hypermail/linux/kernel/0512.0/1609.html
linux-4.4/arch/x86/mm/extable.c: fixup_exception

你可能感兴趣的:(Linux)