WinPcap的内核驱动程序NPF是一个协议驱动程序,其涉及到一些编写Windows驱动程序的基础知识与NDIS协议驱动程序编写的基础知识。本章主要对这些基础知识进行简要的描述以便于后续的理解。
本节主要描述在WinPcap的NPF中经常使用一些编写Windows驱动程序所需掌握的部分基础知识,以便于后面的理解。
每个驱动程序都有唯一的驱动对象与之对应,该驱动对象在驱动程序被加载时由内核的对象管理程序所创建。
驱动对象用DRIVER_OBJECT数据结构表示,它作为驱动程序的一个实例被内核加载,对一个驱动程序内核I/O管理器只加载一个实例。
驱动对象数据结构在wdm.h文件中的定义如下。
typedef struct _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请求越界的驱动名称
*/
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 DriverUnload;
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT;
typedef struct _DRIVER_OBJECT *PDRIVER_OBJECT;
下面分别描述驱动对象中驱动程序可访问的成员。
PDEVICE_OBJECT DeviceObject
每个驱动对象会有一个或多个设备对象。每个设备对象都有一个指针(NextDevice)指向下一个驱动对象,最后一个设备对象指向空。此处的DeviceObject指向驱动对象的第一个设备对象。该成员在成功调用IoCreateDevice后自动更新。一个驱动程序使用该程成员与设备对象(DEVICE_OBJECT)的NextDevice可遍历给驱动对象的所有设备对象。在驱动被卸载的时候,需要遍历每个设备对象,并将其删除。
PUNICODE_STRING HardwareDatabase
指向注册表中硬件配置信息的路径,用UNICODE字符串表示。该字符串一般为/REGISTRY/MACHINE/HARDWARE/DESCRIPTION/SYSTEM。
PFAST_IO_DISPATCH FastIoDispatch
指向一个定义驱动快速 I/O结构体的入口点,该成员只用于文件系统驱动与网络传输驱动。
PDRIVER_INITIALIZE DriverInit
是DriverEntry例程的入口点,由I/O管理器设置。
PDRIVER_STARTIO DriverStartIo
是Startl0例程的的入口点,如果需要,由DriverEntry例程设置,否则为NULL。
PDRIVER_UNLOAD DriverUnload
指向驱动卸载时所用回调函数的入口点。
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION+1]
一个函数指针数组, 数组MajorFunction中的每个成员保存着一个指针,每一个指针指向一个处理对应IRP(IRP_MJ_XXX)的派遣函数(DispatchXxx)。
每个派遣函数(DispatchXxx)声明如下:
NTSTATUS
(*PDRIVER_DISPATCH) (
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
);
设备对象保存设备特征和状态的信息。一个设备对象表示一个逻辑的、虚拟的或物理的设备,由一个驱动对象操控设备对象的I/O请求。每一个内核模式的驱动必须创建设备对象,通过调用IoCreateDevice一次或多次。
每个驱动程序会创建一个或多个设备对象,用DEVICE_OBJECT数据结构表示。每个设备对象有一个指针(NextDevice)指向下一个设备对象,从而形成一个设备链表。设备链表第一个设备是由驱动对象结构体中DeviceObject指明的。
设备对象数据结构在wdm.h文件中的定义如下。
typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _DEVICE_OBJECT
{
CSHORT Type;
USHORT Size;
LONG ReferenceCount;
struct _DRIVER_OBJECT *DriverObject;
struct _DEVICE_OBJECT *NextDevice;
struct _DEVICE_OBJECT *AttachedDevice;
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;
/*
*下列成员是为支持文件系统的互斥操作,
*为了对文件系统处理线程使用设备的计数保持跟踪
*/
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_OBJECT DriverObject
指向驱动程序中的驱动对象。同属于一个驱动程序的驱动对象指向的是同一个驱动对象。
PDEVICE_OBJECT NextDevice
指向下一个设备对象。这里的下一个设备对象是同一个驱动程序创建的若干设备对象。每个设备对象根据NextDevice域形成链表,从而可以遍历每个设备对象。在每次成功调用IoCreateDevice 后I/O管理器更新该链表。在驱动被卸载的时候,需要遍历该链表,删除每个设备对象。
PIRP CurrentIrp
如果驱动使用Startl0例程时,此成员指向当前IRP结构。否则为NULL。
ULONG Flags
此成员是一个32位昀无符号整型,每个位有不同的含义。通过位或操作为新创建的设备对象设置不同的特性。
ULONG Characteristics
当驱动程序调用IoCreateDevice时,设置下列一个合适的值:
FILE_REMOVABLE_MEDIA
FILE_READ_ONLY_DEVICE
FILE_FLOPPY_DISKETTE
FILE_WRITE_ONCE_MEDIA
FILE_DEVICE_SECURE_OPEN
PVOID DeviceExtension
指向设备扩展对象。设备扩展对象是由程序员在驱动中自行定义的结构体,结构体的大小在调用IoCreateDevice时设置。每个设备都会指定一个设备扩展对象,设备扩展对象记录的是特别定义的结构体。在驱动程序中应该尽量避免全局变量的使用,因为全局变量涉及不容易同步的问题,解决的办法可将全局变量存储在设备扩展中。
DEVICE_TYPE DeviceType
指明设备的类型,由IoCreateDevice设置。根据设备需要填写相应的设备类型。.
CCHAR StackSize
在多层驱动的情况下,驱动与驱动之间形成类似堆栈的结构。IRP会依次从最高层传递到最底层。StackSize就是指定发送到该驱动的IRP在堆栈位置的最小层数。IoCreateDevice在一个新创建的设备对象中设置该成员。
ULONG AlignmentRequirement
设备在大容量传输的时候,为了保证传输速度需要内存对齐。每个设备对象在它新创建的设备对象中设置该成员。
设备对象记录设备的“通用”信息,而另外一些“特殊”信息记录在设备扩展中。设备扩展由程序员自行定义,指定内容与大小,由I/O管理器创建,保存在非分页内存中。
在驱动程序中,尽量避免使用全局函数,因为全局函数往往导致函数的不可重入性。一个解决办法就是将全局变量以设备扩展的形式存储,并加以适当的同步保护措施。
WinPcap中NPF的设备扩展结构体,主要用于存储每个被NPF绑定的适配器的一些信息,结构体定义如下:
typedef struct _DEVICE_EXTENSION {
//适配器名称
NDIS_STRING AdapterName;
//设备导出的名称,也就是通过WinPcap应用程序使用该名称来打开该适配器
PWSTR ExportString;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
驱动程序的主要功能是负责处理l/O请求,大部分1/0请求是在派遣函数中处理的。IRP的处理机制类似Windows应用程序中的“消息处理”机制。用户空间对驱动程序的所有1/0请求,全部由操作系统转化为一个IRP数据结构,不同的IRP数据会被“派遣”到不同的派遣函数中,在派遣函数中处理IRP。
IRP是Windows内核中输入输出请求包(I/O Request Package,IRP)的缩写。IRP具有两个基本属性:MajorFunction与MinorFunction,分别记录IRP的主功能和子功能。操作系统根据MajorFunction将IRP“派遣”到不同的派遣函数中,在派遣函数中还可以根据MinorFunction继续判断该IRP属于哪种子功能。
一般来说驱动程序都是在DriverEntry函数中注册派遣函数的。在DriverEntry的驱动对象pDriverObject中,有个函数指针数组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、ZwClose也同样会创建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 PIRP 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 API获得错误代码。所得的错误代码会和IRP设置的状态一致。
除了设置IRP的完成状态,函数还要设置这个IRP请求操作了多少字节。在本代码中,将操作字节数设置成了0。如果是ReadFile产生的IRP,这个字节数代表从设备读了多少字节。如果是WriteFile产生的IRP,这个字节数代表对设备写了多少字节。最后函数通过IoCompleteR0quest函数将IRP请求结束。
IoCompleteRequest的声明如下:
VOID IoCompleteRequest(
IN PIRP Irp,
IN CCHAR PriorityBoost
);
参数Irp代表需要被结束的IRP。参数PriorityBoost代表线程恢复时的优先级别,指的是被阻塞的线程以何种优先级恢复运行。一般情况下,优先级设置为IO_NO_INCREMENT。