驱动程序的主要功能是用来处理IO请求,而大部分的IO请求是在派遣函数中完成的,用户模式下所有的IO请求都会被IO管理器封装为一个IRP结构,类似于Windows窗口程序中的消息,不同的IRP被发送到不同的派遣函数中处理
IRP与派遣函数
IRP
IRP(I/O Request Package)输入输出请求包,IRP的两个最基本的结构是MajorFunction和MinorFunction,分别记录IRP的主要类型和子类型,它们是一组函数指针数组,不同的项纪录的是处理当前请求的回调函数,可以在这些派遣函数中继续通过MinorFunction来判断
每个驱动都有一个唯一的DRIVER_OBJECT结构,这个结构中有一个MajorFunction数组,通过这个数组可以将IRP与处理它的派遣函数关联起来,当应用层有一个针对于某个设备对象的I/O请求时,会根据这个设备对象所在驱动找到对应的MajorFunction结构,再根据请求类型来找到它对应的处理函数。IRP类型
与应用层中有不同的消息类型,系统会根据消息类型调用具体消息处理函数类似,IRP也有不同的类型,在应用层调用不同的函数时会产生不同的IRP类型,例如调用应用层函数CreateFile或者内核函数ZwCreateFile会产生IRP_MJ_CREATE类型的IRP。下面是不同操作所对应产生的IRP请求列表
IRP类型 | 来源 |
---|---|
IRP_MJ_CREATE | 创建设备,CreateFile会产生此IRP |
IRP_MJ_CLOSE | 关闭设备,CloseHandle会产生此IRP |
IRP_MJ_CLEANUP | 清除工作,CloseHandle会产生此IRP |
IRP_MJ_DEVICE_CONTROL | DeviceIoControl函数会产生此IRP |
IRP_MJ_PNP | 即插即用消息,NT驱动不支持此中IRP,只有WDM驱动才支持此中驱动 |
IRP_MJ_POWER | 在操作系统处理电源消息时会产生此IRP |
IRP_MJ_QUERY_INFORMATION | 获取文件长度,GetFileSize会产生此IRP |
IRP_MJ_READ | 读取设备内容,ReadFile会产生此IRP |
IRP_MJ_SET_INFORMATION | 设置文件长度,SetFileSize |
IRP_MJ_SHUTDOWN | 关闭系统前会产生此IRP |
IRP_MJ_SYSTEM_CONTROL | 系统内部产生控制信息,蕾西与调用DeviceIoControl函数 |
IRP_MJ_WRITE | 对设备进行WriteFile时会产生此IRP |
对派遣函数的简单处理
大部分的I/O请求都来自于应用层调用相应的API对设备进行I/O操作类似于CreateFile、ReadFile等函数产生,最简单的做法是将IRP设置为成功,然后结束IRP请求,并让派遣函数返回成功,结束这个IRP调用函数IoCompleteRequest。
VOID IoCompleteRequest( IN PIRP Irp, //代表要结束的IRP IN CCHAR PriorityBoost//代笔线程恢复时的优先级别 );
其实当应用层调用相关函数进行I/O操作时,会陷入睡眠或者阻塞状态,等待派遣函数成功返回,当派遣函数返回时会唤醒之前的等待线程,而第二个参数就是制定这个被唤醒的线程以何种优先级别运行。一般设置为IO_NO_INCREMENT表示不增加优先级,对于键盘,或者鼠标一类的需要更快的相应,这个时候可以设置为IO_MOUSE_INCREMENT 或者IO_KEYBOARD_INCREMENT下面是完成优先级的一个表
在应用层打开设备
在应用层一般通过设备名称打开驱动中的设备对象,设备名称一般只能在内核层使用,应用层能看到的是设备的符号链接名,符号链接名一般以”\??\”开头,在应用层的写法有些不同,应用层设备的符号链接名称以“\.\开头”,因此在内核层符号链接为:”\??\HelloDevice”到了应用层则应该写为”\.\HelloDevice”。
设备栈
驱动对象会创建多个设备对象,并将这些设备对象叠成一个垂直的结构,这种垂直结构被称作设备栈,IRP请求首先被发往设备栈上的顶层设备上,如果这个设备不处理可以将它下发到下层的设备对象中,直到某个设备结束这个IRP请求。为了记录IRP在每层设备中做的操作,IRP中有一个IO_STACK_LOCATION数组,这个数组对应于设备栈中各个设备对IRP所做的操作。在本层的设备中可以使用函数IoGetCurrentIrpStackLocation得到本层设备对应的IO_STACK_LOCATION结构,下面是它对应的结构图
缓冲区方式读写操作
在调用IoCreateDeivce函数完成设备对象的创建之后,需要设置该设备对象的缓冲区读写方式,这个值是由DEVICE_OBJECT中的Flag来设置,主要有三种DO_DIRECT_IO、DO_BUFFERED_IO 、0。
应用层在对设备进行读写操作时,会提供一个缓冲区用于保存需要传入到设备对象或者保存由设备对象传入的数据,Flag值规定就规定了设备对象是如何使用这个缓冲区的。 1. DO_DIRECT_IO:内核直接通过地址映射的方式将那块缓冲区映射为内核地址,然后在驱动中使用。当使用这种方式时内核可以在IO_STACK_LOCATION结构中的MdlAddress拿到这块内存,通过函数MmGetSystemAddressFromMdlSafe传入MdlAddress值可以得到应用层传下来的缓冲区地址 2. DO_BUFFERED_IO:内核会在内核的地址空间空另外开辟一段内存,将缓冲区的数据简单拷贝到这个新开辟的空间中。通过这种方式的读写可以在IRP结构的AssociatedIrp.SystemBuffer中获取。 3. 0:内核直接使用应用层的地址,对那块内存进行操作,这种方式是十分危险的,如果进行线程切换,这个地址就不一定指向之前的内存,这样就可能造成系统崩溃蓝屏。这种方式可以通过IRP中的UserBuffer拿到缓冲区地址 另外缓冲区的长度可以通过IO_STACK_LOCATION中的Parameters.Read.Length和Parameters.WriteLength分别获取读写缓冲区的长度IO设备控制操作
DeviceIoControl与驱动设备交互
BOOL DeviceIoControl( HANDLE hDevice, //驱动对象句柄 DWORD dwIoControlCode, //控制码 LPVOID lpInBuffer, //传入到驱动中的数据缓冲 DWORD nInBufferSize, //缓冲大小 LPVOID lpOutBuffer, //驱动传出数据的缓冲 DWORD nOutBufferSize, //输出数据缓冲区的大小 LPDWORD lpBytesReturned, //实际返回数据的大小 LPOVERLAPPED lpOverlapped//异步函数);
这是一个应用层的API函数,用于向驱动发送控制码,在驱动中,根据控制吗的不同而采用不同的处理方式进行处理,应用层可以通过后面几个参数实现与驱动的数据共享。
控制码采用宏CTL_CODE来定义#define CTL_CODE(DeviceType, Function, Method, Access)
这个宏有四个参数,第一个是设备对象的类型,就是在调用IoCreateDevice创建设备对象时传入的DeviceType的值,第二个参数是设备对象的控制码,为了与系统定义的区分开,一般用户自定义的取值在0x800之上。第三个参数是操作模式,主要有这样几个值:METHOD_BUFFERED、METHOD_IN_DIRECT、METHOD_OUT_DIRECT、METHOD_NEITHER,这些值主要针对的是设备对象的三种缓冲区的读写方式。第四个参数是访问权限,一般给FILL_ANY_ACCESS;
这个函数在使用的时候需要注意下面几点: 1. 这个函数是在应用层调用,所以必须在调用这个函数前使用CreateFile打开这个设备对象。在调用CreateFile时会向I/O管理器发送一个Create请求,这个请求被I/O管理器包装成IRP,这个IRP的类型为IRP_MJ_CREATE,I/O管理器需要根据驱动的返回值来判断怎么处理这个请求,只有当驱动向I/O管理器返回一个成功的时候才会为其分配句柄,所以驱动中需要自己实现Create的分发派遣函数。 2. 驱动中需要自定义一个分发函数用于处理这个IOControl发下来的信息,函数中可以从IO_STACK_LOCATION结构中的Parameters.DeviceIoControl.IoControlCode获得用户层传下来的控制码 3. 默认情况下我们会在结束IOControl这个IRP的时候会给定一个返回长度为0,这个时候I/O管理器会将这个值回填到DeviceIoControl函数中的倒数第二个参数中,因此DeviceIoControl的这个参数不能为NULL,否则会造成程序崩溃