免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 5374 | 回复: 6
打印 上一主题 下一主题

【转】Copy_from&to_user详解 [复制链接]

论坛徽章:
3
金牛座
日期:2014-06-14 22:04:062015年辞旧岁徽章
日期:2015-03-03 16:54:152015年迎新春徽章
日期:2015-03-04 09:49:45
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2008-11-11 09:25 |只看该作者 |倒序浏览
copy_from&toza_user详解
copy_from_user函数的目的是从用户空间拷贝数据到内核空间,失败返回没有被拷贝的字节数,成功返回0.
这么简单的一个函数却含盖了许多关于内核方面的知识,比如内核关于异常出错的处理.从用户空间拷贝
数据到内核中时必须很小心,假如用户空间的数据地址是个非法的地址,或是超出用户空间的范围,或是
那些地址还没有被映射到,都可能对内核产生很大的影响,如oops,或被造成系统安全的影响.所以
copy_from_user函数的功能就不只是从用户空间拷贝数据那样简单了,他还要做一些指针检查连同处理这些
问题的方法.下面我们来仔细分析下这个函数.函数原型在[arch/i386/lib/usercopy.c]中
unsigned long
copy_from_user(void *to, const void __user *from, unsigned long n)
{
    might_sleep();   
    if (access_ok(VERIFY_READ, from, n))
        n = __copy_from_user(to, from, n);
    else
        memset(to, 0, n);
    return n;
}
首先这个函数是能够睡眠的,他调用might_sleep()来处理,他在include/linux/kernel.h中定义,
本质也就是调用schedule(),转到其他进程.接下来就要验证用户空间地址的有效性.他在
[/include/asm-i386/uaccess.h]中定义.
#define access_ok(type,addr,size) (likely(__range_ok(addr,size) == 0)),进一步调用__rang_ok
函数来处理,他所做的测试很简单,就是比较addr+size这个地址的大小是否超出了用户进程空间的大小,
也就是0xbfffffff.可能有读者会问,只做地址范围检查,怎么不做指针合法性的检查呢,假如出现前面
提到过的问题怎么办?这个会在下面的函数中处理,我们慢慢看.在做完地址范围检查后,假如成功则调用
__copy_from_user函数开始拷贝数据了,假如失败的话,就把从to指针指向的内核空间地址到to+size范围
填充为0.__copy_from_user也在uaceess.h中定义,
static inline unsigned long
__copy_from_user(void *to, const void __user *from, unsigned long n)
{
       might_sleep();
       return __copy_from_user_inatomic(to, from, n);
}
这里继续调用__copy_from_user_inatomic.
static inline unsigned long
__copy_from_user_inatomic(void *to, const void __user *from, unsigned long n)
{
    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);
}
这里先判断要拷贝的字节大小,假如是8,16,32大小的话,则调用__get_user_size来拷贝数据.
这样做是一种程式设计上的优化了。
#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","","=r",errret);break;    \
    default: (x) = __get_user_bad();                \
    }                                \
} while (0)
#define __get_user_asm(x, addr, err, itype, rtype, ltype, errret)    \
    __asm__ __volatile__(                        \
        "1:    mov"itype" %2,%"rtype"1\n"            \
        "2:\n"                            \
        ".section .fixup,\"ax\"\n"                \
        "3:    movl %3,%0\n"                    \
        "    xor"itype" %"rtype"1,%"rtype"1\n"        \
        "    jmp 2b\n"                    \
        ".previous\n"                        \
        ".section __ex_table,\"a\"\n"                \
        "    .align 4\n"                    \
        "    .long 1b,3b\n"                    \
        ".previous"                        \
        : "=r"(err), ltype (x)                    \
        : "m"(__m(addr)), "i"(errret), "0"(err))
