百分百源码网-让建站变得如此简单! 登录 注册 签到领金币!

主页 | 如何升级VIP | TAG标签

当前位置: 主页>网站教程>服务器> 深入理解Linux中的copy_{to,from}_user()(附代码)-linux运维
分享文章到:

深入理解Linux中的copy_{to,from}_user()(附代码)-linux运维

发布时间:09/01 来源:未知 浏览: 关键词:
我们对“copy_{to,from}_user()”接口的运用应当是再熟知不外。根本Linux书籍都会介绍它的作用。究竟它是“kernelspace和userspace”沟通的桥梁。下面就让我们来理解一下吧。

深入理解Linux中的copy_{to, from}_user()(附代码)

引言

我们对copy_{to,from}_user()接口的运用应当是再熟知不外吧。根本Linux书籍都会介绍它的作用。究竟它是kernel space和user space沟通的桥梁。所有的数据交互都应当运用相似这种接口。所以,我们没有理由不晓得接口的作用。但是,我也曾经有过下列疑难。

  1. 为何需要copy_{to,from}_user(),它毕竟在背后为我们做了什么?

  2. copy_{to,from}_user()和memcpy()的区别有哪些,直接运用memcpy()可以吗?

  3. memcpy()替换copy_{to,from}_user()是不是一定会有题目?

一下子寻回了那年困惑的本人。我所提出的每个题目,曾经我也思索过。还不止一次的思索,每一次都有不一样的设法。固然是由于从一开端就我就没有完全了解。此刻又从新回到这个繁重的话题,继续思索这曾经的题目。

百家争鸣

针对以上题目固然是先baidu。baidu关于该题目的博客也是许多,脚以看出这个题目确定困惑着一大批Linux的喜爱者。关于我的查阅效果来说,观念主要分成下列两种:

1、copy_{to,from}_user()比memcpy()多了传入地址合法性校验。

例如可否属于会员空间地址范畴。理论上说,内核空间可以直接运用会员空间传过来的指针,即便要做数据拷贝的行动,也可以直接运用memcpy(),事实上在没有MMU的体系架构上,copy_{to,from}_user()终究的实现就是应用了mencpy()。

但是关于大多数有MMU的平台,状况就有了些变化:会员空间传过来的指针是在虚拟地址空间上的,它所指向的虚拟地址空间很可能尚无真正映照到现实的物理页面上。但是这又能如何呢?缺页致使的异样会很透亮地被内核予以修复(为缺页的地址空间提交新的物理页面),拜访到缺页的指令会继续运转宛然什么都没有产生同样。但这只是会员空间缺页异样的行为,在内核空间这种缺页异样必需被显式地修复,这是由内核供给的缺页异样处置函数的设计模式决议的。

其背后的思想是:在内核态,要是程序试图拜访一个尚未被提交物理页面的会员空间地址,内核必需对此维持警觉而不克不及像会员空间那样毫无察觉。

2、要是我们确保会员态通报的指针的准确性,我们完全可以用memcpy()函数替换copy_{to,from}_user()。经过一些试验测试,发明运用memcpy(),程序的运转上并没有题目。因而在确保会员态指针平安的状况下,二者可以替代。

从各家博客上,观念主要集中在首先点。看起来首先点挨到大家的宽泛认可。但是,重视实践的人又得出了第二种观念,究竟是实践出真知。真谛毕竟是是把握在少数人手里呢?还是大众的眼睛是雪亮的呢?固然,我不否认以上任何一种观念。也不克不及向你保障哪种观念准确。由于,我信赖即便是曾经自圆其说的理论,随着工夫的推移或者特定状况的转变理论也可能不再准确。比方,牛顿的经典力学理论(宛如扯得有点远)。要是要我说人话,就是:随着工夫的推移,Linux的代码在一直的变化。也许以上的观念在曾经准确。固然,也可能此刻还准确。下面的剖析就是我的观念了。一样,大家也是需要维持嫌疑的态度。下面我就抛砖引玉。

抛砖引玉

第一我们看下memcpy()和copy_{to,from}_user()的函数定义。参数险些没有差别,都包括目的地址,源地址和需要复制的字节size。

