现在的位置: 首页 > 综合 > 正文

Window XP驱动开发(二十三)Window驱动的派遣函数

2013年08月30日 ⁄ 综合 ⁄ 共 10743字 ⁄ 字号 评论关闭
文章目录

转载请标明是引用于 http://blog.csdn.net/chenyujing1234 

欢迎大家拍砖

 

参考书籍<<Windows驱动开发技术详解>>

 

用户模式下所有对驱动程序的I/O请求,全部由操作系统转化一个叫做IRP的数据结构,不同的IRP数据会被“派遣”到不同

的派遣函数(Dispatch Function)中,这也是派遣函数名字的由来。

1、 IRP与派遣函数

IRP的处理机制类似Windows应用程序中的“消息处理”机制,驱动程序收到不同类型的IRP后,会进入不同的派遣函数

1、1 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与派遣函数关系如下:

 

1、2 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的即插即用组件在即将关闭系统时发出的。

 

1、3 对派遣函数的简单处理

大部分的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,对某些特殊情况,需要将阻塞的线程以“优先”的身分运行,如键盘,鼠标等,它们需要更快的反应。

 1、4  通过设备链接打开设备

要打开设备,必须通过设备的名字得到设备句柄,每个设备都有设备名,如"\Device\MyDDKDevice",,

但是设备名无法被用户模式下的应用程序查询到,设备名只能被内核模式下的程序查询到

 在应用程序中,设备可以通过符号链接进行访问,驱动通过IoCreateSymbolicLink函数创建符号链接,HelloDDK驱动程序的设备所对应的符号链接是

"\??\HelloDDK"。在编写程序时,符号链接需要稍微修改一下,将前面的\??\改为\\.\。因此符号链接"\??\HelloDDK"就变成了\\.\HelloDDK,写成C语言的字符是

\\\\.\\HelloDDK

1、5 编写一个更通用的派遣函数

首先介绍一个重要的数据结构---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。如下图:

 

 

1、6 跟踪IRP的利器IRPTrace

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派遣

 

2、缓冲区方式读写操作

驱动程序所创建的设备一般会有三种读写方式:一种是缓冲区方式,一种是直接方式,一种是其他方式。这里主要介绍缓冲区方式。

2、1 缓冲区设备

在驱动程序创建设备对象的时候,需要考虑好设备是采用何种读写方式。当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的派遣函数将会对内核模式下的缓冲区操作,而不是操作用户模式地址的缓冲区。

这样做的优点是,比较简单地解决了将用户地址传入驱动的问题。缺点是需要在用户模式和内核模式之间复制数据,影响运行效率

2、2 缓冲区设备读写

 以缓冲区方式写设备时,操作系统将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

 

 

 

 

 

抱歉!评论已关闭.