突破内核边界:__get_user与put_user如何守护Linux内存安全
【免费下载链接】linux Linux kernel source tree 项目地址: https://gitcode.com/GitHub_Trending/li/linux
你是否曾在调试内核模块时遇到过神秘的-EFAULT错误?是否困惑于用户空间与内核空间的数据交换为何如此复杂?本文将深入解析Linux内核中最关键的内存安全函数——__get_user与put_user,带你掌握用户空间内存访问的底层逻辑,彻底解决数据传输中的安全隐患。读完本文,你将能够:
- 理解内核与用户空间隔离的底层机制
- 正确使用
__get_user/put_user系列函数 - 识别并修复常见的内存访问错误
- 通过实例掌握安全数据传输的最佳实践
内核与用户空间:一道不可逾越的鸿沟
在Linux系统中,内存被严格划分为内核空间(Kernel Space)和用户空间(User Space)两大区域。这种隔离是操作系统安全的基石,防止用户进程直接访问内核敏感数据。当内核需要与用户空间交换数据时,必须通过特殊的接口函数,而__get_user与put_user就是最基础也最重要的两个工具。
内存隔离的底层实现
x86架构中,内核通过页表(Page Table)和特权级(Ring Level)实现内存隔离。内核运行在最高特权级(Ring 0),可以访问所有内存;而用户进程运行在最低特权级(Ring 3),其访问范围受到页表限制。当用户进程试图访问内核内存时,CPU会触发一般保护异常(General Protection Fault),终止进程执行。
用户空间访问的特殊挑战
内核访问用户空间内存面临两大挑战:
- 地址有效性验证:用户提供的指针可能指向无效地址或内核空间
- 页面可用性:用户空间页面可能被换出到磁盘(Page Out)
为解决这些问题,Linux内核提供了两类函数:
- 安全版本:
get_user()和put_user(),自动进行地址验证和异常处理 - 快速版本:
__get_user()和__put_user(),需要调用者提前验证地址
__get_user:从用户空间安全读取数据
__get_user函数用于从用户空间读取数据到内核空间,其定义位于架构相关的头文件中。以x86架构为例,实现代码位于arch/x86/include/asm/uaccess.h。
函数原型与工作原理
#define __get_user(x, ptr) do_get_user_call(get_user_nocheck,x,ptr)
__get_user是一个宏函数,通过内联汇编实现,核心逻辑包括:
- 验证用户空间指针有效性(需调用者提前通过
access_ok()验证) - 切换到用户空间访问模式(通过
stac指令启用SMAP保护) - 执行数据传输(通过
mov指令) - 处理可能的页面错误(通过异常表
_ASM_EXTABLE_UA)
典型使用场景
在bpf_trace.c中,__get_user被用于读取用户提供的探针参数:
if (__get_user(usymbol, usyms + i)) {
// 错误处理逻辑
}
这段代码从用户空间数组usyms读取第i个元素到内核变量usymbol,如果发生错误(如无效地址),函数返回非零值。
与get_user的关键区别
__get_user与get_user的主要区别在于安全检查:
get_user会自动调用might_fault()和access_ok()进行安全检查__get_user假设调用者已完成地址验证,因此执行速度更快
// get_user定义(自动检查)
#define get_user(x,ptr) ({ might_fault(); do_get_user_call(get_user,x,ptr); })
// __get_user定义(无检查)
#define __get_user(x,ptr) do_get_user_call(get_user_nocheck,x,ptr)
put_user:向用户空间安全写入数据
与__get_user相对应,put_user函数用于将内核数据写入用户空间,其实现同样位于arch/x86/include/asm/uaccess.h。
函数原型与工作流程
#define put_user(x, ptr) ({ might_fault(); do_put_user_call(put_user,x,ptr); })
put_user的工作流程包括:
- 验证用户空间指针有效性(
access_ok()) - 切换到用户空间访问模式
- 执行数据写入操作
- 恢复内核访问模式并检查错误
实际应用案例
在kernel/power/user.c中,put_user用于向用户空间返回系统休眠状态:
error = put_user(in_suspend, (int __user *)arg);
这段代码将内核变量in_suspend(表示系统是否正在休眠)的值写入用户提供的地址arg。
错误处理机制
put_user通过内联汇编中的异常处理机制捕获内存访问错误:
asm volatile("call __put_user_%c[size]"
: "=c" (__ret_pu), ASM_CALL_CONSTRAINT
: "0" (__ptr_pu), "r" (__val_pu), [size] "i" (sizeof(*(ptr)))
:"ebx");
当发生页面错误时,CPU会跳转到异常处理程序,最终返回-EFAULT错误码。
实战指南:安全使用的最佳实践
完整使用流程
安全访问用户空间内存的标准流程包括三个步骤:
- 地址验证:使用
access_ok()检查指针有效性 - 数据传输:使用
__get_user/__put_user传输数据 - 错误处理:检查返回值并处理可能的错误
// 完整示例代码
int copy_from_user_example(void __user *user_ptr) {
int data;
if (!access_ok(user_ptr, sizeof(int))) {
return -EFAULT; // 地址无效
}
if (__get_user(data, (int __user *)user_ptr)) {
return -EFAULT; // 读取失败
}
// 成功读取数据,进行处理
return 0;
}
常见错误与解决方案
| 错误类型 | 原因分析 | 解决方案 |
|---|---|---|
-EFAULT | 用户指针无效或指向内核空间 | 使用access_ok()提前验证 |
| 数据损坏 | 未考虑数据对齐问题 | 使用get_user自动处理对齐 |
| 性能问题 | 频繁调用get_user导致多次检查 | 批量传输或使用__get_user |
| SMAP错误 | 未正确处理SMAP保护 | 确保使用stac/clac指令对 |
性能优化技巧
- 批量传输:对于大量数据,使用
copy_from_user()/copy_to_user()而非多次__get_user/put_user - 地址缓存:如果多次访问同一用户空间地址,缓存
access_ok()的结果 - 使用快速版本:在循环内部使用
__get_user而非get_user
底层实现:从宏定义到汇编代码
宏定义展开过程
__get_user的宏定义看似简单,实则展开为复杂的内联汇编:
#define do_get_user_call(fn,x,ptr) ({ \
int __ret_gu; \
register __inttype(*(ptr)) __val_gu asm("%edx"); \
__chk_user_ptr(ptr); \
asm volatile("call __" #fn "_%c[size]" \
: "=a" (__ret_gu), "=r" (__val_gu), ASM_CALL_CONSTRAINT \
: "0" (ptr), [size] "i" (sizeof(*(ptr)))); \
(x) = (__force __typeof__(*(ptr))) __val_gu; \
__builtin_expect(__ret_gu, 0); \
})
这段宏定义通过GCC扩展实现了类型安全和高效的汇编调用。
汇编级实现细节
x86架构中,__get_user_4(读取4字节)的汇编实现如下:
__get_user_4:
movl 4(%esp),%edx ; 获取用户空间指针
movl (%edx),%eax ; 读取数据
xorl %edx,%edx ; 成功返回0
ret
_ASM_EXTABLE_UA(1b, 2b) ; 异常处理
当访问失败时,异常处理程序会设置%eax为-EFAULT并返回。
跨架构兼容性
虽然不同架构的实现细节不同,但__get_user和put_user的接口在所有架构中保持一致。例如:
- ARM架构:arch/arm/include/asm/uaccess.h
- PowerPC架构:arch/powerpc/include/asm/uaccess.h
- RISC-V架构:arch/riscv/include/asm/uaccess.h
这种接口一致性确保了内核代码的跨平台可移植性。
总结与展望
__get_user与put_user看似简单,却是Linux内核安全的关键防线。它们通过精巧的宏定义和内联汇编,在性能和安全之间取得了完美平衡。随着硬件安全特性(如SMAP/SMEP)的不断增强,这些函数的实现也在持续进化。
核心要点回顾
__get_user/put_user是内核访问用户空间的基础工具- 安全版本(
get_user/put_user)自动处理地址验证 - 快速版本(
__get_user/__put_user)需要手动验证地址 - 所有用户空间访问都必须通过这些函数,禁止直接指针操作
扩展学习资源
- 内核文档:Documentation/arm64/memory.txt
- 代码示例:samples/目录下的用户空间交互示例
- 异常处理:arch/x86/mm/fault.c中的页面错误处理
掌握这些基础工具,将为你深入理解Linux内核内存管理打下坚实基础。无论是开发内核模块、调试系统问题,还是优化性能瓶颈,对__get_user与put_user的深入理解都将成为你的得力助手。
【免费下载链接】linux Linux kernel source tree 项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



