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_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月
毕业那两年在做嵌入式应用开发,主要是单片机和arm linux上的应用开发,后来又做了两年arm linux驱动开发,15年到现在在做pc端及嵌入式端开发,包括服务器系统裁剪、底层框架实现、硬件加速等。喜欢技术分享、交流!联系方式: 907882971@qq.com、rongpmcu@gmail.com