前言

copy_from_user函数的目的是从用户空间拷贝数据到内核空间,失败返回没有被拷贝的字节数,成功返回0。它内部的实现当然不仅仅拷贝数据,还需要考虑到传入的用户空间地址是否有效,比如地址是不是超出用户空间范围啊,地址是不是没有对应的物理页面啊,否则内核就会oops的。不同的架构,该函数的实现不一样。下面主要以arm和x86为例进行说明(分析过程会忽略一些无关的代码)。

arm copy_from_user

arm架构下,copy_from_user相关的文件主要有arch/arm/include/asm/uaccess.h  arch/arm/lib/copy_from_user.S  arch/arm/lib/copy_template.S。下面先来看copy_from_user,它的实现在arch/arm/include/asm/uaccess.h中:

static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)
{
if (access_ok(VERIFY_READ, from, n))
n = __copy_from_user(to, from, n);
else /* security hole - plug it */
memset(to, 0, n);
return n;
}

该函数先通过access_ok做第一层的地址范围有效性检查,然后通过__copy_from_user进行正式的拷贝。之所以只做第一层的检查,是因为第二层的检查(地址是不是没有对应的物理页面)只能通过异常处理来解决!

下面看access_ok的实现吧!(代码实现还是在同一个文件里)同样,不同的架构,实现方式不同。甚至有mmu和无mmu也不同。

#ifdef CONFIG_MMU
...
...
...
#define __range_ok(addr,size) ({ \
unsigned long flag, roksum; \
__chk_user_ptr(addr); \
__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; })
#else /* CONFIG_MMU */
...
...
...
#define __range_ok(addr,size) ((void)(addr),0)
#endif #define access_ok(type,addr,size) (__range_ok(addr,size) == 0)

对于无mmu的,检查就是不检查,因为无mmu也就是意味着没有虚拟地址映射,用的都是物理地址(出了问题,也无法解决)。

对于有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; })

这是一段c内嵌汇编(linux采用AT&T编码方式,左边值为原操作数,右边值为目的操作数,与intel编码方式不同,可参考GNU C内嵌汇编语言)!核心思想就是判断源地址+要拷贝的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)

好了,分析完__range_ok的实现,现在继续看__copy_from_user,还是在相同的文件里(同样有mmu和非mmu之分):

#ifdef CONFIG_MMU
extern unsigned long __must_check __copy_from_user(void *to, const void __user *from, unsigned long n);
...
...
...
#else
#define __copy_from_user(to,from,n) (memcpy(to, (void __force *)from, n), 0)
...
...
...
#endif

有mmu的时候,它对应的实现在arch/arm/lib/copy_from_user.S里面:

...
...
...
ENTRY(__copy_from_user) #include "copy_template.S" ENDPROC(__copy_from_user) .pushsection .fixup,"ax"
.align 0
copy_abort_preamble
ldmfd sp!, {r1, r2}
sub r3, r0, r1
rsb r1, r3, r2
str r1, [sp]
bl __memzero
ldr r0, [sp], #4
copy_abort_end
.popsection

核心的实现在arch/arm/lib/copy_template.S中,arch/arm/lib/copy_template.S里面的具体逻辑会因为arch/arm/lib/copy_from_user.S之前所定义的宏而不同。这里就不再跟进去分析了,异常表的处理我打算通过分析x86实现的时候来完成。

x86 copy_from_user

x86架构下,copy_from_user相关的文件主要有arch/x86/include/asm/uaccess.h  arch/x86/lib/usercopy_32.S  arch/x86/include/asm/uaccess_32.h arch/x86/include/asm/uaccess_64.h。下面先来看copy_from_user,它的实现在arch/x86/include/asm/uaccess.h中:

static inline unsigned long __must_check
copy_from_user(void *to, const void __user *from, unsigned long n)
{
int sz = __compiletime_object_size(to); might_fault(); /*
* While we would like to have the compiler do the checking for us
* even in the non-constant size case, any false positives there are
* a problem (especially when DEBUG_STRICT_USER_COPY_CHECKS, but even
* without - the [hopefully] dangerous looking nature of the warning
* would make people go look at the respecitive call sites over and
* over again just to find that there's no problem).
*
* And there are cases where it's just not realistic for the compiler
* to prove the count to be in range. For example when multiple call
* sites of a helper function - perhaps in different source files -
* all doing proper range checking, yet the helper function not doing
* so again.
*
* Therefore limit the compile time checking to the constant size
* case, and do only runtime checking for non-constant sizes.
*/ if (likely(sz < 0 || sz >= n))
n = _copy_from_user(to, from, n);
else if(__builtin_constant_p(n))
copy_from_user_overflow();
else
__copy_from_user_overflow(sz, n); return n;
}

