Detailliertes Verständnis von copy_{to, from}_user() in Linux (mit Code)
Wir sollten mit der Verwendung der Schnittstelle copy_{to,from}_user() bestens vertraut sein. Grundlegende Linux-Bücher stellen seine Funktion vor. Schließlich ist es die Brücke zwischen Kernel-Space und User-Space. Alle Dateninteraktionen sollten solche Schnittstellen verwenden. Daher gibt es keinen Grund, warum wir die Rolle von Schnittstellen nicht kennen sollten. Allerdings hatte ich auch folgende Fragen.
Warum brauchen wir copy_{to,from}_user() und was macht es für uns hinter den Kulissen?
Was ist der Unterschied zwischen copy_{to,from}_user() und memcpy()?
Gibt es ein Problem, wenn memcpy() copy_{to,from}_user() ersetzt?
Plötzlich erlangte ich mein verwirrtes Selbst zurück. Über jede Frage, die ich jemals gestellt habe, habe ich schon einmal nachgedacht. Ich habe mehr als einmal darüber nachgedacht und jedes Mal hatte ich eine andere Idee. Natürlich, weil ich es von Anfang an nicht ganz verstanden habe. Jetzt komme ich wieder auf dieses schwere Thema zurück und denke weiter über dieses frühere Problem nach.
Um die oben genannten Probleme anzugehen, sollte natürlich zuerst Baidu verwendet werden. Baidu hat auch viele Blogs zu diesem Thema, was ausreicht, um zu zeigen, dass dieses Problem eine große Anzahl von Linux-Enthusiasten verwirren muss. In Bezug auf meine Überprüfungsergebnisse sind Meinungen hauptsächlich in die folgenden zwei Typen unterteilt:
1. copy_{to,from}_user() verfügt über eine stärkere Überprüfung der Legalität eingehender Adressen als memcpy() test.
Zum Beispiel, ob es zum User-Space-Adressbereich gehört. Theoretisch kann der Kernelraum direkt vom Benutzerraum übergebene Zeiger verwenden. Selbst wenn Sie Daten kopieren möchten, können Sie memcpy() tatsächlich auf einer Architektur ohne MMU verwenden Die endgültige Implementierung verwendet mencpy().
Bei den meisten Plattformen mit MMU hat sich die Situation jedoch geändert: Der vom Benutzerraum übergebene Zeiger befindet sich im virtuellen Adressraum, und der virtuelle Adressraum, auf den er zeigt, ist wahrscheinlich nicht tatsächlich auf der tatsächlichen physischen Seite abgebildet . Aber was nun? Durch Seitenfehler verursachte Ausnahmen werden vom Kernel transparent repariert (übermitteln Sie eine neue physische Seite für den Adressraum der fehlerhaften Seite), und die Anweisungen, die auf die fehlerhafte Seite zugreifen, werden weiterhin ausgeführt, als ob nichts passiert wäre. Dies ist jedoch nur das Verhalten von Ausnahmen für fehlende Seiten im Benutzerbereich. Im Kernelbereich muss diese Ausnahme für fehlende Seiten explizit repariert werden. Dies wird durch das Entwurfsmuster der vom Kernel bereitgestellten Funktion zur Behandlung fehlender Seiten festgelegt.
Die Idee dahinter ist: Wenn ein Programm im Kernel-Modus versucht, auf eine User-Space-Adresse zuzugreifen, die noch keine physische Seite festgeschrieben hat, muss der Kernel darauf aufmerksam sein und darf nicht wie ein Benutzer davon unbemerkt bleiben Raum.
2. Wenn wir die Richtigkeit des im Benutzermodus übergebenen Zeigers sicherstellen, können wir die Funktion memcpy() anstelle von copy_{to,from}_user() vollständig verwenden. Nach einigen experimentellen Tests stellte ich fest, dass die Verwendung von memcpy() kein Problem beim Ausführen des Programms darstellt. Daher können die beiden ersetzt werden und gleichzeitig die Sicherheit der Benutzermoduszeiger gewährleistet werden.
In verschiedenen Blogs konzentrieren sich die Meinungen hauptsächlich auf den ersten Punkt. Es scheint, dass der erste Punkt von allen allgemein anerkannt wird. Menschen, die auf die Praxis achten, sind jedoch zum zweiten Standpunkt gelangt. Schließlich bringt Übung wahres Wissen. Liegt die Wahrheit in den Händen einiger weniger Menschen? Oder sind die Augen der Massen scharf? Selbstverständlich leugne ich keine der oben genannten Ansichten. Ich kann Ihnen nicht garantieren, welche Ansicht richtig ist. Weil ich glaube, dass selbst eine einst einwandfreie Theorie im Laufe der Zeit oder wenn sich bestimmte Umstände ändern, möglicherweise nicht mehr wahr ist. Zum Beispiel Newtons Theorie der klassischen Mechanik (scheint etwas weit weg zu sein). Wenn Sie möchten, dass ich menschliche Worte spreche, dann Folgendes: Mit der Zeit ändert sich der Linux-Code ständig. Vielleicht war der obige Standpunkt einmal richtig. Natürlich kann es auch jetzt noch wahr sein. Die folgende Analyse ist mein Standpunkt. Ebenso müssen alle skeptisch bleiben. Jetzt werde ich einige Ideen geben.
Werfen wir zunächst einen Blick auf die Funktionsdefinitionen von memcpy() und copy_{to,from}_user(). Bei den Parametern gibt es fast keinen Unterschied, sie umfassen alle die Zieladresse, die Quelladresse und die Größe der zu kopierenden Bytes.
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);
Eines wissen wir jedoch mit Sicherheit. Das heißt, memcpy() prüft nicht die Gültigkeit der eingehenden Adresse. Und copy_{to,from}_user() führt für die eingehende Adresse eine Legalitätsprüfung ähnlich der folgenden durch (einfach ausgedrückt: Weitere Verifizierungsdetails finden Sie im Code).
Wenn Daten vom Userspace in den Kernelspace kopiert werden, müssen sich die Userspace-Adresse nach und nach plus der kopierten Bytelänge n im Userspace-Adressraum befinden.
Wenn Sie Daten vom Kernel-Space in den User-Space kopieren, müssen Sie natürlich auch die Rechtmäßigkeit der Adresse überprüfen. Zum Beispiel, ob es sich um einen Out-of-Bounds-Zugriff handelt oder ob es sich um Daten in einem Codesegment usw. handelt. Kurz gesagt: Alle illegalen Operationen müssen sofort gestoppt werden.
经过简单的对比之后,我们再看看其他的差异以及一起探讨下上面提出的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配置选项是不一样的。这个配置项是 <span class="pln">CONFIG_ARM64_SW_TTBR0_PAN</span>
或者 <span class="pln">CONFIG_ARM64_PAN</span>
(针对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
ttbr1_el1存储的是内核页表基地址,因此其值就是swapper_pg_dir。
swapper_pg_dir减去RESERVED_TTBR0_SIZE就是上面描述的特殊页表。
将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
uaccess_enable_not_uao和uaccess_disable_not_uao是上面说到的内核态访问用户空间的开关。
copy_template.S文件是汇编实现的memcpy()的功能,稍后看看memcpy()的实现代码就清楚了。
<span class="pun">.section.fixup,“ax”</span>
定义一个section,名为“.fixup”,权限是ax(‘a’可重定位的段,‘x’可执行段)。<span class="lit">9998</span>
标号处的指令就是“未雨绸缪”的善后处理工作。还记得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个选择。<span class="pun">.fixup</span>
段就是为了实现这个修复功能。当copy过程中出现访问非法用户空间地址的时候,do_page_fault()返回的地址变成<span class="lit">9998</span>
标号处,此时可以计算剩余未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条路,尝试救赎自己。这条路就是利用 <span class="pun">.fixup</span>
和 <span class="pln">__ex_table</span>
段。
如果无力回天只能给当前进程发送SIGSEGV信号。并且,轻则kernel oops,重则panic(取决于kernel配置选项CONFIG_PANIC_ON_OOPS)。在内核态访问非法用户空间地址的情况下,do_page_fault()最终会跳转 <span class="pln">no_context</span>
标号处的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存储着异常地址及其对应修复的地址。
例如上述的 <span class="lit">9998:subx0,end,dst</span>
指令的地址就会被找到并修改do_page_fault()函数的返回地址,以达到跳转修复的功能。其实查找过程是根据出问题的地址addr,查找_extable段(exception table)是否有对应的exception table entry,如果有就代表可以被修复。由于32位处理器和64位处理器实现方式有差别,因此我们先从32位处理器异常表的实现原理说起。
_extable段的首尾地址分别是 <span class="pln">__start___ex_table</span>
和 <span class="pln">__stop___ex_table</span>
(定义在include/asm-generic/vmlinux.lds.h。这段内存可以看作是一个数组,数组的每个元素都是 <span class="kwd">struct exception_table_entry</span>
类型,其记录着异常发生地址及其对应的修复地址。
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, <span class="pln">__start___ex_table</span>
值是0xffff000080000000。那么第一个exception table entry的insn和fixup成员的值分别是:0x80000000和0xbffffffc(这两个值都是负数)。因此,针对copy{to,from}user()汇编代码中每一处用户空间地址访问的指令都会创建一个entry。所以0xffff000000000008地址处的汇编指令也需要创建一个exception table entry。
所以,如果内核态访问非法用户空间地址究竟发生了什么?上面的分析流程可以总结如下:
访问非法用户空间地址:
0xffff000000000000:ldr x1,[x0]
MMU触发异常
CPU调用do_page_fault()
do_page_fault()调用search_exception_table()(regs->pc == 0xffff000000000000)
查看_extable段,寻找0xffff000000000000 并且返回修复地址0xffff000040000000
do_page_fault()修改函数返回地址(regs->pc = 0xffff000040000000)并返回
程序继续执行,处理出错情况
修改函数返回值x0 = -EFAULT (-14) 并返回(ARM64通过x0传递函数返回值)
到了回顾总结的时候,copy_{to,from}_user()的思考也到此结束。我们来个总结结束此文。
无论是内核态还是用户态访问合法的用户空间地址,当虚拟地址并未建立物理地址的映射关系的时候,page fault的流程几乎一样,都会帮助我们申请物理内存并创建映射关系。所以这种情况下memcpy()和copy_{to,from}_user()是类似的。
当内核态访问非法用户空间地址的时候,根据异常地址查找修复地址。这种修复异常的方法并不是建立地址映射关系,而是修改do_page_fault()返回地址。而memcpy()无法做到这点。
在使能 <span class="pln">CONFIG_ARM64_SW_TTBR0_PAN</span>
或者 <span class="pln">CONFIG_ARM64_PAN</span>
(硬件支持的情况下才有效)的时候,我们只能使用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运维》
Das obige ist der detaillierte Inhalt vonVertiefendes Verständnis von copy_{to, from}_user() unter Linux (mit Code). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!