当前位置: 首页 > news >正文

第6章:字符设备驱动的高级操作5:Using the ioctl Argument

In continuation of the previous text第6章:字符设备驱动的高级操作4:The Return Value And The Predefined Commands, let’s GO ahead.

Another point we need to cover before looking at the ioctl code for the scull driver is how to use the extra argument. If it is an integer, it’s easy: it can be used directly. If it is a pointer, however, some care must be taken.

在查看 scull 驱动的 ioctl 代码之前,我们还需要说明如何使用附加参数。如果它是一个整数,那么很简单:可以直接使用。但是,如果它是一个指针,则必须格外小心。

When a pointer is used to refer to user space, we must ensure that the user address is valid. An attempt to access an unverified user-supplied pointer can lead to incorrect behavior, a kernel oops, system corruption, or security problems. It is the driver’s responsibility to make proper checks on every user-space address it uses and to return an error if it is invalid.

当使用指针指向用户空间时,我们必须确保用户地址是合法有效的。尝试访问一个未经验证的、由用户提供的指针,可能会导致行为异常、内核崩溃(oops)、系统损坏甚至安全漏洞。驱动程序有责任对它使用的每一个用户空间地址进行正确检查,如果地址无效,则返回一个错误。

In Chapter 3, we looked at the copy_from_user and copy_to_user functions, which can be used to safely move data to and from user space. Those functions can be used in ioctl methods as well, but ioctl calls often involve small data items that can be more efficiently manipulated through other means. To start, address verification (without transferring data) is implemented by the function access_ok, which is declared in <linux/uaccess.h>:

在第 3 章中,我们学习了copy_from_usercopy_to_user函数,它们可以用来安全地与用户空间交换数据。这些函数同样可以用于 ioctl 方法中,但是 ioctl 调用通常只涉及少量数据,这些数据可以通过其他方式更高效地操作。首先,仅进行地址验证(不传输数据)是由函数access_ok实现的,该函数声明在<linux/uaccess.h>中:

int access_ok(int type, const void *addr, unsigned long size);

The first argument should be either VERIFY_READ or VERIFY_WRITE, depending on whether the action to be performed is reading the user-space memory area or writing it. The addr argument holds a user-space address, and size is a byte count. If ioctl, for instance, needs to read an integer value from user space, size is sizeof(int). If you need to both read and write at the given address, use VERIFY_WRITE, since it is a superset of VERIFY_READ.

第一个参数应当是VERIFY_READVERIFY_WRITE,具体取决于要执行的操作是读取用户空间内存区域还是写入。addr参数保存的是一个用户空间地址,size是字节数。例如,如果 ioctl 需要从用户空间读取一个整数值,那么size就是sizeof(int)。如果你需要对给定地址既读又写,请使用VERIFY_WRITE,因为它是VERIFY_READ的超集。

Unlike most kernel functions, access_ok returns a boolean value: 1 for success (access is OK) and 0 for failure (access is not OK). If it returns false, the driver should usually return -EFAULT to the caller.

与大多数内核函数不同,access_ok返回一个布尔值:1 表示成功(访问合法),0 表示失败(访问不合法)。如果它返回 0,驱动通常应该向调用者返回-EFAULT

There are a couple of interesting things to note about access_ok. First, it does not do the complete job of verifying memory access; it only checks to see that the memory reference is in a region of memory that the process might reasonably have access to. In particular, access_ok ensures that the address does not point to kernel-space memory. Second, most driver code need not actually call access_ok. The memory-access routines described later take care of that for you. Nonetheless, we demonstrate its use so that you can see how it is done.

The scull source exploits the bitfields in the ioctl number to check the arguments before the switch:

关于access_ok,有几点值得注意:

  1. 并不完成内存访问验证的全部工作;它仅仅检查该内存引用是否位于进程合理有权访问的内存区域。特别地,access_ok确保该地址没有指向内核空间内存
  2. 大多数驱动代码实际上不需要调用access_ok。后面会讲到的内存访问函数会为你处理这件事。尽管如此,我们还是演示它的用法,让你明白其工作原理。

scull 源码利用 ioctl 命令号中的位域,在switch语句之前检查参数:

int err = 0, tmp; int retval = 0; /* * extract the type and number bitfields, and don't decode * wrong cmds: return ENOTTY (inappropriate ioctl) before access_ok( ) */ if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC) return -ENOTTY; if (_IOC_NR(cmd) > SCULL_IOC_MAXNR) return -ENOTTY; /* * the direction is a bitmask, and VERIFY_WRITE catches R/W * transfers. `Type' is user-oriented, while * access_ok is kernel-oriented, so the concept of "read" and * "write" is reversed */ if (_IOC_DIR(cmd) & _IOC_READ) err = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd)); else if (_IOC_DIR(cmd) & _IOC_WRITE) err = !access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd)); if (err) return -EFAULT;

After calling access_ok, the driver can safely perform the actual transfer. In addition to the copy_from_user and copy_to_user functions, the programmer can exploit a set of functions that are optimized for the most used data sizes (one, two, four, and eight bytes). These functions are described in the following list and are defined in<linux/uaccess.h>:

调用access_ok之后,驱动就可以安全地执行实际的数据传输了。除了copy_from_usercopy_to_user函数之外,程序员还可以使用一组为最常用数据大小(1、2、4、8 字节)优化的函数。这些函数定义在<linux/uaccess.h>中,说明如下:

  • put_user(datum, ptr) __put_user(datum, ptr)

These macros write the datum to user space; they are relatively fast and should be called instead of copy_to_user whenever single values are being transferred. The macros have been written to allow the passing of any type of pointer to put_user, as long as it is a user-space address. The size of the data transfer depends on the type of the ptr argument and is determined at compile time using the sizeof and typeof compiler builtins. As a result, if ptr is a char pointer, one byte is transferred, and so on for two, four, and possibly eight bytes. put_user checks to ensure that the process is able to write to the given memory address. It returns 0 on success, and -EFAULT on error. __put_user performs less checking (it does not call access_ok), but can still fail if the memory pointed to is not writable by the user. Thus, __put_user should only be used if the memory region has already been verified with access_ok. As a general rule, you call __put_user to save a few cycles when you are implementing a read method, or when you copy several items and, thus, call access_ok just once before the first data transfer, as shown above for ioctl.

这些宏将数据写入用户空间;它们速度相对较快,在传输单个值时,应优先调用它们,而不是copy_to_user。这些宏允许传递任意类型的指针给put_user,只要它是用户空间地址。数据传输的大小取决于ptr参数的类型,并在编译时通过sizeoftypeof内置函数确定。因此,如果ptrchar指针,就传输 1 个字节,以此类推传输 2、4 甚至 8 个字节。

  • put_user会检查确保进程能够写入给定的内存地址。成功返回 0,失败返回-EFAULT
  • __put_user执行的检查更少(它不调用access_ok),但如果指向的内存对用户不可写,它仍然可能失败。
  • 因此,只有在内存区域已经通过access_ok验证过的情况下,才能使用__put_user
  • 通常规则:当你实现read方法,或者复制多个数据项(因此在第一次传输前只调用一次access_ok,就像上面 ioctl 的示例)时,可以调用__put_user来节省几个时钟周期。
  • get_user(local, ptr) __get_user(local, ptr)

These macros are used to retrieve a single datum from user space. They behave like put_user and __put_user, but transfer data in the opposite direction. The value retrieved is stored in the local variable local; the return value indicates whether the operation succeeded. Again, __get_user should only be used if the address has already been verified with access_ok.

If an attempt is made to use one of the listed functions to transfer a value that does not fit one of the specific sizes, the result is usually a strange message from the compiler, such as “conversion to non-scalar type requested.” In such cases, copy_to_user or copy_from_user must be used.

这些宏用于从用户空间获取单个数据。它们的行为类似于put_user__put_user,只是数据传输方向相反。获取到的值存储在局部变量local中;返回值表示操作是否成功。同样,只有在地址已经通过access_ok验证过的情况下,才能使用__get_user

如果尝试使用这些函数传输不满足特定大小的值,编译器通常会报出奇怪的错误,例如 “conversion to non-scalar type requested”。在这种情况下,必须使用copy_to_usercopy_from_user

补充说明:

1. 为什么必须严格校验用户态指针

用户态传入的指针完全不可信:地址可能非法、越界、指向内核空间、或是恶意构造的伪造地址。驱动若直接裸访问用户指针,会直接触发内核 Oops、段错误、系统宕机,还会带来越界读写、内核信息泄露、提权等严重安全漏洞。因此 Linux 驱动强制要求:所有用户态指针,必须经过内核安全接口校验与拷贝,绝不直接解引用。

2. access_ok 函数核心要点

  • 头文件依赖:定义于<linux/uaccess.h>,必须包含该头文件才能使用。
  • 作用范围:只做区间合法性粗校验,判断该地址是否属于当前进程合法用户态地址区间,不做实际内存读写检查,也不校验页表权限。
  • 参数含义
    • VERIFY_WRITE:最高权限校验,代表该内存允许读 + 写,可覆盖双向传输场景;
    • VERIFY_READ:仅校验读权限,只适用于单纯读取用户内存。
  • 返回值逻辑:返回 1 代表地址合法,返回 0 代表非法;代码中常通过!access_ok(...)判断错误,非法则返回-EFAULT
  • 局限性:不能单独依赖它保证安全,它只是前置过滤,真正安全的内存访问仍要搭配get_user / put_user / copy_xx_user

3. ioctl 方向位与 access_ok 方向颠倒的核心原因

  • _IOC_READ:站在用户态视角,表示用户要从设备读数据;对应内核行为:驱动需要写入用户缓冲区→ 内核需使用VERIFY_WRITE校验。
  • _IOC_WRITE:站在用户态视角,表示用户要向设备写数据;对应内核行为:驱动需要读取用户缓冲区→ 内核需使用VERIFY_READ校验。这是内核固定设计规范,也是新手最容易写错、引发内存错误的关键点。

4. 两套用户态读写接口的区别与选型

