Copy_from&to_user详解

 

http://www.chinaunix.net 作者:dreamice 发表于:2008-11-11 17:19:10

【发表评论】【查看原文】【Linux讨论区】【关闭】 

 

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

 

 

 

--------------------------------------------------------------------------------

 

http://www.chinaunix.net 作者:dreamice 发表于:2008-11-11 17:19:10

【发表评论】【查看原文】【Linux讨论区】【关闭】 

 

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

 

原文链接:http://linux.chinaunix.net/bbs/viewthread.php?tid=1044255

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值