分享

WDM驱动开发之路(3)

 李林侠图书馆 2011-12-09
WDM驱动开发之路(3)



WDM驱动开发之路(三)
(续上期)
在上期中我们讲了符号连接,在应用层开发中我们可以调用以下函数来创建一个\??目录下的符号链接:
BOOL okay = DefineDosDevice(DDD_RAW_TARGET_PATH, "barf", "\\Device\\SECTEST_0");
调用成功后,将会在设备命名空间的\??目录下生成一个名为”barf“的符号链接,该链接指向”“\\Device\\SECTEST_0“这个对象。
在核心态的驱动程序中,我们需要调用以下的函数来创建相应的符号链接:
IoCreateSymbolicLink(linkname, targname);
Linkname是要创建的符号链接名,相当于上面函数中的”barf”,targname是该链接指向的设备对象。
如果你创建了一个指向不存在的设备对象的符号链接,系统并不会作任何检查,当你访问这个符号链接时只会收到一个错误报告。所以你必须要自己保证链接的目的对象真正存在。如果你想允许用户模式程序能超越这个连接而转到其它地方,应使用IoCreateUnprotectedSymbolicLink函数替代上面的IoCreateSymbolicLink函数。
给设备命名后我们就可以很方便地打开该设备进行访问了。但在方便的同时你需要注意一个很严重的问题:“安全性”。一旦为设备命名后,符何核心态的驱动程序都可以打开该设备的句柄,从而访问此设备。而且更糟的是,任何用户态的应用程序也可以通过建立该设备名的符号链接而访问到该设备。而这种情况可能是你不愿意看到的。
一旦你决定要为你的设备命名时,你应该将这个设备对象的名称放到对象名空间的“\Device”目录中,我们可以使用以下的核心态函数来创建设备,同时给设备命名:
UNICODE_STRING devname;
RtlInitUnicodeString(&devname, L"\\Device\\Simple0");
IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), &devname, ...);
这里的UNICODE_STRING devname就是用来存放设备名的地方。RtlInitUnicodeString是unicode串初始化函数,第一个参数是要初始化的变量地址,第二个为设备名常量。第二个参数前的大写L是将这个常量转换成此函数需要的宽字符串。一般我们使用如下的格式为设备命名:
设备名0
其中的0为设备的实例号(即产生实例的顺序)。
说到这里我要提醒一下大家,在驱动程序中一般不使用Ansi字符串,取而代之的是UniCode字符串,它以16位表示一个字符。这点和在WinCE下开发软件很相似。
在以前的老式驱动程序中(Win 3.2 or Win95)中大量使用设备命名(包括直接用名字和名字的符号链接)的方式来访问设备。这样做有两个很主要的问题。一是安全性问题。在上面我们已经讲了这样做有潜在的安全性问题,符何程序只要知道该设备的名字就可以访问它。第二个问题是你的应用程序要访问该设备必须事先知道它的名字,否则不能访问。这在测试用的设备或私有设备(只为你的应用程序服务而不向第三方提供接口)的情况下是可以的。但是如果你的硬件设备还要为第三方的程序服务或者有可能有第三方的公司为你的设备写驱动时就会有很多问题。可能你对该设备的命名会和其它的设备相重复。而且这样的命名很依赖程序员本身所使用的自然语言。
为了解决这个问题,微软在设计WDM框架时引入了一个新的命名方案。该方案与任何自然语言无关,且易于扩展,可广泛地用于软件用硬件,并且易于归档。该方案依靠一个设备接口的概念。它基本上是软件如何访问硬件的一个说明。一个设备接口由一个唯一的128位的GUID标识。一般情况下我们可以使用GUIDGEN工具生成这个标识(GUIDGEN工具可以在VC++企业版的可执行程序目录下找到)。由于采用了独特的生成算法,你永远也不用担心重复出现GUID的情况。这样一个GUID就唯一标识了一种设备接口。
 