static __always_inline unsigned long __must_check	
copy_to_user(void __user *to, const void *from, unsigned long n);	
static __always_inline unsigned long __must_check	
copy_from_user(void *to, const void __user *from, unsigned long n);	
void *memcpy(void *dest, const void *src, size_t len);

但是,有一点我们确定是晓得的。那就是memcpy()没有传入地址合法性校验。而copy_{to,from}_user()针对传入地址进行相似下面的合法性校验(简略说点,更多校验详情可以参照 代码)。

  1. 要是从会员空间copy数据到内核空间,会员空间地址to及to加上copy的字节长度n必需位于会员空间地址空间。

  2. 要是从内核空间copy数据到会员空间,固然也需要检查地址的合法性。例如,可否越界拜访或者是不是代码段的数据等等。总之一切分歧法地操纵都需要立即杜绝。

经过简略的对照之后,我们再看看其他的悬殊以及一起探究下上面提出的2个观念。我们先从第2个观念提及。波及实践,我还是有点信赖实践出真知。从我测试的效果来说,实现效果分成两种状况。

首先种状况的效果是:运用memcpy()测试,没有涌现题目,代码正常运转。测试代码如下(仅仅展现proc文件系统下file_operations对应的read接口函数):

static ssize_t test_read(struct file *file, char __user *buf,	
                         size_t len, loff_t *offset)	
{	
        memcpy(buf, "test\n", 5);    /* copy_to_user(buf, "test\n", 5) */	
        return 5;	
}

我们运用cat下令读取文件内容,cat会通过系统调取read调取test_read,而且通报的buf大小是4k。

测试很顺利,效果很喜人。成功地读到了“test”字符串。看起来,第2点观念是没弊端的。但是,我们还需要继续验证和探讨下去。由于第1个观念提到,“在内核空间这种缺页异样必需被显式地修复”。

因而我们还需要验证的状况是:要是buf在会员空间已经分配虚拟地址空间,但是并没有创立和物理内存的具体映照关系,这种状况下会涌现内核态page fault。我们第一需要新建这种前提,寻到相符的buf,然后测试。这里我固然没测啦。由于有测试结论(主如果由于我懒,结构这个前提我觉得比拼费事)。

这个测试是我的一个伴侣,人称宋老师的“阿助教”阿克曼大牛。他曾经做个这个实验,而且得到的结论是:即便是没有创立和物理内存的具体映照关系的buf,代码也可以正常运转。在内核态产生page fault,并被其修复(分配具体物理内存,添补页表,创立映照关系)。同时,我从代码的角度剖析,结论也是如此。

经过上面的剖析,看起来宛如是memcpy()也可以正常运用,鉴于平安地考虑倡议运用copy_{to,from}_user()等接口。

第二种状况的效果是:以上的测试代码并没有正常运转,而且会触发kernel oops。固然本次测试和上次测试的kernel配置选项是不同的。这个配置项是 CONFIG_ARM64_SW_TTBR0_PAN或者 CONFIG_ARM64_PAN(针对ARM64平台)。两个配置选项的功能都是阻止内核态直接拜访会员地址空间。只不外CONFIG_ARM64_SW_TTBR0_PAN是软件仿真实现这种功能,而CONFIG_ARM64_PAN是硬件实现功能(ARMv8.1扩展功能)。我们以CONFIG_ARM64_SW_TTBR0_PAN作为剖析对象(软件仿真才有代码供给剖析)。BTW,要是硬件不支撑,即便配置CONFIG_ARM64_PAN也没用,只能运用软件仿真的办法。要是需要拜访会员空间地址需要通过相似copy_{to,from}_user()的接口,不然会致使kernel oops。

在打开CONFIG_ARM64_SW_TTBR0_PAN的选项后,测试以上代码就会致使kernel oops。缘由就是内核态直接拜访了会员空间地址。因而,在这种状况我们就不成以运用memcpy()。我们别无选中,只能运用copy_{to,from}_user()。

为何我们需要PAN(Privileged Access Never)功能呢?缘由可能是会员空间和内核空间数据交互上容易引入平安题目,所以我们就不让内核空间轻易拜访会员空间,要是非要这么做,就必需通过特定的接口关闭PAN。另一方面,PAN功能可以更加标准化内核态和会员态数据交互的接口运用。在使能PAN功能的状况下,可以迫使内核或者驱动开发者运用copy_{to,from}_user()等平安接口,提拔系统的平安性。相似memcpy()非标准操纵,kernel就oops给你看。

