驱动开发笔记5—驱动对象、设备对象、IRP和派遣函数

驱动对象

每个驱动程序都会有唯一的驱动对象与之对应,并且这个驱动对象是在驱动加载的时候,被内核中的对象管理程序所创建的。

0: kd> dt _DRIVER_OBJECT
nt!_DRIVER_OBJECT
   +0x000 Type             : Int2B
   +0x002 Size             : Int2B
   +0x004 DeviceObject     : Ptr32 _DEVICE_OBJECT	//驱动程序创建的第一个设备对象,通过它可以遍历驱动对象里的所有设备对象。
   +0x008 Flags            : Uint4B
   +0x00c DriverStart      : Ptr32 Void
   +0x010 DriverSize       : Uint4B
   +0x014 DriverSection    : Ptr32 Void				//对应LDR_DATA_TABLE_ENTRY结构体,双向链表。可通过DriverSection链表遍历系统模块
   +0x018 DriverExtension  : Ptr32 _DRIVER_EXTENSION
   +0x01c DriverName       : _UNICODE_STRING		//驱动程序的名称,一般为"\Driver\DriverName"
   +0x024 HardwareDatabase : Ptr32 _UNICODE_STRING	//设备的硬件数据库键名,一般为"\Registry\Machine\Hardware\Description\System"
   +0x028 FastIoDispatch   : Ptr32 _FAST_IO_DISPATCH	//文件驱动中用到的派遣函数
   +0x02c DriverInit       : Ptr32     long			//指向DriverEntry函数,这是通过IO管理器来建立的
   +0x030 DriverStartIo    : Ptr32     void			//记录StartIO的函数地址,用于串行化操作 
   +0x034 DriverUnload     : Ptr32     void 		//驱动卸载例程
   +0x038 MajorFunction    : [28] Ptr32     long 	//一个函数指针数组,数组中的每个成员记录着一个指针,每个指针指向一个IRP的派遣函数

设备对象

每个驱动程序会创建一个或多个设备对象,用 DEVICE_OBJECT 数据结构表示。每个设备对象都会有一个指针指向下一个设备对象,最后一个设备对象指向空,因此就形成一个设备链。设备链的第一个设备就是DriverObject->DeviceObject
设备对象是由程序员自己创建的(IoCreateDevice),因此在驱动被卸载时,要遍历设备对象,挨个将其删除(IoDeleteDevice)。

0:021> dt _DEVICE_OBJECT
ntdll!_DEVICE_OBJECT
   +0x000 Type             : Int2B
   +0x002 Size             : Uint2B
   +0x004 ReferenceCount   : Int4B					//引用计数
   +0x008 DriverObject     : Ptr32 _DRIVER_OBJECT	//所属的驱动对象
   +0x00c NextDevice       : Ptr32 _DEVICE_OBJECT	//指向下一个设备对象(设备链)
   +0x010 AttachedDevice   : Ptr32 _DEVICE_OBJECT	//上一层的设备对象(设备栈)
   +0x014 CurrentIrp       : Ptr32 _IRP			//在使用StartIO例程时,指向当前IRP指针
   +0x018 Timer            : Ptr32 _IO_TIMER	//计时器指针
   +0x01c Flags            : Uint4B				//一个32位的无符号整型,每一位由具体的含义
   +0x020 Characteristics  : Uint4B				//设备对象特性
   +0x024 Vpb              : Ptr32 _VPB
   +0x028 DeviceExtension  : Ptr32 Void			//设备的扩展对象,每个设备都会指定一个设备扩展对象,设备扩展对象记录的是设备自己特殊定义的结构体,也就是程序员自己定义的结构体。在驱动程序中,应尽量避免全局变量,而改用设备扩展
   +0x02c DeviceType       : Uint4B				//设备类型
   +0x030 StackSize        : Char				//在多层驱动的情况下,驱动与驱动之间会形成类似堆栈的结构,IRP会依次从最高层传递到最底层,StsckSize描述的就是这个层数
   +0x034 Queue            : <anonymous-tag>	//IRP链表
   +0x05c AlignmentRequirement : Uint4B			//设备在大容量传输的时候,需要内存对齐,以保证传输速度
   +0x060 DeviceQueue      : _KDEVICE_QUEUE		//用来实现串行的IRP队列头
   +0x074 Dpc              : _KDPC				//延迟过程调用
   +0x094 ActiveThreadCount : Uint4B			//当前线程的数量
   +0x098 SecurityDescriptor : Ptr32 Void		//安全描述符表
   +0x09c DeviceLock       : _KEVENT			//设备锁
   +0x0ac SectorSize       : Uint2B
   +0x0ae Spare1           : Uint2B
   +0x0b0 DeviceObjectExtension : Ptr32 _DEVOBJ_EXTENSION	//设备对象扩展
   +0x0b4 Reserved         : Ptr32 Void

