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,但並沒有增加任何多余引用
數據到內核中時必須很小心,假如用戶空間的數據地址是個非法的地址,或是超出用戶空間的范圍,或是
那些地址還沒有被映射到,都可能對內核產生很大的影響,如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,但並沒有增加任何多余引用