因为编程的不标准而引入平安破绽。例如:Linux内核破绽CVE-2017-5123可以提拔权限。该破绽的引入缘由就是是短少access_ok()检查会员通报地址的合法性。因而,为了不本人编写的代码引入平安题目,针对内核空间和会员空间数据交互上,我们要分外小心。

刨根问底

既然提到了CONFIG_ARM64_SW_TTBR0_PAN的配置选项。固然我也但愿理解其背后设计的道理。因为ARM64的硬件特别设计,我们运用两个页表基地址存放器ttbr0_el1和ttbr1_el1。处置器依据64 bit地址的高16 bit推断拜访的地址属于会员空间还是内核空间。要是是会员空间地址则运用ttbr0_el1,反之运用ttbr1_el1。因而,ARM64进程切换的时候,只需要转变ttbr0_el1的值即可。ttbr1_el1可以选中不需要转变,由于所有的进程同享雷同的内核空间地址。

当进程切换到内核态(中止,异样,系统调取等)后,怎样才干以免内核态拜访会员态地址空间呢?其实不难想出,转变ttbr0_el1的值即可,指向一段不法的映照即可。因而,我们为此预备了一份特别的页表,该页表大小4k内存,其值满是0。当进程切换到内核态后,修改ttbr0_el1的值为该页表的地址即可保障拜访会员空间地址是不法拜访。由于页表的值是不法的。这个特别的页表内存通过链接足天职配。

#define RESERVED_TTBR0_SIZE    (PAGE_SIZE)	
SECTIONS	
{	
        reserved_ttbr0 = .;	
        . += RESERVED_TTBR0_SIZE;	
        swapper_pg_dir = .;	
        . += SWAPPER_DIR_SIZE;	
        swapper_pg_end = .;	
}

这个特别的页表和内核页表在一起。和swapper_pg_dir仅仅差4k大小。reserved_ttbr0地址开端的4k内存空间的内容会被清零。

当我们进入内核态后会通过__uaccess_ttbr0_disable切换ttbr0_el1以关闭会员空间地址拜访,在需要拜访的时候通过_uaccess_ttbr0_enable打开会员空间地址拜访。这两个宏定义也不复杂,就以_uaccess_ttbr0_disable为例注明道理。其定义如下:

.macro    __uaccess_ttbr0_disable, tmp1	
    mrs    \tmp1, ttbr1_el1                        // swapper_pg_dir (1)	
    bic    \tmp1, \tmp1, #TTBR_ASID_MASK	
    sub    \tmp1, \tmp1, #RESERVED_TTBR0_SIZE      // reserved_ttbr0 just before	
                                                // swapper_pg_dir (2)	
    msr    ttbr0_el1, \tmp1                        // set reserved TTBR0_EL1 (3)	
    isb	
    add    \tmp1, \tmp1, #RESERVED_TTBR0_SIZE	
    msr    ttbr1_el1, \tmp1                       // set reserved ASID	
    isb	
.endm
  1. ttbr1_el1存储的是内核页表基地址,因而其值就是swapper_pg_dir。

  2. swapper_pg_dir减去RESERVED_TTBR0_SIZE就是上面描述的特别页表。

  3. 将ttbr0_el1修改指向这个特别的页表基地址,固然可以保障后续拜访会员地址都是不法的。

__uaccess_ttbr0_disable对应的C说话实现可以参照 这里。

怎样允许内核态拜访会员空间地址呢?也很简略,就是__uaccess_ttbr0_disable的反操纵,给ttbr0_el1给予合法的页表基地址。这里就无须反复了。

我们此刻需要晓得的事实就是,在配置CONFIG_ARM64_SW_TTBR0_PAN的状况下,copy_{to,from}_user()接口会在copy以前允许内核态拜访会员空间,并在copy完毕之后关闭内核态拜访会员空间的能力。因而,运用copy_{to,from}_user()才是正统做法。主要表现在平安性检查及平安拜访处置。这里是其比memcpy()多的首先个特性,背面还会介绍另一个重要特性。