设备对象由IoCreateDevice创建,创建完成后,需要对设备对象的Flags子域进行设置,设置不同的Flags,会导致以不同的方式操作设备。介绍完IRP后会详细介绍设备的读写方式。

IRP和派遣函数

IRP

I/O Request Package,即输入输出请求包。应用程序与驱动程序通信时,应用程序会发出I/O请求,操作系统将I/O请求转化为相应的IRP数据,不同类型的IRP会根据类型传递到不同的派遣函数内。

IRP类型

IRP类型 来源
IRP_MJ_CREATE 创建设备,如CreateFile
IRP_MJ_READ 读设备,如ReadFile
IRP_MJ_WRITE 写设备,如WriteFile
IRP_MJ_QUERY_INFORMATION 获取设备信息,如GetFileSize
IRP_MJ_SET_INFORMATION 设置设备信息,如SetFileSize
IRP_MJ_DEVICE_CONTROL 自定义操作,如DeviceIoControl
IRP_MJ_SYSTEM_CONTROL 系统内部产生的控制信息,类似于内核调用DeviceIoControl函数
IRP_MJ_CLOSE 关闭设备,如CloseHandle
IRP_MJ_CLEANUP 清除工作,如CloseHandle
IRP_MJ_SHUTDOWN 关闭系统前会产生此IRP
IRP_MJ_PNP 即插即用消息,NT驱动不支持,只有WDM驱动才支持
IRP_MJ_POWER 操作系统处理电源消息时,产生此IRP

设置派遣函数

DriverObject->MajorFunction是个函数指针数组,数组的每个元素都记录着一个函数地址,通过这个数组,可以把IRP类型和派遣函数关联起来

一般来说,NT式驱动程序和WDM驱动程序都是在DriverEntry中注册派遣函数。而在进入DriverEntry之前,操作系统会将 _IopInvalidDeviceRequest 的地址填满整个MajorFunction数组。

NTSTATUS NTAPI IopInvalidDeviceRequest(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    
    
	Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;
	Irp->IoStatus.Information = 0;
	IoCompleteRequest(Irp, IO_NO_INCREMENT);
	return STATUS_INVALID_DEVICE_REQUEST;
}

在DriverEntry中设置派遣函数:

DriverObject->MajorFunction[IRP_MJ_CREATE] = CreateThroughDispatch;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = CloseThroughDispatch;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = ControlThroughDispatch;

上面的例子中只对三种类型的IRP设置了派遣函数,但是IRP的类型并不只有三种,对于没有设置的IRP类型,系统默认这些IRP类型与 _IopInvalidDeviceRequest 函数关联。

处理IRP

处理IRP最简单的方法就是:在派遣函数中将IRP的状态设置为成功,然后结束IRP的请求,并让派遣函数返回成功。

扫描二维码关注公众号,回复: 13266763 查看本文章
NTSTATUS DispatchRoutin(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    
    
	NTSTATUS Status = STATUS_SUCCESS;
	Irp->IoStatus.Information = 0;			 //设置IRP操作了多少字节
	Irp->IoStatus.Status = STATUS_SUCCESS;	 //设置IRP的完成状态
	IoCompleteRequest(Irp, IO_NO_INCREMENT); //结束IRP请求
	return Status;
}

举例说明

