转载请标明是引用于 http://blog.csdn.net/chenyujing1234
欢迎大家拍砖
参考书籍<<Windows驱动开发技术详解>>
用户模式下所有对驱动程序的I/O请求,全部由操作系统转化一个叫做IRP的数据结构,不同的IRP数据会被“派遣”到不同
的派遣函数(Dispatch Function)中,这也是派遣函数名字的由来。
IRP的处理机制类似Windows应用程序中的“消息处理”机制,驱动程序收到不同类型的IRP后,会进入不同的派遣函数。
在Windows内核中,有一种数据结构叫IRP(I/O Request Package),即输入输出请求包。它是与输入输出相关的数据结构。上层应用与底层程序通信时,应用程序
会发I/O请求。OS将I/O请求转为相应的IRP数据,不同类型的IRP会据类型传递到不同的派遣函数内。
这里我们先了解IRP的两个基本性情:
(1)MajorFunction 记录IRP的主类型;
(2)MinorFunction 记录IRP的子类型。
操作系统根据MajorFunction将IRP“派遣”到不同的派遣函数中,在派遣函数中还可以继续判断这个IRP属于哪种MinorFunction。如我们在IRP_MJ_PNP的派遣函数中:
switch(irpStack->MinorFunction) { case IRP_MN_START_DEVICE:
NT程序程序和WDM驱动程序都是在DriverEntry函数中注册派遣函数的。
在DriverEntry的驱动对象pDriverObject中,有个函数指针数组MajorFunction。函数指针数组是个组组,每个元素都记录一个函数的地址。
通过设置这个数组,可以将IRP的类型和派遣函数关联起来。
在进入DriverEntry之前,操作系统会将_IopInvalidDeviceRequest的地址填满整个MajorFunction数组,IRP与派遣函数关系如下:
在Win32中,程序是由“消息”驱动的。不同的消息会被分发到不同的消息处理函数中。
(1)IRP的处理类似这种方式,文件I/O的相关函数,如CreateFile、ReadFile、WriteFile、CloseHandle等会使操作系统发出
IRP_MJ_CREATE、IRP_MJ_READ、IRP_MJ_WRITE、IRP_MJ_CLOSE等不同的IRP,这些IRP会被传到驱动程序的派遣函数中。
另外,内核中的文件I/O处理函数,如ZwCreateFile、ZwReadFile、ZwWriteFile也同样会将IRP传送到相应派遣函数中。
(2)还有些IRP是由系统中的某个组件创建的,如IRP_MJ_SHUTDOWN 是在Windows的即插即用组件在即将关闭系统时发出的。
大部分的IRP都源于文件I/O处理的API,如CreateFile、ReadFile等。处理这些IRP最简单的方法是在相应的派遣函数中,
将IRP的状态设置成功,然后结束IRP的请求(使用IoCompleteRequest),并让派遣函数返回成功。
/************************************************************************ * 函数名称:HelloDDKDispatchRoutin * 功能描述:对读IRP进行处理 * 参数列表: pDevObj:功能设备对象 pIrp:从IO请求包 * 返回 值:返回状态 *************************************************************************/ #pragma PAGEDCODE NTSTATUS HelloDDKDispatchRoutin(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp) { KdPrint(("Enter HelloDDKDispatchRoutin\n")); PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); //建立一个字符串数组与IRP类型对应起来 static char* irpname[] = { "IRP_MJ_CREATE", "IRP_MJ_CREATE_NAMED_PIPE", "IRP_MJ_CLOSE", "IRP_MJ_READ", "IRP_MJ_WRITE", "IRP_MJ_QUERY_INFORMATION", "IRP_MJ_SET_INFORMATION", "IRP_MJ_QUERY_EA", "IRP_MJ_SET_EA", "IRP_MJ_FLUSH_BUFFERS", "IRP_MJ_QUERY_VOLUME_INFORMATION", "IRP_MJ_SET_VOLUME_INFORMATION", "IRP_MJ_DIRECTORY_CONTROL", "IRP_MJ_FILE_SYSTEM_CONTROL", "IRP_MJ_DEVICE_CONTROL", "IRP_MJ_INTERNAL_DEVICE_CONTROL", "IRP_MJ_SHUTDOWN", "IRP_MJ_LOCK_CONTROL", "IRP_MJ_CLEANUP", "IRP_MJ_CREATE_MAILSLOT", "IRP_MJ_QUERY_SECURITY", "IRP_MJ_SET_SECURITY", "IRP_MJ_POWER", "IRP_MJ_SYSTEM_CONTROL", "IRP_MJ_DEVICE_CHANGE", "IRP_MJ_QUERY_QUOTA", "IRP_MJ_SET_QUOTA", "IRP_MJ_PNP", }; UCHAR type = stack->MajorFunction; if (type >= arraysize(irpname)) KdPrint((" - Unknown IRP, major type %X\n", type)); else KdPrint(("\t%s\n", irpname[type])); //对一般IRP的简单操作,后面会介绍对IRP更复杂的操作 NTSTATUS status = STATUS_SUCCESS; // 完成IRP pIrp->IoStatus.Status = status; pIrp->IoStatus.Information = 0; // bytes xfered IoCompleteRequest( pIrp, IO_NO_INCREMENT ); KdPrint(("Leave HelloDDKDispatchRoutin\n")); return status; }
(1)上面代码中,派遣函数设置了IRP的完成状态为STATUS_SUCCESS,这样发起I/O请求的API(如WriteFile)将会返回TRUE;
相反,如果将IRP的完成状态设置为不成功,这时发起I/O请求的API将会返回FALSE。这种情况下,可以用GetLastError得到错误代码,代码会和IRP设置的状态相一致;
(2)除了设置IRP的完成状态,派遣函数还要设置这个IRP请求操作了多少字节。上述代码中我们是设置为0;
如果是ReadFile产生的IRP,这个字节数代表从设备读了多少字节。如果是WriteFile产生的IRP,这个字节数代表对设备写了多少字节。
(3)最后派遣函数将IRP请求结束,通过IoCompleteRequest 。
NTKERNELAPI VOID FASTCALL IofCompleteRequest( IN PIRP Irp, IN CCHAR PriorityBoost );
Irp:代表需要被结束的IRP;
PriorityBoost:代表线程恢复时的优先级别。
为了解释优先级的概念,需要了解一下与文件I/O相关的API的内部操作过程。这里以ReadFile为例:
(1)ReadFile调用ntdll 中的NtReadFile 。其中ReadFile函数是win32 API,而NtReadFile函数是Native API。
(2)ntdll中的NtReadFile进入到内核模式,并调用系统服务中的NtReadFile函数。
(3)系统服务NtReadFile创建IRP_MJ_WRITE 类型的事件,这时当前线程进入睡眠状态,也可以说当前线程被阻塞;
(4)在派遣函数中一般会将IRP请求结束,结束IRP是通过IoCompleteRequest函数。在IoCompleteRequest内部会设置刚才等待的事件,睡眠的线程恢复运行。
例如在读取一个很大文件时,ReadFile不会立刻返回,而是等待一段时间,这时间就是线程睡眠时间,IRP请求结束,标志这个操作完毕。
IoCompletRequest函数中第二个参数PriorityBoost代表一个优先级,指的是被阻塞的线程以何种优先级恢复运行。
一般情况下为IO_NO_INCREMENT,对某些特殊情况,需要将阻塞的线程以“优先”的身分运行,如键盘,鼠标等,它们需要更快的反应。
要打开设备,必须通过设备的名字得到设备句柄,每个设备都有设备名,如"\Device\MyDDKDevice",,
但是设备名无法被用户模式下的应用程序查询到,设备名只能被内核模式下的程序查询到。
在应用程序中,设备可以通过符号链接进行访问,驱动通过IoCreateSymbolicLink函数创建符号链接,HelloDDK驱动程序的设备所对应的符号链接是
"\??\HelloDDK"。在编写程序时,符号链接需要稍微修改一下,将前面的\??\改为\\.\。因此符号链接"\??\HelloDDK"就变成了\\.\HelloDDK,写成C语言的字符是
\\\\.\\HelloDDK。
首先介绍一个重要的数据结构---IO_STACK_LOCATION,即I/O 堆栈,这个数据结构和IRP紧密相连。
驱动对象会创建一个个的设备对象,并将这些设备对象“叠”成一个垂直结构,这种垂直结构很像栈,因此称“设备栈”。
IRP会被操作系统发送到设备栈的项层,如果项层的设备对象的派遣函数结束了IRP的请求,则这次I/O请求结束;如果没有将IRP的请求结束,那么OS将IRP转发到设备栈的下一个设备处理。
因此一个IRP可能会被转发多次,为了记录IRP在每层设备中做的操作,IRP会有一个IO_STACK_LOCATION数组。数组的元素应该大于IRP穿越过的设备数。
每个IO_STACK_LOCATION元素记录着对应设备中做的操作。对于本层设备对应的IO_STACK_LOCATION ,可以通过IoGetCurrentStackLocation函数得到,如:
IO_STACK_LOCATION 结构中会记录IRP的类型,即IO_STACK_LOCATION 中的MajorFunction子域。
下面的代码演示了派遣函数如何获得当前IO_STACK_LOCATION ,以及如何获得IRP的类型,在Driver中将所有的IRP类型都和一个派遣函数相关联:
/************************************************************************ * 函数名称:DriverEntry * 功能描述:初始化驱动程序,定位和申请硬件资源,创建内核对象 * 参数列表: pDriverObject:从I/O管理器中传进来的驱动对象 pRegistryPath:驱动程序在注册表的中的路径 * 返回 值:返回初始化驱动状态 *************************************************************************/ #pragma INITCODE extern "C" NTSTATUS DriverEntry ( IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath ) { NTSTATUS status; KdPrint(("Enter DriverEntry\n")); //设置卸载函数 pDriverObject->DriverUnload = HelloDDKUnload; //设置派遣函数 pDriverObject->MajorFunction[IRP_MJ_CREATE] = HelloDDKDispatchRoutin; pDriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloDDKDispatchRoutin; pDriverObject->MajorFunction[IRP_MJ_WRITE] = HelloDDKDispatchRoutin; pDriverObject->MajorFunction[IRP_MJ_READ] = HelloDDKDispatchRoutin; pDriverObject->MajorFunction[IRP_MJ_CLEANUP] = HelloDDKDispatchRoutin; pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = HelloDDKDispatchRoutin; pDriverObject->MajorFunction[IRP_MJ_SET_INFORMATION] = HelloDDKDispatchRoutin; pDriverObject->MajorFunction[IRP_MJ_SHUTDOWN] = HelloDDKDispatchRoutin; pDriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = HelloDDKDispatchRoutin; //创建驱动设备对象 status = CreateDevice(pDriverObject); KdPrint(("Leave DriverEntry\n")); return status; }
/************************************************************************ * 函数名称:HelloDDKDispatchRoutin * 功能描述:对读IRP进行处理 * 参数列表: pDevObj:功能设备对象 pIrp:从IO请求包 * 返回 值:返回状态 *************************************************************************/ #pragma PAGEDCODE NTSTATUS HelloDDKDispatchRoutin(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp) { KdPrint(("Enter HelloDDKDispatchRoutin\n")); PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); //建立一个字符串数组与IRP类型对应起来 static char* irpname[] = { "IRP_MJ_CREATE", "IRP_MJ_CREATE_NAMED_PIPE", "IRP_MJ_CLOSE", "IRP_MJ_READ", "IRP_MJ_WRITE", "IRP_MJ_QUERY_INFORMATION", "IRP_MJ_SET_INFORMATION", "IRP_MJ_QUERY_EA", "IRP_MJ_SET_EA", "IRP_MJ_FLUSH_BUFFERS", "IRP_MJ_QUERY_VOLUME_INFORMATION", "IRP_MJ_SET_VOLUME_INFORMATION", "IRP_MJ_DIRECTORY_CONTROL", "IRP_MJ_FILE_SYSTEM_CONTROL", "IRP_MJ_DEVICE_CONTROL", "IRP_MJ_INTERNAL_DEVICE_CONTROL", "IRP_MJ_SHUTDOWN", "IRP_MJ_LOCK_CONTROL", "IRP_MJ_CLEANUP", "IRP_MJ_CREATE_MAILSLOT", "IRP_MJ_QUERY_SECURITY", "IRP_MJ_SET_SECURITY", "IRP_MJ_POWER", "IRP_MJ_SYSTEM_CONTROL", "IRP_MJ_DEVICE_CHANGE", "IRP_MJ_QUERY_QUOTA", "IRP_MJ_SET_QUOTA", "IRP_MJ_PNP", }; UCHAR type = stack->MajorFunction; if (type >= arraysize(irpname)) KdPrint((" - Unknown IRP, major type %X\n", type)); else KdPrint(("\t%s\n", irpname[type])); //对一般IRP的简单操作,后面会介绍对IRP更复杂的操作 NTSTATUS status = STATUS_SUCCESS; // 完成IRP pIrp->IoStatus.Status = status; pIrp->IoStatus.Information = 0; // bytes xfered IoCompleteRequest( pIrp, IO_NO_INCREMENT ); KdPrint(("Leave HelloDDKDispatchRoutin\n")); return status; }
将驱动程序成功加载后,执行应用程序,应用程序就是“打开”和“关闭”设备。随后用DbugView查看驱动输出的log信息。
可以发现仿次进入的是IRP_MJ_CREATE、IRP_MJ_CLEANUP和IRP_MJ_CLOSE。如下图:
IRP是驱动程序中重要的数据结构,可以说驱动程序的运行是由IRP所“驱动”的。在驱动程序中,仅凭查看LOG信息是不能满足调试需要的,程序员
往往需要更直观的跟踪IRP的传递、转发、结束等操作。
有时候IRP的处理非常复杂,跟踪IRP就重要了。这里介绍珍上工具软件IRPTrace,这个软件可以方便地跟踪IRP的各种操作。
(1)IRPTrace界面
(2)用IRPTrace跟踪各类IRP
选择图标,会弹出一个窗口,如下图所示,选中左边的“drivers”选项卡,程序会枚举出系统加载的所有驱动程序。
在本例中我们选择驱动程序HelloDDK,这里右边公列出需要跟踪的IRP类型,选择ALL。
然后在下面的设备中选择(这个如果不选,可能导致看不到消息):
(3)用IRPTrace观察IRP数据结构中的各项内容
运行应用程序,然后在IRPTrace程序中会发现三个IRP被跟踪下来,分别是IRP_MJ_CREATE、IRP_MJ_CLEANUP、IRP_MJ_CLOSE,
和预期的一致。
我们也可以通过IRPTrace来查看IRP_MJ_READ消息,并知道读到的值是多少:
在右下角的Output Parameters项中可以看到:
读到的值:
(3)用winDbg查看IRPTrace 打印出来的信息,用来了解IRP的执行过程:
如果在IRPTrace中选择了Capture Kenel output:
那么我们在windbg会发现先执行完了所有的IRP,然后才打印具体的消息:
上图中打印出了第一个IRP:IRP_MJ_CREATE。
接下来是IRP_MJ_READ:
奇怪的事情发生了,虽然我们的驱动程序中并没有针对IRP_MJ_CLEANUP、IRP_MJ_CLOSE的派遣函数,可是这里却调用了系统的IRP派遣。
驱动程序所创建的设备一般会有三种读写方式:一种是缓冲区方式,一种是直接方式,一种是其他方式。这里主要介绍缓冲区方式。
在驱动程序创建设备对象的时候,需要考虑好设备是采用何种读写方式。当IoCreateDevice创建设备完成后,需要对设备对象的Flags子域进行设置。
设置不同的Flags会导致不同的方式操作设备。
//创建设备名称 UNICODE_STRING devName; RtlInitUnicodeString(&devName,L"\\Device\\MyDDKDevice"); //创建设备 status = IoCreateDevice( pDriverObject, sizeof(DEVICE_EXTENSION), &(UNICODE_STRING)devName, FILE_DEVICE_UNKNOWN, 0, TRUE, &pDevObj ); if (!NT_SUCCESS(status)) return status; // 设置读写方式 pDevObj->Flags |= DO_BUFFERED_IO;
设备对象的三种读写方式的Flags 分别对应为DO_BUFFERED_IO、DO_RIRECT_IO和0。缓冲区方式读写相对简单。
读写操作一般是由ReadFile或WriteFile函数引起的,这里先以WriteFile 为例。WriteFile要求用户提供一段缓冲区,并且说明缓冲区的大小,然后WriteFile
将这段内存的数据存入到驱动程序中。
这段缓冲区内存是用户模式的内存地址,驱动程序加载如果直接引用这段内存是十分危险的。因为Windows操作系统是多任务的,它可能随时切换到别的进程。
如果驱动程序需要访问这段内存,而这时OS已经切换到另一个进程,这样驱动程序访问的内存地址必定是错误的。
有很多方法可以解决这个问题,其中一个方法是使用缓冲区方式读写。对于这种方法,操作系统将应用程序提供缓冲区的数据复制到内核模式下的地址中。
这样,无论操作系统如何切换进程,内核模式地址都不会改变。IRP的派遣函数将会对内核模式下的缓冲区操作,而不是操作用户模式地址的缓冲区。
这样做的优点是,比较简单地解决了将用户地址传入驱动的问题。缺点是需要在用户模式和内核模式之间复制数据,影响运行效率。
以缓冲区方式写设备时,操作系统将WriteFile 提供的用户模式的缓冲区复制到内核 模式下。这个地址由WriteFile创建的IRP的AssociatedIrp.SystemBuffer子域记录。
以“缓冲区”方式读设备时,操作系统会分配一段内核模式下的内存。这段内存大小等于ReadFile或者WriteFile的指定的字节数,并且ReadFile或WriteFile 创建的IRP的
AssociatedIrp.SystemBuffer子域会记录这段内存地址。当IRP请求结束时(一般都是由IoCompleteRequest函数结束IRP),这段内存地址会被复制到ReadFile提供的
缓冲区中。
以缓冲区方式无论是“读”还是“写”设备,都会发生用户模式与内核模式地址的数据复制,复制过程由操作系统负责。
另外,在派遣函数中,也可以通过IO_STACK_LOCATION中的Parameters.Read.Length子域知道ReadFile请求多少字节。
然而,WriteFile和ReadFile指定对设备操作多少字节,并不意味着操作了这么多字节,在派遣函数中,应该设备IRP的子域IoStatus.Information,这个子域记录实际
操作了多少字节。
下面代码演示了如何利用“缓冲区”方式读设备,本例的驱动程序返回给应用程序的数据都是0xAA :
NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp) { KdPrint(("Enter HelloDDKRead\n")); //对一般IRP的简单操作,后面会介绍对IRP更复杂的操作 NTSTATUS status = STATUS_SUCCESS; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); ULONG ulReadLength = stack->Parameters.Read.Length; // 完成IRP //设置IRP完成状态 pIrp->IoStatus.Status = status; //设置IRP操作了多少字节 pIrp->IoStatus.Information = ulReadLength; // bytes xfered memset(pIrp->AssociatedIrp.SystemBuffer,0xAA,ulReadLength); //处理IRP IoCompleteRequest( pIrp, IO_NO_INCREMENT ); KdPrint(("Leave HelloDDKRead\n")); return status; }
应用程序使用ReadFile对设备进行读写:
int main() { HANDLE hDevice = CreateFile("\\\\.\\HelloDDK", GENERIC_READ | GENERIC_WRITE, 0, // share mode none NULL, // no security OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); // no template if (hDevice == INVALID_HANDLE_VALUE) { printf("Failed to obtain file handle to device: " "%s with Win32 error code: %d\n", "MyWDMDevice", GetLastError() ); return 1; } UCHAR buffer[10]; ULONG ulRead; BOOL bRet = ReadFile(hDevice,buffer,10,&ulRead,NULL); if (bRet) { printf("Read %d bytes:",ulRead); for (int i=0;i<(int)ulRead;i++) { printf("%02X ",buffer[i]); } printf("\n"); } CloseHandle(hDevice); return 0; }
P200