此刻我们可以解答上一节中遗留的题目。如何才干继续运用memcpy()?此刻就很简略了,在memcpy()调取以前通过uaccess_enable_not_uao()允许内核态拜访会员空间地址,调取memcpy(),最后通过uaccess_disable_not_uao()关闭内核态拜访会员空间的能力。

有备无患

以上的测试用例都是创立在会员空间通报合法地址的根基上测试的,何为合法的会员空间地址?

会员空间通过系统调取申请的虚拟地址空间包括的地址范畴,便是合法的地址(岂论可否分配物理页面创立映照关系)。既然要写一个接口程序,固然也要考虑程序的强健性,我们不克不及假如所有的会员通报的参数都是合法的。我们应当预判不法传参状况的产生,并提早做好预备,这就是有备无患。

我们第一运用memcpy()的测试用例,随机通报一个不法的地址。经过测试发明:会触发kernel oops。继续运用copy_{to,from}_user()替换memcpy()测试。

测试发明:read()仅仅是返回差错,但不会触发kernel oops。这才是我们想要的效果。究竟,一个利用程序不该该触发kernel oops。这种机制的实现道理有哪些呢?

我们以copy_to_user()为例剖析。函数调取流程是:

copy_to_user()->_copy_to_user()->raw_copy_to_user()->__arch_copy_to_user()

_arch_copy_to_user()在ARM64平台是汇编代码实现,这局部代码很关键。

end    .req    x5	
ENTRY(__arch_copy_to_user)	
        uaccess_enable_not_uao x3, x4, x5	
        add    end, x0, x2	
#include "copy_template.S"	
        uaccess_disable_not_uao x3, x4	
        mov    x0, #0	
        ret	
ENDPROC(__arch_copy_to_user)	
        .section .fixup,"ax"	
        .align    2	
9998:    sub x0, end, dst            // bytes not copied	
        ret	
        .previous
  1. uaccess_enable_not_uao和uaccess_disable_not_uao是上面说到的内核态拜访会员空间的开关。

  2. copy_template.S文件是汇编实现的memcpy()的功能,稍后看看memcpy()的实现代码就分明了。

  3. .section.fixup,“ax”定义一个section,名为“.fixup”,权限是ax(‘a’可重定位的段,‘x’可施行段)。 9998标号处的指令就是“有备无患”的善后处置工作。还记得copy_{to,from}_user()返回值的意义吗?返回0代表copy成功,不然返回剩余没有copy的字节数。这行代码就是盘算剩余没有copy的字节数。当我们拜访不法的会员空间地址的时候,就一定会触发page fault。这种状况下,内核态产生的page fault并返回的时候并没有修复异样,所以确定不克不及返回产生异样的地址继续运转。所以,系统可以有2个选中:第1个选中是kernel oops,并给目前进程发送SIGSEGV信号;第2个选中是不返回涌现异样的地址运转,而是选中一个已经修复的地址返回。要是运用的是memcpy()就只要第1个选中。但是copy_{to,from}_user()可以有第2个选中。 .fixup段就是为了实现这个修复功能。当copy历程中涌现拜访不法会员空间地址的时候,do_page_fault()返回的地址酿成 9998标号处,此时可以盘算剩余未copy的字节长度,程序还可以继续施行。

对照前面剖析的效果,其实_arch_copy_to_user()可以近似等效如下关系。

uaccess_enable_not_uao();	
memcpy(ubuf, kbuf, size);      ==     __arch_copy_to_user(ubuf, kbuf, size);	
uaccess_disable_not_uao();

先插播一条新闻,解释copy_template.S为什么是memcpy()。memcpy()在ARM64平台是由汇编代码实现。其定义在arch/arm64/lib/memcpy.S文件。

.weak memcpy	
ENTRY(__memcpy)	
ENTRY(memcpy)	
#include "copy_template.S"	
        ret	
ENDPIPROC(memcpy)	
ENDPROC(__memcpy)

所以很显明,memcpy()和__memcpy()函数定义是同样的。而且memcpy()函数声明是weak,因而可以重写memcpy()函数(扯得有点远)。再扯一点,为什么运用汇编呢?为什么不运用lib/string.c文件的memcpy()函数呢?固然是为了优化memcpy() 的施行速度。lib/string.c文件的memcpy()函数是按照字节为单位进行copy(再好的硬件也会被粗糙的代码毁掉)。

