Windows内核驱动中的派遣函数
Windows内核驱动中的派遣函数驱动程序的一个重要功能就是处理IO请求其中大部分IO请求都是在派遣函数中完成的。在用户模式下对内核驱动发出的IO请求全部都由系统转化为一个IRPIO请求包不同的IRP会被派遣到不同的派遣函数中进行处理。这有点类似于Windows中的消息机制。IRP与派遣函数在Windows内核中有一种数据结构叫做IRPI/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-StartVamdl-ByteOffset。其它方式在调用IoCreateDevice创建设备后对pDevObj-Flags既不设置DO_BUFFERED_IO也不设置DO_DIRECT_IO此时采用的读写方式就是其他读写方式。在使用其他方式读写设备时派遣函数直接读写应用程序提供的缓冲区地址。在驱动程序中直接操作应用程序的缓冲区地址是很危险的。只有驱动程序与应用程序运行在相同线程上下文的情况下才能使用这种方式。用其他方式读写时ReadFile或者WriteFile提供的缓冲区内存地址可以在派遣函数中通过IRP的pIrp-UserBuffer字段得到。读取的字节数可以从I/O堆栈中的stack-Parameters.Read.Length字段中得到。使用用户模式的内存时要格外小心因为ReadFile有可能把空指针地址或者非法地址传递给驱动程序。因此驱动程序使用用户模式地址前需要探测这段内存是否可读或者可写。探测可读或者可写应该使用ProbeForWrite函数和try块。