第6章:字符设备驱动的高级操作5:Using the ioctl Argument
In continuation of the previous text第6章字符设备驱动的高级操作4The 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_user和copy_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_READ或VERIFY_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有几点值得注意它并不完成内存访问验证的全部工作它仅仅检查该内存引用是否位于进程合理有权访问的内存区域。特别地access_ok确保该地址没有指向内核空间内存。大多数驱动代码实际上不需要调用access_ok。后面会讲到的内存访问函数会为你处理这件事。尽管如此我们还是演示它的用法让你明白其工作原理。scull 源码利用 ioctl 命令号中的位域在switch语句之前检查参数int err 0, tmp; int retval 0; /* * extract the type and number bitfields, and dont 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 inlinux/uaccess.h:调用access_ok之后驱动就可以安全地执行实际的数据传输了。除了copy_from_user和copy_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参数的类型并在编译时通过sizeof和typeof内置函数确定。因此如果ptr是char指针就传输 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_user或copy_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. 两套用户态读写接口的区别与选型1get_user / put_user自带安全检查内部隐含基础地址合法性判断专门针对char/short/int/long 等基础定长单变量优化指令更少、效率更高适合 ioctl 中传递简单整型、单个基础数据类型的场景适用不确定地址是否提前校验、常规驱动开发通用场景。2__get_user / __put_user去掉了冗余安全检测运行开销更低、性能更好不主动调用 access_ok完全依赖开发者手动提前完成地址校验强制使用前提必须先通过access_ok验证过用户地址合法适用高频调用路径、批量拷贝、追求极致性能的驱动代码。3copy_from_user / copy_to_user通用性最强支持任意长度、结构体、数组、复杂大数据块性能略低因为包含循环拷贝、完整权限校验适用结构体传输、数组、大块数据、长度不固定的缓冲区交互。5. 常见编译错误场景说明当强行用get_user/put_user拷贝结构体、嵌套联合体、非标量复杂类型时编译器会抛出conversion to non-scalar type requested原因这类宏依靠编译期sizeof、typeof自动推导数据宽度仅支持基础标量类型。解决方式复杂多字节复合数据必须改用copy_from_user/copy_to_user。6. 内核标准错误码规范-ENOTTY非法命令、当前设备不支持该 ioctl 指令-EFAULT用户态指针非法、地址越界、内存访问无效-EINVAL参数数值错误、配置非法、参数大小不匹配。严格使用标准错误码能让用户态程序统一识别错误类型便于上层开发与问题排查。7. 工程编码最佳实践ioctl 处理流程固定顺序校验幻数 → 校验命令序号 → 校验用户地址 → 数据读写 → 业务逻辑小数据优先使用get_user/put_user减少拷贝函数开销批量 / 频繁 ioctl 场景提前统一执行一次 access_ok后续使用__get_user/__put_user提速绝不直接强制解引用arg用户指针杜绝所有裸指针访问用户内存的写法。