但是此刻的处置器根本都是32或者64位,完全可以4 bytes或者8 bytes甚至16 bytes copy(考虑地址对齐的状况下)。可以显明提拔施行速度。所以,ARM64平台运用汇编实现。这局部见识可以参照 这篇博客《ARM64 的 memcpy 优化与实现》。

下面继续进入正题,再反复一遍:内核态拜访会员空间地址,要是触发page fault,只有会员空间地址合法,内核态也会像什么也没有产生同样修复异样(分配物理内存,创立页表映照关系)。但是要是拜访不法会员空间地址,就选中第2条路,尝试救赎本人。这条路就是应用 .fixup__ex_table段。

要是无力回天只能给目前进程发送SIGSEGV信号。而且,轻则kernel oops,重则panic(取决于kernel配置选项CONFIG_PANIC_ON_OOPS)。在内核态拜访不法会员空间地址的状况下,do_page_fault()终究会跳转 no_context标号处的do_kernel_fault()。

static void __do_kernel_fault(unsigned long addr, unsigned int esr,	
                              struct pt_regs *regs)	
{	
        /*	
         * Are we prepared to handle this kernel fault?	
         * We are almost certainly not prepared to handle instruction faults.	
         */	
        if (!is_el1_instruction_abort(esr) && fixup_exception(regs))	
                return;	
        /* ... */	
}

fixup_exception()继续调取search_exception_tables(),其通过查寻_extable段。__extable段存储exception table,每个entry存储着异样地址及其对应修复的地址。

例如上述的 9998:subx0,end,dst指令的地址就会被寻到并修改do_page_fault()函数的返回地址,以达到跳转修复的功能。其实查寻历程是依据出题目的地址addr,查寻_extable段(exception table)可否有对应的exception table entry,要是有就代表可以被修复。因为32位处置器和64位处置器实现方式有差别,因而我们先从32位处置器异样表的实现道理提及。

