前言
copy_from_user
函數的目的是從用戶空間拷貝數據到內核空間,失敗返回沒有被拷貝的字節數,成功返回0。它內部的實現當然不僅僅拷貝數據,還需要考慮到傳入的用戶空間地址是否有效,比如地址是不是超出用戶空間范圍啊,地址是不是沒有對應的物理頁面啊,否則內核就會oops的。不同的架構,該函數的實現不一樣。下面主要以arm和x86為例進行說明(分析過程會忽略一些無關的代碼)。
arm copy_from_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
中,arch/arm/lib/copy_template.S
里面的具體邏輯會因為arch/arm/lib/copy_from_user.S
之前所定義的宏而不同。這里就不再跟進去分析了,異常表的處理我打算通過分析x86實現的時候來完成。
x86 copy_from_user
x86架構下,copy_from_user
相關的文件主要有arch/x86/include/asm/uaccess.h arch/x86/lib/usercopy_32.S arch/x86/include/asm/uaccess_32.h arch/x86/include/asm/uaccess_64.h
。下面先來看copy_from_user
,它的實現在arch/x86/include/asm/uaccess.h
中:
static inline unsigned long __must_check
copy_from_user(void *to, const void __user *from, unsigned long n)
{
int sz = __compiletime_object_size(to);
might_fault();
/*
* While we would like to have the compiler do the checking for us
* even in the non-constant size case, any false positives there are
* a problem (especially when DEBUG_STRICT_USER_COPY_CHECKS, but even
* without - the [hopefully] dangerous looking nature of the warning
* would make people go look at the respecitive call sites over and
* over again just to find that there's no problem).
*
* And there are cases where it's just not realistic for the compiler
* to prove the count to be in range. For example when multiple call
* sites of a helper function - perhaps in different source files -
* all doing proper range checking, yet the helper function not doing
* so again.
*
* Therefore limit the compile time checking to the constant size
* case, and do only runtime checking for non-constant sizes.
*/
if (likely(sz < 0 || sz >= n))
n = _copy_from_user(to, from, n);
else if(__builtin_constant_p(n))
copy_from_user_overflow();
else
__copy_from_user_overflow(sz, n);
return n;
}
GCC的內建函數__builtin_constant_p
用於判斷一個值是否為編譯時常數,如果參數值是常數,函數返回 1,否則返回 0。copy_from_user
核心的實現在_copy_from_user
中:
unsigned long _copy_from_user(void *to, const void __user *from, unsigned n)
{
if (access_ok(VERIFY_READ, from, n))
n = __copy_from_user(to, from, n);
else
memset(to, 0, n);
return n;
}
其中,access_ok
相關代碼(代碼比較簡單,不再分析):
static inline bool __chk_range_not_ok(unsigned long addr, unsigned long size, unsigned long limit)
{
/*
* If we have used "sizeof()" for the size,
* we know it won't overflow the limit (but
* it might overflow the 'addr', so it's
* important to subtract the size from the
* limit, not add it to the address).
*/
if (__builtin_constant_p(size))
return addr > limit - size;
/* Arbitrary sizes? Be careful about overflow */
addr += size;
if (addr < size)
return true;
return addr > limit;
}
#define __range_not_ok(addr, size, limit) \
({ \
__chk_user_ptr(addr); \
__chk_range_not_ok((unsigned long __force)(addr), size, limit); \
})
#define access_ok(type, addr, size) \
likely(!__range_not_ok(addr, size, user_addr_max()))
下面看__copy_from_user
相關的代碼實現(以32位系統為例),注釋直接添加到代碼中:
static __always_inline unsigned long
__copy_from_user(void *to, const void __user *from, unsigned long n)
{
might_fault();
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);//如果不能識別n是一個常量,就調用
}
先看__get_user_size
實現,__chk_user_ptr
之前已經說過,不再重復。主要看__get_user_asm
:
#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", "k", "=r", errret); \
break; \
case 8: \
__get_user_asm_u64(x, ptr, retval, errret); \
break; \
default: \
(x) = __get_user_bad(); \
} \
} while (0)
__get_user_size
根據要copy的size傳入不同的參數,最終會使用movb或者movw或者movl來實現1、2\4字節的拷貝。主要需要注意的就是.section .fixup
和_ASM_EXTABLE
,.section .fixup
指定了.fixup section
,且該段為可重定位的代碼段,_ASM_EXTABLE
定義了__ex_table
段,且該段為可重定位的數據段,實際上它指定了3b處異常時的跳轉地址,即3b,3b剛好就是.fixup段處。
#define __get_user_asm(x, addr, err, itype, rtype, ltype, errret) \
asm volatile(ASM_STAC "\n" \
"1: mov"itype" %2,%"rtype"1\n" \
"2: " ASM_CLAC "\n" \
".section .fixup,\"ax\"\n" \
"3: mov %3,%0\n" \
" xor"itype" %"rtype"1,%"rtype"1\n" \
" jmp 2b\n" \
".previous\n" \
_ASM_EXTABLE(1b, 3b) \
: "=r" (err), ltype(x) \
: "m" (__m(addr)), "i" (errret), "0" (err))
分析完1、2、4字節的拷貝后,繼續看非1、2、4字節的拷貝實現,現在繼續看__copy_from_user_ll
:
unsigned long __copy_from_user_ll(void *to, const void __user *from,
unsigned long n)
{
stac();
if (movsl_is_ok(to, from, n))
__copy_user_zeroing(to, from, n);
else
n = __copy_user_zeroing_intel(to, from, n);
clac();
return n;
}
先通過movsl_is_ok
判斷下,然后分別調用__copy_user_zeroing
或者__copy_user_zeroing_intel
。movsl_is_ok
的實現:
static inline int __movsl_is_ok(unsigned long a1, unsigned long a2, unsigned long n)
{
#ifdef CONFIG_X86_INTEL_USERCOPY
if (n >= 64 && ((a1 ^ a2) & movsl_mask.mask))
return 0;
#endif
return 1;
}
#define movsl_is_ok(a1, a2, n) \
__movsl_is_ok((unsigned long)(a1), (unsigned long)(a2), (n))
從這里可以知道,只有配置了CONFIG_X86_INTEL_USERCOPY
,才有可能返回0,不然一般多事返回1。我們不考慮定義CONFIG_X86_INTEL_USERCOPY
的情況,也就是該函數返回1時,繼續轉入到__copy_user_zeroing
的調用,代碼實現如下:
#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" \
_ASM_EXTABLE(4b,5b) \
_ASM_EXTABLE(0b,3b) \
_ASM_EXTABLE(1b,6b) \
: "=&c"(size), "=&D" (__d0), "=&S" (__d1), "=r"(__d2) \
: "3"(size), "0"(size), "1"(to), "2"(from) \
: "memory"); \
} while (0)
同樣是匯編實現,.section .fixup
和_ASM_EXTABLE
部分前面已經說了,而指令部分就是我們通常的數據拷貝,因此也就不再分析了。
這里摘抄下網上的一段敘述,同時他對__copy_user_zeroing
的指令部分有詳細的注釋,大家可以看看:
在cpu進行訪址的時候,內核空間和用戶空間使用的都是線性地址,cpu在訪址的過程中會自動完成從線性地址到物理地址的轉換[用戶態、內核態都得依靠進程頁表完成轉換],而合理的線性地址意味着:該線性地址位於該進程
task_struct->mm
虛存空間的某一段vm_struct_mm
中,而且建立線性地址到物理地址的映射,即線性地址對應內容在物理內存中。如果訪存失敗,有兩種可能:該線性地址存在在進程虛存區間中,但是並未建立於物理內存的映射,有可能是交換出去,也有可能是剛申請到線性區間[內核是很會偷懶的],要依靠缺頁異常去建立申請物理空間並建立映射;第2種可能是線性地址空間根本沒有在進程虛存區間中,這樣就會出現常見的壞指針,就會引發常見的段錯誤[也有可能由於訪問了無權訪問的空間造成保護異常]。如果壞指針問題發生在用戶態,最嚴重的就是殺死進程[最常見的就是在打dota時候出現的大紅X,然后dota程序結束],如果發生在內核態,整個系統可能崩潰[xp的藍屏很可能就是這種原因形成的]。所以linux當然不會任由這種情況的發生,其措施如下:
linux內核對於可能發生問題的指令都會准備"修復地址",比如前面的fixup部分,而且遵循誰使用這些指令,誰負責修復工作的原則。比如前面的代碼中,標號5即為標號4的修復指令,3為0,6為1的修復指令。在編譯過程中,編譯器會將5,4等的地址對應的存入struct exception_table_entry{unsigned long insn,fixup;}
中。insn即可能為4的地址,而fixup可能為5的地址,如果4為壞地址[即該地址並未在虛存區間中],則在頁面異常處理過程中,會轉入bad_area
處,如果發生在用戶態直接殺死進程即可。如果發生在內核態,首先通過search_exception_table
查找異常處理表exception_table
。即找到某一個exception_table_entry
,假設其insn=標號4地址,fixup=標號5地址.內核將發生:
regs->ip=fixup,即通過修改當前的內核地址,從而將內核從死亡的邊緣拉回來,通過標號5地址處的修復工作從而全身而退。
總結
主要分析了copy_from_user
接口的內部實現,copy_to_user
實現類似,不再重復分析。總的來說,copy_from_user
完成了數據的拷貝的同時,處理了可能發生了地址訪問異常。理論上,內核空間可以直接使用用戶空間傳過來的指針,即使要做數據拷貝的動作,也可以直接使用memcpy,事實上,在沒有MMU的體系架構上,copy_form_user
最終的實現就是利用了memcpy。但對於大多數有MMU的平台,情況就有了一些變化:用戶空間傳過來的指針是在虛擬地址空間上的,它指向的虛擬地址空間很可能還沒有真正映射到實際的物理頁面上。用戶空間的缺頁導致的異常會透明的被內核予以修復(為缺頁的地址空間提交新的物理頁面),訪問到缺頁的指令會繼續運行仿佛什么都沒有發生一樣。內核空間必須被顯示的修復,這是由內核提供的缺頁異常處理函數的設計模式決定的(其背后的思想后:在內核態中,如果程序試圖訪問一個尚未提交物理頁面的用戶空間地址,內核必須對此保持警惕而不能像用戶空間那樣毫無察覺。如果內核訪問一個尚未被提交物理頁面的空間,將產生缺頁異常,這個時候內核會調用do_page_fault
,因為異常發生在內核空間,do_page_fault
的處理邏輯將調用search_exception_tables
在__ex_table
中查找異常指令的修復指令),正因為這樣,copy_from_user
的實現才會看起來有些復雜,當然性能方面提升也是它的復雜度提升的一個原因。
完!
2015年7月