第四章 驱动程序的基本结构
数据结构是计算机控制程序的核心,I/O 管理器定义了一些数据结构,这些数据结构式编写驱动程序锁必须掌握的。
Windows驱动程序中重要的数据结构
驱动对象(DRIVER_OBJECT)
每个驱动程序会有唯一的驱动对象与之一一对应,并且这个驱动对象是在驱动加载时候被内核中的对象管理器程序创建的。去东方对象用DRIVER_OBJECT数据结构表示:
typedef struct _DRIVER_OBJECT{
CSHORT Type;
CSHORT Size;
PDEVICE_OBJECT DeviceObject;
ULONG Plags;
PVOID DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension;
UNICODE_STRING DriverName;
PUNICODE_STRING HardwareDatabase;
PFAST_IO_DISPATCH FastIoDispatch;
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; 每个驱动程序都会有一个或者多个设备对象,其中每个设备对象都有一个指针指向下一个驱动对象,最后一个设备对象指向空。此处的DeviceObject指向驱动对象的第一个设备对象。通过DeviceObject,可以遍历驱动对象里的所有设备,驱动对象有程序员自己创建,而非操作系统,在驱动卸载的时候,遍历每个设备对象,并将其删除。
UNICODE_STRING DriverName; 记录驱动程序的名字,该字符串一般为\Driver\[驱动程序名称]
PUNICODE_STRING HardwareDatabase; 设备的硬件数据库键名,该字节一般为\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM 。
PFAST_IO_DISPATCH FastIoDispatch;文件驱动中用到的派遣函数。
PDRIVER_STARTIO DriverStartIo;记录StartIo例程的函数地址,用于串行化操作。
PDRIVER_UNLOAD DriverUnload;指定驱动卸载时所用的回调函数地址。
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];域记录的是一个函数指针数组,也就是MajorFunction是一个数组,其每个成员是一个指针,每个指针指向的是一个函数。这个函数就是处理IRP的派遣函数。
设备对象(DEVICE_OBJECT)
每个驱动都会创建一个或者多个对象,每个对象都会有一个指针指向下一个设备对象,形成 一个设备链。设备链的第一个有DRIVER_OBJECT结构体中指明。每个驱动对象用DEVICE_OBJECT数据结构表示:
typedef struct _DEVICE_OBJECT{
...
struct _DRIVER_OBJECT *DriverObject;
struct _DRIVER_OBJECT NextDevice;
struct _DRIVER_OBJECT *AttacheDevice;
struct _IRP *CurrentIrp;
ULONG Flags;
struct _DRIVER_OBJECT *DeviceObjectExtension;
}DEVICE_OBJECT;
typedef struct _DEVICE_OBJECT *PDEVICE_OBJECT;
struct _DRIVER_OBJECT *DriverObject; 指向驱动程序中的驱动对象。同属于一个驱动程序的驱动对象指向的是同一驱动对象。struct _DRIVER_OBJECT NextDevice;指向下一个设备对象。这里的下一个设备对象同属于一个驱动对象的设备。
struct _DRIVER_OBJECT *AttacheDevice;指向下一个设备对象。这里指的是有更高一层的驱动附加到这个驱动的时候,其指向更高一层的驱动。
struct _IRP *CurrentIrp;在使用StartIO例程的时候,此域指向的是当前IRP结构。
ULONG Flags; 32位无符号的整数,每一个位有具体的含义
struct _DRIVER_OBJECT *DeviceObjectExtension;指向的是设备的扩展对象,每个驱动设备都会指定一个设备扩展对象
设备扩展
设备对象记录“通用”设备信息,而另外一些“特殊”信息记录在设备扩展里。各个扩展程序有程序员自己定义,每个设备的设备扩展也不尽相同。在驱动程序中,尽量避免使用全局函数,因为全局函数往往导致函数的不可重入性。解决办法是将全局变量以设备扩展的形式存储,并加以适当同步保护措施。
驱动加载
驱动加载的时候,系统进程启动新的进程,调用执行体组件中的对象管理器,创建一个驱动对象。这个驱动对象是一个DRIVER_OBJECT的结构体,另外系统进程执行体组件中的配置管理程序,查看此驱动对应的注册表中的项。系统进程调用驱动程序的DriverEntry例程时,同时传进两个参数,分别是pDriverObject和pRegistryPath。其中一个指向刚被创建驱动对象的指针,另一个指向设备服务的键名字符串的指针。在DriverEntry中,主要功能是对系统进程创建的驱动对象进行初始化。另外,设备服务键的键名有时候需要保存下来,因为这个字符串不是长期存在的。如果以后想使用这个UNICODE字符串就必须先把他复制到安全的地方。在驱动程序中,字符串用UNICODE字符串来表示。UNICODE是宽字符集,每个字符用16位表示。其中UNICODE是用数据结构UNICODE_STRING表示;
typedef struct _UNICODE_STRING{
USHORT Length; //记录这个字符串用多少字节记录。如果字符串有N个字符,那么Length将会是N的2倍
USHORT MaximumLength; // 记录buffer的大小,也就是说这个结构最大能记录的字节数。MaximumLength要大于或等于Length
PWSTR Buffer;//记录字符串的指针,每个字符都有16位
}UNICODE_STRING *PUNICODE_STRING;
KdPrint(("%S\n",pRegistryPath->Buffer));
KdPrint(("%ws\n",pRegistryPath->Buffer));
最后需要说明的是DriverEntry参数的修饰“IN”。“IN”、“OUT”、“INOUT”在DDK中被定义成空串,他的功能类十余程序的注释,当看到一个“IN”参数时,应该认定该参数是纯粹用于输入目的的。“OUT”参数代表这个参数仅用于函数的输出参数。“INOUT”用于既可以输入又可以输出的参数。
在DriverEntry函数中,一般设置卸载例程和IRP的派遣函数,另外还有一部分代码负责创建设备对象。设置卸载例程和设置派遣函数都是对驱动对象的设置。设备对象中大MajorFunction是一个函数指针数组,IRP_MJ_CREATE、IRP_MJ_CLOSE、IRP_MJ_WRITE代表数组的第几个元素。
物理设备对象与功能设备对象
在WDM模型中,完成一个设备的操作,至少有两个设备对象共同完成。其中一个是物理设备对象(Physical Device Object 简称PDO),另一个功能设备对象(Function Device Object 一下简称为FDO)。其关系式“附加”与“被附加”的关系。
当PC插入一个设备时,PFO会自动创建。确切的说,是由总线驱动创建的。PDO不能单独工作,需要配合一个FDO一起工作,系统会提示检测到新设备,要求安装驱动程序。需要安装的驱动程序就是WDM程序,此驱动程序负责创建FDO,并且附加到PDO上。
上图是最简单的一种情况,事实上比这个复杂一些,在FDO和PDO之间还会存在过滤驱动,在FDO上面的过滤驱动被称作上层过滤驱动。在FDO下层的驱动被称为下层过滤驱动。另外每个设备对象,有个StackSize子域,表明操作这个设备需要几层才能达到最下面的物理设备。过滤驱动不是必须存在的,在WDM模型中PDO和FDO是必须的。
WDM驱动的AddDevice例程
AddDevice例程是WDM驱动所独有的,在NT驱动中没有该例程。在DriverEntry中,需要设置AddDevice例程的函数地址。设置的方式是驱动对象中有个DriverExtension子域,DriverExtension中有个AddDevice子域,将该子域指向AddDevice例程的函数地址。
pDriverObject->DriverExtension->AddDevice = HelloWDMAddDevice;
AddDevice函数有两个输入参数,一个是驱动对象DriverObject,另一个是设备对象PhysicalDeviceObject。驱动对象是I/O管理器创建的驱动对象。设备对象是PhysicalDeviceObject是底层总线驱动创建的PDO设备对象。传进该参数的目的就是将FDO附加在PDO上。
/************************************************************************
* 函数名称:HelloWDMAddDevice
* 功能描述:添加新设备
* 参数列表:
DriverObject:从I/O管理器中传进来的驱动对象
PhysicalDeviceObject:从I/O管理器中传进来的物理设备对象
* 返回 值:返回添加新设备状态
*************************************************************************/
#pragma PAGEDCODE
NTSTATUS HelloWDMAddDevice(IN PDRIVER_OBJECT DriverObject,
IN PDEVICE_OBJECT PhysicalDeviceObject)
{
PAGED_CODE();
KdPrint(("Enter HelloWDMAddDevice\n"));
NTSTATUS status;
PDEVICE_OBJECT fdo;
UNICODE_STRING devName;
RtlInitUnicodeString(&devName,L"\\Device\\MyWDMDevice");
status = IoCreateDevice(
DriverObject,
sizeof(DEVICE_EXTENSION),
&(UNICODE_STRING)devName,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&fdo);
if( !NT_SUCCESS(status))
return status;
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;
pdx->fdo = fdo;
pdx->NextStackDevice = IoAttachDeviceToDeviceStack(fdo, PhysicalDeviceObject);
UNICODE_STRING symLinkName;
RtlInitUnicodeString(&symLinkName,L"\\DosDevices\\HelloWDM");
pdx->ustrDeviceName = devName;
pdx->ustrSymLinkName = symLinkName;
status = IoCreateSymbolicLink(&(UNICODE_STRING)symLinkName,&(UNICODE_STRING)devName);
if( !NT_SUCCESS(status))
{
IoDeleteSymbolicLink(&pdx->ustrSymLinkName);
status = IoCreateSymbolicLink(&symLinkName,&devName);
if( !NT_SUCCESS(status))
{
return status;
}
}
fdo->Flags |= DO_BUFFERED_IO | DO_POWER_PAGABLE;
fdo->Flags &= ~DO_DEVICE_INITIALIZING;
KdPrint(("Leave HelloWDMAddDevice\n"));
return STATUS_SUCCESS;
}
在AddDevice可以分为以下几个步骤:
1、在AddDevice通过IoCreateDevice等函数,创建了设备对象,该设备对象就是FDO,即功能驱动设备对象。和NT驱动一样,可以设置驱动对象的设备名称,也可以不设置。如果不设置IO管理器会自动以一个数字作为设备的名称。
2、创建完FDO后,需要将FDO的地址保存下来,以便以后使用。保存的位置是在设备扩展中(前面介绍过驱动程序应该尽量避免使用全局变量,而是用设备扩展)。如果电脑中存在多个同类的设备,例如插入相同型号的网卡,操作系统会两次调用AddDevice例程。每个例程创建各自的FDO,分别记录在各自的设备扩展中。
3、驱动程序将创建的FDO附加在PDO上,附加这个动作是依靠IoAttachDeviceToDeviceStack函数实的。
4、设置fdo的Flags子域。DO_BUFFERED_IO是定义设备为“缓冲内存设备”。另外~DO_DEVICE_INITIALIZING,是将DO_DEVICE_INITIALIZING位清零。保证设备初始化完毕,这一步是必需的。
DriverUnload 例程
在NT式驱动中,DriverUnload主要负责做删除设备和取消符号链接。而在WDM驱动中,这部分操作被IRP_MN_REMOVE_DEVICE IRP的处理函数所负责,而在DriverUnload例程中变得相对简单。如果在DriverEntry中有申请内存的操作,可以在DriverUnload例程中回收这些内存。
对IRP_MN_REMOVE_DEVICE IRP的处理
驱动程序是由IRP驱动的。创建IRP的原因有很多,IRP_MN_REMOVE_DEVICE这个IRP是当设备需要被卸载时由即插即用管理器创建,并发送到驱动程序中的。IRP一般由两个号码指定该IRP的具体意义,一个是主IRP号(Major IRP),另一个是辅IRP号(Minor IRP)。每个IRP都由对应的派遣函数所处理,派遣函数是在DriverEntry中指定的。例如:
pDriverObject->MajorFunction[IRP_MJ_PNP] = HelloWDMPnp;
当这杯需要卸载时,会发出多个IRP_MJ_PNP。这些IRP的辅IRP号会有所不同,其中之一是IRP_MN_REMOVE_DEVICE。
在WDM驱动程序中,对设备的卸载一般是在对IRP_MN_REMOVE_DEVICE的函数中进行卸载。HelloWDM中负责处理IRP_MN_REMOVE_DEVICE的函数是HandleRemoveDevice代码如下:
在这个函数中,需要删除设备、取消符号链接,同时在此函数中还需要将FDO从PDO上的堆栈中“摘除”下来。函数原型为:
void IoDetachDevice(IN OUT PDEVICE_OBJECT TargetDevice)
参数TargetDevice参数是下层堆栈上的设备对象。
用Device Tree查看WDM设备对象FDO附加在PDO上面,同时可能还会将其他设备也附加在其上,这样一层层的附加操作,从底层设备到高层设备形成了一个设备链,有时也被形象的称为设备对象堆栈。
Device Tree是查看设备堆栈的优秀工具,它可以帮助程序员清楚的查看设备栈的形成过程。通过使用Device Tree工具,可以更好地理解WDM驱动程序的结构。
设备驱动的垂直结构
设备的创建顺序是,先创建PDO,再创建高层的FDO,这也就是设备堆栈的生长方向,即从底层设备到高层设备。每层 的设备对象由不同的驱动程序创建。底层设备对象寻找上层的设备对象是依靠设备对象的AttachedDevice来寻找的,如果某一设备的AttachedDevice为空,说明已经到了设备堆栈的顶部。高层设备寻找低一层的设备,设备对象没有相关子域可以使用,解决的办法是,通过程序员自定义设备扩展,在设备扩展记录低一层的设备对象。这样就可以从最低层的设备对象达到设备顶层,再从设备顶层达到设备堆栈底部。