1.copy_from_user
copy_from_user函数的目的是从用户空间拷贝数据到内核空间,失败返回没有被拷贝的字节数,成功返回0。它内部的实现当然不仅仅拷贝数据,还需要考虑到传入的用户空间地址是否有效,比如地址是不是超出用户空间范围啊,地址是不是没有对应的物理页面啊,否则内核就会oops的。不同的架构,该函数的实现不一样。下面主要以arm例进行说明。
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;
}
该函数先通过access_ok做第一层的地址范围有效性检查,然后通过__copy_from_user进行正式的拷贝。之所以只做第一层的检查,是因为第二层的检查(地址是不是没有对应的物理页面)只能通过异常处理来解决。
1.1.三个参数:
*to是内核空间的指针
*from是用户空间指针
n表示从用户空间想内核空间拷贝数据的字节数。
如果成功执行拷贝操作,则返回0,否则返回还没有完成拷贝的字节数。
1.2.分为两个部分:
1.2.1.access_ok
#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)
//检查用户地址空间指针
extern void __chk_user_ptr(void __user *);
# define __chk_user_ptr(x) (void)0
对于有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; })
核心思想:判断源地址+要拷贝的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)。
refer to