上层和驱动通信用DeviceIoControl函数,这是一个Win32 API,在SDK中定义。这个函数都会产生一个IRP_MJ_DEVICE_CONTROL包,如果驱动中注册过相应的例程,那么这个包就会引发该例程的工作。如果是驱动和驱动间的通信,那么用IoBuildDeviceControlRequest函数,该函数在DDK中定义,会产生一个IRP_INTERNAL_DEVICE_CONTROL包,并引发相应的例程。这两个IRP包中都有一个非常重要的结构叫IOCTL(io control code),用于指定通信中的各类细节。该数据结构是一个32比特的数据块,有6个区域,每个区域包含一类信息。IOCTL的结构如下图所示
DDK中有一个CTL_CODE宏,用这个宏我们可以很方便的定义IOCTL。不管是IRP_MJ_DEVICE_CONTROL还是IRP_INTERNAL_DEVICE_CONTROL包,IOCTL都用如下形式定义:
#define IOCTL_Device_Function CTL_CODE(DeviceType, Function, Method, Access)
DeviceType:设备类型,和DEVICE_OBJECT结构中的DeviceType必须一致。注意:0x8000以下的数字被微软占用了。
Function Code:功能代码,可以自定义,用来区分操作类型。注意:0x800以下的数字被微软占用了。
Method:IO缓冲类型,有METHOD_BUFFERED,METHOD_IN_DIRECT,METHOD_OUT_DIRECT,METHOD_NEITHER四种类型。
METHOD_BUFFERED表明输入输出都用系统缓冲,这种策略下输入输出指向的是同一个内存块,该内存块有IO Manager管理。输入的时候把数据拷贝到缓冲中,然后缓冲再拷贝到驱动;输出的时候数据拷贝到缓冲中,然后缓冲拷贝到用户空间。由于用的是同一块缓冲,所以调用者自己得管理好里面的数据,防止弄混。缓冲区地址存放在IRP.AssociatedIrp.SystemBuffer中,输入数据大小为Parameter.DeviceIoControl.InputBufferLength,输出数据大小为Parameter.DeviceIoControl.OutputBufferLength,两者都在IO_STACK_LOCATION结构中。
METHOD_IN_DIRECT表明输出用缓冲,输入用直接IO。这种策略下输出和上面的方法一致,而输入则是直接访问指定的内存区域,不通过缓冲。IOManager先把输入数据的内存块锁定,然后把地址存放在IRP.MdlAddress中。输入输出数据块的大小和上面一致。
METHOD_OUT_DIRECT表明输入用缓冲,输出直接IO。IO Manager把输出数据的内存快锁定,存放在IRP.MdlAddress中,驱动直接通过该地址访问数据,输入数据通过系统缓冲,存放在IRP.AssociatedIrp.SystemBuffer中。输入输出数据块的大小和上面一致。
METHOD_NEITHER表明输入输出都不用缓冲,I/O Manager把调用者的输入缓冲区的地址放到IRP当前I/O堆栈单元的Parameters.Devi ceIoControl.TypeInputBuffer域中,把输出缓冲 区的地址存放到IRP的UserBuffer域中。这两个地址都是用户空间地 址。
从上面的说明可以看出,在执行缓冲I/O时,I/O管理器将在非份页池 中分配内存,如果调用者的缓冲区比较大时,分配的非份页池也将 比较大。非份页池是系统比较宝贵的资源,因此,如果调用者的缓 冲区比较大时,我们一般采用直接I/O的方式(例如磁盘读写请求等), 这样不仅节省系统资源,另一方面由于省去了I/O管理器在系统缓冲 区和调用者缓冲区之间的数据拷贝,也提高了效率,这对存在大量 数据传送的驱动程序尤其明显。不过需要注意的是,直接io要求驱动和IOCTL的发起者运行在同一个线程里。
Access:指明调用者的访问权限,有FILE_ANY_ACCESS,FILE_READ_DATA,FILE_WRITE_DATA三个选项可选。FILE_ANY_ACCESS表明用户拥有所有的权限,FILE_READ_DATA表明权限为只读,FILE_WRITE_DATA表明权限为可写。FILE_WRITE_DATA | FILE_READ_DATA表明权限为可读可写,但还没达到FILE_ANY_ACCESS的权限。
用户定义IOCTL时要注意以下几条原则:
1. FunctionCode总是定义成0x800以上的数字,因为0x800以下的数字被微软占用了。
2. 仔细考虑访问权限,如果指定了你不具备的权限,那么IO Manager会忽略IOCTL
3. 仔细考虑要访问的内存区域,如果去读写一个关键内存,那么系统会重启
驱动内部执行IOCTL时要注意以下几条原则:
1. 接收到IOCTL时,要先检查整个32比特的数据完整性
2. 用IoValidateDeviceIoControlAccess检查访问权限是否有效
3. 严格遵照Parameter.DeviceIoControl.InputBufferLength和Parameter.DeviceIoControl.OutputBufferLength指定的大小访问输入输出区域,否则系统会重启
4. 驱动中申请一块内存后,总是先用RtlZeroMemory清空区域
5. 直接io策略中,用MmGetSystemAddressForMdlSafe获取相应内存区域时,要判断是否为NULL
6. 直接io中,用ProbeForRead 和ProbeForWrite检查内存是否可以访问。