实际上在完成一些宏的转换后,也就是利用movb,movw,movl指令传输数据了,对于
内嵌汇编中的.section .fixup, .section __ex_table,我们呆会要仔细讲。
假如不是那些特别大小时,则调用__copy_from_user_ll处理。
unsigned long
__copy_from_user_ll(void *to, const void __user *from, unsigned long n)
{
    if (movsl_is_ok(to, from, n))
        __copy_user_zeroing(to, from, n);
    else
        n = __copy_user_zeroing_intel(to, from, n);
    return n;
}
直接调用__copy_user_zeroing开始真正的拷贝数据了,绕了那么多弯,总算快看到
出路了。copy_from_user函数的精华部分也就都在这了。
#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"                        \
        ".section __ex_table,\"a\"\n"                \
        "    .align 4\n"                    \
        "    .long 4b,5b\n"                    \
        "    .long 0b,3b\n"                    \
        "    .long 1b,6b\n"                    \
        ".previous"                        \
        : "=&c"(size), "=&D" (__d0), "=&S" (__d1), "=r"(__d2)    \
        : "3"(size), "0"(size), "1"(to), "2"(from)        \
        : "memory");                        \
} while (0)
这个函数的前一部分比较简单,也就是拷贝数据.关于后一部分就会涉及到我们前面
提到过的那些情况了,假如用户空间的地址没被映射怎么办呢?在一些老的内核版本
中是用verify_area()来验证地址地址合法性的,比如在早期的linux 0.11内核.
[linux0.11/kenrel/fork.c]
// 进程空间写前验证函数。在现代CPU中,其控制寄存器CR0有个写保护标志位(wp:16),内核能够通过配置
// 该位来禁止特权级0的代码向用户空间只读页面执行写数据,否则将导致写保护异常。
// addr为内存物理地址
void verify_area(void * addr,int size)
{
        unsigned long start;
        start = (unsigned long) addr;
        size += start & 0xfff;  // start & 0xfff为起始地址addr在页面中的偏移,2^12=4096
        start &= 0xfffff000;    // start为页开始地址,即页面边界值。此时start为当前进程空间中的逻辑地址
        start += get_base(current->ldt[2]); // get_base(current->ldt[2])为进程数据段在线性地址空间中的开始地址,在加上start,变为系统这个线性空间中的地址
              页边界         addr ----size-----     页边界
        +--------------------------------------------------------+
        |  ...   | start&0xfff |               |       |  ...    |
        +--------------------------------------------------------+
                 |           start             |
               start-----------size-------------
         while (size>0) {
                size -= 4096;
                write_verify(start);    // 以页为单位,进行写保护验证,假如页为只读,则将其变为可写
                start += 4096;
        }
}
[linux0.11/mm/memory.c]
// 验证线性地址是否可写
void write_verify(unsigned long address)
{
        unsigned long page;
        // 假如对应页表为空的话,直接返回
        if (!( (page = *((unsigned long *) ((address>>20) & 0xffc)) )&1))
                return;
        page &= 0xfffff000;
        page += ((address>>10) & 0xffc);
        // 经过运算后page为页表项的内容,指向实际的一页物理地址
        if ((3 & *(unsigned long *) page) == 1)  // 验证页面是否可写,不可写则执行un_wp_page,取消写保护.
                un_wp_page((unsigned long *) page);
        return;
}
但是假如每次在用户空间复制数据时,都要做这种检查是很浪费时间的,毕竟坏指针是很少
存在的,在新内核中的做法是,在从用户空间复制数据时,取消验证指针合法性的检查,
只多地址范围的检查,就象access_ok()所做的那样,一但碰上了坏指针,就要页异常出错处理
程式去处理他了.我们去看看do_page_fault函数.
[arch/asm-i386/mm/fault.c/do_page_falut()]
fastcall void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
...
...
    if (!down_read_trylock(&mm->mmap_sem)) {
        if ((error_code & 4) == 0 &&
            !search_exception_tables(regs->eip))
            goto bad_area_nosemaphore;
        down_read(&mm->mmap_sem);
    }
...
...
bad_area_nosemaphore:
...
no_context:
   
    if (fixup_exception(regs))
        return;
