copy_from_user分析


前言

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_intelmovsl_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月


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM