目录
一、背景介绍
二、copy_to_user 执行流程
三、Exception Table
四、用户态非法地址访问后的异常处理
五、总结
(代码:linux 6.3.1,架构:arm64)
One look is worth a thousand words. —— Tess Flanders
一、背景介绍
在Linux中,用户空间(User Space)和内核空间(Kernel Space)是相互隔离的,用户空间的程序无法直接访问内核空间的内存,反之亦然。然而,在实际应用中,用户程序经常需要与内核进行数据交换,比如通过系统调用(System Call)传递参数或读取内核数据。
为了安全高效地完成这种跨空间的数据拷贝,Linux内核提供了copy_from_user和copy_to_user这两个关键接口。它们的主要作用是:
-
copy_from_user
()
:将数据从用户空间安全地拷贝到内核空间。 -
copy_to_user
()
:将数据从内核空间安全地拷贝到用户空间。
这两个接口不仅检查用户空间指针的有效性,防止非法访问导致系统崩溃,还在底层优化了拷贝效率,确保数据传递的可靠性。本章后续将深入探讨它们的实现原理,主要围绕以下几点进行展开:
1)copy_from_user/copy_to_user如何确保指针的有效性?
2)如何保证当出现非法访问时,避免内核crash?
二、copy_to_user 执行流程
copy_from_user 与 copy_to_user 这两个接口的底层实现原理一致,因此后续本文仅针对copy_to_user接口的实现展开讨论。
copy_to_user(void __user *to, const void *from, unsigned long n) {n = _copy_to_user(to, from, n) {/* 1) 检查访问的用户态地址是否合法 */ret = access_ok(const void __user *addr = to, size = n) {return __access_ok(const void __user *ptr = addr, size) {unsigned long limit = TASK_SIZE_MAXunsigned long addr = (unsigned long)ptrreturn (size <= limit) && (addr <= (limit - size))}}if (ret) {/* 2) 若访问的用户态地址位于合法范围内, 则执行真正的拷贝动作 */n = raw_copy_to_user(to, from, n) {unsigned long __actu_retuaccess_ttbr0_enable()__actu_ret = __arch_copy_to_user(__uaccess_mask_ptr(to), (from), (n))uaccess_ttbr0_disable()return __actu_ret}return n}}return n
}
以上就是linux内核中,copy_to_user的实现代码(精简版),可以看到在进行真正的内存拷贝之前,会调用access_ok接口,判断即将访问的用户态地址是否合法。这里的判断条件:访问的地址是否位于用户态虚拟地址空间内,即:是否小于TASK_SIZE_MAX这个宏(该宏代表用户态虚拟地址空间的最大值)
当access_ok判断通过后,才会调用__arch_copy_to_user接口去执行真正的内存拷贝动作。
.macro strh1 reg, ptr, val
user_ldst 9997f, sttrh, \reg, \ptr, \val
.endm.macro str1 reg, ptr, val
user_ldst 9997f, sttr, \reg, \ptr, \val
.endm.macro stp1 reg1, reg2, ptr, val
user_stp 9997f, \reg1, \reg2, \ptr, \val
.endmSYM_FUNC_START(__arch_copy_to_user)add end, x0, x2mov srcin, x1
#include "copy_template.S" // 高效内存拷贝的实现,利用64位寄存器拷贝mov x0, #0ret// Exception fixups
9997: cmp dst, dstinb.ne 9998f// Before being absolutely sure we couldn't copy anything, try harderldrb tmp1w, [srcin]
USER(9998f, sttrb tmp1w, [dst])add dst, dst, #1
9998: sub x0, end, dst // bytes not copied(计算剩余没有copy的字节数,作为copy_{to,from}user()的返回值)ret
SYM_FUNC_END(__arch_copy_to_user)
EXPORT_SYMBOL(__arch_copy_to_user)
上述代码中,strh1、str1、stp1 这3个宏,是用于copy_template.S汇编函数中的,该汇编函数是真正执行内存拷贝动作的代码实现。
strh1、str1、stp1这三个宏,实际调用的是sttrh、sttr这两个arm64指令,之所以需要将sttrh、sttr 包装成宏的形式,是为了统一copy_template.S汇编函数中使用的内存拷贝指令的地址,因为这些访问用户态内存的指令(sttrh、sttr),可能会由于用户态VA未映射物理页,触发同步异常。这么设计是为了后续Exception Table而服务的。
copy_to_user 通过 access_ok,解决了第一个问题:访问的用户态指针,是否指向合法的用户态虚拟地址空间。
但是,这个虚拟地址背后是否映射了物理页,以及该虚拟地址是否是OS分配的虚拟地址空间(即:是否分配了VMA),copy_to_user并没有进行检查。针对这两种情况,若没有进行一些特殊处理,就会发生如下情况:
为了解决“用户态指针指向的区域未被OS分配虚拟地址空间,导致内核panic” 的问题。内核引入了 Exception Table 机制,配合 page fault 的流程,成功规避了上述问题。
三、Exception Table
Exception Table 是一张记录内核中“可能出现异常的指令地址、以及修复指令地址” 的 table,该表所在的内存区域是通过链接脚本规定好的一片区域,这块区域的页表属性是只读的。内核链接脚本指定的exception table区域:
SECTIONS
{.... = ALIGN(4);__ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) {__start___ex_table = .;ARM_MMU_KEEP(*(__ex_table))__stop___ex_table = .;}...
}
该Table中的表项,是通过编译阶段宏定义展开的方式,将指定内容设置到该表中。Exception Table元素定义:
struct exception_table_entry
{int insn, fixup;short type, data;
};
结构体exception_table_entry中,insn、fixup字段的含义如下(详见__ASM_EXTABLE_RAW宏):
- insn含义 : “异常发生地址” 相对于 “exception_table_entry->insn变量所在地址” 的 偏移量;
- fixup含义: “修复地址” 相对于 “exception_table_entry->fixup变量所在地址” 的 偏移量;
以__arch_copy_to_user为例,其中的 user_ldst、USER 宏,会将一些可能触发异常的指令地址、以及修复信息,最终通过展开后的__ASM_EXTABLE_RAW宏,记录到 Exception Table 所在section内存区域。
user_ldst_asm_extable_uaccess_ASM_EXTABLE_UACCESS_ASM_EXTABLE_UACCESS_ERR_ZERO__ASM_EXTABLE_RAWUSER_asm_extable_uaccess_ASM_EXTABLE_UACCESS_ASM_EXTABLE_UACCESS_ERR_ZERO__ASM_EXTABLE_RAW#define __ASM_EXTABLE_RAW(insn, fixup, type, data) \.pushsection __ex_table, "a"; \ // 将后续内容放入名为 __ex_table 的段中, “a”表示该段是可重定位的段.align 2; \ .long ((insn) - .); \ // 计算 insn(可能发生异常的指令地址)相对于当前地址(.)的偏移量,并存储为一个4字节的值;(即: 记录异常指令的位置).long ((fixup) - .); \ // 计算 fixup(修复代码地址)相对于当前地址的偏移量,并存储为一个 4 字节的值; (即: 记录异常发生后需要跳转的修复代码位置).short (type); \.short (data); \.popsection;
这样一来,内核中就清楚记录了所有copy_to_user过程中,可能触发异常的指令地址,以及触发异常后的“修复地址”。这里的修复地址,就是 __arch_copy_to_user 汇编函数中 9997f、9998f 标记所对应的地址。
当 copy_to_user 访问了一个没有被内核映射过的用户态地址(即:没有对应vma),然后触发同步异常,走到 page_fault 的处理流程后,就会用到Exception Table 的信息,来对本次异常进行修复。
四、用户态非法地址访问后的异常处理
1)sttrb tmp1w, [dst] 是 copy_to_user 执行过程中的一条 “访问用户态空间” 的指令;
2)当访问该指令后,由于MMU发现该VA没有页表映射,于是触发同步异常陷入内核同步异常处理入口 el1h_64_sync_handler;
3)在内核中会走page fault 的处理流程,但由于没有找到对应的vma,最终会走到__do_kernel_fault 函数中;
3)随后会尝试执行 fixup_exception函数,查找当前触发异常的指令是否记录在内核Exception Table中;
4)若找到对应表项的话,就将任务上下文regs中的pc修改为“fixup地址”;
5)接着,执行 eret 从异常返回,并直接跳转到预先设定好的修复地址处(即:__arch_copy_to_user 汇编函数 中的 9998f 标签处)继续执行;
6)最终返回未拷贝的字节数。
代码如下:
extern struct exception_table_entry __start___ex_table[]
extern struct exception_table_entry __stop___ex_table[]el1h_64_sync_handler(struct pt_regs *regs) {unsigned long esr = read_sysreg(esr_el1)switch (ESR_ELx_EC(esr)) {case ESR_ELx_EC_DABT_CUR:el1_abort(regs, esr) {unsigned long far = read_sysreg(far_el1)do_mem_abort(far, esr, regs) {...do_page_fault(far, esr, regs) {...no_context:__do_kernel_fault(addr, esr, regs) {/* Detail see below !!! */}return 0}//do_page_fault}//do_mem_abort}//el1_abortbreak}
}do_page_fault {...__do_kernel_fault(addr, esr, regs) {/* 判断是否是内核态指令异常 */is_el1_iabort = is_el1_instruction_abort(esr) {return ESR_ELx_EC(esr) == ESR_ELx_EC_IABT_LOW}/* 查找异常表(exception table,__ex_table)来修复发生异常的指令,避免系统崩溃或内核oops */ret = fixup_exception(regs) {const struct exception_table_entry *exex = search_exception_tables(unsigned long addr = instruction_pointer(regs)) {const struct exception_table_entry *ee = search_kernel_exception_table(addr) {return search_extable(*base = __start___ex_table, num = __stop___ex_table - __start___ex_table, value = addr) {# 注: cmp_ex_search的搜索逻辑see below!!!return bsearch(&value, base, num, sizeof(struct exception_table_entry), cmp_ex_search)}}//search_kernel_exception_tableif (!e)e = search_module_extables(addr)if (!e)e = search_bpf_extables(addr)return e}//search_exception_tablesif (!ex)return falseswitch (ex->type) {case EX_TYPE_UACCESS_ERR_ZERO:case EX_TYPE_KACCESS_ERR_ZERO:return ex_handler_uaccess_err_zero(ex, regs){int reg_err = FIELD_GET(EX_DATA_REG_ERR, ex->data)int reg_zero = FIELD_GET(EX_DATA_REG_ZERO, ex->data)pt_regs_write_reg(regs, reg_err, -EFAULT)pt_regs_write_reg(regs, reg_zero, 0)### 修改异常返回地址, 修改为fixup地址(即: 内联汇编函数__arch_copy_to_user中的9998f处)regs->pc = get_ex_fixup(ex) {/ 注:exception_table_entry->fixup变量中存储的是“修复地址相对与fixup变量所在地址的偏移值!!!” /return ((unsigned long)&ex->fixup + ex->fixup)}return true}//ex_handler_uaccess_err_zero}}//fixup_exception/* 若成功修复异常指令(修改了pc值跳转到fixup地址), 直接从page fault同步异常处理中返回 */if (!is_el1_iabort && ret) {return}...die_kernel_fault(msg, addr, esr, regs) {die("Oops", regs, esr)crash_kexec}}//__do_kernel_faultreturn 0
}
五、总结
copy_from_user、copy_to_user 除了高效拷贝用户态内存之外,还提供了两个非常重要的功能。
1)判断访问的用户地址,是否位于合法的用户态虚拟地址空间范围内;
2)当拷贝过程中访问了未映射的非法用户态地址,借助 Exception Table、page fault 等机制,强制修改出现异常后的代码执行路径,避免内核发生crash;