原理上,由于内核空间的优先级是高于用户空间的,内核态是可以直接访问用户态的虚拟地址空间的,所以如果需要在内核态获取用户态地址空间的数据的话,理论上应该是可以直接访问的,但为什么还需要使用copy_from_user接口呢?
直接访问的话,无法保证被访问的用户态虚拟地址是否有对应的页表项,即无法保证该虚拟地址已经分配了相应的物理内存,如果此时没有对应的页表项,那么此时将产生page fault,导致流程混乱,原则上如果没有页表项(即没有物理内存时),是不应该对其进行操作的。
所以直接操作有比较大的风险,而copy_from_user本质上也只是做了相关判断和校验,保证不会出现相关异常而已。
static inline unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
{
if (access_ok(VERIFY_READ, from, n))
n = __arch_copy_from_user(to, from, n);
else /* security hole - plug it */
memzero(to, n);
return n;
}
从结构上来分析,其实都可以分为两个部分: 1.首先检查用户空间的地址指针是否有效; 2.调用__arch_copy_from_user函数。
access_ok用来对用户空间的地址指针from作某种有效性检验,这个宏和体系结构相关
#define __range_ok(addr,size) ({ \
unsigned long flag, sum; \
__chk_user_ptr(addr); \
__asm__("adds %1, %2, %3; sbcccs %1, %1, %0; movcc %0, #0" \
: "=&r" (flag), "=&r" (sum) \
: "r" (addr), "Ir" (size), "0" (current_thread_info()->addr_limit) \
: "cc"); \
flag; })
#define access_ok(type,addr,size) (__range_ok(addr,size) == 0)
access_ok中第一个参数type并没有用到,__range_ok的作用在于判断addr+size之后是否还在进程的用户空间范围之内。
__range_ok宏其实等价于:
-
如果(addr + size) >= (current_thread_info()->addr_limit) - 1,返回非零值 //越界
-
如果(addr + size) < (current_thread_info()->addr_limit),返回零 //没越界
而access_ok就是检验将要操作的用户空间的地址范围是否在当前进程的用户地址空间限制中。
如果在驱动中使用这两个函数,必须是在实现系统调用的函数中使用,不可在实现中断处理的函数中使用。如果在中断上下文中使用了,那代码就很可能操作了根本不相关的进程地址空间。其次由于操作的页面可能被换出,这两个函数可能会休眠,所以同样不可在中断上下文中使用。
如果内核访问一个尚未被提交物理页面的空间,将产生缺页异常,内核会调用do_page_fault,因为异常发生在内核空间,do_page_fault将调用search_exception_tables在“ __ ex_table”中查找异常指令的修复指令,在__ arch_copy_from_user函数中经常使用USER宏,这个宏中了定义了“__ex_table” section。
#define USER(x...) \
9999: x; \
.section __ex_table,"a"; \
.align 3; \
.long 9999b,9001f; \
.previousc
其中9999b对应标号9999处的指令,9001f是9001处的指令,是9999b处指令的修复指令。这样,当标号9999处发生缺页异常时,系统将调用do_page_fault提交物理页面,然后跳到9001继续执行。
如果在驱动程序中不使用copy_from_user而用memcpy来代替,对于上述的情形会产生什么结果呢?
当标号9999出发生缺页异常时,系统在“__ ex_table”section总将找不到修复地址,因为memcpy没有像copy_from_user那样定义一个“__ex_table”section,此时do_page_fault将通过no_context函数产生Oops。
本文部分内容参考自网络,如有侵权请私信联系我删除