(1)get_user / put_user
  • 自带安全检查,内部隐含基础地址合法性判断;
  • 专门针对char/short/int/long 等基础定长单变量优化,指令更少、效率更高;
  • 适合 ioctl 中传递简单整型、单个基础数据类型的场景;
  • 适用:不确定地址是否提前校验、常规驱动开发通用场景。
(2)__get_user / __put_user
  • 去掉了冗余安全检测,运行开销更低、性能更好;
  • 不主动调用 access_ok,完全依赖开发者手动提前完成地址校验;
  • 强制使用前提:必须先通过access_ok验证过用户地址合法;
  • 适用:高频调用路径、批量拷贝、追求极致性能的驱动代码。
(3)copy_from_user / copy_to_user
  • 通用性最强,支持任意长度、结构体、数组、复杂大数据块;
  • 性能略低,因为包含循环拷贝、完整权限校验;
  • 适用:结构体传输、数组、大块数据、长度不固定的缓冲区交互。

5. 常见编译错误场景说明

当强行用get_user/put_user拷贝结构体、嵌套联合体、非标量复杂类型时,编译器会抛出:

conversion to non-scalar type requested原因:这类宏依靠编译期sizeoftypeof自动推导数据宽度,仅支持基础标量类型。解决方式:复杂多字节复合数据,必须改用copy_from_user/copy_to_user

6. 内核标准错误码规范

  • -ENOTTY:非法命令、当前设备不支持该 ioctl 指令;
  • -EFAULT:用户态指针非法、地址越界、内存访问无效;
  • -EINVAL:参数数值错误、配置非法、参数大小不匹配。严格使用标准错误码,能让用户态程序统一识别错误类型,便于上层开发与问题排查。

7. 工程编码最佳实践

  1. ioctl 处理流程固定顺序:校验幻数 → 校验命令序号 → 校验用户地址 → 数据读写 → 业务逻辑
  2. 小数据优先使用get_user/put_user,减少拷贝函数开销;
  3. 批量 / 频繁 ioctl 场景,提前统一执行一次 access_ok,后续使用__get_user/__put_user提速;
  4. 绝不直接强制解引用arg用户指针,杜绝所有裸指针访问用户内存的写法。
http://www.cnnetsun.cn/news/2070366.html

相关文章:

  • 【Hot 100 刷题计划】 LeetCode 543. 二叉树的直径 | C++ 后序遍历 (求深度的完美副产品)
  • HZERO微服务实战:手把手教你配置hzero-file对接MinIO,并集成OnlyOffice实现在线编辑
  • C# WinForm与MATLAB混合编程实战:从函数打包到无缝调用
  • 会议灭绝计划:测试工程师的异步协作实践指南
  • PlantUML在线编辑器:3分钟学会用代码绘制专业UML图的完整教程
  • 诊断测试效率翻倍:深度解析CDD文件在CANoe、Diva与VTsystem中的核心配置项
  • Qwen3-4B-Instruct功能体验:256K上下文窗口下的长文本智能对话实测
  • 别再傻傻等在线下载了!手把手教你Arthas离线安装(附Maven仓库下载地址)
  • 从手机充电到光伏发电:聊聊PN结这个‘半导体心脏’的日常应用
  • 2025届最火的六大降AI率方案解析与推荐
  • SAC算法里的“熵”到底在干嘛?深入聊聊它比TD3更稳定、探索更强的秘密
  • 别再只盯着MACD了!用Python回测SuperTrend指标在A股的表现到底怎么样?
  • VSCode多人协同时CPU飙升至92%?深度剖析Extension Host内存泄漏链与4个零侵入修复补丁
  • 元宇宙中的软件测试:虚拟世界的质量如何保障
  • 终极本地多人游戏解决方案:Nucleus Co-Op让单台电脑变身游戏派对中心
  • 深度解析:TDengine 时序数据库在 IT 运维监控中的底层逻辑与实践
  • 基于条件风险价值CVaR的微网虚拟电厂多场景随机规划与风险价值度量研究
  • 实战解析:如何用D3KeyHelper构建暗黑3自动化战斗系统
  • Windows 11任务栏拖放功能终极修复指南:快速恢复文件拖拽体验
  • CubeMX配置DMAMUX的隐藏细节:以STM32H723的EXTI触发DMA为例,聊聊那些容易踩的坑
  • 具身智能体在复杂环境中的责任归属
  • 虚拟机技术
  • 终极指南:在Windows电脑上直接运行安卓APK的完整解决方案
  • 别再只盯着小木虫了!2024年CCF期刊投稿,这3个新工具帮你高效避坑
  • vJoy虚拟手柄:解锁Windows游戏控制的无限可能
  • 如何用SMUDebugTool解锁AMD Ryzen处理器的隐藏性能:完整免费教程
  • 索尼相机完全解锁指南:OpenMemories-Tweak让你的摄影体验升级
  • Python实战:基于Matchms的质谱数据预处理与相似度计算全流程
  • 英雄联盟全能助手League Akari:3分钟快速部署的免费游戏效率神器
  • MATLAB转Python必看:信号处理之EMD分解,两个平台结果为啥不一样?