天天看點

COPY_FROM_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" \

      ...

      : "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);

}

...

   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_user的使用,有一個前提:

1) 目前程序必須未鎖定from所在的page,

或者,

2)from所在的page已經up_to_data,并且page -> count多餘一個引用。

否則,如果from所在的page不在影射中,則缺頁異常處理程式會搜尋/新增這個page,在page未up_to_data時,要求鎖定這個page,然後送出IO讀page。

如 ( 目前程序已鎖定本page ) && (page未up_to_data)成立,則死鎖。

那麼,在generic_file_write中,因to所在的page必須被目前程序鎖定,則當(from所在page == to所在page)時,隻能用第二種保證辦法。

kernel好象并沒有這樣做,而隻是在鎖定to所在page之前,另from所在page為up_to_data,但并沒有增加任何多餘引用

繼續閱讀