生成的代码如下所示
// {CAF53C68-A94C-11D2-BB4A-00C04FA330A6}
DEFINE_GUID(<<name>>,//将此处改成你的名字 
0xCAF53C68, 0xA94C, 0x11D2, 0xBB, 0x4A, 0x00, 0xC0, 0x4F, 0xA3, 0x30, 0xA6);
此为GUIDGEN程序工作时的截屏。可以选择四种格式输出。一般情况下我们选择第二种。并且为了便于管理,我们把要用于的GUID声明集中放到一个头文件中。
你可以把设备接口想象成锁和钥匙。这样应用程序就可以准确地访问需要访问的设备。
我们可以在功能驱动程序的AddDevice例程序中注册一个或多个设备接口,程序如下:
#include <initguid.h>                            #include "guids.h"                        
//其它声明
NTSTATUS AddDevice(...)
{
//其它代码  
  IoRegisterDeviceInterface(pdo, &GUID_SIMPLE, NULL, &pdx->ifname);    
//其它代码
}
其中的GUID_SIPMLE就是我们要注册的接口的GUID的定义。对此段代码,我们作如下说明:
我们包含了GUIDS.H头文件,那里定义了DEFINE_GUID宏。DEFINE_GUID通常声明一个外部变量。在驱动程序的某些地方,我们不得不为将要引用的每个GUID保留初始化的存储空间。系统头文件INITGUID.H利用某些预编译指令使DEFINE_GUID宏在已经定义的情况下仍能保留该存储空间。 
我使用单独的头文件来保存我要引用的GUID定义。这是一个好的想法,因为用户模式的代码也需要包含这些定义,但它们不需要那些仅与内核模式驱动程序有关的声明。 
IoRegisterDeviceInterface的第一个参数必须是设备PDO的地址。第二个参数指出与接口关联的GUID,第三个参数指出额外的接口细分类名。只有Microsoft的代码才使用名称细分类方案。第四个参数是一个UNICODE_STRING串的地址,该串用于接收设备对象的符号连接名。 
IoRegisterDeviceInterface的返回值是一个Unicode 字符串,这样可以在不知道驱动程序的具体编码的情况下(也就是说没看过你的驱动程序的具体代码),应用程序可以确定并打开该设备的句柄。这个返回值是很奇怪的,形如以下情形:\DosDevices\0000000000000007#{CAF53C68-A94C-11d2-BB4A-00C04FA330A6}.
即它的名字是0000000000000007#{CAF53C68-A94C-11d2-BB4A-00C04FA330A6}
注册过程实际上是先创建一个符号链接,然后把它记入注册表。当驱动程序在响应PnP请求IRP+MN_START_DEVICE时,驱动程序将调用IoSetDeviceInterfaceState函数”使能”该接口:
IoSetDeviceInterfaceState(&pdx->ifname, TRUE);
所谓使能也就是使此符号链接指向具体的PDO对象。
在响应这个调用过程中,I/O管理器将创建一个指向设备PDO的符号连接对象。以后,驱动程序会执行一个功能相反的调用禁止该接口(用FALSE做参数调用IoSetDeviceInterfaceState)。最后,I/O管理器删除符号连接对象,但它保留了注册表项,即这个名字将总与设备的这个实例关联;但符号连接对象与硬件一同到来或消失。
枚举设备接口
 内核模式代码和用户模式代码都能定位含有它们感兴趣接口的设备。下面我将解释如何在用户模式中枚举所有含有特定接口的设备。枚举代码写起来十分冗长,最后我不得不写一个C++类来实现。你可以在DEVICELIST.CPP和DEVICELIST.H文件中找到这些代码。它们声明并实现了一个CDeviceList类,该类包含一个CDeviceListEntry对象数组。
