linux的copy_from_user与copy_to_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中。arm的这段代码相当复杂,网上有很多类似的概述,都是对x86架构下面的描述。

该代码设计的如此复杂的原因,在《linux内核源代码情景分析》一书中找到了相关概述:

当内核从一个进程得到从用户空间传递过来的指针时,时很难保证这个指针的合法性的,更难保证在长度为len的整个区间都是合法的。所以,为了安全起见应该先检查这个区间的合法性,看看由指针和长度两个参数所决定的虚存空间是否已经映射。每个进程都有个代表其许村空间的mm_struct结构,记录着该进程在用户空间所有已经建立映射的区间。只要搜索这个数据结构中的链表,就能发现该片虚存空间是否已经建立映射。老的linux版本确实是这样做的,但是这种检测会带来很大的负担。指针真正有问题的可能性其实是比较小的,所以新版本的linux去除了这些检测。碰上坏指针,就让页面异常发生。

具体怎么做。当碰上坏真正而页面异常真的发生时,在do_page_fault中,首先通过find_vma搜索当前进程当前的虚存链表,如果搜索失败就转入bad_area。虽然访问失败的目标地址在用户空间中,但是cpu的执行地址却在系统空间中。也就是说,如果内核能够在一个异常表中找到异常的指令所在的地址,并得到相应的修复地址fixup,就将在异常返回后将要重新执行的地址替换成这个修复地址。为什么要这么做呢?因为在这种情况下内核不能为当前进程补上一个页面,那样的话要复制的用户空间的内容就会全部为空(这就是内核态不能像用户态那样遇到缺页异常,在缺页异常处理中给该异常地址分配空间,解决缺页问题的原因)。而如果任其自然的话,则从异常返回以后,当前进程必然会接连不断的因执行同一条指令而产生新的异常,所以必须把它从泥坑里面拉出来。其相应的精心设计的修复地址fixup处的代码就是起这样的作用。

原文有x86下面异常修复地址跳转的描述,更多细节可以参考原文。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值