这份文件,也链接在这个答案中,应该更深入地阐明这一点。一个有趣的摘录(我添加了粗体文本)可能如下:
在用户空间中,ioctl 系统调用具有以下原型:
int ioctl(int fd, unsigned long cmd, ...);
由于点,原型在 Unix 系统调用列表中脱颖而出,这些点通常将函数标记为具有可变数量的参数。然而,在实际系统中,系统调用实际上不能有可变数量的参数。系统调用必须有明确定义的原型,因为用户程序只能通过硬件“门”访问它们。因此,原型中的点表示的不是可变数量的参数,而是单个可选参数,传统上标识为char *argp
。这些点只是为了防止编译期间的类型检查。第三个参数的实际性质取决于发出的特定控制命令(第二个论点)。有些命令不带参数,有些带整数值,有些带指向其他数据的指针。使用指针是将任意数据传递给ioctl调用的方法;然后,该设备能够与用户空间交换任意数量的数据。
ioctl调用的非结构化特性使其在内核开发人员中失宠。每个ioctl命令本质上都是一个单独的、通常没有记录的系统调用,并且无法审计这些调用以任何全面的方式。也很难让非结构化 ioctl 参数在所有系统上都一样工作;例如,考虑在 32 位模式下运行用户空间进程的 64 位系统。结果,通过几乎任何其他方式实施杂项控制操作的压力很大。可能的替代方法包括将命令嵌入到数据流中(我们将在本章后面讨论这种方法)或使用虚拟文件系统,无论是 sysfs 还是特定于驱动程序的文件系统。(我们将在第 14 章讨论 sysfs。)然而,对于真正的设备操作来说,ioctl通常是最简单和最直接的选择。
这意味着没有任何方法可以理解如何将 ioctl 参数解释为外部观察者,而无需深入了解设备驱动程序约定/内部。从用户空间的角度来看, ioctl 参数是无类型的,并且在内核空间中以某种方式松散地键入,因为它被处理为unsigned long
只是为其保留空间。它是一个“纯”数字或任何适合unsigned long integer
空间的位序列,可以用作(非常短的)字符串、(小)char 数组、(小)结构 - 但要注意字节顺序和特定于体系结构大小 - 可以表示设备板载芯片的操作码,甚至可以通过类型处理作为浮点数处理!
此外,这意味着很容易搞砸事情,通过将不一致的数据传递给驱动程序(不仅仅是错误的数据,而是错误类型的错误数据!),最终导致设备的未定义行为或用户空间内存损坏(例如通过在读取的 ioctl 中传递一个指向错误大小的结构的指针)。
还有几行:
[...]cmd
参数不变地从用户传递,可选arg
参数以 an 的形式传递unsigned long
,无论它是由用户作为整数还是指针给出。如果调用程序没有传递第三个参数,arg
则驱动程序操作接收到的值是未定义的。因为在额外参数上禁用了类型检查,所以如果将无效参数传递给ioctl ,编译器无法警告您,并且任何相关的错误都很难发现。
无论如何,如果您想尝试对设备驱动程序 ioctl 调用进行“盲目”审核,而不查看头文件和源文件,则可以尝试先使用 copy_from_user() 将 arg 作为指针处理,如果失败,则可能是立即值(或发生错误),然后可以尝试记录其值以查看并尝试解释它(但为什么要反转 ioctl 而不是研究驱动程序代码?);在成功的情况下,在没有知识的情况下,可以读取并记录不同大小的内存以进行解码尝试(同样,只要有可用的资源,它们应该是毫无意义的)。
更有趣的操作肯定是解码 ioctl 代码 (cmd),因为它可以指向正确的方向以找到与其数值相关的驱动程序 - 如果 ioctl 定义的约定是,应该只有一个应用,无论如何,不同的驱动程序都允许使用相同的魔术字符,因此一个正则表达式来 grep 所有内核源文件以查找#define
包含 'r' 或 'D' 等的文件可以挑选出许多文件来检查 ioctl 定义,而不是与函数编号匹配应该会排除一些或所有错误的,并且寻找正确的参数大小将完成搜索。
问候。