1、深度剖析 WinPcap 之(三)所涉及的 Windows 驱动基础知识(1)1.1 Windows 驱动的基础知识本节主要描述在 WinPcap 的 NPF 中经常使用一些编写 Windows 驱动程序所需掌握的部分基础知识,以便于后面的理解。1.1.1 驱动对象(DRIVER_OBJECT)每个驱动程序都有唯一的驱动对象与之对应,该驱动对象在驱动程序被加载时由内核的对象管理程序所创建。驱动对象用 DRIVER_OBJECT 数据结构表示,它作为驱动程序的一个实例被内核加载,对一个驱动程序内核 I/O 管理器只加载一个实例。驱动对象数据结构在 wdm.h 文件中的定义如下。typedef s
2、truct _DRIVER_OBJECT CSHORT Type;CSHORT Size;/*DeviceObject 为每个驱动程序所创建的一个或多个设备对象链表,*Flags 提供一个扩展的标识定位驱动对象*/PDEVICE_OBJECT DeviceObject;ULONG Flags;/*下列各成员字段描述驱动程序从哪儿被加载*/PVOID DriverStart;ULONG DriverSize;PVOID DriverSection;PDRIVER_EXTENSION DriverExtension;/*DriverName 成员被错误日志线程用来*确定一个 I/O 请求越界的驱动
3、名称*/UNICODE_STRING DriverName;/*指向注册表中硬件信息的路径*/PUNICODE_STRING HardwareDatabase;/*如果驱动支持“fast I/O”,*就指向一个“fast I/O”的派遣函数数组*/PFAST_IO_DISPATCH FastIoDispatch;/*描述该特定驱动的入口点。*主函数(major function)派遣函数表必须是对象最后的成员,*因此它仍然是可扩展的*/PDRIVER_INITIALIZE DriverInit;PDRIVER_STARTIO DriverStartIo;PDRIVER_UNLOAD Drive
4、rUnload;PDRIVER_DISPATCH MajorFunctionIRP_MJ_MAXIMUM_FUNCTION + 1; DRIVER_OBJECT;typedef struct _DRIVER_OBJECT *PDRIVER_OBJECT;下面分别描述驱动对象中驱动程序可访问的成员。PDEVICE_OBJECT DeviceObject每个驱动对象会有一个或多个设备对象。每个设备对象都有一个指针(NextDevice)指向下一个驱动对象,最后一个设备对象指向空。此处的DeviceObject 指向驱动对象的第一个设备对象。该成员在成功调用IoCreateDevice 后自动更新。
5、一个驱动程序使用该程成员与设备对象(DEVICE_OBJECT)的 NextDevice 可遍历给驱动对象的所有设备对象。在驱动被卸载的时候,需要遍历每个设备对象,并将其删除。PUNICODE_STRING HardwareDatabase指向注册表中硬件配置信息的路径,用 UNICODE 字符串表示。该字符串一般为REGISTRYMACHINEHARDWAREDESCRIPTIONSYSTEM。PFAST_IO_DISPATCH FastIoDispatch指向一个定义驱动快速 I/O 结构体的入口点,该成员只用于文件系统驱动与网络传输驱动。PDRIVER_INITIALIZE Driver
6、Init是 DriverEntry 例程的入口点,由 I/O 管理器设置。PDRIVER_STARTIO DriverStartIo是 Startl0 例程的的入口点,如果需要,由 DriverEntry 例程设置,否则为 NULL。PDRIVER_UNLOAD DriverUnload指向驱动卸载时所用回调函数的入口点。PDRIVER_DISPATCH MajorFunctionIRP_MJ_MAXIMUM_FUNCTION+1一个函数指针数组, 数组 MajorFunction 中的每个成员保存着一个指针,每一个指针指向一个处理对应 IRP(IRP_MJ_XXX)的派遣函数(Dispatc
7、hXxx)。每个派遣函数(DispatchXxx)声明如下:NTSTATUS(*PDRIVER_DISPATCH) (IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp);1.1.2 设备对象(DEVICE_OBJECT)设备对象保存设备特征和状态的信息。一个设备对象表示一个逻辑的、虚拟的或物理的设备,由一个驱动对象操控设备对象的 I/O 请求。每一个内核模式的驱动必须创建设备对象,通过调用 IoCreateDevice 一次或多次。每个驱动程序会创建一个或多个设备对象,用 DEVICE_OBJECT 数据结构表示。每个设备对象有一个指针(NextDevice
8、)指向下一个设备对象,从而形成一个设备链表。设备链表第一个设备是由驱动对象结构体中 DeviceObject 指明的。设备对象数据结构在 wdm.h 文件中的定义如下。typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _DEVICE_OBJECTCSHORT Type;USHORT Size;LONG ReferenceCount;struct _DRIVER_OBJECT *DriverObject;struct _DEVICE_OBJECT *NextDevice;struct _DEVICE_OBJECT *Attach
9、edDevice;struct _IRP *CurrentIrp;PIO_TIMER Timer;ULONG Flags;ULONG Characteristics;_volatile PVPB Vpb;PVOID DeviceExtension;DEVICE_TYPE DeviceType;CCHAR StackSize;union LIST_ENTRY ListEntry;WAIT_CONTEXT_BLOCK Wcb; Queue;ULONG AlignmentRequirement;KDEVICE_QUEUE DeviceQueue;KDPC Dpc;/*下列成员是为支持文件系统的互斥操
10、作,*为了对文件系统处理线程使用设备的计数保持跟踪*/ULONG ActiveThreadCount;PSECURITY_DESCRIPTOR SecurityDescriptor;KEVENT DeviceLock;USHORT SectorSize;USHORT Spare1;struct _DEVOBJ_EXTENSION *DeviceObjectExtension;PVOID Reserved; DEVICE_OBJECT;typedef struct _DEVICE_OBJECT *PDEVICE_OBJECT;下面分别描述设备对象中驱动程序可访问的成员。PDRIVER_OBJEC
11、T DriverObject指向驱动程序中的驱动对象。同属于一个驱动程序的驱动对象指向的是同一个驱动对象。PDEVICE_OBJECT NextDevice指向下一个设备对象。这里的下一个设备对象是同一个驱动程序创建的若干设备对象。每个设备对象根据 NextDevice 域形成链表,从而可以遍历每个设备对象。在每次成功调用 IoCreateDevice 后 I/O 管理器更新该链表。在驱动被卸载的时候,需要遍历该链表,删除每个设备对象。PIRP CurrentIrp如果驱动使用 Startl0 例程时,此成员指向当前 IRP 结构。否则为 NULL。 ULONG Flags此成员是一个 32
12、位昀无符号整型,每个位有不同的含义。通过位或操作为新创建的设备对象设置不同的特性。ULONG Characteristics当驱动程序调用 IoCreateDevice 时,设置下列一个合适的值:FILE_REMOVABLE_MEDIAFILE_READ_ONLY_DEVICEFILE_FLOPPY_DISKETTEFILE_WRITE_ONCE_MEDIAFILE_DEVICE_SECURE_OPENPVOID DeviceExtension指向设备扩展对象。设备扩展对象是由程序员在驱动中自行定义的结构体,结构体的大小在调用 IoCreateDevice 时设置。每个设备都会指定一个设备扩展
13、对象,设备扩展对象记录的是特别定义的结构体。在驱动程序中应该尽量避免全局变量的使用,因为全局变量涉及不容易同步的问题,解决的办法可将全局变量存储在设备扩展中。DEVICE_TYPE DeviceType指明设备的类型,由 IoCreateDevice 设置。根据设备需要填写相应的设备类型。.CCHAR StackSize在多层驱动的情况下,驱动与驱动之间形成类似堆栈的结构。IRP 会依次从最高层传递到最底层。StackSize 就是指定发送到该驱动的 IRP 在堆栈位置的最小层数。IoCreateDevice 在一个新创建的设备对象中设置该成员。ULONG AlignmentRequireme
14、nt设备在大容量传输的时候,为了保证传输速度需要内存对齐。每个设备对象在它新创建的设备对象中设置该成员。1.1.3 设备扩展(_DEVICE_EXTENSION)设备对象记录设备的“通用”信息,而另外一些“特殊”信息记录在设备扩展中。设备扩展由程序员自行定义,指定内容与大小,由 I/O 管理器创建,保存在非分页内存中。在驱动程序中,尽量避免使用全局函数,因为全局函数往往导致函数的不可重入性。一个解决办法就是将全局变量以设备扩展的形式存储,并加以适当的同步保护措施。WinPcap 中 NPF 的设备扩展结构体,主要用于存储每个被 NPF 绑定的适配器的一些信息,结构体定义如下:typedef s
15、truct _DEVICE_EXTENSION /适配器名称NDIS_STRING AdapterName;/设备导出的名称,也就是通过 WinPcap 应用程序使用该名称来打开该适配器PWSTR ExportString; DEVICE_EXTENSION, *PDEVICE_EXTENSION;1.1.4 IRP 与派遣函数驱动程序的主要功能是负责处理 l/O 请求,大部分 1/0 请求是在派遣函数中处理的。IRP 的处理机制类似 Windows 应用程序中的“消息处理”机制。用户空间对驱动程序的所有 1/0 请求,全部由操作系统转化为一个 IRP 数据结构,不同的 IRP 数据会被“派遣
16、”到不同的派遣函数中,在派遣函数中处理 IRP。 IRP 是 Windows 内核中输入输出请求包(I/O Request Package,IRP)的缩写。IRP 具有两个基本属性:MajorFunction 与 MinorFunction,分别记录 IRP的主功能和子功能。操作系统根据 MajorFunction 将 IRP“派遣”到不同的派遣函数中,在派遣函数中还可以根据 MinorFunction 继续判断该 IRP 属于哪种子功能。一般来说驱动程序都是在 DriverEntry 函数中注册派遣函数的。在DriverEntry 的驱动对象 pDriverObject 中,有个函数指针数组
17、MajorFunction,每个数组元素都记录着一个派遣函数的地址。通过设置该数组,可以将不同类型的 IRP 和对应的派遣函数关联起来。大部分的 IRP 都源于文件 I/O 处理的 API,如CreateFile、ReadFile、WriteFile、CloseHandle 等函数会使操作系统产生IRP_MJ_CREATE、IRP_MJ_READ、IRP_ MJ_WRITE、IRP_MJ_CLOSE 等不同类型的IRP,这些 IRP 会被传送到驱动程序中,调用对应的派遣函数。此外,内核中的文件 I/O 处理函数,如 ZwCreateFile、ZwReadFile、ZwWriteFile、ZwC
18、lose 也同样会创建 IRP_MJ_CREATE、IRP_MJ_READ、lRP_MJ_WRITE、IRP_MJ_CLOSE 等IRP,并将 lRP 传送到驱动程序中,调用对应的派遣函数。 处理 IRP 最简单的方法就是在相应的派遣函数中,将 IRP 的状态设置为成功,然后结束 IRP 的请求,并让派遣函数返回成功。结束 IRP 的请求使用函数IoCompleteRequest。下面为 NPF 中 NPF_Close 的代码,为处理 IRP_MJ_CLOSE类型 IRP 的派遣函数。NTSTATUS NPF_Close(IN PDEVICE_OBJECT DeviceObject,IN PI
19、RP Irp)Irp-IoStatus.Status = STATUS_SUCCESS;Irp-IoStatus.Information = 0;IoCompleteRequest(Irp, IO_NO_INCREMENT);return STATUS_SUCCESS;函数 NPF_Close 设置 IRP 的完成状态为 STATUS_SUCCESS。这样发起请求的 API(如 CloseHandle)将会返回 TRUE。相反,如果将 IRP 的完成状态设置为不成功,发起 I/O 请求的 API(如 CloseHandle)将会返回 FALSE。出现该情况时,可以使用 GetLastError
20、 API 获得错误代码。所得的错误代码会和 IRP 设置的状态一致。除了设置 IRP 的完成状态,函数还要设置这个 IRP 请求操作了多少字节。在本代码中,将操作字节数设置成了 0。如果是 ReadFile 产生的 IRP,这个字节数代表从设备读了多少字节。如果是 WriteFile 产生的 IRP,这个字节数代表对设备写了多少字节。最后函数通过 IoCompleteR0quest 函数将 IRP 请求结束。IoCompleteRequest 的声明如下:VOID IoCompleteRequest(IN PIRP Irp,IN CCHAR PriorityBoost);参数 Irp 代表需要
21、被结束的 IRP。参数 PriorityBoost 代表线程恢复时的优先级别,指的是被阻塞的线程以何种优先级恢复运行。一般情况下,优先级设置为 IO_NO_INCREMENT。1.5.1 同步处理Windows 是个多任务抢占式的操作系统,如果没有同步机制的控制,所有的线程会任意运行。如果多个线程要求操作同一个资源,这时就需要同步处理。如果驱动程序没有很好地处理同步问题,程序会出错误、操作系统的性能下降、甚至出现死锁等现象。1.5.1.1 自旋锁自旋锁是一种同步处理机制,它能保证某个资源只能被一个线程所拥有,可用于驱动程序的同步处理。自旋锁的作用一般是使各派遣函数之间同步。初始化的自旋锁处于解
22、锁状态,这时它可以被程序“获取”。“获取”后的自旋锁处于锁住状态,不能被再次“获取”。锁住的自旋锁必须被“释放”后,才能再次被 “获取”。如果自旋锁已被锁住,这时有程序申请“获取”这个自旋锁,程序则处于“自旋”状态。所谓自旋状态,就是不停地询问是否可以“获取”自旋锁。自旋锁不同于线程中的等待事件。在线程中如果等待某个事件,操作系统会使这个线程进入休眠状态,CPU 会运行其它的线程。而自旋锁则不同,CPU 不会切换到别的线程,而是让这个线程一直“自旋”等待。因此,对自旋锁占用时间不宜过长,否则会导致申请自旋锁的其它线程处于自旋,浪费 CPU 的处理时间。NDIS 库提供的自旋锁可用来在相同 IR
23、QL 的线程之间同步访问共享资源,当共享资源的两个线程运行在不同的 IRQL 时,NDIS 库提供一种机制临时提升低 IRQL 代码的 IRQL,从而达到对共享资源的串行访问。NDIS 库的自旋锁用 NDIS_SPIN_LOCK 数据结构表示。使用自旋锁前,首先对其初始化,可使用 NdisAllocateSpinLock 函数。VOID NdisAllocateSpinLock( IN PNDIS_SPIN_LOCK SpinLock );申请获得自旋锁可以使用 NdisAcquireSpinLock 函数。VOID NdisAcquireSpinLock( IN PNDIS_SPIN_LOC
24、K SpinLock );释放自旋锁使用 NdisReleaseSpinLock 内核函数,VOID NdisReleaseSpinLock(IN PNDIS_SPIN_LOCK SpinLock );下面的代码为 WinPcap 中使用自旋锁的实例:NDIS_SPIN_LOCK WriteLock;/在_OPEN_INSTANCE 声明自旋锁NdisAllocateSpinLock(/初始化自旋所NdisAcquireSpinLock(/ 申请获得自旋锁if(Open-WriteInProgress)NdisReleaseSpinLock( / 释放自旋锁SET_FAILURE_UNSUCC
25、ESSFUL();break;elseOpen-WriteInProgress = TRUE;NdisReleaseSpinLock(/ 释放自旋锁1.1.5.2 用户模式的等待在应用程序中,可以使用 WaitForSingleObject 等待一个同步对象。WaitForSingleObject 函数声明如下:DWORD WaitForSingleObject( HANDLE hHandle, /同步对象句柄DWORD dwMilliseconds /等待时间);第二个参数 dwMiUiseconds 是等待时间,单位为毫秒。同步对象有两种状态,一种是激发状态,一种是未激发状态。如果同步对象
26、处于未激发状态,WaitForSingleObject 则进入休眠,等待同步对象被激发。如果同步对象在指定的等待时间内,还没有处于激发状态,则自动停止休眠。dwMilliseconds 也可以设定为 INFINITE这表示无限期地等待下去。另外,dwMilliseconds 也可以为 0,其作用是强迫操作系统将当前线程切换到其他线程。1.1.5.3 用户模式的事件事件是一种典型的同步对象。用户模式下的事件和内核模式的事件对象紧密相连。在使用事件之前,需要对事件进行初始化,使用 CreateEvent 函数。HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpE
27、ventAttributes, / 安全属性BOOL bManualReset, / 复位方式BOOL bInitialState, / 初始化状态LPCTSTR lpName / 对象名称);CreateEvent 函数会使操作系统创建一个内核事件对象。CreateEvent 返回的句柄值就代表了这个内核事件对象。应用程序无法获得这个内核事件对象的指针,而用一个句柄(一个 32 位的无符号整数)代表事件对象。一般情况下,CreateEvent 的安全属性设置为 NULL。它的第二个参数 bManualReset,表示创建的事件是否是手动模式。如果是手动模式的事件,事件处于激发状态后,需要手动
28、设置才能回到未激发状态。如果是自动模式,当事件处于激发状态后,遇到任意一个等待(如 WaitForSingleObject),则自动变回未激发状态。下面的代码为 WinPcap 中创建一个事件的实例:HANDLE hEvent;hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);1.1.5.4 NDIS 库提供的事件NDIS 库的事件用 NDIS_EVENT 数据结构表示。使用事件前,首先对其初始化,可使用 NdisInitializeEvent 函数。VOID NdisInitializeEvent(IN PNDIS_EVENT Event);把调用者
29、置为等待状态,直到给定的事件为信号状态,或等待超时,可以使用 NdisWaitEvent 函数。BOOLEAN NdisWaitEvent(IN PNDIS_EVENT Event,IN UINT MsToWait); 把一个给定的事件设为信号状态 ,可使用 NdisSetEvent 函数,VOID NdisSetEvent(IN PNDIS_EVENT Event);清除一个给定的事件的信号状态,可使用 NdisResetEvent 函数,VOID NdisResetEvent(IN PNDIS_EVENT Event);下面的代码为 WinPcap 中使用 NDIS 库事件的实例:NDIS_EVENT NdisWriteCompleteEvent;NTSTATUS NPF_Open(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)NdisInitializeEvent(NTSTATUS NPF_Write(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)