一些声明代码去掉了,详细的文章请看驱动开发网上的志宁专栏(http://www./column.php?sortid=3)。
所有实际的工作都发生在CDeviceList::Initialize函数中。其执行过程大致是这样:先枚举所有接口GUID与构造函数得到的GUID相同的设备,然后确定一个“友好”名,我们希望向最终用户显示这个名字。最后返回找到的设备号。下面是这个函数的代码:
int CDeviceList::Initialize()//枚举设备的接口的具体实现代码
{
  HDEVINFO info = SetupDiGetClassDevs(&m_guid, NULL, NULL, DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);    
  if (info == INVALID_HANDLE_VALUE)
    return 0;
  SP_INTERFACE_DEVICE_DATA ifdata;
  ifdata.cbSize = sizeof(ifdata);
  DWORD devindex;
  for (devindex = 0; SetupDiEnumDeviceInterfaces(info, NULL, &m_guid, devindex, &ifdata); ++devindex)    
  {
    DWORD needed;
    SetupDiGetDeviceInterfaceDetail(info, &ifdata, NULL, 0, &needed, NULL);                

    PSP_INTERFACE_DEVICE_DETAIL_DATA detail = (PSP_INTERFACE_DEVICE_DETAIL_DATA) malloc(needed); 
    detail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA);
    SP_DEVINFO_DATA did = {sizeof(SP_DEVINFO_DATA)};
    SetupDiGetDeviceInterfaceDetail(info, &ifdata, detail, needed, NULL, &did));
    TCHAR fname[256];                                            
    if (!SetupDiGetDeviceRegistryProperty(info,
                      &did,
                      SPDRP_FRIENDLYNAME,
                      NULL,
                      (PBYTE) fname,
                      sizeof(fname),
                      NULL)
      && !SetupDiGetDeviceRegistryProperty(info,
                       &did,
                       SPDRP_DEVICEDESC,
                       NULL,
                       (PBYTE) fname,
                       sizeof(fname),
                       NULL)
    )
      _tcsncpy(fname, detail->DevicePath, 256);
    CDeviceListEntry e(detail->DevicePath, fname);                            
    free((PVOID) detail);
    m_list.Add(e);
  }
  SetupDiDestroyDeviceInfoList(info);
  return m_list.GetSize();
}
该语句打开一个枚举句柄,我们用它寻找包含了指定GUID接口的所有设备。 
循环调用SetupDiEnumDeviceInterfaces以寻找每个与此相匹配的设备。 
有两项信息是我们需要的,接口的“细节”信息和设备实例信息。这个“细节”信息就是设备的符号名。因为它的长度可变,所以我们两次调用了SetupDiGetDeviceInterfaceDetail。第一次调用确定了长度,第二次调用获得了名字。 
通过询问注册表中的FriendlyName键或DeviceDesc键,我们获得了设备的“友好”名称。 
我们用设备符号名同时作为连接名和友好名创建了类CDeviceListEntry的一个临时实例e。 
友好名 
你可能会疑惑,注册表怎么会有设备的FriendlyName名。安装设备驱动程序的INF文件中有一个指定设备参数的段,这些参数将被添加到注册表中。通常我们可以在这里为设备提供一个FriendlyName名。
注:在windows 2k下和Windows 98下的inf文件有少许的不同,即使是同一个设备的inf文件,也要作过适当修改后才能同时用于两个平台下。
作好以上工作后,我们还要初始化一些其它的数据结构才能完成设备加载工作。
在AddDevice中还需要加入其它一些步骤来初始化设备对象,下面我将按顺序描述这些步骤。
设备扩展的内容和管理全部由用户决定。该结构中的数据成员应直接反映硬件的专有细节以及对设备的编程方式。大多数驱动程序都会在这里放入一些数据项,下面代码声明了一个设备扩展结构:
typedef struct _DEVICE_EXTENSION {                
  PDEVICE_OBJECT DeviceObject;                    
  PDEVICE_OBJECT LowerDeviceObject;                
  PDEVICE_OBJECT Pdo;                        
  UNICODE_STRING ifname;                    
  IO_REMOVE_LOCK RemoveLock;    
  DEVSTATE devstate;                        
  DEVSTATE prevstate;
  DEVICE_POWER_STATE devpower;
  SYSTEM_POWER_STATE syspower;
  DEVICE_CAPABILITIES devcaps;
 ...//其它一些结构
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
我模仿DDK中官方的结构声明模式声明了这个结构。 
我们可以用设备对象中的DeviceExtension指针定位自己的设备扩展。同样,我们有时也需要在给定设备扩展时能定位设备对象。因为某些函数的逻辑参数就是设备扩展本身(这里有设备每个实例的全部信息)。所以,我认为这里应该有一个DeviceObject指针。 
我在一些地方曾提到过,在调用IoAttachDeviceToDeviceStack函数时,应该把紧接着你下面的设备对象的地址保存起来。LowerDeviceObject成员用于保存这个地址。 
有一些服务例程需要PDO的地址,而不是堆栈中某个高层设备对象的地址。由于定位PDO非常困难,所以最好的办法是在AddDevice执行时在设备扩展中保存一个PDO地址。 
无论你用什么方法(符号连接或设备接口)命名你的设备,都希望能容易地获得这个名字。所以,这里我用一个Unicode串成员ifname来保存设备接口名。如果你使用一个符号连接名而不是设备接口,应该使用一个有相关含义的成员名,例如“linkname”。 
当你调用IoDeleteDevice删除这个设备对象时,需要使用一个自旋锁来解决同步安全问题,我将在第六章中讨论同步问题。因此,需要在设备扩展中分配一个IO_REMOVE_LOCK对象。AddDevice有责任初始化这个对象。 
你可能需要一个成员来记录设备当前的PnP状态和电源状态。DEVSTATE和POWERSTATE是枚举类型变量,我假设事先已经在头文件中声明了这些变量类型。我将在后面章节中讨论这些状态变量的用途。 
电源管理的另一个部分涉及电源能力设置的恢复,设备扩展中的devcaps结构用于保存这些设置。 
下面是AddDevice中的初始化语句(着重设备扩展部分的初始化):
NTSTATUS AddDevice(...)
{
  PDEVICE_OBJECT fdo;
  IoCreateDevice(..., sizeof(DEVICE_EXTENSION), ..., &fdo);
  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;
  pdx->DeviceObject = fdo;
  pdx->Pdo = pdo;
  IoInitializeRemoveLock(&pdx->RemoveLock, ...);
  pdx->devstate = STOPPED;
  pdx->devpower = PowerDeviceD0;
  pdx->syspower = PowerSystemWorking;
  IoRegisterDeviceInterface(..., &pdx->ifname);
  pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(...);
}
初始化默认的DPC对象
许多设备使用中断来完成操作。中断服务例程(ISR)不能调用用于报告IRP完成的函数(IoCompleteRequest)。利用DPC(延迟过程调用)可以避开这个限制。你的设备对象中应包含一个辅助DPC对象,它可以调度你的DPC例程,该对象应该在设备对象创建后不久被初始化。DPC例程还有其它的作用,比如你需要在中断中处理很多很耗时的操作,这在通常情况下是不可以的,这样会影响操作系统的响应速度,为此我们把这些处理操作移到DPC例程中去。
NTSTATUS AddDevice(...)
{
  IoCreateDevice(...);
  IoInitializeDpcRequest(fdo, DpcForIsr);
}
设置缓冲区对齐掩码
执行DMA传输的设备直接使用内存中的数据缓冲区工作。HAL(硬件抽象层)要求DMA传输中使用的缓冲区必须按某个特定界限对齐,而且设备也可能有更严格的对齐需求。设备对象中的AlignmentRequirement域表达了这个约束,它是一个位掩码,等于要求的地址边界减一。下面语句可以把任何地址圈入这个界限:
PVOID address = ...;
SIZE_T ar = fdo->AlignmentRequirement;
address = (PVOID) ((SIZE_T) address & ~ar);
还可以把任意地址圈入下一个对齐边界:
PVOID address = ...;
SIZE_T ar = fdo->AlignmentRequirement;
address = (PVOID) (((SIZE_T) address + ar) & ~ar);
在这两段代码中,我使用了SIZE_T把指针类型(它可以是32位也可以是64位,这取决于编译的目标平台)转化成一个整型,该整型与原指针有同样的跨度范围。
IoCreateDevice把新设备对象中的AlignmentRequirement域设置成HAL要求的值。例如,Intel的x86芯片没有对齐需求,所以AlignmentRequirement的默认值为0。如果设备需要更严格的缓冲区对齐(例如设备有总线主控的DMA能力,要求对齐数据缓冲区),应该修改这个默认值,如下:
if (MYDEVICE_ALIGNMENT - 1 > fdo->AlignmentRequirement)
  fdo->AlignmentRequirement = MYDEVICE_ALIGNMENT - 1;
我假设你在驱动程序某处已定义了一个名为MYDEVICE_ALIGNMENT的常量,它是2的幂,代表设备的数据缓冲区对齐需求。
其它对象
设备可能还有其它一些需要在AddDevice中初始化的对象。这些对象可能包括各种同步对象,各种队列头(queue anchors),聚集/分散列表缓冲区,等等。
初始化设备标志
设备对象中有两个标志位需要在AddDevice中初始化,并且它们在以后也不会改变,它们是DO_BUFFERED_IO和DO_DIRECT_IO标志。你只能设置并使用其中一个标志,它将决定你以何种方式处理来自用户模式的内存缓冲区。(我将在第七章中讨论这两种缓冲模式的不同,以及你如何选择) 由于任何在后面装入的上层过滤器驱动程序将复制你的标志设置,所以在AddDevice中做这个选择十分重要。如果你在过滤器驱动程序装入后改变了设置,它们可能会不知道。
设备对象中有两个标志位属于电源管理范畴。与前两个缓冲区标志不同,这两个标志在任何时间都可以被改变。我将在第八章中详细讨论它们,但这里我先介绍一下。DO_POWER_PAGABLE意味着电源管理器将在PASSIVE_LEVEL级上向你发送IRP_MJ_POWER请求。DO_POWER_INRUSH意味着你的设备在上电时将汲取大量电流,因此,电源管理器将确保没有其它INRUSH设备同时上电。
设置初始电源状态
大部分设备一开始就进入全供电状态。如果你知道你的设备的初始电源状态,应该告诉电源管理器:
POWER_STATE state;
state.DeviceState = PowerDeviceD0;
PoSetPowerState(fdo, DevicePowerState, state)
建立设备堆
每个过滤器驱动程序和功能驱动程序都有责任把自己的设备对象放到设备堆栈上,从PDO开始一直向上。你可以调用IoAttachDeviceToDeviceStack完成你那部分工作:
NTSTATUS AddDevice(..., PDEVICE_OBJECT pdo)
{
  PDEVICE_OBJECT fdo;
  IoCreateDevice(..., &fdo);
  pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(fdo, pdo);
}
IoAttachDeviceToDeviceStack的第一个参数是新创建的设备对象的地址。第二个参数是PDO地址。AddDevice的第二个参数也是这个地址。返回值是紧接着你下面的任何设备对象的地址,它可以是PDO,也可以是其它低级过滤器设备对象。如果该函数失败则返回一个NULL指针,因此你的AddDevice函数也是失败的,应返回STATUS_DEVICE_REMOVED。
在AddDevice中最后一件需要做的事是清除设备对象中的DO_DEVICE_INITIALIZING标志:
fdo->Flags &= ~DO_DEVICE_INITIALIZING;
当这个标志设置时,I/O管理器将拒绝任何打开该设备句柄的请求或向该设备对象上附着其它设备对象的请求。在驱动程序完成初始化后,必须清除这个标志。在以前版本的Windows NT中,大部分驱动程序在DriverEntry中创建所有需要的设备对象。当DriverEntry返回时,I/O管理器自动遍历设备对象列表并清除该标志。但在WDM驱动程序中,设备对象在DriverEntry返回后才创建,所以I/O管理器不会自动清除这个标志,驱动程序必须自己清除它。
这是本节的内容。下节中我们将介绍在驱动程序中经常要使用到的内核函数。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多