在read/write/ioctl等系统调用里,经常需要从用户空间读取数据,或者向用户空间的地址写入数据。如果应用程序传入了一个参数user_arg,指向的是用户空间的地址。那么我们在内核态里能否直接从这个地址读取数据呢?答案是肯定的,因为内核能够看到进程的整个地址空间,属于这个进程的所有page在此进程的page table里,内核函数当然可以访问那个指针user_arg。那么为什么一定要用copy_from_user/copy_to_user,而不是直接用memcpy或者直接dereference那个地址?
下面以__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)函数里发生什么了呢?
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