概述
文章目录
- 驱动对象
- 设备对象
- IRP和派遣函数
- IRP
- IRP类型
- 设置派遣函数
- 处理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 //驱动程序的名称,一般为"DriverDriverName"
+0x024 HardwareDatabase : Ptr32 _UNICODE_STRING //设备的硬件数据库键名,一般为"RegistryMachineHardwareDescriptionSystem"
+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的请求,并让派遣函数返回成功。
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。但这样很危险,只有驱动程序和应用程序运行在相同线程上下文的情况下,才能使用这种方式。
最后
以上就是失眠流沙为你收集整理的驱动开发笔记5—驱动对象、设备对象、IRP和派遣函数驱动对象设备对象IRP和派遣函数设备读写方式的全部内容,希望文章能够帮你解决驱动开发笔记5—驱动对象、设备对象、IRP和派遣函数驱动对象设备对象IRP和派遣函数设备读写方式所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复