我们以WriteFile函数为例,它的调用过程如下:

  • 用户程序调用WriteFile函数,WriteFile调用ntdll!NtWriteFile。
  • ntdll!NtWriteFile通过系统调用进入内核,调用SSDT中的系统服务NtWriteFile。
  • 系统服务NtWriteFile创建 IRP_MJ_WRITE 类型的IRP,将其发送到某个驱动的派遣函数中,然后进入睡眠状态(等待一个事件)。
  • 派遣函数通过调用 IoCompleteRequest 将IRP结束(函数内部设置事件),睡眠线程恢复运行。

设备读写方式

驱动程序所创建的设备一般会有三种读写方式,缓冲区方式(DO_BUFFERES_IO)、直接方式(DO_DIRECT_IO)、其他方式(0)。

读写操作一般是由ReadFile或WriteFile函数引起的。以WriteFile函数为例,用户程序调用它时,会提供一段缓冲区及大小,然后将其传递给驱动程序。

正常来说,驱动程序直接引用这块缓冲区是很危险的,因为Windows是多任务的,随时可能切换到其他进程。举个例子:A进程将0x4000地址传给驱动程序,驱动程序访问0x4000时,系统切换到了进程B,那么驱动程序访问到的是进程B的0x4000地址,这是很严重的错误。

获得读/写操作请求的字节数:

PIO_STACK_LOCATION IoStackLocation = IoGetCurrentIrpStackLocation(Irp);
ULONG ReadLength = IoStackLocation->Parameters.Read.InputBufferLength;
ULONG WriteLength = IoStackLocation->Parameters.Write.InputBufferLength;

缓冲区方式读写

操作系统将应用程序提供缓冲区的数据复制到内核模式下的地址中,即 Irp->AssociatedIrp.SystemBuffer。这块地址由操作系统分配和释放。IRP的派遣函数将会对内核缓冲区操作,不管进程如何切换,内核缓冲区地址都不会改变。但缺点是复制数据会影响效率。

DeviceObject->Flags |= DO_BUFFERES_IO;	// 设置缓冲区方式读写

直接方式读写

操作系统会将这块用户缓冲区锁住,然后将这块缓冲区在内核模式地址中再映射一次。这样,用户模式的缓冲区和内核模式的缓冲区指向的是同一块物理内存。不管进程怎么切换,内核缓冲区地址都不会改变。

DeviceObject->Flags |= DO_DIRECT_IO;	// 设置缓冲区方式读写

操作系统把用户层的地址空间映射到内核空间,这需要在页表中增加一个映射,通过MDL(Memory Descriptor Link,内存描述符链表)来实现。

#define MmGetMdlByteCount(_Mdl)		 ((_Mdl)->ByteCount)
#define MmGetMdlByteOffset(_Mdl)	 ((_Mdl)->ByteOffset)
#define MmGetMdlBaseVa(Mdl)			 ((Mdl)->StartVa)
#define MmGetMdlVirtualAddress(_Mdl) ((PVOID) ((PCHAR) ((_Mdl)->StartVa) + (_Mdl)->ByteOffset))
// 获得锁定缓冲区的长度,值应该等于IoStackLocation->Parameters.Read.InputBufferLength;
ULONG MdlLength = MmGetMdlByteCount(Irp->MdlAddress);
// 获得锁定缓冲区的首地址
PVOID MdlAddress = MmGetMdlVirtualAddress(Irp->MdlAddress);
// 获得锁定缓冲区的偏移
ULONG MdlOffset = MmGetMdlByteOffset(Irp->MdlAddress);

// 获得MDL在内核模式下的映射
PVOID KernelAddress = MmGetSystemAddressForMdlSafa(Irp->MdlAddress, NormalPagePriority);

其他方式读写

不设置Flags时默认采用这种方式,派遣函数直接读写应用程序提供的缓冲区地址,即 Irp->UserBuffer。但这样很危险,只有驱动程序和应用程序运行在相同线程上下文的情况下,才能使用这种方式。

猜你喜欢

转载自blog.csdn.net/qq_42814021/article/details/121008646