_extable段的首尾地址离别是 __start___ex_table__stop___ex_table(定义在include/asm-generic/vmlinux.lds.h。这段内存可以看作是一个数组,数组的每个元素都是 struct exception_table_entry类型,其记载着异样产生地址及其对应的修复地址。

                        exception tables	
__start___ex_table --> +---------------+	
                       |     entry     |	
                       +---------------+	
                       |     entry     |	
                       +---------------+	
                       |      ...      |	
                       +---------------+	
                       |     entry     |	
                       +---------------+	
                       |     entry     |	
__stop___ex_table  --> +---------------+

在32位处置器上,struct exception_table_entry定义如下:

struct exception_table_entry {	
        unsigned long insn, fixup;	
};

有一点需要明白,在32位处置器上,unsigned long是4 bytes。insn和fixup离别存储异样产生地址及其对应的修复地址。依据异样地址ex_addr查寻对应的修复地址(未寻到返回0),其示意代码如下:

unsigned long search_fixup_addr32(unsigned long ex_addr)	
{	
        const struct exception_table_entry *e;	
        for (e = __start___ex_table; e < __stop___ex_table; e++)	
                if (ex_addr == e->insn)	
                        return e->fixup;	
        return 0;	
}

在32位处置器上,新建exception table entry相对简略。针对copy{to,from}user()汇编代码中每一处会员空间地址拜访的指令都会新建一个entry,而且insn存储目前指令对应的地址,fixup存储修复指令对应的地址。

当64位处置器开端开展起来,要是我们继续运用这种方式,势必须要2倍于32位处置器的内存存储exception table(由于存储一个地址需要8 bytes)。所以,kernel换用另一种方式实现。在64处置器上,struct exception_table_entry定义如下:

struct exception_table_entry {	
        int insn, fixup;	
};

每个exception table entry占用的内存和32位处置器状况同样,因而内存占用不变。但是insn和fixup的意义产生变化。insn和fixup离别存储着异样产生地址及修复地址相关于目前构造体成员地址的偏移(有点拗口)。例如,依据异样地址ex_addr查寻对应的修复地址(未寻到返回0),其示意代码如下:

unsigned long search_fixup_addr64(unsigned long ex_addr)	
{	
        const struct exception_table_entry *e;	
        for (e = __start___ex_table; e < __stop___ex_table; e++)	
                if (ex_addr == (unsigned long)&e->insn + e->insn)	
                        return (unsigned long)&e->fixup + e->fixup;	
        return 0;	
}

因而,我们的关注点就是怎样去构建exception_table_entry。我们针对每个会员空间地址的内存拜访都需要新建一个exception table entry,并插入_extable段。例如下面的汇编指令(汇编指令对应的地址是随便写的,不消纠结对错。了解道理才是王道)。

0xffff000000000000: ldr x1, [x0]	
0xffff000000000004: add x1, x1, #0x10	
0xffff000000000008: ldr x2, [x0, #0x10]	
/* ... */	
0xffff000040000000: mov x0, #0xfffffffffffffff2    // -14	
0xffff000040000004: ret

假如x0存放器保留着会员空间地址,因而我们需要对0xffff000000000000地址的汇编指令新建一个exception table entry,而且我们奢望当x0是不法会员空间地址时,跳转返回的修复地址是0xffff000040000000。为了盘算简略,假如这是新建首先个entry, __start___ex_table值是0xffff000080000000。那么首先个exception table entry的insn和fixup成员的值离别是:0x80000000和0xbffffffc(这两个值都是负数)。因而,针对copy{to,from}user()汇编代码中每一处会员空间地址拜访的指令都会新建一个entry。所以0xffff000000000008地址处的汇编指令也需要新建一个exception table entry。

所以,要是内核态拜访不法会员空间地址毕竟产生了什么?上面的剖析流程可以总结如下:

  1. 拜访不法会员空间地址:

    0xffff000000000000:ldr x1,[x0]

  2. MMU触发异样

  3. CPU调取do_page_fault()

  4. do_page_fault()调取search_exception_table()(regs->pc == 0xffff000000000000)

  5. 查看_extable段,寻觅0xffff000000000000 而且返回修复地址0xffff000040000000

  6. do_page_fault()修改函数返回地址(regs->pc = 0xffff000040000000)并返回

  7. 程序继续施行,处置出错状况

  8. 修改函数返回值x0 = -EFAULT (-14) 并返回(ARM64通过x0通报函数返回值)

总结

到了回忆总结的时候,copy_{to,from}_user()的思索也到此完毕。我们来个总结完毕此文。

  1. 不管是内核态还是会员态拜访合法的会员空间地址,当虚拟地址并未创立物理地址的映照关系的时候,page fault的流程险些同样,都会帮忙我们申请物理内存并新建映照关系。所以这种状况下memcpy()和copy_{to,from}_user()是相似的。

  2. 当内核态拜访不法会员空间地址的时候,依据异样地址查寻修复地址。这种修复异样的办法并不是创立地址映照关系,而是修改do_page_fault()返回地址。而memcpy()没法做到这点。

  3. 在使能 CONFIG_ARM64_SW_TTBR0_PAN或者 CONFIG_ARM64_PAN(硬件支撑的状况下才有效)的时候,我们只能运用copy_{to,from}_user()这种接口,直接运用memcpy()是不过关的。

最后,我想说,即便在某些状况下memcpy()可以正常工作。但是,这也是不举荐的,不是良好的编程习惯。在会员空间和内核空间数据交互上,我们必需运用相似copy_{to,from}_user()的接口。为何相似呢?由于还有其他的接口用于内核空间和会员空间数据交互,只是没有copy_{to,from}_user()出名。例如:{get,put}_user()。

谢谢您的耐心浏览。

本文转载自蜗窝科技:http://www.wowotech.net/memory_management/454.html

举荐教程:《Linux运维》

以上就是深入理解Linux中的copy_{to, from}_user()(附代码)的细致内容,更多请关注 百分百源码网 其它相干文章!

打赏

打赏

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

百分百源码网 建议打赏1~10元,土豪随意,感谢您的阅读!

共有151人阅读,期待你的评论!发表评论
昵称: 网址: 验证码: 点击我更换图片
最新评论

本文标签

广告赞助

能出一分力是一分吧!

订阅获得更多模板

本文标签

广告赞助

订阅获得更多模板