Windows内核驱动中的派遣函数
Windows内核驱动中的派遣函数
驱动程序的一个重要功能就是处理IO请求,其中大部分IO请求都是在派遣函数中完成的。
在用户模式下对内核驱动发出的IO请求,全部都由系统转化为一个IRP(IO请求包),不同的IRP会被派遣到不同的派遣函数中进行处理。这有点类似于Windows中的消息机制。
IRP与派遣函数
在Windows内核中,有一种数据结构叫做IRP(I/O Request Package),即输入输出请求包。它是与输入输出相关的重要数据结构。上层应用程序与底层驱动程序通信时,应用程序会发出I/O请求。操作系统将I/O请求转化为相应的IRP数据,不同类型的IRP会根据类型传递到不同的派遣函数内。
IRP中有两个重要的基本属性:一个是MajorFunction,另一个是MinorFunction,分别记录IRP的主类型和子类型。操作系统根据MajorFunction将IRP“派遣”到不同的派遣函数中,在派遣函数中还可以继续判断这个IRP属于哪种MinorFunction。
下图展示了IRP类型:
在派遣函数中对IRP进行简单处理
大部分的IRP都源于文件I/O处理Win32 API,如CreateFile、ReadFile等。处理这些IRP最简单的方法就是在相应的派遣函数中,将IRP的状态设置为成功,然后结束IRP的请求,并让派遣函数返回成功。结束IRP的请求使用函数IoCompleteRequest。
三种读写操作
当IoCreateDevice创建完设备后,需要对设备对象的Flags子域进行设置。设置不同的Flags,会导致以不同的方式操作设备。
设备对象一共可以有三种读写方式,分别是缓冲区方式读写、直接方式读写、其他方式读写。这三种方式的Flags分别对应为DO_BUFFERED_IO、DO_DIRECT_IO和0。
缓冲区方式
以缓冲区方式写设备时,操作系统将WriteFile提供的用户模式的缓冲区复制到内核模式地址下。这个地址由WriteFile创建的IRP的AssociatedIrp.SystemBuffer子域记录。
这段缓冲区内存是用户模式的内存地址,驱动程序如果直接引用这段内存是十分危险的。因为Windows操作系统是多任务的,它可能随时切换到别的进程。如果驱动程序需要访问这段内存,而这时操作系统可能已经切换到另外一个进程。如果这样,驱动程序访问的内存地址必定是错误的,这种错误会引起系统崩溃。
有很多方法可以解决这个问题,其中一个方法是使用缓冲区方式读写。对于这种方法,操作系统将应用程序提供缓冲区的数据复制到内核模式下的地址中。这样,无论操作系统如何切换进程,内核模式地址都不会改变。IRP的派遣函数将会对内核模式下的缓冲区操作,而不是操作用户模式地址的缓冲区。这样做的优点是,比较简单地解决了将用户地址传入驱动的问题。缺点是需要在用户模式和内核模式之间复制数据,影响了运行效率。在少量内存操作时,可以采用这种办法。
以“缓冲区”方式读设备时,操作系统会分配一段内核模式下的内存。这段内存大小等于ReadFile或者
WriteFile指定的字节数,**并且ReadFile或者WriteFile创建的IRP的AssociatedIrp.SystemBuffer子域会记录这段内存地址。**当IRP请求结束时(一般都是由IoCompleteRequest函数结束IRP),这段内存地址会被复制到ReadFile提供的缓冲区中。
直接方式
除了“缓冲区”方式读写设备外,另外一种方式是直接方式读写设备。这种方式需要创建完设备对象后,在设置设备属性的时候,设置为DO_DIRECT_IO,而不是设置DO_BUFFERED_IO属性。
在应用程序进行读写的时候,例如,用WriteFile写设备时,会要求用户提供一段缓冲区,这段缓冲区里面是要写入设备的数据。操作系统在创建IRP_MJ_WRITE的时候,将需要写入的数据复制到IRP数据结构中的AssociatedIrp.SystemBuffer中,对于读设备操作也是类似的。
和缓冲区方式读写设备不同,直接方式读写设备,操作系统会将用户模式下的缓冲区锁住。然后操作系统将这段缓冲区在内核模式地址再次映射一遍。这样,用户模式的缓冲区和内核模式的缓冲区指向的是同一区域的物理内存。无论操作系统如何切换进程,内核模式地址都保持不变。
操作系统先将用户模式的地址锁定后,操作系统用内存描述符表(MDL数据结构)记录这段内存。
MDL记录这段虚拟内存,这段虚拟内存的大小存储在mdl->ByteCount里,这段虚拟内存的第一个页地址是mdl->StartVa,这段虚拟内存的首地址对于第一个页地址的偏移量是mdl->ByteOffset。因此,这段虚拟内存的首地址应该是mdl->StartVa+mdl->ByteOffset。
其它方式
在调用IoCreateDevice创建设备后,对pDevObj->Flags既不设置DO_BUFFERED_IO,也不设置
DO_DIRECT_IO,此时采用的读写方式就是其他读写方式。
在使用其他方式读写设备时,派遣函数直接读写应用程序提供的缓冲区地址。在驱动程序中,直接操作应用程序的缓冲区地址是很危险的。只有驱动程序与应用程序运行在相同线程上下文的情况下,才能使用这种方式。
用其他方式读写时,ReadFile或者WriteFile提供的缓冲区内存地址,可以在派遣函数中通过IRP的pIrp->UserBuffer字段得到。读取的字节数可以从I/O堆栈中的stack->Parameters.Read.Length字段中得到。
使用用户模式的内存时要格外小心,因为ReadFile有可能把空指针地址或者非法地址传递给驱动程序。因此,驱动程序使用用户模式地址前,需要探测这段内存是否可读或者可写。探测可读或者可写,应该使用ProbeForWrite函数和try块。