GCC的内建函数__builtin_constant_p用于判断一个值是否为编译时常数,如果参数值是常数,函数返回 1,否则返回 0。copy_from_user核心的实现在_copy_from_user中:

unsigned long _copy_from_user(void *to, const void __user *from, unsigned n)
{
if (access_ok(VERIFY_READ, from, n))
n = __copy_from_user(to, from, n);
else
memset(to, 0, n);
return n;
}

其中,access_ok相关代码(代码比较简单,不再分析):

static inline bool __chk_range_not_ok(unsigned long addr, unsigned long size, unsigned long limit)
{
/*
* If we have used "sizeof()" for the size,
* we know it won't overflow the limit (but
* it might overflow the 'addr', so it's
* important to subtract the size from the
* limit, not add it to the address).
*/
if (__builtin_constant_p(size))
return addr > limit - size; /* Arbitrary sizes? Be careful about overflow */
addr += size;
if (addr < size)
return true;
return addr > limit;
} #define __range_not_ok(addr, size, limit) \
({ \
__chk_user_ptr(addr); \
__chk_range_not_ok((unsigned long __force)(addr), size, limit); \
}) #define access_ok(type, addr, size) \
likely(!__range_not_ok(addr, size, user_addr_max()))

下面看__copy_from_user相关的代码实现(以32位系统为例),注释直接添加到代码中:

static __always_inline unsigned long
__copy_from_user(void *to, const void __user *from, unsigned long n)
{
might_fault();
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);//如果不能识别n是一个常量,就调用
}

先看__get_user_size实现,__chk_user_ptr之前已经说过,不再重复。主要看__get_user_asm

#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", "k", "=r", errret); \
break; \
case 8: \
__get_user_asm_u64(x, ptr, retval, errret); \
break; \
default: \
(x) = __get_user_bad(); \
} \
} while (0)

__get_user_size根据要copy的size传入不同的参数,最终会使用movb或者movw或者movl来实现1、2\4字节的拷贝。主要需要注意的就是.section .fixup_ASM_EXTABLE.section .fixup指定了.fixup section,且该段为可重定位的代码段,_ASM_EXTABLE定义了__ex_table段,且该段为可重定位的数据段,实际上它指定了3b处异常时的跳转地址,即3b,3b刚好就是.fixup段处。

#define __get_user_asm(x, addr, err, itype, rtype, ltype, errret)	\
asm volatile(ASM_STAC "\n" \
"1: mov"itype" %2,%"rtype"1\n" \
"2: " ASM_CLAC "\n" \
".section .fixup,\"ax\"\n" \
"3: mov %3,%0\n" \
" xor"itype" %"rtype"1,%"rtype"1\n" \
" jmp 2b\n" \
".previous\n" \
_ASM_EXTABLE(1b, 3b) \
: "=r" (err), ltype(x) \
: "m" (__m(addr)), "i" (errret), "0" (err))

分析完1、2、4字节的拷贝后,继续看非1、2、4字节的拷贝实现,现在继续看__copy_from_user_ll

unsigned long __copy_from_user_ll(void *to, const void __user *from,
unsigned long n)
{
stac();
if (movsl_is_ok(to, from, n))
__copy_user_zeroing(to, from, n);
else
n = __copy_user_zeroing_intel(to, from, n);
clac();
return n;
}

先通过movsl_is_ok判断下,然后分别调用__copy_user_zeroing或者__copy_user_zeroing_intelmovsl_is_ok的实现:

static inline int __movsl_is_ok(unsigned long a1, unsigned long a2, unsigned long n)
{
#ifdef CONFIG_X86_INTEL_USERCOPY
if (n >= 64 && ((a1 ^ a2) & movsl_mask.mask))
return 0;
#endif
return 1;
}
#define movsl_is_ok(a1, a2, n) \
__movsl_is_ok((unsigned long)(a1), (unsigned long)(a2), (n))

从这里可以知道,只有配置了CONFIG_X86_INTEL_USERCOPY,才有可能返回0,不然一般多事返回1。我们不考虑定义CONFIG_X86_INTEL_USERCOPY的情况,也就是该函数返回1时,继续转入到__copy_user_zeroing的调用,代码实现如下:

#define __copy_user_zeroing(to, from, size)				\
do { \
int __d0, __d1, __d2; \
__asm__ __volatile__( \
" cmp $7,%0\n" \
" jbe 1f\n" \
" movl %1,%0\n" \
" negl %0\n" \
" andl $7,%0\n" \
" subl %0,%3\n" \
"4: rep; movsb\n" \
" movl %3,%0\n" \
" shrl $2,%0\n" \
" andl $3,%3\n" \
" .align 2,0x90\n" \
"0: rep; movsl\n" \
" movl %3,%0\n" \
"1: rep; movsb\n" \
"2:\n" \
".section .fixup,\"ax\"\n" \
"5: addl %3,%0\n" \
" jmp 6f\n" \
"3: lea 0(%3,%0,4),%0\n" \
"6: pushl %0\n" \
" pushl %%eax\n" \
" xorl %%eax,%%eax\n" \
" rep; stosb\n" \
" popl %%eax\n" \
" popl %0\n" \
" jmp 2b\n" \
".previous\n" \
_ASM_EXTABLE(4b,5b) \
_ASM_EXTABLE(0b,3b) \
_ASM_EXTABLE(1b,6b) \
: "=&c"(size), "=&D" (__d0), "=&S" (__d1), "=r"(__d2) \
: "3"(size), "0"(size), "1"(to), "2"(from) \
: "memory"); \
} while (0)

同样是汇编实现,.section .fixup_ASM_EXTABLE部分前面已经说了,而指令部分就是我们通常的数据拷贝,因此也就不再分析了。

这里摘抄下网上的一段叙述,同时他对__copy_user_zeroing的指令部分有详细的注释,大家可以看看:

在cpu进行访址的时候,内核空间和用户空间使用的都是线性地址,cpu在访址的过程中会自动完成从线性地址到物理地址的转换[用户态、内核态都得依靠进程页表完成转换],而合理的线性地址意味着:该线性地址位于该进程task_struct->mm虚存空间的某一段vm_struct_mm中,而且建立线性地址到物理地址的映射,即线性地址对应内容在物理内存中。如果访存失败,有两种可能:该线性地址存在在进程虚存区间中,但是并未建立于物理内存的映射,有可能是交换出去,也有可能是刚申请到线性区间[内核是很会偷懒的],要依靠缺页异常去建立申请物理空间并建立映射;第2种可能是线性地址空间根本没有在进程虚存区间中,这样就会出现常见的坏指针,就会引发常见的段错误[也有可能由于访问了无权访问的空间造成保护异常]。如果坏指针问题发生在用户态,最严重的就是杀死进程[最常见的就是在打dota时候出现的大红X,然后dota程序结束],如果发生在内核态,整个系统可能崩溃[xp的蓝屏很可能就是这种原因形成的]。所以linux当然不会任由这种情况的发生,其措施如下:

linux内核对于可能发生问题的指令都会准备"修复地址",比如前面的fixup部分,而且遵循谁使用这些指令,谁负责修复工作的原则。比如前面的代码中,标号5即为标号4的修复指令,3为0,6为1的修复指令。在编译过程中,编译器会将5,4等的地址对应的存入struct exception_table_entry{unsigned long insn,fixup;}中。insn即可能为4的地址,而fixup可能为5的地址,如果4为坏地址[即该地址并未在虚存区间中],则在页面异常处理过程中,会转入bad_area处,如果发生在用户态直接杀死进程即可。如果发生在内核态,首先通过search_exception_table查找异常处理表exception_table。即找到某一个exception_table_entry,假设其insn=标号4地址,fixup=标号5地址.内核将发生:

regs->ip=fixup,即通过修改当前的内核地址,从而将内核从死亡的边缘拉回来,通过标号5地址处的修复工作从而全身而退。

总结

主要分析了copy_from_user接口的内部实现,copy_to_user实现类似,不再重复分析。总的来说,copy_from_user完成了数据的拷贝的同时,处理了可能发生了地址访问异常。理论上,内核空间可以直接使用用户空间传过来的指针,即使要做数据拷贝的动作,也可以直接使用memcpy,事实上,在没有MMU的体系架构上,copy_form_user最终的实现就是利用了memcpy。但对于大多数有MMU的平台,情况就有了一些变化:用户空间传过来的指针是在虚拟地址空间上的,它指向的虚拟地址空间很可能还没有真正映射到实际的物理页面上。用户空间的缺页导致的异常会透明的被内核予以修复(为缺页的地址空间提交新的物理页面),访问到缺页的指令会继续运行仿佛什么都没有发生一样。内核空间必须被显示的修复,这是由内核提供的缺页异常处理函数的设计模式决定的(其背后的思想后:在内核态中,如果程序试图访问一个尚未提交物理页面的用户空间地址,内核必须对此保持警惕而不能像用户空间那样毫无察觉。如果内核访问一个尚未被提交物理页面的空间,将产生缺页异常,这个时候内核会调用do_page_fault,因为异常发生在内核空间,do_page_fault的处理逻辑将调用search_exception_tables__ex_table中查找异常指令的修复指令),正因为这样,copy_from_user的实现才会看起来有些复杂,当然性能方面提升也是它的复杂度提升的一个原因。

完!

2015年7月

