正如前面所述,对USBD.SYS驱动操作的中介只URB结构体,因此对USBD.SYS的操作主要可以分构造URB和调用请求两部分,如下以USB读写操作过程为例进行分析。
第一步构造URB:由于EasyArm2400下位机固件程序USB采用的是中断方式进行数据传输,因此我们就需要构造一个中断传输的URB,实现如下代码。
UsbBuildInterruptOrBulkTransferRequest(urb, sizeof(struct _URB_BULK_OR_INTERRUPT_TRANSFER), pipeInformation->PipeHandle, NULL, mdl, stageLength, urbFlags, NULL); |
其中_URB_BULK_OR_INTERRUPT_TRANSFER类型指明了构造的RB用于中断方式传输的请求。
第二步调用请求:在对挂载的设备进行操作的时候,从DDK DOC中可以发现采用的函数是IoCallDriver。IoCallDriver函数的第一个参数指定的为挂载的设备对象,第二参数为当前的IRP。
其实对USBD.SYS的操作有以上两步基本能够完成,但是你会发现其实并没有真正意义上操作了USBD.SYS。分析一下可以发现,其实的构造的URB并没有传输到挂载的设备上。
在DDK中提供IoGetNextIrpStackLocation函数获取挂载的设备当前的IRP,如下代码帮你实现URB的传输。
nextStack = IoGetNextIrpStackLocation(Irp); nextStack->MajorFunction = IRP_MJ_INTERNAL_DEVICE_CONTROL; nextStack->Parameters.Others.Argument1 = (PVOID) urb; nextStack->Parameters.DeviceIoControl.IoControlCode =IOCTL_INTERNAL_USB_SUBMIT_URB; |
其中nextStack->Parameters.Others.Argument1保存了构造后的URB,nextStack->Parameters.DeviceIoControl.IoControlCode保存了需要执行的I/O请求。
有了进一步的补充,操作USBD.SYS已经可以实现了,但是挂载的设备的何时操作完成,返回的状态是什么?这些对于上层的驱动程序来说根本无法知道。因此DDK引入了完成用例的概念,完成用例将帮助实现以上的这些需求。
完成用例的概念就是当下层设备完成操作时,IRP会产生回滚,当回滚到上层设备IRP的时候,如果该层IRP存在完成用例子时,系统将进入用例函数进行处理,DDK提供的设置完成用例函数如下。
VOID IoSetCompletionRoutine( IN PIRP Irp, IN PIO_COMPLETION_ROUTINE CompletionRoutine, IN PVOID Context, IN BOOLEAN InvokeOnSuccess, IN BOOLEAN InvokeOnError, IN BOOLEAN InvokeOnCancel); |
其中CompletionRoutine指向的是用例服务函数,Context指定为需要传递的参数。一般传递需要触发的事件。
到此为止,操作USBD.SYS的整个过程已经完成,如下展示读写操作请求的核心代码。
;申请一段内存,用于保存URB结构体。 urb = (PURB)ExAllocatePool(NonPagedPool, sizeof(struct _URB_BULK_OR_INTERRUPT_TRANSFER)); ;构建一个用于中断传输的URB。 UsbBuildInterruptOrBulkTransferRequest(urb, sizeof(struct _URB_BULK_OR_INTERRUPT_TRANSFER), pipeInformation->PipeHandle, NULL, mdl, stageLength, urbFlags, NULL); nextStack = IoGetNextIrpStackLocation(Irp); nextStack->MajorFunction = IRP_MJ_INTERNAL_DEVICE_CONTROL; nextStack->Parameters.Others.Argument1 = (PVOID) urb; nextStack->Parameters.DeviceIoControl.IoControlCode = IOCTL_INTERNAL_USB_SUBMIT_URB。 ;设置完全用例。 IoSetCompletionRoutine(Irp, (PIO_COMPLETION_ROUTINE)LPC2478_USB_ReadWriteCompletion, rwContext, TRUE, TRUE, TRUE); ;设置Pending位 IoMarkIrpPending(Irp); ;向挂载设备发送URB,请求传输数据。 ntStatus = IoCallDriver(deviceExtension->NextDeviceObject,Irp); |
提示:IoSkipCurrentIrpStackLocation与IoCopyCurrentIrpStackLocationToNext区别
在操作USDB.SYS的时候,上述的两个函数也是比较常见,同时也是比较容易混淆的。其实这两个函数的实际功能基本上一样,都是将当前的IRP I/O堆栈拷贝给下层的驱动的I/O堆栈。但是这两个函数也存在一些特殊用途的区别,当操作USBD.SYS,设置了完成用例时,DDK规定就不能使用IoSkipCurrentIrpStackLocation函数了,只能使用IoCopyCurrentIrpStackLocationToNext。
DriverEntry函数执行完成后,开始执行AddDevice函数。这个函数创建设备对象把设备对象连接到设备堆栈上,清除DO_DEVICE_INITIALIZING标志。然后配置管理器向驱动程序发送一个即插即用请求IRP_MN_START_DEVICE,该IRP例程将配置设备。
配置USB的步骤一般是首先为设备选择一个配置(大多数设备仅有一种配置)。选定了某种配置后,接着应该选择配置中的一个或多个接口。然后向总线驱动程序发送配置选择URB,总线驱动程序接收到该URB后向设备发出命令使用选定的配置和接口。
1. 设备选择配置的过程其实就是获取设备的配置描述符的过程,首先获取固定大小的配置描述符,这时,此描述符不包含接口描述符和端点描述符。然后获取全部的配置描述符,包括接口描述符和端点描述符,代码如下。
//首先获取固定大小的配置描述符,这时,此描述符不包含接口描述符和端点描述符。 siz = sizeof(USB_CONFIGURATION_DESCRIPTOR); configurationDescriptor = ExAllocatePool(NonPagedPool, siz); if(configurationDescriptor) { UsbBuildGetDescriptorRequest(urb, (USHORT) sizeof(struct _URB_CONTROL_DESCRIPTOR_REQUEST), USB_CONFIGURATION_DESCRIPTOR_TYPE, 0, 0, configurationDescriptor, NULL, sizeof(USB_CONFIGURATION_DESCRIPTOR), NULL); /* 向USBD.SYS发送URB */ ntStatus = CallUSBD(DeviceObject, urb); …… } //然后获取全部的配置描述符,包括接口描述符和端点描述符 siz = configurationDescriptor->wTotalLength; ExFreePool(configurationDescriptor); configurationDescriptor = ExAllocatePool(NonPagedPool, siz); if(configurationDescriptor) { UsbBuildGetDescriptorRequest(urb, (USHORT)sizeof(struct _URB_CONTROL_DESCRIPTOR_REQUEST), USB_CONFIGURATION_DESCRIPTOR_TYPE, 0, 0, configurationDescriptor, NULL, siz, NULL); ntStatus = CallUSBD(DeviceObject, urb); …… } |
2. 从配置描述符中提取感兴趣的接口描述符,总线驱动程序提供了函数USBD_ParseConfigurationDescriptorEx以简化这个过程。
interfaceDescriptor =USBD_ParseConfigurationDescriptorEx( ConfigurationDescriptor,ConfigurationDescriptor, interfaceindex,0, -1, -1, -1); |
该函数各个参数的含义是:第一个参数是上一步获取的完整的配置描述符;第二个参数是描述符内部开始搜索的地址,如果从头开始搜索,需要设置和第一个参数相同;剩下的五个参数是和感兴趣的接口相关搜索关键字,分别是InterfaceNumber, AlternateSetting, InterfaceClass, InterfaceSubClass, InterfaceProtoco。但相关的关键字不需要的时候,可以设置成-1。
由于配置描述符中可能包含多个接口,所以驱动程序需要将上述函数返回的接口描述符保存在USBD_INTERFACE_LIST_ENTRY类型的数组中。iso_usb程序首先使用ExAllocatePool函数为接口描述符分配足够的内存。
interfaceList =ExAllocatePool(NonPagedPool, sizeof(USBD_INTERFACE_LIST_ENTRY) * (numberOfInterfaces + 1)); |
然后通过循环使用USBD_ParseConfigurationDescriptorEx函数获取的接口描述符对数组进行初始化。初始化时,应该把接口描述符地址赋给USBD_INTERFACE_LIST_ENTRY结构的InterfaceDescriptor成员,并把Interface成员置NULL。最后需要将数组的最后一个元素的两个成员全部置为NULL。
3. 初始化接口。首先调用USBD_CreateConfigurationRequestEx函数创建一个urb。然后需要对接口中的管道进行相应的初始化,最后将这个urb传递给底层驱动程序,由底层总线驱动程序完成接口的初始化。
urb = USBD_CreateConfigurationRequestEx(ConfigurationDescriptor, tmp); Interface = &urb->UrbSelectConfiguration.Interface; /* 需要初始化管道的MaximumTransferSize成员。它代表单一URB能携带的最大数据量 */ for(i=0; i<Interface->NumberOfPipes; i++) { Interface->Pipes[i].MaximumTransferSize = <constant> } ntStatus = CallUSBD(DeviceObject, urb); |
配置USB的过程有这几步基本上就可以完成了,配置后的信息可以通过上位机枚举显示出来。
Windows 2000支持一种称为Windows管理诊断(WMI)的控件,用于管理计算机系统。WBEM(基于Web的企业管理)是一个广泛的工业标准,而WMI是这个工业标准的Microsoft实现。WMI的目标是为系统管理和企业网络中管理数据的描述提供了一个模型,并尽可能独立于专用API或数据对象模型。这种独立性促进了能创建、传输,和显示控制数据的独立系统部件的发展。
WDM驱动程序以三种方式适应WMI,见图3-3-2-5-0。第一,WMI通常能响应提取性能数据的请求。第二,各种控制器应用程序可以使用WMI方式控制设备的通用特征。第三,WMI提供了一个事件通知机制,允许驱动程序通知应用程序有重要的事件发生。
在WMI模型中,数据和事件被分成了消费者和生产者两类。数据块就是抽象类的实例,其概念与C++中的类概念一致。如同C++中的类,WMI类也有数据成员和实现对象行为的方法。数据块中的内容并不是由WMI指定,而是由数据生产者和数据的使用目的决定的。送往驱动程序的数据最有可能来自管理者本身的操作。而驱动程序发出的数据通常是某种性能的统计数据,这些数据的消费者可能是某个性能监视程序,图3-3-2-5-1展示了WMI的整体结构。
WDM驱动程序可以作为WMI类实例的生产者。一个描述了驱动程序支持的各种类(驱动程序可以为这些类提供数据)的脚本称为驱动程序规划(schema)。我们可以使用MOF(Managed Object Format)语言定义规划。系统则维护一个称为repository的数据字典,它包含了所有已知的规划定义。如果驱动程序做得正确,系统将在初始化驱动程序时自动把规划放到repository中。
在WDM驱动当中,对WMI的支持需要实例化IRP_MJ_SYSTEM_CONTROL例程。在该IRP例程服务函数当中再调用IoWMIRegistrationControl函数为该驱动注册对WMI服务支持。
IoWMIRegistrationControl(DeviceExtension->FunctionalDeviceObject, WMIREG_ACTION_REGISTER); |
相反如果要取消对WMI服务支持,需要调用同样需要调用IoWMIRegistrationControl,但是地二个参数需要设置为WMI_ACTION_DEREGISTER。
IoWMIRegistrationControl(DeviceExtension->FunctionalDeviceObject, WMIREG_ACTION_DEREGISTER); |
在系统控制IRP的派遣例程中,你可以委托WMILIB来完成大部分工作。WMILIB_CONTEXT结构体是WMILIB提供的一个用于处理WMI的服务器,该结构体描述如下:
typedef struct _WMILIB_CONTEXT { ULONG GuidCount; PWMIGUIDREGINFO GuidList; PWMI_QUERY_REGINFO QueryWmiRegInfo; PWMI_QUERY_DATABLOCK QueryWmiDataBlock; PWMI_SET_DATABLOCK SetWmiDataBlock; PWMI_SET_DATAITEM SetWmiDataItem; PWMI_EXECUTE_METHOD ExecuteWmiMethod; PWMI_FUNCTION_CONTROL WmiFunctionControl; } WMILIB_CONTEXT, *PWMILIB_CONTEXT; |
Ø GuidCount域指定注册WMI的驱动WMIGUIDREGINFO结构体数量,另外GuidList则指定WMIGUIDREGINFO数组。
Ø QueryWmiRegInfo指向QueryRegInfo回调函数;QueryWmiDataBlock指向QueryDataBlock回调函数;SetWmiDataBlock指向SetDataBlock回调函数;SetWmiDataItem指向SetDataItem回调函数。
Ø ExecuteWmiMethod和WmiFunctionControl一般设置为NULL。
有了这个结构体,你可以委托WMILIB来完成大部分工作,代码如下;
WMIGUIDREGINFO guidlist[] = { <--1 {&GUID_WMI42_SCHEMA, 1, WMIREG_FLAG_INSTANCE_PDO}, }; WMILIB_CONTEXT libinfo = { arraysize(guidlist), guidlist, QueryRegInfo, QueryDataBlock, SetDataBlock, SetDataItem, ExecuteMethod, FunctionControl, }; NTSTATUS DispatchWmi(IN PDEVICE_OBJECT fdo, IN PIRP Irp) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension; NTSTATUS status = IoAcquireRemoveLock(&pdx->RemoveLock, Irp); <--2 if (!NT_SUCCESS(status)) return CompleteRequest(Irp, status, 0); SYSCTL_IRP_DISPOSITION disposition; status = WmiSystemControl(&libinfo, fdo, Irp, &disposition); <--3 switch (disposition) <--4 { case IrpProcessed: break; case IrpNotCompleted: <--5 IoCompleteRequest(Irp, IO_NO_INCREMENT); break; default: <--6 case IrpNotWmi: case IrpForward: <--7 IoSkipCurrentIrpStackLocation(Irp); status = IoCallDriver(pdx->LowerDeviceObject, Irp); break; } IoReleaseRemoveLock(&pdx->RemoveLock, Irp); return status; } |
Ø guidlist声明的有效范围是整个文件,它描述了驱动程序支持的类GUID,并列出几个WMILIB用于处理WMI请求的回调函数。
Ø 与其它派遣例程相同,我们在处理这种IRP时也获取和释放删除锁。我们要防止PnP事件使下层设备对象消失。我们自己的设备对象不会消失,因为IoWMIRegistrationControl调用获得了对它的一次引用。
Ø status = WmiSystemControl(&libinfo, fdo, Irp, &disposition)语句调用WMILIB来处理该IRP。我们传递了WMILIB_CONTEXT结构的地址。通常我们应使用一个静态上下文结构,因为从一个IRP到另一个IRP,其中的信息不可能被改变。WmiSystemControl返回两个信息:一个NTSTATUS代码和一个SYSCTL_IRP_DISPOSITION值。
Ø 执行这个IRP时我们可能需要做一些额外工作,这取决于它的特征代码,如果这个代码为IrpProcessed,则该IRP已经完成,我们不需要再做任何事情。对于除IRP_MN_REGINFO之外的其它副功能码,这种情况就是通常情况。
Ø 如果代码是IrpNotCompleted,则我们有责任完成该IRP。这也是通常情况,除了IRP_MN_REGINFO。WMILIB已经填充完IRP中的IoStatus块,所以我们仅需要调用IoCompleteRequest。
Ø default和IrpNotWmi情况不应该发生在Windows 2000中。如果不能处理所有可能的特征代码,我们将到达default。如果我们向WMILIB发送一个IRP,但其副功能码在WMI中未定义,则我们到达IrpNotWmi处。
Ø IrpForward情况发生于该系统控制IRP是发往其它驱动程序的。回想一下ProviderId参数,它指出处理该IRP的驱动程序。WmiSystemControl用设备对象指针指向的值与第二个参数比较。如果不相同,它就返回到IrpForward,然后我们把该IRP下传到下一个驱动程序。
WMI消费者通过查看WMILIB_CONTEXT结构中的GUID来判断驱动程序是否是一个WMI生产者。当一个消费者想提取数据时,它通过(间接地)访问WMI数据字典( repository),把一个符号对象名翻译成一个GUID,这个GUID就是以前提到的MOF语句的一部分,它应该与WMILIB_CONTEXT结构中的GUID一致,WMILIB会关心这个匹配。
WMILIB将回调驱动程序中的例程来执行设备相关或驱动程序相关的处理。回调函数大部分时间以同步方式执行IRP的操作。除了IRP_MN_REGINFO之外,我们可以推迟IRP处理并返回STATUS_PENDING。如果一个回调例程挂起了该IRP,那么它应该额外再调用一次IoAcquireRemoveLock。任何完成该IRP的例程都应该调用相反的IoReleaseRemoveLock函数。
在SetDataBlock回调函数和SetDataItem回调函数完成之后,需要将修改后的信息保存在资源文件当中,由于委托WMILIB处理WMI服务,因此WMILIB为我们提供了一个WmiCompleteRequest函数,该函数将实现我们需要的功能,函数描述如下;
NTSTATUS WmiCompleteRequest( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN NTSTATUS Status, IN ULONG BufferUsed, IN CCHAR PriorityBoost ); |
BufferUsed指定实际需要完成的数据量,如果该值设置过小,系统会返回STATUS_BUFFER_TOO_SMALL,如果该值设置过大,系统会自动根据实际需要设置完成的数据量。
在WmiCompleteRequest函数实现过程当中,其实操作系统仍然调用IoCompleteRequest函数完成相关的IRP例程,PriorityBoost域指定I/O处理的优先级别。PriorityBoost值一般设置为IO_NO_INCREMENT。