1、Windows驱动程序中重要的数据结构
1.1、驱动对象(DRIVER_OBJECT)
每个驱动程序会有唯一的驱动对象与之对应,并且这个驱动对象是在驱动加载的时候,被内核中的对象管理程序所创建的。
驱动对象用DRIVER_OBJECT数据结构表示,它作为驱动的一个实例被内核加载并且内核对一个驱动只加载一个实例。确切的说,是由内核中的I/O管理器负责加载的。驱动程序需要在DriverEntry中初始化。
typedef struct _DRIVER_OBJECT
{
CSHORT Type;
CSHORT Size;
PDEVICE_OBJECT DeiceObject;//对应的设备对象,为链表
ULONG Flags;
PVOID DriverStart;
ULONG DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension;
UNICODE_STRING DriverName;//该字符一般为\Driver\[驱动程序名称]
PUNICODE_STRING HardwareDataBase;//记录设备的硬件数据库键名,值一般指向注册表位置的字符串
PFAST_IO_DISPATCH FastIoDispatch;//文件驱动中用到的派遣函数
PDRIVER_INITIALIZE DriverInit;
PDRIVER_STARTIO DriverStartIo;//记录StartIO例程的函数地址,用于串行化操作
PDRIVER_UNLOAD DriverUnload;//用于卸载驱动
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION]; //记录的是一个函数指针数组,其每个成员都指向的是一个IRP派遣函数
}DRIVER_OBJECT;
typedef struct _DRIVER_OBJECT *PDRIVER_OBJECT;
1.2、设备对象(DEVICE_OBJECT)
每个驱动程序会创建一个或多个设备对象,用DEVICE_OBJECT数据结构表示。每个设备对象都会有一个指针指向下一个设备对象,因此就形成一个设备链。设备链的第一个设备是由DRIVER_OBJECT结构体中指明的。设备对象保存设备特征和状态的信息,其数据结构如下:
typedef struct _DEVICE_OBJECT {
CSHORT Type;
USHORT Size;
LONG ReferenceCount;
PDRIVER_OBJECT DriverObject;
PDEVICE_OBJECT NextDevice;//指向下一个同属于一个驱动对象的设备对象
PDEVICE_OBJECT AttachedDevice;//指向更高一层的驱动的设备对象
PIRP CurrentIrp;//在使用StartIO例程的时候,此域指向的是当前的IRP结构
PIO_TIMER Timer;
ULONG Flags;//标志位
ULONG Characteristics;
__volatile PVPB Vpb;
PVOID DeviceExtension;
DEVICE_TYPE DeviceType;//指明设备的类型
CCHAR StackSize;//在多层驱动情况下,驱动与驱动之间会形成类似堆栈的结构。IRP会依次从最高层穿戴到最底层。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;
PDEVOBJ_EXTENSION DeviceObjectExtension;
PVOID Reserved;
} DEVICE_OBJECT, *PDEVICE_OBJECT;
1.3、设备扩展
设备对象记录“通用”设备的信息,而另外一些特殊信息记录在设备扩展里。各个设备扩展有程序员自己定义,每个设备的设备扩展也不尽相同。设备扩展是由程序员指定内容和大小,由I/O管理器创建的,并保存在非分页内存中。
在驱动程序中,应尽量避免使用全局函数,因为全局函数往往导致函数的不可重入性。重入性指的是,在多线程的程序中,多个函数并行运行,函数的运行结果不会根据函数的调用先后顺序而导致不同。解决办法是将全局变量以设备扩展的形式存储,并加以设当的同步保护措施。
2、NT式驱动的基本结构
对于NT式驱动来说,主要的函数是DriverEntry例程、卸载例程及各个IRP的派遣例程
2.1、驱动加载过程与驱动入口函数(DriverEntry)
DriverEntry主要是对驱动程序进行初始化工作,它是由系统进程所调用。在WIndows中有个特殊的进程叫做系统进程(System),系统进程在系统启动的时候就已经被创建了。
驱动加载的时候,系统进程启动新的线程,调用执行体组件中的对象管理器,创建一个驱动对象。这个驱动对象时一个DRIVER_OBJECT的结构体。另外,系统进程调用执行体组件中的配置管理程序,查询此驱动程序对应的注册表中的项。
系统线程调用驱动程序的DriverEntry例程时,同时传进两个参数(pDriverObject和pRegistryPath)。
DriverEntry参数的修饰:“IN”、“OUT”、“INOUT”在DDK中都被定义成空串,它们的功能类似于程序注释,当看到一个IN参数时,应该认为该参数是纯粹用于输入目的。“OUT”参数代表这个参数仅用于参数的输出参数。“INOUT”用于既可以输入又可以输出的参数。
2.2、创建设备对象
在NT式驱动中,创建设备对象是由IoCreateDevice内核函数完成的:
NTSTATUS
IoCreateDevice(
IN PDRIVER_OBJECT DriverObject,//指向驱动对象的指针
IN ULONG DeviceExtensionSize,//指定设备扩展的大小
IN PUNICODE_STRING DeviceName OPTIONAL,//设备对象的名字
IN DEVICE_TYPE DeviceType,
IN ULONG DeviceCharacteristics,//设备对象的特征
IN BOOLEAN Exclusive,//设备对象是否为内核模式下使用,一般为TRUE
OUT PDEVICE_OBJECT *DeviceObject//I/O管理器负责创建这个设备对象,并返回对象地址
);
设备名称用UNICODE字符串指定,并且字符串必须是“\Device\[设备名]”的形式。在Windows下所有设备都是以类似名字命名的。例如,磁盘分区的C盘、D盘、E盘、F盘就被命名为“\Device\HarddiskVolume1”、“\Device\HarddiskVolume2”、“\Device\HarddiskVolume3”、“\Device\HarddiskVolume4”。
当然,也可以不指定设备的名字,如果不指定,I\O管理器会自动分配一个数组作为设备的设备名,如:“\Device\00000001”。如果指定了设备名,只能被内核模式下的其他驱动所识别。但是在用户模式下的应用程序无法识别这个设备。让用户模式下的应用程序能识别设备有两种方法,第一种是通过符号链接找到设备,第二种是通过设备接口找到设备。设备接口的办法在NT驱动很少使用。
符号链接可以理解为设备对象起的一个别名。设备对象的名称只能被内核模式下驱动识别,而别名也可以被用户模式下的应用程序识别。例如,常说的C盘,D盘就是符号链接。创建符号链接的函数:
NTSTATUS
IoCreateSymbolicLink(
IN PUNICODE_STRING SymbolicLinkName,//符号链接的字符串
IN PUNICODE_STRING DeviceName//设备对象的字符串
);
在内核模式下,符号链接是以“\??\”开头的(或者是“\DosDevice\”开头的),如C盘就是“\??\C”(或者“\DosDevice\C”),而在用户模式下,则是以”\\.\”开头的,如C盘就是”\\.\C”。
//创建设备对象
NTSTATUS CreateDevice(IN PDRIVER_OBJECT pDriverObject){
NTSTATUS status;
PDEVICE_OBJECT pDevObj;
PDEVICE_EXTENSION pDevExt;
//创建设备名称
UNICODE_STRING devName;
RtlInitUnicodeString(&devName,L"\\Device\\MyDDKDevice");
//创建设备
status = IoCreateDevice(pDriverObject,sizeof(DEVICE_EXTENSION),&(UNICODE_STRING)devName,FILE_DEVICE_UNKONOWN,0,TRUE,&pDevObj);
//创建设备对象是否创建成功
if(!NT_SUCCESS(status))
return status;
//将设备设置为缓冲区设备
pDevObj->Flags!=DO_BUFFERED_IO;
//得到设备扩展
pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
//设置设备扩展中的设备名称
pDeviExt->ustrDeviceName = devName;
//创建符号链接
UNICODE_STRING symLinkName;
RtlInitUnicodeString(&symLinkName,L"\\??\\HelloDDK");
//创建符号链接
status = IoCreateSymbolicLink(&symLinkName,&devName);
//判断是否成功创建符号链接
if(!NT_SUCCESS(status))
{
//删除符号链接
IoDeleteDevice(pDevObj);
return status;
}
return STATUS_SUCCESS;
}
设备类型为FILE_DEVICE_UNKNOWN说明此设备是常用设备之外的设备,一般虚拟设备常使用这个作为设备类型
2.3、DriverUnload例程
在驱动对象中会设置DriverUnload例程,此例程在驱动被卸载的时候调用。在NT式驱动中,DriverUnload一般负责删除在DriverEntry中创建的设备对象,并且将设备对象所关联的符号链接删除。另外,DriverUnload还负责对一些资源进行回收。
VOID
IoDeleteDevice(
IN PDEVICE_OBJECT DeviceObject
);
在DriverUnload中,除了要删除设备对象,同时,还要对设备对象关联的符号链接进行删除。删除符号链接的函数是IoDeleteSymbolicLink:
NTSTATUS
IoDeleteSymbolicLink(
IN PUNICODE_STRING SymbolicLinkName//已经被注册了的符号链接
);
3、WDM式驱动的基本结构
3.1、物理设备对象与功能设备对象
在WDM模型中,完成一个设备的操作,至少有两个设备对象共同完成。其中,一个是物理设备对象(Physical Device Object——PDO),另外一个是功能设备对象(Function Device Object——FDO)。其关系是“附加”与“被附加”的关系
当PC插入某个设备的时候,PDO会自动创建。确切的说,是由总线驱动创建的。PDO不能单独操作设备,需要配合FDO一起使用。系统会提示检测到新设备,要求安装驱动程序。需要安装的驱动程序指的是WDM程序,此驱动程序负责创建FDO,并附加到PDO上。
3.2、WDM驱动的入口程序
和NT驱动一样,WDM驱动的入口程序也是DriverEntry,但是初始化作用被分散到其他例程中。例如,创建设备对象的责任就被放在AddDevice例程。
WDM和NT式驱动的DriverEntry有以下几点不同:
a)、增加了对AddDevice函数的设置。这是WDM驱动和NT驱动非常重要的不同点。因为NT驱动是主动加载设备的,也就是驱动一旦加载就创建设备。而WDM驱动是被动加载设备的,操作系统必须加载PDO以后,调用驱动的AddDevice例程,AddDevice例程中负责创建FDO,并且附加到PDO之上。
b)、创建设备对象已经不再这个函数中了,而在AddDevice例程中创建。
c)、必须加入IRP_MJ_PNP的派遣回调函数。IRP_MJ_PNP主要是负责计算机中即插即用的处理,在WDM驱动中加入了很多即插即用的处理。
3.3、WDM驱动的AddDevice例程
a)、通过IoCreateDevice创建设备对象FDO
b)、将FDO地址保存在设备扩展中
c)、将FDO使用IoAttachDeviceToDeviceStack函数附加到PDO上
d)、设置FDO的Flags子域
3.4、DriverUnload例程
在NT式驱动中,DriverUnload例程主要负责做删除设备和取消符号链接。而在WDM驱动中,这部分操作被IRP_MN_REMOVE_DEVICE这个IRP的处理函数负责。IRP_MN_REMOVE_DEVICE这个IRP是当设备需要被卸载的时候,由即插即用管理器创建,并发送到驱动程序中的。IRP一般由两个号码指定该IRP的具体意义,一个是主IRP号,另一个是辅IRP号。每个IRP都由对应的派遣函数所处理。
4、小结
本章重点介绍了NT式驱动和WDM驱动的结构,从中可以看出WDM驱动程序是在NT驱动程序的基础上变化过来的,可以将WDM驱动程序看成是NT驱动程序的特例。同时,还介绍了驱动程序的入口函数,卸载函数,IRP的派遣函数等。
以上内容参考自张帆 史彩成等编著的《Windows 驱动开发技术详解》第四章