copy_from_user分析的更多相关文章

  1. 基于ARM 构架(带MMU)的copy_from_user与copy_to_user详细分析

      [转自:http://blog.chinaunix.net/uid-20543672-id-3195249.html]   在学习Linux内核驱动的时候,一开始就会碰到copy_from_use ...

  2. [转] copy_to_user和copy_from_user两个函数的分析

    在内核的学习中会遇到很多挺有意思的函数,而且能沿着一个函数扯出来很多个相关的函数.copy_to_user和copy_from_user就是在进行驱动相关程序设计的时候,要经常遇到的两个函数.由于内核 ...

  3. linux 系统内核空间与用户空间通信的实现与分析<转>

    linux 系统内核空间与用户空间通信的实现与分析 2 评论: 陈鑫 (chen.shin@hotmail.com), 自由软件爱好者, 南京邮电学院电子工程系 2004 年 7 月 01 日 内容 ...

  4. epoll源码实现分析[整理]

    epoll用法回顾 先简单回顾下如何使用C库封装的3个epoll相关的系统调用.更详细的用法参见http://www.cnblogs.com/apprentice89/archive/2013/05/ ...

  5. [tty与uart]3.tty驱动分析

    转自:http://www.wowotech.net/linux_kenrel/183.html 目录: 1 首先分析设备驱动的注册 1.1 uart_register_driver分析 1.2 tt ...

  6. Linux驱动修炼之道-RTC子系统框架与源码分析【转】

    转自:http://helloyesyes.iteye.com/blog/1072433 努力成为linux kernel hacker的人李万鹏原创作品,为梦而战.转载请标明出处 http://bl ...

  7. epoll分析

      Epoll详解及源码分析 1.什么是epoll epoll是当前在Linux下开发大规模并发网络程序的热门人选,epoll 在Linux2.6内核中正式引入,和select相似,都是I/O多路复用 ...

  8. SEAndroid安全机制对Binder IPC的保护分析

    在SEAndroid安全机制中,除了文件和属性,还有Binder IPC须要保护.Binder IPC是Android系统的灵魂,使用得相当广泛又频繁.比如,应用程序都是Binder IPC请求訪问系 ...

  9. Linux TCP/IP 协议栈之 Socket 的实现分析(一)

    内核版本:2.6.37参考[作者:kendo的文章(基于内涵版本2.6.12)] 第一部份 Socket套接字的创建 socket 并不是 TCP/IP协议的一部份. 从广义上来讲,socket 是U ...

随机推荐

  1. CentOS ACL

    ACL:访问控制列表(Access Control List). 一般来说权限是针对某一类用户设置的.例如:一个文件只有拥有者.组.其他用户三种设置方式,如果希望对某个指定的用户进行单独的权限控制,就 ...

  2. BZOJ1876:[SDOI2009]SuperGCD——C++高精度良心题解

    http://www.lydsy.com/JudgeOnline/problem.php?id=1876 Description Sheng bill有着惊人的心算能力,甚至能用大脑计算出两个巨大的数 ...

  3. SPOJ1825/FTOUR2:Free tour II——包看得懂/看不懂题解

    http://www.spoj.com/problems/FTOUR2/en/ 题目大意:给一棵黑白染色的树,求边权和最大且经过黑点不超过K的路径. ———————————————————— 前排膜拜 ...

  4. Mac安装mysqldb

    一. 安装mysql (一)下载地址 https://pan.baidu.com/s/1slw50LZ 安装成功后,在系统偏好设置里有MySQL图标,可以启动或关闭MySQL 二. Mysql roo ...

  5. [Leetcode] single number ii 找单个数

    Given an array of integers, every element appears three times except for one. Find that single one. ...

  6. CF1073E Segment Sum 自闭了

    CF1073E Segment Sum 题意翻译 给定\(K,L,R\),求\(L\)~\(R\)之间最多不包含超过\(K\)个数码的数的和. \(K<=10,L,R<=1e18\) 我 ...

  7. ACE线程管理机制-线程的创建与管理

    转载于:http://www.cnblogs.com/TianFang/archive/2006/12/04/581369.html 有过在不同的操作系统下用c++进行过多线程编程的朋友对那些线程处理 ...

  8. hzwer分块九题(暂时持续更新)

    hzwer分块9题 分块1:区间加法,单点查询 Code #include<bits/stdc++.h> #define in(i) (i=read()) using namespace ...

  9. udhcpd源码分析2--读取配置文件

    1:重要的结构体 读取配置文件信息到全局的结构体struct server_config_t server_config中,这个结构在很多文件中都有引用到很重要. /* dhcpd.h */ stru ...

  10. ioctrl 获取本机IP及MAC地址

    通过使用ioctl可以获得本机的一些信息,这里记录获得interface IP及MAC的过程. 1:ioctl 函数的作用是什么 man ioctl: DESCRIPTION The ioctl() ...