...
...
}
error_code保存的是出错码,(error_code & 4) == 0代表产生异常的原因是在内核中.
他调用fixup_exception(regs)来处理这个问题.既然出错了,那么怎样来修复他呢?
先看下fixup_exception()函数的实现:
[arch/asm-i386/mm/extable.c]
int fixup_exception(struct pt_regs *regs)
{
    const struct exception_table_entry *fixup;
...
    fixup = search_exception_tables(regs->eip);
    if (fixup) {
        regs->eip = fixup->fixup;
        return 1;
    }
...
}
[kernel/extable.c]
const struct exception_table_entry *search_exception_tables(unsigned long addr)
{
    const struct exception_table_entry *e;
    e = search_extable(__start___ex_table, __stop___ex_table-1, addr);
    if (!e)
        e = search_module_extables(addr);
    return e;
}
[/lib/extable.c]
const struct exception_table_entry *
search_extable(const struct exception_table_entry *first,
           const struct exception_table_entry *last,
           unsigned long value)
{
    while (first insn insn > value)
            last = mid - 1;
        else
            return mid;
        }
        return NULL;
}
在内核中有个异常出错地址表,在地址表中有个出错地址的修复地址也气对应,他结构如下:
[/include/asm-i386/uaccess.h]
struct exception_table_entry
{
    unsigned long insn, fixup;
};
insn是产生异常指令的地址,fixup用来修复出错地址的地址,也就是当异常发生后,用他的
地址来替换异常指令发生的地址。__copy_user_zeroing中的.section __ex_table代表异常出错
地址表的地址,.section .fixup代表修复的地址。他们都是elf文档格式中的2个特别节。
        ".section __ex_table,\"a\"\n"                \
        "    .align 4\n"                    \
        "    .long 4b,5b\n"                    \
        "    .long 0b,3b\n"                    \
        "    .long 1b,6b\n"   
4b,5b的意思是当出错地址在4b标号对应的地址上时,就转入5b标号对应的地址去接着运行,
也就是修复的地址。依次类推。所以理解这一点后,fixup_exception()函数就很容易看明白了
就是根据出错地址搜索异常地址表,找到对应的修复地址,跳转到那里去执行就ok了。
ok,到这里copy_from_user函数也就分析完了,假如有什么不明白的话,能够通过阅读
/usr/src/linux/Documentation/exception.txt来得到更多关于异常处理方面的知识。

copy_from&to_user.pdf

83.52 KB, 下载次数: 290

论坛徽章:
3
金牛座
日期:2014-06-14 22:04:062015年辞旧岁徽章
日期:2015-03-03 16:54:152015年迎新春徽章
日期:2015-03-04 09:49:45
2 [报告]
发表于 2008-11-11 09:26 |只看该作者
喜欢看pdf或收藏的朋友,可以从附件得到文章的pdf。

论坛徽章:
5
摩羯座
日期:2014-07-22 09:03:552015元宵节徽章
日期:2015-03-06 15:50:392015亚冠之大阪钢巴
日期:2015-06-12 16:01:352015年中国系统架构师大会
日期:2015-06-29 16:11:2815-16赛季CBA联赛之四川
日期:2018-12-17 14:10:21
3 [报告]
发表于 2008-11-11 10:38 |只看该作者
好东西啊,以前因为对这个不是很熟,还啥呵呵的直接传buffer进驱动里呢,当时没有使用copy_from_user,我还纳闷是什么问题呢,后来才想起来kernel space和user space的问题,呵呵

[ 本帖最后由 T-bagwell 于 2008-11-11 10:42 编辑 ]

论坛徽章:
0
4 [报告]
发表于 2008-11-11 10:53 |只看该作者
正好没弄清楚
多谢楼主

论坛徽章:
3
金牛座
日期:2014-06-14 22:04:062015年辞旧岁徽章
日期:2015-03-03 16:54:152015年迎新春徽章
日期:2015-03-04 09:49:45
5 [报告]
发表于 2008-11-11 11:11 |只看该作者
原帖由 T-bagwell 于 2008-11-11 10:38 发表
好东西啊,以前因为对这个不是很熟,还啥呵呵的直接传buffer进驱动里呢,当时没有使用copy_from_user,我还纳闷是什么问题呢,后来才想起来kernel space和user space的问题,呵呵



我在细读ldd3时发现这个没太清楚,就查了一下资料,跟了一下代码,贴出来跟大家分享一下。希望都能弄明白

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
6 [报告]
发表于 2008-11-11 16:49 |只看该作者
原帖由 dreamice 于 2008-11-11 11:11 发表



我在细读ldd3时发现这个没太清楚,就查了一下资料,跟了一下代码,贴出来跟大家分享一下。希望都能弄明白


对内核理解的多一些之后,再看这个感觉清晰多了

论坛徽章:
3
金牛座
日期:2014-06-14 22:04:062015年辞旧岁徽章
日期:2015-03-03 16:54:152015年迎新春徽章
日期:2015-03-04 09:49:45
7 [报告]
发表于 2008-11-11 17:19 |只看该作者
原帖由 Godbach 于 2008-11-11 16:49 发表


对内核理解的多一些之后,再看这个感觉清晰多了


是的,这两个函数,我不知道看了多少遍了,这次才算领悟到了一半吧,等过段时间再来看看。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP