加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

深入浅出windows驱动开发学习笔记

(2015-10-16 15:59:49)
标签:

it

分类: Windows驱动
深入浅出windows驱动开发
一、驱动程序内部参数详解
1. NTSTATUS
DriverEntry (
    __in PDRIVER_OBJECT DriverObject,
    __in PUNICODE_STRING RegistryPath
    )
这个函数是所有驱动程序的入口函数,类似于C语言的 main() 函数。它由操作系统内核中的 I/O 管理器调用。两个参数分别代表
驱动对象的指针和注册表子键的字符串指针。__in 是一个宏,这个参数是入口参数,常见的还有__out,代表出口函数。
2. DbgPrint("Hello, Windows Driver!");  
这个函数类似于C语言的 printf 函数,打印一串字符串。

3. #pragma once 其作用是防止头文件多次被包含,保证头文件只被编译一次,移植性稍差。
4. 
typedef struct _DEVICE_EXTENSION {

    PDEVICE_OBJECT DeviceObject;    // 指回设备对象的指针 
    UNICODE_STRING DeviceName;      // 设备名称 
    UNICODE_STRING SymbolicLink;    // 符号链接名 
          
}DEVICE_EXTENSION, *PDEVICE_EXTENSION;
 结构体定义,用以描述驱动程序的设备扩展。它保存了我们自定义所需的一些信息有助于更加方便的编程。
 5.
#pragma alloc_text(INIT, DriverEntry)
#pragma alloc_text(PAGE, DefaultDispatch)
#pragma alloc_text(PAGE, DriverUnload)
在驱动开发中需要为每一个函数指定其是分页内存还是非分页内存。 INIT 标识是指此函数在驱动加载时使用,是初始化相关的函数,驱动成功加载以后可以从内存卸载。
PAGE 标识是指被此函数在驱动运行时可以被交换到磁盘上。如果不指定,编译器默认为非分页内存。
一般情况下,我们不需考虑这些问题,但是有些特殊情况,代码是不允许被交换到磁盘上的,否则将导致操作系统蓝屏或者自动重启。这里需要注意一点,那就是函数声明
必须在这些指定内存分配的预处理之前,否则无法通过编译。
6. UNREFERENCED_PARAMETER 是一个宏,经常被用来指定参数未被引用,可以避免不必要的警告。
7. UNICODE_STRING deviceName;
RtlInitUnicodeString(&deviceName, L"\\Device\\HelloDRIVER");
Windows内核中大量使用 Unicode 字符串,其具体操作有一系列函数,这一系列函数属于 Rtl系列,也就是微软推荐使用的额运行时函数。
8.
for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++)
    {
        DriverObject->MajorFunction[i] = DefaultDispatch; 
    }
DriverObject->DriverUnload = DriverUnload;
    DriverObject->MajorFunction[IRP_MJ_CREATE] = DefaultDispatch; 
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = DefaultDispatch; 
    DriverObject->MajorFunction[IRP_MJ_READ] = DefaultDispatch; 
    DriverObject->MajorFunction[IRP_MJ_WRITE] = DefaultDispatch; 
宏 IRP_MJ_MAXIMUM_FUNCTION ,代表驱动程序最大的派遣函数指针。这里使用一个默认的派遣函数来初始化他们,然后紧跟着在下面修改我们不打算使用默认的派遣函数指针。
这些派遣函数又可以称为回调函数,有定义实现,提供给操作系统调用。回调函数的意义和应用程序中的没有差别,只不过在驱动程序中,这些派遣函数是我们的主要工作重点。
对于普通的驱动程序,可以不考虑对所有的派遣函数指针进行初始化,但是若果想要实现一个过滤驱动程序,那么请参照以上方式初始化。如果没有进行全部初始化,编译器会
对未处理的派遣函数指针进行默认处理。
DriverUnload  卸载函数,这个派遣函数必须单独提供,并且在操作系统版本不同的情况下,这个函数可能需要注意一些不同的东西。如果不打算对驱动程序进行卸载,这个函
数可以不提供。
9.
// 创建设备 
    status = IoCreateDevice( DriverObject,
                             sizeof(DEVICE_EXTENSION),
                             &deviceName,
                             FILE_DEVICE_UNKNOWN,
                             0,
                             TRUE,
                             &deviceObject);
 
使用 IoCreateDevice 函数创建一个设备对象,其名称称为 "HelloDRIVER" 。 HelloDRIVER 的设备类型为 "FILE_DEVICE_UNKNOWN" ,是一种独占的设备,在运行时只能被一个
应用程序所使用。
10.
// 创建符号链接 
    status = IoCreateSymbolicLink(&symbolicLink, &deviceName);
    
    if(!NT_SUCCESS(status))
    {
        IoDeleteDevice(deviceObject);
        return status;
    }
使用 IoCreateSymbolicLink 函数宏创建了设备符号链接,并对创建结果判断进行必要的失败处理。这个符号连接名主要用来与应用程序进行通信。如果创建失败,则删除已经创
建的设备对象。
驱动程序的设备名称对应用程序是透明的,所以只能用于内核程序。这也是为什么要创建设备符号链接的原因。
二、 WDF 概述
WDF(Windows Driver FrameWork,Windows驱动框架)是 UMDF(User Mode Driver FrameWork,用户模式驱动框架)和 KMDF(Kernel Mode Driver FrameWork,内核模式驱动框架)的总和。
以下以阐释内核开发内容为主,所以凡是讲到 WDF 的地方,除特殊情况外,都仅指 KMDF 而言。
1. 主要特点
WDF主要具有以下几个主要特点。
(1)系统兼容性。
由于框架内部磨合了系统、平台间的差异,而对外提供统一的 DDI(Device Driver Interface)接口,从而使得使用 WDF 编写的驱动在跨平台、系统上表现非常优秀。
(2)基于对象的框架。
有一个最基本的对象,其他对象都基于这个对象进行扩展。而对所有的 WDF 框架对象,一般又对应于某个标准的 WDM 驱动对象。最重要的对象包括:驱动对象、设备对象、 I/O 
请求对象、队列对象、目标对象等。
(3)框架管理着所有对象的生命周期。
通过通用的引用计数,以及一套精心设计的父子层级关系来实现这个维护工作。
(4)为框架对象所设计的一套设施,如上下文空间、同步锁等,使得框架对象极易操作,有安全性保障。
(5)一套精心定义的 PME(Property/Method/Event)编程接口。在 WDK 中我们所能看到的形如 Wdfxxx 的函数,都是 WDF 框架提供的编程接口,称为 "框架DDI接口"。
(6)对于 WDM 进行了完美的封装,最大的突破在于实现了趋于完美的复杂的 PNP 与电源管理状态机(State Machine)。 WDF 驱动此程序受益于此,只要提供非常简单的几个 PNP 与电源事件回调,
就能实现完整功能。大多数时候,甚至根本就可以忽略他们,完全由框架代为处理。
(7)处理 IO 请求更简便。通过使用框架 IO 请求对象(WDFREQUEST)能够轻松实现异步、同步处理,未完成请求的取消操作也极为方便。此外又引入了 IO 队列概念,能够轻松实现多个 IO 请求的
串行、并行和手动处理;并且 IO 队列还支持 PNP 和电源管理。

2. 框架视图
分析:
用户程序:
用户程序运行在用户子系统中,它对内核可为无知,只能通过 Win32 API 或更高级的用户层 API 来完成自己的工作。它与 WDF 内核驱动的交互,基本上是使用这几种 API 来实现:
CreateFile 与 CloseHandle 用于打开和关闭; ReadFile 、 WriteFile 与 DeviceIoControl 用于进行 IO 操作。虽然在内核中 WDF 重新封装了驱动编程架构(WDM),但用户程序对二者使用
完全相同的编程模式。

内核子系统:
内核子系统收到用户程序的请求后,将它封装成 IRP 并发送给正确的设备处理。内核子系统还要负责请求的完成后处理。
WDF框架:
WDF框架的内部实现,其实就是一个内核库形式的 WDM 驱动,并提供框架 DDI 接口给 WDF 驱动使用。框架内部实现了各种 IRP 命令的分发函数,这些内部分发函数成就了 WDF 框架的上
边沿和下边沿。从系统角度讲,所有驱动程序的上边沿负责接收 IRP 包,下边沿负责送出 IRP 包。这两件事,归根结底都是 IRP 分发函数要做的事情(这里不考虑文件驱动里面的 Fast IO
调用和总线驱动的接口调用,他们不通过 IRP 发送请求)。所以笔者把内核分发函数作为重要组成部分,在框图中画出来了。
WDF框架的最大价值,在于其实现的三个重要模块,即对象模块、IO模块和 PNP/电源模块。这些是本章后面详细介绍的重点。

WDF驱动:
一个纯粹的 WDF 驱动,可以完全利用 WDF 框架提供的 DDI 来完成任务。如上图所示,笔者将 WDF 驱动完全包括在 WDF 框架之中,就像鱼儿游在水里一样。但内核服务并不对 WDF 驱动
封闭,所以在必要的时候,WDF 驱动还可以直接调用内核服务。这就好比鱼儿虽然总是在水里,但有时候也可以浮出水面。在驱动的下边沿,在正常情况下, WDF 驱动会让 WDF 框架帮助它将
IO 请求传递到下层驱动或设备;但在特殊情况下,WDF 驱动也可以直接获取 IRP 并自己将其传递到下层处理。

3. 对象模型
计算机里的“对象”一词译自英文 Object ,若从字面理解,Object 的中文直译最通常的是“物体”或事物,从其本意出发,“对象”不必是C++中的”面向对象“的对象,更多的其他东西都可以
“对象”称呼。如果觉得把整形数、浮点数这些基本类型变量称为对象显得小题大做的话,那么把结构体变量称为对象就非常合适了。也正因为如此,内核中的绝大多数(如果不是全部的话)称为
对象者,其实都是结构体变量。比如, DEVICE_OBJECT(设备对象)、 DRIVER_OBJECT(驱动对象)等被称为内核对象,EPROCESS 、 KPROCESS 等被称为执行体对象。
WDF中更广泛地使用了“对象”概念,从某些线索来看, WDF 框架的内部实现大量使用了 C++编程。面向对象模型能很好地实现模块扩展,给编程带来极大的方便;在理解上,也更能够带来
便利。 WDF 框架的对象成员包括:驱动对象、设备对象、内存对象、队列对象、IO请求对象、文件对象。大部分框架对象都能够在 WDM 构架中找到对应物。
WDF 对象有共同的根。就好比说它们有同样的脑袋,却长着高矮不一的身体。 WDF_OBJECT 对象中包含了这样一些基本信息:对象类型(Type)、对象长度(Len)、引用计数、指向 Parent 对
象和子对象列表的指针,用来维护对象的继承关系,通过这种继承关系, WDF 驱动中的所有框架对象共同构成一棵拥有唯一跟对象(驱动对象)的对象树。

3.1 对象和句柄
我们是看不到对象的,我们能看到的是一些称为句柄的东西。句柄是什么呢?一般来说,句柄是一个指针长度的索引(32位系统下是32位值,64位系统下是64位的值),指示它所代表的“对象
指针”在一张表中的位值。

3.2 引用计数
WDF 框架对象,它的设计更为简单因为 WDF 框架对象并不是全局对象,所以根本轮不到对象管理器来管理,WDF 框架必须自己提供管理逻辑。下面是几点实现原则。
(1) WDF 框架对象只能以句柄形式被引用,不能以指针形式被引用。
(2)每个框架对象只有唯一的句柄,这个句柄在创建时返回(WdfObjectCreate),此时框架对象的引用计数为1。此后不能产生一个新的句柄代表这个内核对象。每次的创建操作,对应一次删除操作
(WdfObjectDelete),删除将导致句柄失效,并减少一次应用计数。对象本身只有在引用计数降到0时才会被删除。WDF 驱动程序总是应该保证:在调用 WdfObjectDelete 函数删除对象前,对象的
引用计数已降为1。
(3)通过使用框架 WdfObjectReferencexxx 函数增加框架对象的引用计数,通过 WdfObjectDereferencexxx 函数减少框架对象的引用计数。调用 WdfObjectDereferencexxx 函数后,如果框架引用
计数将为0,则框架对象被删除,其句柄失效。

下面来看一种情况,初看未必会发现问题。
WDFOBJECT Obj;
WdfObjectCreate(WDF_NO_OBJECT_ATTRIBUTE,&Obj); //1
WdfObjectReference(Obj); //2
......
WdfObjectDelete(Obj); //3
WdfObjectDereference(Obj); //4

第1行,创建一个框架对象并返回句柄Obj,此时框架对象的引用计数为1;
第2行,增加一次引用计数,此时应用计数为2;
第3行,经过一系列操作后,不再使用此对象而删除它,对象句柄 Obj 失效,引用计数将为1,框架对象依旧有效;
第4行,程序试图解除第2行添加的一个引用,但由于句柄 Obj 已经失效,将导致不可预料的结果。
这个错误是由于违反了上面第2条原则最后的内容。有3种解决方法:
第1种方法,把第3行删除,这样驱动就把删除的工作交给 WDF 框架来完成。在默认情况下,所有框架对象的跟对象是 DriverEntry 函数中生成的驱动对象。驱动被卸载时,驱动对象会被删除,并
附带将所有子对象都删除。但这种方法会带来运行时资源泄露,如果句柄 Obj 所代表的对象在第4行过后就不在被用到的话,驱动就应及时删除此对象以释放所占内存,否则这个对象将一直存在,
直到驱动卸载为止。
第2种方法,就是将3、4两行交换,这比第1种方法好。但更典型的解决方法乃是第3种。
第3种方法,在对象创建时设置 EvtCleanupCallback 回调中调用 WdfObjectDereference 来完成对象删除。此法最经典!
最后再次重申:不管在什么情况下,一旦调用 WdfObjectDelete ,句柄即告失败。

3.3 上下文空间
每一个 WDF 框架对象都可以有自己的上下文环境,这是一个优雅的特征。就像 WDM 驱动中 DEVICE_OBJECT 对象的设备扩展一样,如果没有这个设备扩展,大部分 WDM 驱动都将难以生存。设备
扩展作为设备对象的上下文,用来保存和设备相关的资源、信息。除了设备扩展这个驱动使用的上下文环境之外,其实 WDM 驱动程序中还有两个系统维护的上下文环境,分别是 DRIVER_OBJECT 对象
中的 DriverExtension(驱动扩展)和 DEVICE_OBJECT 对象中的 DeviceObjectExtension(系统设备对象扩展),这二者用来维护系统所需的上下文环境。总而言之,上下文环境很重要!
在 WDF 框架中,上下文环境称为 Context Space(上下文空间)。与 WDM 相比, WDF 框架对象的上下文空间有如下两个特点。
(1)每个框架对象都可以拥有上下文空间。
(2)每个框架对象可以拥有若干个(数量可以大于1)上下文空间,每个上下文空间由唯一的类型信息结构体(WDF_OBJECT_CONTEXT_TYPE_INFO)来标识。
WDF 框架为我们提供了两种申请上下文空间的方法,笔者把他们称为创建时方法和创建后方法。这种区分乃是针对框架对象的创建来说的,如果在框架对象被创建的同时申请了上下文空间,称为
创建时方法;如果框架对象以创建好,再为它申请上下文空间,则叫做创建后方法。
WDF 使用一种非常艰奥的方式来实现“创建时方法”,下面是它的步骤。
(1)定义一个结构体,作为上下文空间内容。
(2)告知结构体长度,也就是此上下文空间的大小,并同时定义一个将来用以从框架对象中获取上下文空间地址(即第1步中定义的结构体变量的指针)的函数。这个函数的类型定义如下:
typedef STRUCT_CONTEXT *FuncGetContextSpace(WDFOBJECT Obj); 
(3)设置对象属性并创建对象
第1步不必细说,是必须的一个步骤。结构体应该如何定义,完全是驱动自己的事。我们假定结构体的名字就是 STRUCT_CONTEXT 。
第2步非常艰奥,它要做两个动作:首先告知上下文空间长度,以使框架根据长度申请内存;其次定义一个函数,函数的作用是从框架对象中获取上下空间指针。
读者稳不住会问“为什么,是不是太繁琐了?”其实只要细想一想就会觉得,非这么实现不可,因为框架对象是不透明的,上下文空间肯定是作为一个变量指针保存在对象中,所以要获取它的任何成
员变量,都只能采用这种不透明的实现方法。
一般我们通过宏 WDF_DECLARE_CONTEXT_TYPE_WITH_NAME 来定义函数。
WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(STRUCT_CONTEXT, //结构体名称 
FuncGetContextSpace); //函数名
第一个宏参数是上下文空间结构体的名称;第二个宏参数 FuncGetContextSpace 是驱动所起的函数名,驱动程序只需给出函数名即可,函数的具体定义是由宏实现的。
这个宏当然很强大,和这个宏并列的还有另外一个宏,干脆连函数名都不用提供;
WDF_DECLARE_CONTEXT_TYPE(STRUCT_CONTEXT); //结构体名称
这个宏是前者的简化版,宏内部会自动起这样一个函数名: WdfObjectGet_ 结构体名,对应于结构体 STRUCT_CONTEXT , 宏为它在内部起的函数名即为 WdfObjectGet_STRUCT_CONTEXT 。
这个宏定义如下:
#define WDF_DECLARE_CONTEXT_TYPE(_contexttype)  \
       WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(_contexttype, WdfObjectGet_ ## _contexttype)
它不过是借了一个粘连符(##)将 "WdfObjectGet_" 和结构体名结合在了一起,最终还是调用了 WDF_DECLARE_CONTEXT_TYPE_WITH_NAME 宏。
现在我们研究一下 WDF_DECLARE_CONTEXT_TYPE_WITH_NAME 的宏定义了,看他们到底使用了什么神通。
#define WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(_contexttype, _castingfunction) \
                                                                        \
WDF_DECLARE_TYPE_AND_GLOBALS(                                           \
    _contexttype,                                                       \
    WDF_GET_CONTEXT_TYPE_INFO(_contexttype),                            \
    NULL,                                                               \
    WDF_TYPE_DEFAULT_SECTION_NAME)                                      \
                                                                        \
WDF_DECLARE_CASTING_FUNCTION(_contexttype, _castingfunction)
这个宏分别执行了另外两个宏: WDF_DECLARE_TYPE_AND_GLOBALS 和 WDF_DECLARE_CASTING_FUNCTION 。对于这两个宏也要详细说一下,否则很难明白。

1. #define WDF_DECLARE_TYPE_AND_GLOBALS(_contexttype, _UniqueType, _GetUniqueType, _section)\
2.                                                                        \
3. typedef _contexttype* WDF_TYPE_NAME_POINTER_TYPE(_contexttype);         \
4.                                                                        \
5. WDF_EXTERN_C __declspec(allocate( _section )) __declspec(selectany) extern const WDF_OBJECT_CONTEXT_TYPE_INFO WDF_TYPE_NAME_TO_TYPE_INFO(_contexttype) =  \
6. {                                                                       \
7.    sizeof(WDF_OBJECT_CONTEXT_TYPE_INFO),                               \
8.    #_contexttype,                                                      \
9.    sizeof(_contexttype),                                               \
10.    _UniqueType,                                                        \
11.    _GetUniqueType,                                                     \
12. };                                                                      \
第3行, WDF_TYPE_NAME_POINTER_TYPE 宏用来定义一个新名称,定义如下:

#define WDF_TYPE_NAME_POINTER_TYPE(_contexttype) \
    WDF_POINTER_TYPE_ ## _contexttype
所以第3行定义了一个结构体类型别名,这个别名下面会用到,先记住它,此处就相当于: WDF_POINTER_TYPE_STRUCT_CONTEXT 。
第 5~12 行,定义了一个  WDF_OBJECT_CONTEXT_TYPE_INFO 类型的全局变量。变量名由一个宏 WDF_TYPE_NAME_TO_TYPE_INFO 产生,变量名形如:_WDF_结构提名_TYPE_INFO 。下面是结构体 WDF_OBJECT_CONTEXT_TYPE_INFO 的定义 
typedef struct _WDF_OBJECT_CONTEXT_TYPE_INFO {
    //
    // The size of this structure in bytes
    //
    ULONG Size;

    //
    // String representation of the context's type name, i.e. "DEVICE_CONTEXT"
    //
    PCHAR ContextName;                     //结构体名称, 此例中即为 "STRUCT_CONTEXT"

    //
    // The size of the context in bytes.  This will be the size of the context
    // associated with the handle unless
    // WDF_OBJECT_ATTRIBUTES::ContextSizeOverride is specified.
    //
    size_t ContextSize; //上下文空间结构长度

    //
    // If NULL, this structure is the unique type identifier for the context
    // type.  If != NULL, the UniqueType pointer value is the unique type id
    // for the context type.
    //
    PCWDF_OBJECT_CONTEXT_TYPE_INFO UniqueType;

    //
    // Function pointer to retrieve the context type information structure
    // pointer from the provider of the context type.  This function is invoked
    // by the client driver's entry point by the KMDF stub after all class
    // drivers are loaded and before DriverEntry is invoked.
    //
    PFN_GET_UNIQUE_CONTEXT_TYPE EvtDriverGetUniqueContextType;

} WDF_OBJECT_CONTEXT_TYPE_INFO, *PWDF_OBJECT_CONTEXT_TYPE_INFO;

WDK文档暴露了这个结构体的前三个变量。从注释来看,当变量 UniqueType 等于 NULL 时,由结构体本身作为上下文空间唯一的类型 ID ;否则就由 UniqueType 变量作为上下文空间唯一的类型 ID 。但由于此例中 UniqueType 
变量的地址为结构体本身,因此结构体将作为所定义的上下文空间的类型 ID 。
再看第二个宏定义,这个宏将定义上下文空间函数。
1. #define WDF_DECLARE_CASTING_FUNCTION(_contexttype, _castingfunction)    \
\
2. __drv_aliasesMem                                                        \
3. WDF_EXTERN_C                                                            \
4. WDF_TYPE_NAME_POINTER_TYPE(_contexttype)                                \
5. FORCEINLINE                                                             \
6. _castingfunction(                                                       \
7.   __in WDFOBJECT Handle                                                     \
8.   )                                                                    \
9. {                                                                       \
10.    return (WDF_TYPE_NAME_POINTER_TYPE(_contexttype))                   \
11.        WdfObjectGetTypedContextWorker(                                 \
12.            Handle,                                                     \
13.            WDF_GET_CONTEXT_TYPE_INFO(_contexttype)->UniqueType         \
14.            );                                                          \
15. }
第4行是函数返回类型,这个宏上面已经讲过,即 WDF_TYPE_NAME_POINTER_TYPE ,它是 STRUCT_CONTEXT* 类型的别名。
第5行是 __forceinline,令此函数为内联型。
第6行是函数名,使用了宏参数传递的函数名称。
第7行是函数参数,即对象句柄。
第10~14 行,是对函数 WdfObjectGetTypedContextWorker 的调用,它是一个未文档化的 WDF 框架接口,已经无法找到他的具体实现了。它实现的内容大概是:以第二个参数代表的类型 ID 作为索引,找到并返回相关的上下文空间。

最后,总结一下,看看最终得到了什么。
//定义结构体变量,即为上下文空间的实体,此乃全局变量

WDF_OBJECT_CONTEXT_TYPE_INFO _WDF_STRUCT_CONTEXT_TYPE_INFO
{
sizeof(WDF_OBJECT_CONTEXT_TYPE_INFO),                               
"STRUCT_CONTEXT",                                                      
    sizeof(STRUCT_CONTEXT),                                               
    &_WDF_OBJECT_CONTEXT_TYPE_INFO,                                                        \
    NULL, 

//定义获取上下文空间函数
typedef STRUCT_CONTEXT*  WDF_POINTER_TYPE_STRUCT_CONTEXT; 
WDF_POINTER_TYPE_STRUCT_CONTEXT FuncGetContextSpace(__in WDFOBJECT Handle)
{
//以全局变量 _WDF_STRUCT_CONTEXT_TYPE_INFO 的地址作为索引
//找到并返回对应的上下文空间
return (WDF_POINTER_TYPE_STRUCT_CONTEXT)                   
       WdfObjectGetTypedContextWorker(                                 
Handle,                                                     
       &_WDF_STRUCT_CONTEXT_TYPE_INFO        
            );    
}

以后就通过函数 FuncGetContextSpace 来获取对象的上下文空间指针。下面列出“创建时方法”的完整代码——只有短短几行。
//首先声明函数指针
typedef struct {......} STRUCT_CONTEXT;
WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(STRUCT_CONTEXT,FuncGetContextSpace);
WDFOBJECT gObj; //定义一个全局句柄

void FunctionXXX()
{
WDF_OBJECT_ATTRIBUTES attributes;
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attributes, STRUCT_CONTEXT);
//调用 WdfObjectCreate 或其他 WdfXXXCreate 函数创建框架对象
WdfObjectCreate(&attributes, &gObj);
//通过Obj句柄获取框架对象的上下文空间指针
STRUCT_CONTEXT* pContext = FuncGetContextSpace(gObj);
}

至此,上下文空间的第一种创建方法讲完了。现在讲第二种创建方法,即“创建后方法”。此时对象已经存在了,在第一种方法中,上下文空间的创建是通过对象属性结构体(WDF_OBJECT_ATTRIBUTES)的设置实现的。第二种方法仍要借助于
它,时步骤如下:
(1)初始化一个 WDF_OBJECT_ATTRIBUTES 结构体,和第一种方法相同。
(2)调用 WdfObjectAllocateContext 并传入一个对象句柄(已创建者),为它申请上下文空间。函数声明如下:
NTSTATUS
FORCEINLINE
WdfObjectAllocateContext(
    __in
    WDFOBJECT Handle, //先创建的对象句柄
    __in
    PWDF_OBJECT_ATTRIBUTES ContextAttributes, //第一步中设置的属性的属性结构
    __deref_opt_out
    PVOID* Context //返回的上下文空间指针
);

下面是实现代码
//声明另一个函数指针
typedef struct {......} STRUCT_CONTEXT2;
WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(STRUCT_CONTEXT2,FuncGetContextSpace2);


void FunctionXXX()
{
PVOID pContext2;

WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attributes, DRIVER_CONTEXT2);
WdfObjectAllocateContext(gObj,&attributes,&pContext2);
ASSERT( pContext2 = FuncGetContextSpace2(gObj));
}
在大多数情况之下仅定义唯一的环境空间,使用一个以上环境空间的情况比较少见。 WDF 对象框架的这种实现,带来了极大的灵活性。由下面的公式:
环境空间基地址 = 对象地址 + 对象长度
3.4 PME 接口
WDF 框架对象的编程接口类似于 PME (Property/Method/Event)接口模型。
对象的属性和方法混在一起,都是以 WDF DDI 接口的形式暴露的,WDF 文档对这二者也没有严格的界定。属性描述对象的特性,每个属性都有对应的 Get 或 Retrieve 方法,
以及可能存在的 Set 或 Assign 方法。比如 WdfDeviceGetDeviceState 和 WdfDeviceSetDeviceState ,都是设备对象的属性方法。
Get/Set 系列方法的特点是其操作必定会成功,没有表示正确与否的返回值。而 Retrieve/Assign 方法则有一个 NTSTATUS 类型的返回值。
除了上面所讲的属性接口外。所有 WDF 框架提供的其他接口函数都是方法(Method)接口。这些例子不胜枚举,如 WdfObjectCreate/Delete 、 WdfDeviceCreate/Delete 等,
WDF 驱动程序通过方法接口对框架对象实施操作。
再讲讲事件。事件对于 WDF 驱动至关重要。WDF 驱动程序,除了入口函数 DriverEntry, 其他的都不外事件函数!不是事件本身,就是被她调用的子函数。
本质上,事件函数时回调函数,以下会把它称为“事件回调”。当创建一个框架对象时,驱动有机会通过属性结构体(WDF_OBJECT_ATTRIBUTES)或其他结构体设置事件回调。当对应的事件发生时,系统就会检查对象是否
有对应的事件回调,若有,则调用之。
一言难尽的事件回调,下面分别以基本对象和扩展对象进行讲解。
(1)基对象 WDFOBJECT 事件回调,通过结构体 WDF_OBJECT_ATTRIBUTES 进行设置。由于 WDFOBJECT 对象是所有框架对象的组成部分,所以 WDFOBJECT 对象的事件回调,同时也是所有框架对象的事件回调。调用 
WdfObjectCreate 函数创建 WDFOBJECT 对象:

NTSTATUS
FORCEINLINE
WdfObjectCreate(
    __in_opt
    PWDF_OBJECT_ATTRIBUTES Attributes,
    __out
    WDFOBJECT* Object
    )
其中参数 Attributes 是对象属性, WDFOBJECT 的事件回调就通过这个参数来设置,可供设置的回调包括如下两种:

PFN_WDF_OBJECT_CONTEXT_CLEANUP EvtCleanupCallback;
PFN_WDF_OBJECT_CONTEXT_CLEANUP EvtDestroyCallback;

当针对对象句柄的 WdfObjectDelete 函数被调用时, EvtCleanupCallback 将被调用;当对象的引用计数降为0时, EvtDestroyCallback 将被调用。关于这两点,有这样的规律: WdfObjectDelete 调用导致 EvtCleanupCallback 事件, WdfObjectDereference 
调用把对象的引用计数减为0时,将导致 EvtDestroyCallback 事件。此外,当两个回调函数同时存在是,一定是 EvtCleanupCallback 回调比 EvtDestroyCallback 回调更早被调用。下面的伪代码描述了这层关系。
WdfObjectDelete(obj)
|
|---->EvtCleanupCallback(obj);
|---->WdfObjectDereference(obj);
|--->if(GetReferenceNumber(obj)==0) EvtDestroyCallback();

(2)子对象事件回调。不同的子对象类型有不同种类的事件回调,由于子对象中包含了一个基对象,所以子对象既可以设置公共的基对象回调,也可以设置子对象回调。这里仅就设备对象举例,框架设备对象的创建函数为 WdfDeviceCreate :

NTSTATUS
FORCEINLINE
WdfDeviceCreate(
    __inout
    PWDFDEVICE_INIT* DeviceInit,
    __in_opt
    PWDF_OBJECT_ATTRIBUTES DeviceAttributes,
    __out
    WDFDEVICE* Device
    )
其中参数 DeviceAttributes 用来设置基对象属性,如第一条所示。 DeviceInit 用来设置设备对象的特有属性,其中可设置的回调相当丰富,包括 PNP 和和电源回调,可通过下面的函数来设置。

VOID WdfDeviceInitSetPnpPowerEventCallbacks(
__in
    PWDFDEVICE_INIT DeviceInit,
    __in
    PWDF_PNPPOWER_EVENT_CALLBACKS PnpPowerEventCallbacks
);
其中参数 PnpPowerEventCallbacks 是一个较大的回调函数数据结构,设置各种 PNP 和电源事件回调函数。
要看各种不同对象的特有事件回调,在 WDK 中搜索 WdfXXXCreate 函数的说明文档,可以得到有用的资料。比如, WDFIOQUEUE 对象的创建函数是 WdfIoQueueCreate, 此函数有一个 WDF_IO_QUEUE_CONFIG
结构体参数, WDFIOQUEUE 对象的特有事件就是通过它来设置的,只要详细了解这个结构体的定义就可以了。
并非所有的子对象都有自己的特有事件,比如 WDFREQUEST 对象就没有,有或没有,多或少,乃是由设计者根据实际情况而定的。
3.5 DDI接口
框架的 DDI接口就是我们可以从 WDK 帮助文档中看到的形如 WdfXXX 的函数。
WdfObjectCreate函数的实现:
NTSTATUS
FORCEINLINE
WdfObjectCreate(
    __in_opt
    PWDF_OBJECT_ATTRIBUTES Attributes,
    __out
    WDFOBJECT* Object
    )
{
    return ((PFN_WDFOBJECTCREATE) WdfFunctions[WdfObjectCreateTableIndex])(WdfDriverGlobals, Attributes, Object);
}

搜索 WdfFunctions  发现:
typedef VOID (*WDFFUNC) (VOID);
extern WDFFUNC WdfFunctions [];
第一行代码定义了 WDFFUNC 函数指针类型,这个类型是极简单的,参数与返回值都是 void 类型。和 PVOID 类型的指针类似, WDFFUNC 函数类型的好处就是能够和所有其他类型的函数指针互换,不会发生错误。
第二行代码是 WDFFUNC 类型数组的声明——并非定义,笔者尝试查找 WdfFunctions 数组的定义,在整个 WDK 目录中进行了大规模的搜索,但终告失败。
每个 WDF 驱动都拥有一个 WdfFunctions 数组,数组 WdfFunctions 的初始化工作是在 WdfDriverCreate 中完成的。

3.6 父子关系
WDF 的对象模型使用了父子概念,即对象之间存在父子关系。这和 C++ 中的父类、子类是有层次上的区别的: 对象概念是具体的,类的父子关系则着眼于更高层次的抽象层次。父对象拥有对子对象的控制权,
父对象被销毁前,先将自己所有的子对象都销毁。父子关系使得对象管理变得很方便,比如对于一个内存对象,我们不必总是记挂着是否要释放他,只要维护好它的父对象就可以了。
每个驱动都有唯一的驱动对象,驱动对象是所有 WDF 对象的根对象。也就是说,在 WDF 驱动中所有创建的 WDF 对象,要么是它的子对象,要么是它的某个子对象的子对象,即孙对象、重孙对象......而驱动
对象的生命周期是由框架维护的。框架在这方面总能可靠地工作,所以我们甚至不用操心去维护一个 WDF 对象,因为到最后当驱动对象被销毁时,所有子对象都会一起销毁。
WDF 框架对象间的父子关系,是一种典型的树形结构,父对象一定有一个指向子对象列表的指针。 Driver 对象是一切 WDF 对象的父对象。框架自动对 Driver 对象进行维护,当 Driver 对象被销毁时,整个
“对象树”都将“倒塌”。

如图 对象树:
框架对象之间的父子关系,并不能任意搭配。比如, WDFREQUEST 对象只能是 WDFDEVICE 的子对象,而不能是 WDFDRIVER 的直接子对象(可以是孙对象)。如图取自 WDK ,非常好地描述了这种父子关系。图中实现箭头
(->)表示确定的父子关系,包括 WDFREQUEST 和 WDFDEVICE 对象之间的关系;长虚箭头(-->)表示默认但可改变的父子关系,包括 WDFKEY 和 WDFDRIVER 对象之间的关系, WDFKEY 默认是 WDFDRIVER 的子对象,但其
实它可以被指定为任何对象的子对象;点虚箭头(....>)表示可以多个对象作为父对象,包括 WDFDPC 对象,它既可以 WDFDEVICE 为父对象,也可以  WDFQUEUE 为父对象。
3.7 对象同步
框架对象是一种可能被争抢的共享资源。和所有共享资源一样,如果存在争抢,就要为它设置同步机制。框架对象内部默认包含了这种锁机制,对于这个默认的对象锁,
外部可通过 WdfObjectAcquireLock 和 WdfObjectReleaseLock 实现手动同步。但更便利的用法是,借助自动同步机制。自动同步机制包括如下两个方面内容。
(1). 同步范围
有两种可选的同步范围:设备同步(WdfSynchronizationScopeDevice)、队列同步(WdfSynchronizationScopeQueue)。当选择设备同步时,对于设备上的所有队列和文件
对象,同一时刻只能有一个对象的事件回调被执行;当选择队列同步时,在同一时刻,每个队列只能有一个事件回调被执行。如果不选择任何同步范围,即为不同步
(WdfSynchronizationScopeNone),那么在同一时刻,设备上可以有任意多个事件回调被执行。
如果是设备同步则所有下属队列或文件对象的事件回调被执行前,都必须申请设备对象的同步锁;而如果是队列同步,则队列中的事件回调被执行前,都必须申请此队
列对象的同步锁。如果不选择任何同步范围,则不必申请同步锁。这是自动同步的实现原理。
此属性通过结构体 WDF_OBJECT_ATTRIBUTES 的 SynchronizationScope 变量进行设置。
(2). 运行级别
运行级别即事件回调最高可在那个中断级别(IRQL)上被调用。可选的值有两个,即运行在 PASSIVE_LEVEL(WdfExcutionLevelPassive),或最高可运行在 DISPATCH_LEVEL 
(WdfExcutionLevelDispatch) 。当然,也可以让子设备自动从父设备那里继承此属性,则设置为 WdfExcutionLevelInheritFromParent。
此属性通过结构体 WDF_OBJECT_ATTRIBUTES 的 ExecutionLevel 变量进行设置。
此外,对于设备对象的 PNP/Power  事件回调,驱动总是对他们实施同步调用的。

4. 驱动对象和设备对象

4.1 驱动对象
驱动对象是最重要的框架对象,它是一切其他框架对象的父对象,也是所有框架对象中第一个被创建,而最后一个被删除的对象。当它的生命期完结后,所有其他子对象
也一定不复存在。
只要得到驱动对象,就可以顺着他的继承路线,搜索到所有的子对象。所谓提纲挚领,这个驱动对象就是整个驱动的“纲”与“领”了。由于框架驱动对象处于这个地位,他
的作用就非常大了,能够随时获得这个驱动对象,也就很有用处了。故而在 WDF 驱动的任何地方,只要调用 WdfGetDriver 函数,就可以获得同一个驱动对象句柄了。
WDFDRIVER Driver = WdfGetDriver();
妙在 WdfGetDriver 调用不需要任何参数,这是因为驱动层序对象的句柄保存在一个全局结构体变量中, WdfGetDriver 直接从结构体变量中获取这个句柄。这个全局结构
体类型的定义如下:
typedef struct _WDF_DRIVER_GLOBALS {

    // backpointer to the handle for this driver
    WDFDRIVER Driver; //驱动对象

    // Flags indicated by the driver during create
    ULONG DriverFlags;

    // Tag generated by WDF for the driver.  Tag used by allocations made on
    // behalf of the driver by WDF.
    ULONG DriverTag;

    CHAR DriverName[WDF_DRIVER_GLOBALS_NAME_LEN];

    // If TRUE, the stub code will capture DriverObject->DriverUnload and insert
    // itself first in the unload chain.  If FALSE, DriverUnload is left alone
    // (but WDF will not be notified of unload and there will be no auto cleanup).
    BOOLEAN DisplaceDriverUnload;

} WDF_DRIVER_GLOBALS, *PWDF_DRIVER_GLOBALS;
这个结构体可以在头文件 WdfGlobals.h 中找到,读者结合下一节中讲到的 WDF_DRIVER_CONFIG 结构体就能基本理解这个结构体的各个成员含义。
WDFDRIVER
FORCEINLINE
WdfGetDriver(
    VOID
    )
{
    return WdfDriverGlobals->Driver;
}

列举一下驱动对象的几个主要作用。
(1)驱动对象代表了加载到系统空间中的驱动模块。相同的驱动文件,不管同时作用于多少个设备,驱动对象总是唯一的。
(2)在驱动程序的任何地方,调用 WdfGetDriver 就可以获得唯一的驱动对象句柄。所以,建议把全局变量保存在驱动对象中。
(3)对于 PNP 类驱动,驱动对象负责注册 EvtDriverDeviceAdd 事件回调,这个事件回调相当于 WDM 架构中的 AddDevice 函数,用以建立设备栈。
(4)对于非 PNP 类驱动,一般通过驱动对象注册 EvtDriverUnload 事件回调,它的作用相当于 WDM 架构中的 DriverUnload 函数,保存在驱动对象中的系统资源一般借助EvtDriverUnload 事件回调
释放。此外,资源泄露在内核中是很严重的错误。
(5)可以为驱动初始化一个事件跟踪(WPP机制)。 

4.2  驱动入口 DriverEntry
WDF 驱动的入口函数(一般为 DriverEntry)和框架对象,特别是驱动对象有着千丝万缕、密不可分的关系。应当在入口函数中创建驱动对象,在驱动对象被创建之前,一切框架 DDI 接口都不应该被驱动调用。
根据驱动类型, DriverEntry 入口函数有多种写法,主要分为:设备驱动、过滤驱动和纯软件驱动。这里所谓的纯软件驱动,不和任何硬件挂钩,是一个在内核中提供接口服务的软件模块。
设备驱动一定要注册 EvtDriverDeviceAdd 事件回调;过滤驱动根据其类型,如果过滤的设备栈属于某个物理设备,则也应当注册 EvtDriverDeviceAdd 事件回调;如若不然,驱动加载之后,将不会起到任何
任何预期的作用。
纯软件驱动则不可注册 EvtDriverDeviceAdd 事件回调;过滤驱动根据其类型,若过滤的设备栈不属于物理设备(如文件驱动设备栈),则也不可注册此事件回调;如若不然,将返回无效参数错误。
创建驱动对象,调用 WdfDriverCreate 接口:
NTSTATUS
FORCEINLINE
WdfDriverCreate(
    __in
    PDRIVER_OBJECT DriverObject,
    __in
    PCUNICODE_STRING RegistryPath,
    __in_opt
    PWDF_OBJECT_ATTRIBUTES DriverAttributes,
    __in
    PWDF_DRIVER_CONFIG DriverConfig,
    __out_opt
    WDFDRIVER* Driver
    )
参数 DriverObject 类型为内核驱动对象(DRIVER_OBJECT),这也是唯一 WDF 驱动中必须用到 WDM 对象的地方;也进一步说明,框架驱动对象是对内核驱动对象的包装。最后一个输出参数 Driver 就是包装后的框架驱动对象。
参数 RegistryPath 是驱动所对应服务键在注册表中的路径,这个信息来自内核配置和管理服务器(Cfg Manager)。
参数 DriverAttributes 是针对 WDFOBJECT 对象的属性配置的。参数 DriverConfig 则是专门针对框架驱动对象的属性配置。它的结构定义如下:
typedef struct _WDF_DRIVER_CONFIG {
    //
    // Size of this structure in bytes
    //
    ULONG Size;

    //
    // Event callbacks
    //
    PFN_WDF_DRIVER_DEVICE_ADD EvtDriverDeviceAdd;

    PFN_WDF_DRIVER_UNLOAD    EvtDriverUnload;

    //
    // Combination of WDF_DRIVER_INIT_FLAGS values
    //
    ULONG DriverInitFlags;

    //
    // Pool tag to use for all allocations made by the framework on behalf of
    // the client driver.
    //
    ULONG DriverPoolTag;

} WDF_DRIVER_CONFIG, *PWDF_DRIVER_CONFIG;
成员 Size 是结构体的长度,估计是为了方便将来对结构体进行扩展。
成员 EvtDriverDeviceAdd 和 EvtDriverUnload 是两个事件回调。前者是当物理设备的设备栈监理师,必须要调用的,是驱动 PNP 设施的重要部分,其地位相当于 WDM 驱动中的 AddDevice 函数;
后者是当驱动镜像从内核空间中被卸载时所要调用的,相当于 WDM 驱动中的 DriverUnload 函数。
参数 DriverInitFlags 用来设置一些初始化标志,可用者有下面两种;
typedef enum _WDF_DRIVER_INIT_FLAGS{
WdfDriverInitNonPnpDriver = 0x00000001, //  If set, no Add Device routine is assigned.
    WdfDriverInitNoDispatchOverride = 0x00000002, //  Useful for miniports.
    WdfVerifyOn = 0x00000004, //  Controls whether WDFVERIFY macros are live.
    WdfVerifierOn = 0x00000008, //  Top level verififer flag.
} WDF_DRIVER_INIT_FLAGS;

枚举值 WdfDriverInitNonPnpDriver 用来区别上面所讲述的两种不同驱动类型,它关系到 EvtDriverDeviceAdd 事件是否有效。
枚举值 WdfDriverInitNoDispatchOverride 用来标识驱动程序是一个小端口驱动,这非常妙,竟使得 WDF 框架不仅能兼容 WDM 架构,也能兼容小端口架构了。 WDF 驱动是怎么做到兼容小端口驱动的呢?
原来是,一但设置了小端口驱动标志后, WDF 驱动将不会用其内部分发函数来处理收到的 IRP ,而是任由 WDF 驱动自己继续调用小端口框架的初始化函数来为其设置分发函数。 
先看第一种驱动类型的入口函数。
NTSTATUS DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath)
{
WDF_DRIVER_CONFIG config;
NTSTATUS status = STATUS_SUCCESS;
WDF_DRIVER_CONFIG_INIT(&config, MyEvtDeviceAdd);
config.EvtDriverUnload = MyEvtDeviceUnload;
return WdfDriverCreate(DriverObject,
RegistryPath,
WDF_NO_OBJECT_ATTRIBUTE,
&config,
WDF_NO_HANDLE);
}
代码格式非常简单,这是因为 WDF 框架总是把用户创建的 WDF 驱动默认为 PNP 类型。下面再看非 PNP 类驱动的入口函数写法。
//定义一个卸载事件回调函数
VOID Unload(IN WDFDRIVER Driver)
{
//如果入口函数中申请了系统资源,可在此处进行释放。最为方便
//用户进程中如果有资源泄露,会随着进程的终结而自动回收全部资源
//但内核中却没有这种便利,甚至会导致错误检查(BugCheck)和蓝屏
}

//非 PNP 类 WDF 驱动入口函数
NTSTATUS DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
WDF_DRIVER_CONFIG config;
NTSTATUS status = STATUS_SUCCESS;
WDFDRIVER driver;
//第二个参数设置为 NULL ,表示不设置 EvtDriverDeviceAdd 事件
WDF_DRIVER_CONFIG_INIT(&config, NULL);
//指明这是一个非 PNP 类驱动
config.DriverInitFlags = WdfDriverInitNonPnpDriver;
//一般会设置 EvtDriverUnload 事件
//这使得非 PNP 类驱动也能够进行"后处理"
//PNP 类驱动的后处理,可在诸如 EvtDeviceD0Exit 、 EvtDeviceReleaseHardware
//这类 PNP 事件中运行
config.EvtDriverUnload = Unload;
return WdfDriverCreate(
DriverObject,
RegistryPath,
NULL,
&config,
&driver);
}
对于非 PNP 类驱动,一般会设置其 Unload 事件回调(EvtDriverUnload)。其原因就是用来后处理,如释放占有的资源等。 PNP 类驱动就没有这种必要(有亦无妨),
因为 PNP 类驱动在相关的 PNP 事件回调(一般是 EvtDeviceD0Exit 、 EvtDeviceReleaseHardware这两种,代表设备断电和设备移除)中进行后处理最为合适。

4.3 设备对象
在 WDM 驱动中,设备对象是驱动程序的核心;但在 WDF 结构里,设备对象的地位显然倒退许多,而驱动对象的地位则有极大的跨进。个人观点: WDF 架构中的核心
位置是由“驱动对象”来承担的。但设备对象依旧极为重要,隶属于设备对象的事件,占据了所有框架事件的四成。与 IO 相关的操作,依旧是围绕设备对象进行的。
先来看不同的设备对象类型,这一点应该是继承自 WDM 架构。
*功能设备对象(FDO):功能驱动程序负责为每个设备创建一个 FDO ,在设备栈中它位于物理设备对象(PDO)的上层。
*物理设备对象(PDO):一个总线驱动创建的 PDO ,在逻辑上代表了物理设备本身,而功能设备对象(FDO)则代表了系统针对这个 PDO 所做的处理。FDO 之所以由某个总线驱动
创建,是因为此 FDO 所代表的物理设备连接到了此总线设备上(此时,此总线驱动承担了总线设备的功能驱动作用)。
*过滤设备对象(Filter DO):微软正在逐步为这种类型的设备对象定义合适的英文缩写,一般就写成比较怪的 Filter DO 。过滤设备对象是设备栈的不速之客,它可以位于设
备栈的任何一个位置,也就是说。他可以对任何一个设备栈中既存的设备对象进行过滤。最精简的设备栈,往往只有两个设备对象,即 FDO 和 PDO , 正是由于若干个 Filter
DO 的加入,设备栈才热闹起来。
*控制设备对象(CDO):最后说道 CDO ,是因为它一般不存在于设备栈中,而是一个独立的设备(但如果有 Filter DO 愿意过滤它,也就构成了设备栈)。这个设备用来作为用户
程序的接口,用户程序通过 CreateFile 将之打开,并通过它发送一些 IO 请求来给驱动程序处理。这正是他名字的由来,用户程序通过它对内核驱动实现控制。

设备对象有很多设备相关的属性,这一点和驱动对象不同,驱动对象暴露出的属性,只有驱动在注册表中的服务键路径这一个(WdfDriverGetRegisterPath)。我们必须用一
个列表来描述设备对象的所有属性,暴露出的属性可通过 WdfDeviceSetXXX 、 WdfDeviceInitXXX 系列函数来获取。如下列出了最常用的一些设备对象属性及简单描述,并附上
了设置和获取接口。
设备对象属性
属性名 描述 DDI
对齐 设备地址对齐,便于内存操作。源自 DEVICE_OBJECT WdfDeviceSetAlignmentRequirement/
结构体中的 AlignmentRequirement 变量 WdfDeviceGetAlignmentRequirement

名称 唯一的设备名称 WdfDeviceAssignName/
无获取接口

安全 ID 针对设备对象的安全属性 WdfDeviceInitAssignSDDLstring/
无获取接口
默认 IO 队列 如果为 IO 请求创建指定的 IO 队列,则对 不能设置/
所有 IO 请求都进入到默认队列中 WdfDeviceGetDefaultQueue

设备特征 掩码形式的设备特征值,源自 DEVICE_OBJECT 结构体 WdfDeviceInitSetCharacteristics &
中的 Characteristics 变量 WdfDeviceSetCharacteristics/
WdfDeviceGetCharacteristics
PNP 特征 包括是否支持软件删除(Eject)、异常拔除、能否 DOC WdfDeviceSetPnpCapalities/
设备(如Hub)等特性,见 WDF_DEVICE_PNP_CAPABILITIES 无获取接口
电源特征 包括电源映射(Dx到Sx)、唤醒等特性,见 WdfDeviceSetPowerCapalities/
WDF_DEVICE_POWER_CAPABILITIES 无获取接口
状态 设备状态,可用的设备状态包括可用、禁止、移除等 WdfDeviceSetDeviceState/
WdfDeviceGetDeviceState
PNP状态 设备 PNP状态,只有 PNP 设备才有。非常复杂,见枚举 不能设置/
类型 WDF_DEVICE_PNP_STATE WdfDeviceGetDevicePnpState

电源状态 设备电源状态,见枚举类型 WDF_DEVICE_POWER_STATE 不能设置/
WdfDeviceGetDevicePowerState
电源策略状态 包括闲时休眠等,见枚举类型 不能设置/
WDF_DEVICE_POWER_POLICY_STATE WdfDeviceGetDevicePowerPolicyState
电源策略所有者 总是让设备的功能驱动作为设备的电源策略所有者。设 WdfDeviceSetDevicePowerPolicyOwnership/
备栈中只能有一个这样的设备对象,否则会发生冲突 无获取接口
过滤设备对象 根据是否是过滤设备对象,框架对 IO 请求的处理有一些 WdfFdoInitSetFilter/
(Filter DO) 差异 无获取接口

默认IO目标对象 框架将设置栈中的下一层设备封装成默认IO目标对象 不能设置/
WdfDeviceGetIoTarget
4.4 创建设备对象
这里只举一个例子:如何创建控制设备对象(CDO)?内核服务或非 PNP 类驱动都会创建 CDO 对象,以接收来自用户程序的控制信息。 PNP 类驱动应当在 EvtDriverDeviceAdd 
事件中创建设备对象,那是最理想的场所。但内核服务或非 PNP 类驱动不可能接收到 EvtDriverDeviceAdd 事件,所以应该在入口函数中创建 CDO 对象。

//和在 WDM 中一样,我们把设备对象作为全局对象保存
WDFDEVICE wdfDevice;

//驱动入口函数
//这里的代码,将省略驱动对象初始化的部分
//其代码在上述小节中已详细讲解

NTSTATUS DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
WDFDRIVER driver;
PWDFDEVICE_INIT deviceInit;
PDEVICE_OBJECT deviceObject;
NTSTATUS status;
//创建框架驱动对象,具体实现省略
WdfDriverCreate(...,&driver);
//Allocate a device initialization structure
deviceInit = WdfControlDeviceInitAllocate(driver,&SDDL_DEVOBJ_KERNEL_ONLY);
//Set the device characteristics
WdfDeviceInitSetCharacteristics(deviceInit,FILE_DEVICE_SECURE_OPEN,FALSE);
//Create a framework device object 
status = WdfDeviceCreate(deviceInit,WDF_NO_OBJECT_ATTRIBUTE,&wdfDevice);
//Check status
if(status == STATUS_SUCCESS)
{
//Initialization if the framework device object is complete
WdfControlFinishInitializing(wdfDevice);
//Get the associated WDM device object
deviceObject = WdfDeviceWdmGetDeviceObject(wdfDevice);
}
return status;
}
有了这么一个设备对象后,就等于建立了和用户程序之间的通信桥梁。

4.5  设备栈

设备栈并没有因为 WDF 框架的加入而有所变化,但 IRP 包在设备栈中的流动会有所改变。下面以一个简单的设备栈为例进行分析。如图所示

这个设备栈由3个设备对象组成,最上层的是功能设备对象,中间的是过滤设备对象,最底层的是物理设备对象。这是一个很普通的一个设备栈,现在假设这个设备栈处于
两个完全不同的驱动栈中,一个是3个设备对象所属的驱动;全部是 WDM 驱动;另外一个相反,全部是 WDF 驱动。当一个 IRP 在这两个本质一样的设备栈中流过的时候,会有
哪些区别?
如图,作图时 WDM 的情况,一个来自 IO 管理器的 IRP 在设备栈中流动;右图是 WDF 的情况,同样是一个来自 IO 管理器的 IRP 在设备栈中流动。

http://s3/mw690/002zn5sBzy6WfuEFASC02&690

作图是正常情况,相比较看右图: IRP 依旧在设备栈中传递,但当进入 WDF 驱动中后,到了设备对象(DEVICE_OBJECT)中的 IRP 却会拐一个弯,把 IRP 封装成 WDF IO 请求
对象(即 WDFREQUEST)后,送给 WDF 设备(WDFDEVICE)去处理了;处理完后, WDF 设备对象再从 WDFREQUEST 对象中提取出 IRP ,并把 IRP 传递到设备栈中的下一层。
如果要控制 WDF 驱动中的 IRP 流向,比如不让 IRP 流到 WDF 设备对象中,可以通过参数 WdfDeviceInitAssignWdmIrpPreprocessCallback 注册一个名为 
EvtDeviceWdmIrpPreprocess 的事件函数,在事件函数中按照 WDM 的方式处理 IRP ,包括传递到下层设备,或者完成 IRP 操作。
5. IO 模型
系统的 IO 请求机制并没有改变,也就是说,IO管理器对IO请求仍然是封装成 IRP 结构体发往内核驱动的,并不管它是 WDM 模式还是 WDF 模式。但 WDF 框架收到 IRP 后,有
足够的睿智判断出此请求是直接在框架内部处理,还是调用 WDF 驱动注册过的事件回调,交回驱动处理。如果调用事件回调,就必须将 IRP 封装成框架请求即 WDFREQUEST 对象。
这样,所有 IRP 都进入到框架中来,一般情况下, WDF 驱动是不可能直接对 IRP 进行操作的。换句话说,WDF 驱动不用为 IRP 担负任何责任,因为它被剥夺了这个荣幸之的差事。
另一方面,所有的 WDFREQUEST 对象都是从框架中创建并流向 WDF 驱动的。我们可以把框架和驱动理解成一对一的关系,框架对一切都有很好的规划,尽量让驱动减轻负担。就拿
WDFREQUEST 对象来说,框架对所有流出它的对象都了如指掌,并自动维护他们的生命周期,不用 WDF 驱动操心。

5.1  IO 目标对象
如图,我们看到, WDFDEVICE 对象对 WDM 设备对象 (DEVICE_OBJECT)进行了封装。图中会给我们这样一种印象: DEVICE_OBJECT 对象是 WDFDEVICE 的子对象。实际上, WDFDEVICE 
对象仅仅保存了一个指向 DEVICE_OBJECT 的指针。
从图中我们还发现了另外一类对象,称作 WDFIOTARGET(IO 目标对象)。 WDFIOTARGET 对象也对 DEVICE_OBJECT 进行了封装,但它和 WDFDEVICE 对象的不同之处在于:只有唯一的
WDFDEVICE 对象与 WDM 设备对象关联,这是因为框架不会允许多个 WDFDEVICE 对象对应于同一个 WDM 设备对象——因为无法从  DEVICE_OBJECT 对象反推出 WDFDEVICE 对象(DEVICE_OBJECT
对象不知道外面有一个 WDFDEVICE 对象),如果不采用一一对应的策略,在管理上势必导致混乱。

WDFDEVICE 和 DEVICE_OBJECT 对象的一一对应也有其不足之处。既无法实现这样的需求:在另一个 WDF 驱动(Driver2)中发送命令给当前驱动(Driver1)的设备对象(WdfDevice)。
首先 Driver1 是设备对象 WdfDev(它封装了 DEVICE_OBJECT 对象 Dev)的拥有者,框架不允许 Driver2 也拥有一个等效设备对象(不存在另一个封装了 Dev 对象的 WdfDev2 对象)。所以
Driver2 要想通过设备对象句柄发送请求,是无法实现的。有人可能想到,把 Driver1 的 WdfDev 对象句柄通过某种方式传递给 Driver2 就可以了。这是可以实现的,但遗憾的是,被传递到
另一个驱动中的 WDF 框架句柄是无法使用的,框架对此有严格的规定:框架对象不能在驱动之间传递。
这个问题的解决之道就是引入了 WDFIOTARGET 对象。在图中代表 WDFIOTARGET 对象的框图有三个:一个是本地 WDFIOTARGET 对象,也称为默认 WDFIOTARGET 对象,这里所谓本地、默认,
是针对设备对象而言的。本地 WDFIOTARGET 对象是唯一的,框架创建 WDFDEVICE 对象时顺便也创建了它,并命令二者一一关联。图中还有若干个远程 WDFIOTARGET 对象,图中画了两个,其实
远程对象可以一个都没有,也可以创建更多。当别的驱动程序或者本驱动程序的其他地方(如果驱动中存在多个设备栈)要发送命令到此设备对象时,可以通过远程 WDFIOTARGET 对象来完成。
再看图中,我们会发现不管是本地的还是远程的 WDFIOTARGET 对象,总有一个队列于此相关联。这是其妙之处,比方说图中,若一驱动程序通过本地 WDFIOTARGET 对象发送 IO 请求到设备
对象,则此 IO 请求要到 QUEUE3 中排队;若另一个驱动程序通过远程 WDFIOTARGET 对象 1 发送 IO 请求,则此请求要到 QUEUE1 中排队。在这种机制下,设备对象退居其次,由 IO 目标对象
作为上层设施,对来自各个方向的请求进行条分缕析的分类管理。
队列在这里带来了一个极大的妙处,由于设备对象的缺陷,所以在 IO 请求的处理上, WDF 框架只知目标对象,不知设备对象,把 IO 请求发送到目标对象中。各个目标对象乃是单独运行的
,它能够实现一些高阶的功能,比如把远程目标对象1删除,此目标对象队列上的 IO 请求都被删除,而其他目标对象上的IO请求并不受影响。只要多添加一个间接层,计算机科学中就没有对此解
决不了的问题。IO目标对象就是设备对象的一个间接层,果然魅力无穷,给旧的 IO 请求处理带来了几多新特性。
总结一下,我们可以得到下面几点重要知识。
*框架设备对象(WDFDEVICE)是对底层设对象的封装,并唯一对应。
*IO 目标对象是框架设备对象(WDFDEVICE)封装,可以有多个目标对象对应于同一个设备对象。
*框架为 WDFDEVICE 对象创建唯一的本地(默认)目标对象。
*WDF 驱动可以创建若干个远程目标对象。
*每个目标对象都有一个内部队列,发送到目标对象的 IO 请求都在队列中等候,最后发送给设备对象处理。

5.2 IO 目标对象的细节
IO 目标是对 IO 请求发送到目标驱动的描述。比如,向另一个内核驱动中的设备发送一个 IOCTL 命令,这个远程设备对象就是 IOCTRL 命令的 IO 目标(IO Target)。新定义这么一个对象并不
是为了形象上的好看,而是赋予了它很多实用的妙处。
(1)请求以同步方式发送到 IO 目标对象。
(2)请求以异步方式发送到 IO 目标对象。
(3)为发送到 IO 目标的请求设置超时,如果预期时间内仍未完成,就取消请求。
(4)IO 目标对象可以被启动、停止、关闭,只有启动后的 IO 目标对象才能接收请求,否则将关闭外界请求通道。
(5)跟踪并维护发送到 IO 目标的 IO 请求,比如删除目标对象后,所有排队于其中的 IO 请求对象都将被删除。

下面就具体阐释这几个方面的内容。 WDFIOTARGET 对象是设备的封装,调用 WdfIoTargetGetDevice 函数可以获取封装的设备对象句柄。
WDFDEVICE
FORCEINLINE
WdfIoTargetGetDevice(
    __in
    WDFIOTARGET IoTarget
    );
IO 目标对象分为普通对象和特殊对象两种,普通目标对象就是 WDFIOTARGET ,特殊目标对象目前只有一种,即 USB IO 目标对象。USB IO 目标对象并没有一个对应的类型被暴露出来,而是
隐藏在诸如 WDFUSBDEVICE 、 WDFUSBPIPE 这些对象内部然后通过这些对象句柄来使用特殊 IO 目标对象。借助特殊目标对象可以轻易地实现许多 USB 功能,大大简化了代码设计。
普通 IO 目标对象,即 WDFIOTARGET 对象分为本地和远程两种,在有些资料中,本地 IO 目标对象还被称为默认(default)对象。如果某个请求继续在当前设备栈中传递,则对应的 
WDFIOTARGET 对象为本地的;若被传递到其他设备栈中,不管此设备是否也通过本驱动,则对应的 WDFIOTARGET 对象通称为远程的,如图
可以直接从设备对象中获取 WDFIOTARGET 对象,所获取的对象对应着当前设备栈中的下一层设备。
WDFIOTARGET WdfDeviceGetIoTarget(IN WDFDEVICE Device);
通过此方式获取的 WDFIOTARGET 对象,称为此设备对象的本地(或默认) WDFIOTARGET 对象,凡是发送到此设备对象的 WDFREQUEST 对象若要在设备栈中继续传递,则应当将请求发送到此本地
目标对象,如图中最左列所示。一个默认事件回调函数可实现如下:
void SomeEvtCallback(WDFDEVICE Device,WDFREQUEST Request)
{
//让 IO 请求在当前设备栈中继续传递
WdfRequestSend(Request,
WdfDeviceGetIoTarget(Device),
NULL
);
}

使用一个远程 IO 目标对象需要经过两个步骤;第一步,创建远程 IO 目标对象;第二步,打开(Open)此对象。调用 WdfIoTargetCreate 接口新建一个 IO 目标对象:
NTSTATUS
FORCEINLINE
WdfIoTargetCreate(
    __in
    WDFDEVICE Device,
    __in_opt
    PWDF_OBJECT_ATTRIBUTES IoTargetAttributes,
    __out
    WDFIOTARGET* IoTarget
)
参数 Device 是被创建的 IO 目标对象拥有者,在默认情况下,就是被创建的 IO 目标对象的父对象(可通过在 IoTargetAttributes 中设置 ParentObject 改变此默认情况)。
参数 IoTargetAttributes 用来设置新建 WDFIOTARGET 对象的属性,可直接传入 WDF_NO_OBJECT_ATTRIBUTES 。参数 IoTarget 返回新建的目标对象。 一个新建的目标对象是
“空的”,并没有和任何“目标”相关联。所以在正式被使用前,还需要通过调用 WdfIoTargetOpen 接口函数将她打开,并和一个“目标挂钩”。下面是示例代码。
WDF_OBJECT_ATTRIBUTES ioTargetAttrib;
WDFIOTARGET ioTarget;
WDF_IO_TARGET_OPEN_PARAMS openParams;

WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&ioTargetAttrib,TARGET_DEVICE_INFO);
status = WdfIoTargetCreate(device,&ioTargetAttrib,&ioTarget);
if(!NT_SUCCESS(status)) return status;

//刚刚创建的 IO 目标对象是空的
//需要通过打开操作,与一个实际的目标挂钩
WDF_IO_TARGET_OPEN_PARAMS_INIT_OPEN_BY_NAME(&openParams,SymbolicLink,STANDARD_RIGHTS_ALL);

status = WdfIoTargetOpen(ioTarget,&openParams);
if(!NT_SUCCESS(status)){
WdfObjectDelete(ioTarget);
return status;
}
WDFIOTARGET 对象有一个标志他可以处于启动、停止和终止等状态。只有当一个 WDFIOTARGET 对象处于启动状态时,发送到它的 IO 请求才会被处理;否则将根据策略,可能被抛弃,
也可能被放入队列中等待启动后再处理。
NTSTATUS WdfIoTargetStart(__in WDFIOTARGET IoTarget);
此接口函数用来启动一个 WDFIOTARGET 对象;相应的,还存在停止与终止接口
NTSTATUS WdfIoTargetStop(__in WDFIOTARGET IoTarget);
NTSTATUS WdfIoTargetClose(__in WDFIOTARGET IoTarget);
由于只有 WDFIOTARGET 对象启动后, IO 请求才能被正确处理,所以在传递请求前先查看 WDFIOTARGET 对象的状态。这是一个好措施。调用 WdfIoTargetGetState 查看当前状态。
WDF_IO_TARGET_STATE WdfIoTargetGetState(WDFIOTARGET Tar);
枚举类型 WDF_IO_TARGET_STATE 定义了所有的 WDFIOTARGET 对象状态:
typedef enum _WDF_IO_TARGET_STATE {
    WdfIoTargetStateUndefined = 0,
    WdfIoTargetStarted,
    WdfIoTargetStopped,
    WdfIoTargetClosedForQueryRemove,
    WdfIoTargetClosed,
    WdfIoTargetDeleted,
} WDF_IO_TARGET_STATE, *PWDF_IO_TARGET_STATE;
下面详细讲解每个状态的含义。
WdfIoTargetStateUndefined 是无效状态,框架内部会使用,驱动框架不会收到这个状态值,也不可以使用。
WdfIoTargetStarted 表明目标状态已经启动,本地目标对象默认总是处于启动状态,除非驱动程序自己让它处于停止状态。
WdfIoTargetStopped 表示目标对象被停止,此时目标对象根据停止操作中的 Action 参数来处理接收到的 IO 请求。
WdfIoTargetClosedForQueryRemove 表示目标对象所代表的设备对象请求移除,故而不久设备将被移除。
WdfIoTargetClosed 表示目标设备所代表的设备对象已经被移除,不仅驱动再也不可以发送任何 IO 请求到此目标对象,而且目标对象原有的 Pending 请求也已经被取消(Cancel),
被关闭的 IO 目标对象可通过 WdfIoTargetOpen 再次开启(Open)。
WdfIoTargetDeleted 表示目标设备所代表的设备对象已经被删除(Delete),目标设备与设备对象都不再可用。

5.3 安全的缓冲区
缓冲区安全是老生长谈的问题。长久以来,为了简单有效的方法解决缓冲区溢出的问题,人们做了很多深入而卓有成效的工作。引起缓冲区溢出的根本原因是对缓冲区的长度缺乏
必要的惊醒,一个简单的解决方法是,把缓冲区长度作为缓冲区的一部分,和缓冲区指针放在一起。以后凡是使用缓冲区指针的地方,都必须判断其长度(以及有效长度,如果缓冲区已
经被部分用过),并根据此进行安全操作。
在 WDF 框架中,所有的 IO 请求对象都使用了 WDFMEMORY 对象来表示输入/输出缓冲区。我们虽然不知道 WDFMEMORY 对象的实现细节,但可以放心的是,它的定义同样实践了上
述缓冲区安全原则。下面是 WDFMEMORY 对象的几个特点。
(1)内部维护了三个数据:内存区指针、内存区长度、有效长度(字符串长度)。这使得内存对象是安全的。
(2)维护内存区生命周期:如果内存区是由框架申请的,则框架将最终负责内存区释放;如果是由 WDF 驱动创建的,则有驱动自己负责释放。
(3)可以使用内存区的任一部分,这通过指定一个偏移值(offset)来实现,如图所示。这种情况下,实际可用的内存长度是总长度减去偏移值,内存对象将非常明白这一点。
http://s7/mw690/002zn5sBzy6WfuOqWDYe6&690

下面从两个方面深入介绍内存对象,首先是创建,然后是使用。
创建 WDFMEMORY 对象的方法有两种。第一种方法是由框架申请内存空间。在这种方法下,根据从哪里申请内存,又能分出两种子方法,第一种子方法是由内存池中申请。
NTSTATUS
WdfMemoryCreate(
__in_opt
    PWDF_OBJECT_ATTRIBUTES Attributes,
    __in
    __drv_strictTypeMatch(__drv_typeCond)
    POOL_TYPE PoolType,
    __in_opt
    ULONG PoolTag,
    __in
    __drv_when(BufferSize == 0, __drv_reportError(BufferSize cannot be zero))
    size_t BufferSize,
    __out
    WDFMEMORY* Memory,
    __out_opt
    PVOID* Buffer
);

参数 PoolType 、 PoolTag 和 BufferSize 用来在内部申请内存时,作为必要的参数传入;而可选参数  Buffer 则返回这个内存区指针,可以想象,函数内部会有类似这样的一个调用;
Buffer = ExAllocatePoolWithTag(PoolType,BufferSize,PoolTag);
所以相关参数都应该遵循内存申请的基本原则,比如,若要在高中断级上使用,则应该让 PoolType 值为 NonPagePool ;参数 Memory 是新创建的对象句柄。使用这种方法创建的内存对
象,内存缓冲区将在 WdfObjectDelete 被执行时和对象一起销毁。

第二种子方法是从旁视列表中申请内存。
NTSTATUS WdfMemoryCreateFromLookaside(
__in
    WDFLOOKASIDE Lookaside,
    __out
    WDFMEMORY* Memory
);
参数 Lookaside 是一个框架旁视列表对象句柄,是通过 WdfLookasideListCreate 方式创建的。旁视列表是一块预先分配好的内存区域,从旁视列表中申请内存,就好像从口袋里拿
樱桃一样;如果旁视列表中的内存用光,则依旧去内存池中申请,就还比口袋里的樱桃吃尽了,想吃就要拿梯子到树上去摘。故而使用旁视列表的好处就是快速。

创建内存对象的第二种方法是预先申请好内存后,再交给框架封装。
NTSTATUS WdfMemoryCreatePreallocated(
__in_opt
    PWDF_OBJECT_ATTRIBUTES Attributes,
    __in __drv_aliasesMem
    PVOID Buffer,
    __in
    __drv_when(BufferSize == 0, __drv_reportError(BufferSize cannot be zero))
    size_t BufferSize,
    __out
    WDFMEMORY* Memory
);
参数 Buffer 指向预先申请好的一块内存,申请的方式不重要(内存池或旁视列表);参数 BufferSize 是这块内存区的字节长度。
使用第二种方法创建的内存对象,对它所控制的内存区并无维护义务,这是与第一种方法最大的不同之处。也就是说,驱动调用 WdfObjectDelete 将此内存对象删除,将仅删除
对象本身,内存区并不被释放而依旧有效。有鉴于此,这种方法的另一个妙处是可以随时替换对象的内存缓冲区。
NTSTATUS WdfMemoryAssignBuffer(
__in
    WDFMEMORY Memory,
    __in
    PVOID Buffer,
    __in
    __drv_when(BufferSize == 0, __drv_reportError(BufferSize cannot be zero))
    size_t BufferSize
);
参数 Buffer 和 BufferSize 代表了新的内存缓冲区。调用此函数后,原来的内存区将被替换掉(但并没有被释放,依旧有效、可用)。
我们已经知道,内存对象与内存缓冲区是一一对应的,可以通过 WdfMemoryGetBuffer 获取内存对象的内存区指针。
PVOID
FORCEINLINE
WdfMemoryGetBuffer(
    __in
    WDFMEMORY Memory,
    __out_opt
    size_t* BufferSize
)
可选参数 BufferSize 返回缓冲区长度,缓冲区指针通过返回值返回给调用者。
上面讲了内存对象的几种创建方法,看上去比较复杂。而使用内存对象的函数却异常简单,只有两个,分别代表写入与读出操作。

//写入
NTSTATUS
WdfMemoryCopyFromBuffer(
__in
    WDFMEMORY DestinationMemory,
    __in
    size_t DestinationOffset,
    __in
    PVOID Buffer,
    __in
    __drv_when(NumBytesToCopyFrom == 0, __drv_reportError(NumBytesToCopyFrom cannot be zero))
    size_t NumBytesToCopyFrom
);

//读出
NTSTATUS
WdfMemoryCopyToBuffer(
__in
    WDFMEMORY SourceMemory,
    __in
    size_t SourceOffset,
    __out_bcount( NumBytesToCopyTo )
    PVOID Buffer,
    __in
    __drv_when(NumBytesToCopyTo == 0, __drv_reportError(NumBytesToCopyTo cannot be zero))
    size_t NumBytesToCopyTo
);
第一个函数用来将指定内存区的内容写入内存对象中。参数 DestinationMemory 和 DestinationOffset 代表了内存对象和内部偏移;参数 Buffer 代表了外部缓冲区;参数 NumBytesToCopyFrom 则表示应将外部缓冲区的多少内容写入内存对象中。

第二个函数用来将内存对象中的内容写入指定外部内存区中。参数 SourceMemory 和 SourceOffset 代表了内存对象和内部偏移;参数 Buffer 代表了外部缓冲区;参数 NumBytesToCopyTo 则表示准备将多少内容拷贝到外部缓冲区,此值不应该大于
外部缓冲区的实际长度,否则就溢出了。
这两个函数内部都有安全检查,比如第一个函数,准备将 NumBytesToCopyFrom 个字节拷贝到内存对象中,但如果这个值比内存对象的有效空间(等于内存区长度减去偏移值 DestinationOffset)大,则函数将返回错误值 STATUS_BUFFER_TOO_SMALL ;
或者若指定的偏移值 DestinationOffset 超过了内存区范围,函数也能检测到此错误,并返回错误值 STATUS_INVALID_BUFFER_SIZE 。

5.4 内存对象(一)

上面说过,如今 WDFREQUEST 对象中的所有缓冲区,都是由 WDFMEMORY 对象封装的。这带来的一个好处就是内核驱动再也不用为用户缓冲区类型担心了。 IRP 中的用户缓冲区有3种类型,即 METHOD_BUFFERD 、 METHOD_DIRECT 、 METHOD_NEITHER 。
这3种类型的缓冲区指针分别存储在 IRP 结构体的不同地方,仅仅为了正确区分他们,就足以让很多人发昏。使用 WDFMEMORY 对象可以减轻这种烦恼。
对于 METHOD_BUFFERD 、 METHOD_DIRECT 这两种缓冲类型的 IO 请求,使用下面的方法获取对应的内存指针。
//获取输入缓冲区所对应的内存对象
NTSTATUS WdfRequestRetrieveInputMemory(
__in
    WDFREQUEST Request,
    __out
    WDFMEMORY* Memory
);
//获取输出缓冲区所对应的内存对象
NTSTATUS WdfRequestRetrieveOutputMemory(
__in
    WDFREQUEST Request,
    __out
    WDFMEMORY* Memory
);
这两个函数再简单不过了,传入一个 WDFREQUEST 句柄,即能返回一个 WDFMEMORY 对象句柄。这两个函数的返回值传达了一些重要信息。
*如果返回值为 STATUS_INTERNAL_ERROR ,表明传入的 WDFREQUEST 对象已经处理完成,在此情况下,不可以再去获取其内存或缓冲区。
*如果返回值为 STATUS_BUFFER_TOO_SMALL ,表明缓冲区长度为0,即缓冲区不存在。
*如果返回值为 STATUS_INVALID_DEVICE_REQUEST ,代表错误的请求对象类型,即传入的 WDFREQUEST 对象的类型错误了。

这个错误较难理解,因为不是所有类型的 WDFREQUEST 对象都可以使用上述两个函数,必须满足下面3个条件才行。
(1)对于 Input 函数,只有写、设备 IO 控制两种命令才可以调用;对于 Output函数,只有读、设备 IO 控制两种命令才可以调用。此处设备 IO 控制命令是指请求码为 IRP_MJ_DEVICE_CONTROL 和 IRP_MJ_DEVICE_INTERNAL_DEVICE_CONTROL 的两种 IO 请求。
(2)必须保证 IO 命令的缓冲方式是 METHOD_BUFFERD 、 METHOD_DIRECT 这两种。
(3)如果 IO 缓冲方式是 METHOD_NEITHER ,有两种情况也是正确的: IO 请求来自内核模块,而非用户程序:IO 请求码为 IRP_MJ_INTERNAL_DEVICE_CONTROL 。 这两种情况都能够保证这一点:缓冲指针指向系统空间,而非用户空间。
若不能满足上述3个条件,就会得到 STATUS_INVALID_DEVICE_REQUEST 错误。
可以通过 WDFMEMORY 对象句柄获取缓冲区指针。
//获取输入缓冲
NTSTATUS WdfRequestRetrieveInputBuffer(
__in
    WDFREQUEST Request,
    __in
    size_t MinimumRequiredLength,
    __deref_out_bcount(*Length)
    PVOID* Buffer,
    __out_opt
    size_t* Length
);

//获取输出缓冲
NTSTATUS
FORCEINLINE
WdfRequestRetrieveOutputBuffer(
    __in
    WDFREQUEST Request,
    __in
    size_t MinimumRequiredSize,
    __deref_out_bcount(*Length)
    PVOID* Buffer,
    __out_opt
    size_t* Length
)
输出参数 Buffer 和 Length 分别用来返回缓冲区指针及其长度。这两个函数的一个妙处是它们各有一个 MinimumRequiredSize 参数,这是做什么用的呢? 驱动在获得缓冲区后,必定要先验证一下其有效性,最简单的验证方法就是通过缓冲区长度进行验证。
比如某设备 IO 控制请求,需要请求者传入一个结构体参数,若输入缓冲区长度小于结构体长度,则说明这个输入参数肯定是错了;如果缓冲区长度小于 MinimumRequiredSize (或等于0),将返回错误 STATUS_BUFFER_TOO_SMALL 。若驱动程序不想做长度判断,只
要将此值设为0即可,因为长度是一个正整数一定是大于等于0的。
另外,不仅可以直接返回缓冲区指针,而且可以返回一个 MDL 指针,这个 MDL 对缓冲区进行封装,使用下面两个函数。
//获得输入缓冲 MDL
NTSTATUS WdfRequestRetrieveInputWdmMdl(
__in
    WDFREQUEST Request,
    __deref_out
    PMDL* Mdl
);
//获得输出缓冲 MDL
NTSTATUS WdfRequestRetrieveOutputWdmMdl(
__in
    WDFREQUEST Request,
    __deref_out
    PMDL* Mdl
);
这两个函数和前面的函数相比,除了缓冲区的返回方式不同外,其他的都是一样的。驱动对象不必未得到的 MDL 指针调用 IoFreeMdl ,应由框架自己维护。
5.5 内存对象(二)
上面讲了  METHOD_BUFFERD 和 METHOD_DIRECT 两种缓冲方式,还有 METHOD_NEITHER 还没有讲,这种方式的缓冲区在 WDF 中处理时比较复杂。
使用 METHOD_NEITHER 缓冲方式的 IO 请求不是来自用户程序,而是来自内存模块,则获取缓冲区的方式和前两种一样。特别之处在于,如果 IO 请求来自用户程序,则缓冲区是在用户空间申请的,在内核中使用用户缓冲,天生有许多限制,它要与指定的进程
上下文相关才有效。所以,这时候获取缓冲区的方式就变得复杂了,必须首先把用户地址转变为能够在内核中自由使用的内核地址。
下面分析一下。
在 WDF 框架中,所有交由 WDF 驱动处理的 IO 请求,都是先入队等候,在交付设备对象处理的。由于这样一个入队等候过程———可以想象——将导致 IO 请求以异步方式处理。而一个用户指针在内核中若以异步方式使用,百分百会出现问题,因为内核所对应的用
户进程环境是在不断变化的。唯一的办法就是,在 IO 请求入队前——此时仍处于请求发送者进程环境——将用户地址所代表的内存页锁定到内存,并重新映射到一个内核地址。
如图中,当用户进程将一个请求发送到内核中时,由于框架代理了内核驱动的所有接口,所以 WDF 框架能够首先得到这个 IO 请求,并将它封装成 IO 请求对象。此时内核恰恰是处于发送请求进程的上下文环境,用户指针是有效的。图中第一步就是抢在 IO 请求入
队前,将其用户指针转换为内核指针。两个小黑球代表了 IO 请求对象的两个状图案,虚线方框代表了两个不同的虚拟地址区域(其物理地址都是同一块)。
第2步是正常入队。
第3步代表了等待过程,因为入队后何时被处理是说不准的,即所谓的异步。
第4步发送给 WDF 驱动处理,此时千万要用内核指针,而不能用用户指针。
上面讲了原理,现在要根据原理来实现它。笔者起先自己想这个问题时,最担心的是第一步的实现:能否得到这样的机会,在每个 IO 请求对象入队前处理他呢?后来发现 EvtIoCallerContext 事件回调正是笔者要找的真命天子。 WDF 驱动可以通过 DDI 接口 
WdfDeviceInitSetIoInCallerContextCallback 注册一个 EvtIoCallerContext 回调函数,在一个 WDFREQUEST 对象被放入队列之前对他进行“前处理”。笔者发现这是唯一可以进行“前处理”的回调,所有其他事件回调的时机都是在 IO 请求入队之后。此回调函数原型如下:
VOID EvtIoCallerContext(
IN WDFDEVICE Device,
IN WDFREQUEST Request
);
我们就来实现此回调吧,应当根据参数 Request 判断其请求类型并获取其缓冲方式,若是 METHOD_NEITHER 就进行如下处理。
(1)获取缓冲区指针,用 WdfRequestRetrieveUnsafeUserInputBuffer 和 WdfRequestRetrieveUnsafeUserOutputBuffer 两个函数。
//从 Neither 类型命令中获取输入缓冲
NTSTATUS
FORCEINLINE
WdfRequestRetrieveUnsafeUserInputBuffer(
    __in
    WDFREQUEST Request,
    __in
    size_t MinimumRequiredLength,
    __deref_out_bcount_opt(*Length)
    PVOID* InputBuffer,
    __out_opt
    size_t* Length
    )
//从 Neither 类型命令中获取输出缓冲
NTSTATUS
FORCEINLINE
WdfRequestRetrieveUnsafeUserOutputBuffer(
    __in
    WDFREQUEST Request,
    __in
    size_t MinimumRequiredLength,
    __deref_out_bcount_opt(*Length)
    PVOID* OutputBuffer,
    __out_opt
    size_t* Length
    )
这两个函数一定要在 EvtIoCallerContext 事件中调用,否则就会返回错误。
另外,若 IO 请求的缓冲类型不是 METHOD_NEITHER ,则调用这两个函数都返回错误。读者需记得一点,即:一个 IO 请求,如果能够用第一种方法正确获取缓冲,则必不能用此处的两个函数获取起缓冲;反之亦然。
(2)正确获取用户缓冲区指针后,要将此指针所代表的虚拟地址所指向的内存锁定在物理内存中(所谓锁定,就是确保不被换出到外部页文件中,而一直保持在物理内存中)。对于读请求,调用函数 WdfRequestProbeAndLockuserBufferForRead 以锁定内存页;
写请求则调用函数 WdfRequestProbeAndLockuserBufferForWrite 已锁定内存页。这两个函数正如其名称所示,会先验证所传入的地址是否有效(Probe),然后锁定(Lock)它。
NTSTATUS
FORCEINLINE
WdfRequestProbeAndLockUserBufferForRead(
    __in
    WDFREQUEST Request,
    __in_bcount(Length)
    PVOID Buffer,
    __in
    size_t Length,
    __out
    WDFMEMORY* MemoryObject
    )

NTSTATUS
FORCEINLINE
WdfRequestProbeAndLockUserBufferForWrite(
    __in
    WDFREQUEST Request,
    __in_bcount(Length)
    PVOID Buffer,
    __in
    size_t Length,
    __out
    WDFMEMORY* MemoryObject
)
上述两个函数除了会验证 Buffer 地址的有效性外,还会做一项有意义的验证,即判断当前线程是否是 Request 对象的创建者线程。倘若不是,就返回错误。这一点非常实用,它确保只在正确的进程上下文中处理用户缓冲。只有在正确的地方做正确的事情,才
能得到正确的结果。
(3)为 IO 请求申请一个上下文空间(Context Space),并将得到的内存对象句柄保存到上下文空间中。可以通过 WdfObjectAllocateContext 函数为指定框架对象申请上下文空间,那么以后正式处理此 IO 请求时(在队列中处理),可以通过上下文空间获取内存对象,
并通过内存对象获取输入/输出缓冲区。断不可在正式处理时,通过第1步中的方法获取缓冲区。
(4)此时, Request 对象的缓冲区已经转换到内核空间了。应令此 Request 对象顺利入队(进入指定的框架队列),并随后通过队列再次将此 Request 经由事件回调传回 WDF 驱动处理。通过 WdfDeviceEnqueueRequest 方法让一个 IO 请求入队;
NTSTATUS
FORCEINLINE
WdfDeviceEnqueueRequest(
    __in
    WDFDEVICE Device,
    __in
    WDFREQUEST Request
    )
此函数也只能在 EvtIoCallerContext 事件回调中调用,否则会返回错误。
上述过程很复杂,让我们看一个 EvtIoCallerContext 的实现例子。此例子中,首先获取请求类型,仅对 WdfRequestTypeDeviceControl 请求做处理(即 IRP_MJ_DEVICE_CONTROL)。获取与此命令相关的 IOCTL 控制码,并根据末两位判断缓冲区类型,若是 
METHOD_NEITHER,则按照上述4个步骤来处理。

//IO 请求对象的上下文空间结构体
typedef srtuct{
WDFMEMORY inMem;
WDFMEMORY outMem;
}REQUEST_CONTEXT,*PREQUEST_CONTEXT;

//此回调函数经由 WdfDeviceInitSetIoInCallerContextCallback 注册后,在一个
//IO 请求对象尚未入队前调用,并进行“前处理”。它有足够的权利来决定是让一个
//IO 请求入队( WdfDeviceEnqueueRequest),还是直接完成(WdfRequestComplete)

VOID EvtDeviceIoInCallerContext(IN WDFDEVICE Device,IN WDFREQUEST Request)
{
DWORD dwCode;
NTSTATUS status = STATUS_SUCCESS;
PREQUEST_CONTEXT reqContext = NULL;
WDFMEMORY InputMem,OutputMem;
WDF_OBJECT_ATTRIBUTES attributes;
WDF_REQUEST_PARAMETERS params;
size_t inBufLen,outBufLen;
PVOID inBuf,outBuf;
//首先获取 IO 请求对象的参数,并判断请求类型
WDF_REQUEST_PARAMETERS_INIT(&params);
WdfRequestGetParameters(Request,&params);
if(params.Type == WdfRequestTypeDeviceControl){
//WdfRequestTypeDeviceControl 类型,即 IRP_MJ_DEVICE_CONTROL 命令
dwCode = params.Parameters.DeviceIoControl.IoControlCode;
if(dwCode & 0x3 == METHOD_NEITHER){
//此 IO 请求的缓冲方式为 METHOD_NEITHER 
//1.获取用户输入/输出缓冲区指针
status = WdfRequestRetrieveUnsafeUserInputBuffer(Request,0,&inBuf,&inBufLen);
if(!NT_SUCCESS(status)) goto End;
status = WdfRequestRetrieveUnsafeUserOutputBuffer(Request,0,&outBuf,&outBufLen);
if(!NT_SUCCESS(status)) goto End;
//2.锁定输入/输出内存
status = WdfRequestProbeAndLockUserBufferForRead(Request,inBuf,inBufLen,&InputMem);
if(!NT_SUCCESS(status)) goto End;
status = WdfRequestProbeAndLockUserBufferForWrite(Request,outBuf,outBufLen,&OutputMem);
if(!NT_SUCCESS(status)) goto End;
//3.为此 IO 请求对象申请上下文空间
//并把得到的内存对象保存到 IO 请求对象的上下文空间中
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attributes,REQUEST_CONTEXT);
status = WdfObjectAllocateContext(Request,&attributes,&reqContext);
if(!NT_SUCCESS(status)) goto End;
reqContext->inMem = InputMem;
reqContext->outMem = OutputMem;
}
}
//4. 所有事情都做好了,可以令 IO 请求入队列了
//如果 IO 请求是其他类型,则直接到这里入队,不必做上述处理
status = WdfDeviceEnqueueRequest(Device,Request);
End:
if(!NT_SUCCESS(status)){
WdfRequestComplete(Request,status);
}
return;
}
这个例子有一个缺陷,就是为了尽量简便,而强迫所有使用 METHOD_NEITHER 缓冲方式的 IO 请求,一定要同时拥有输入输出缓冲,否则就令其失败。因为这里仅仅为了演示,所以并不重要。
5.6 框架和 IO 请求
如图所示 
第3步,IO 管理器为读者请求构造类型的 IRP_MJ_READ 的 IRP 。
第4步,IO 管理器找到由 WDF 框架创建的设备对象(DEVICE_OBJECT),并将 IRP 发送到它的读分发函数(DispatchRead);
第5步, WDF 框架收到 IRP 后,查看 WDF 驱动程序是否注册了读回调,如果注册了就将 IRP 封装成一个 IO 请求对象(WDFREQUEST),并把它放到 WDF 驱动框架的某个指定队列中。
第6步,队列将 IO 请求对象发送给 WDF 驱动处理, WDF 驱动注册的读回调被执行。

5.7 更详细的处理流程
框架收到 IO 管理器传递给它的 IRP 请求后,将 IRP 封装在 WDFREQUEST 中,并作为参数调用驱动注册的事件回调函数。首先看看 WDFREQUEST 对象的创建过程。
IO 管理器将用户请求封装成 IRP 对象并发送到指定的设备栈,这一点没有变。首先得到 IRP 的是 WDF 框架,框架对 IRP 做一定处理后,检查 WDF 驱动是否注册了所需要的事件回调,如果没有注册,框架就自己将 IRP 发送到设备栈的下层设备, IRP 将得到处理,并最终返回到 IO 管理器。
在 WDF 驱动注册了事件回调的情况下,只有在下述情况下框架才会创建 WDFREQUEST 对象: IRP 对象类型为 IRP_MJ_CREATE 、 IRP_MJ_DEVICE_CONTROL 、 IRP_MJ_READ 、 IRP_MJ_WRITE 、 IRP_MJ_INTERNAL_DEVICE_CONTROL 五者之一。
创建的 WDFREQUEST 对象通过参数传递到事件回调中, WDF 驱动通过 WdfRequestXXX 系列函数处理对象,比如,发送到底层设备栈,直接完成请求。也可以调用 DDI 接口函数 WdfRequestWdmGetIrp ,从对象中获取 IRP 结构体并以 WDM 方式处理。
如果框架检查发现 IRP 的命令类型并非上述五种之一,则不会创建 WDFREQUEST 对象,这样 WDF 驱动即使注册了事件回调,也不能拥有对命令进行最终处理机会,框架把最终的处理权留给了自己。比如,与 IRP_MJ_CLEANUP 命令相对应的事件回调函数定义如下:
VOID EvtCleanupCallback(IN WDFOBJECT Object);
CleanUp 回调函数能够针对设备对象做一些有限的处理,但对于 CleanUp 命令无任何权利。由于此回调函数的的返回值为 void ,故而也不能通过其返回值来影响框架的下一步处理。
所有的 Pnp/电源事件回调都不接受 WDFREQUEST 参数,所以 WDF 驱动都没有权利直接干预 Pnp/电源命令的最终处理,而只能根据 Pnp/电源状态迁移对设备进行相应处理。这种方法避免了 WDF 驱动程序可能会做出的一些傻事,值得赞赏。
如果命令继续在设备栈中传递(即第二种情况),则 WDF 驱动有机会完成“后处理”操作,通过注册完成函数来实现对 IO 请求的后”处理“ 。用户既可以通过 IRP 调用 IoSetIrpCompletionRoution 注册完成函数,也可以通过 WDFREQUEST 对象调用接口函数 WdfRequestSetCompletionRoutine 注册完成回调。如图是对这个过程的简单描述。

从图中可以看到, WDF 框架和 WDF 驱动是紧密结合在一起的,所以图中有何大一部分的重合。方框“默认分发函数1”表示上面提到的5中命令类型 IRP 的分发函数,方框“默认分发函数2”表示上述5中命令类型之外的其他命令类型 IRP 的分发函数。图中未能把完成函数或完成回调函数相关逻辑表现出来。
如下图所示,他表现了读、写和 IO 控制3种用户命令的处理流程。

5.8 IO请求参数
我们从 IO_STATCK_LOCATION 结果体的定义中,能够找到各种 IRP 命令子结构体的身影。但由于 WDFREQUEST 只对部分 IRP 类型进行了封装,所以它只用到了 IO_STATCK_LOCATION 结构体定义的一个子集。
使用 DDI 接口 WdfRequestGetParameters ,可以从 WDFREQUEST 对象中获得 WDFREQUEST 对象类型相关的结构体。

VOID  WdfRequestGetParameters(
__in
    WDFREQUEST Request,
    __out
    PWDF_REQUEST_PARAMETERS Parameters
)
指定一个 WDFREQUEST 对象句柄,即可得到一个相关的 WDF_REQUEST_PARAMETERS 结构体,我们可以从中找到各种相关结构体。
typedef struct _WDF_REQUEST_PARAMETERS {

    USHORT Size;

    UCHAR MinorFunction;

    WDF_REQUEST_TYPE Type;

    //
    // The following user parameters are based on the service that is being
    // invoked.  Drivers and file systems can determine which set to use based
    // on the above major and minor function codes.
    //
    union {

        //
        // System service parameters for:  Create
        //

        struct {
            PIO_SECURITY_CONTEXT SecurityContext;
            ULONG Options;
            USHORT POINTER_ALIGNMENT FileAttributes;
            USHORT ShareAccess;
            ULONG POINTER_ALIGNMENT EaLength;
        } Create;


        //
        // System service parameters for:  Read
        //

        struct {
            size_t Length;
            ULONG POINTER_ALIGNMENT Key;
            LONGLONG DeviceOffset;
        } Read;

        //
        // System service parameters for:  Write
        //

        struct {
            size_t Length;
            ULONG POINTER_ALIGNMENT Key;
            LONGLONG DeviceOffset;
        } Write;

        //
        // System service parameters for:  Device Control
        //
        // Note that the user's output buffer is stored in the UserBuffer field
        // and the user's input buffer is stored in the SystemBuffer field.
        //

        struct {
            size_t OutputBufferLength;
            size_t POINTER_ALIGNMENT InputBufferLength;
            ULONG POINTER_ALIGNMENT IoControlCode;
            PVOID Type3InputBuffer;
        } DeviceIoControl;

        struct {
            PVOID Arg1;
            PVOID  Arg2;
            ULONG POINTER_ALIGNMENT IoControlCode;
            PVOID Arg4;
        } Others;

    } Parameters;

} WDF_REQUEST_PARAMETERS, *PWDF_REQUEST_PARAMETERS;
联合类型变量 Parameters 中包含了5类型 IRP 的结构体定义,进一步证实了框架只选择这5种 IRP 进行封装的事实。
5.9 队列
在 WDM 驱动中,我们有时候也会维护一个内核队列,以处理多个同类型的异步请求,比如当读取键盘、鼠标的输入信息时,一个维持一定数量的请求队列能够保证信息不丢失。笔者经常使用 LIST_ENTRY 构造双向链表来实现队列,实现这样一个队列算不上一件很容易的事(也可以直接使用 KQUEUE 
内核对象简化实现难度)。而 WDF 框架借助于队列,让它在 IO 模型设计中大显神威,实现了对 IO 请求对象的高效管理,由我们手动创建、维护队列,这样的工作基本上没有必要了。 WDF 驱动中的队列有三种类型,定义如下:
typedef enum _WDF_IO_QUEUE_DISPATCH_TYPE{
    WdfIoQueueDispatchInvalid = 0,
    WdfIoQueueDispatchSequential,
    WdfIoQueueDispatchParallel,
    WdfIoQueueDispatchManual,
    WdfIoQueueDispatchMax,
} WDF_IO_QUEUE_DISPATCH_TYPE;
在这个枚举类型值中,第一种和最后一种都是无效的,只有中间三种是有效的类型值。以下对中间三种进行解释。
第一种是串行队列,队列中的 IO 请求是一个挨一个被处理的,前一个处理中的对象未完成,后续对象一定会被处理,串行队列实现了序列化。
第二种是并行队列, IO 请求一旦入列,只要有可能都会以最快的速度被处理,不必等待前面的对象被处理完。
第三种是手动队列, IO 请求入队以后,框架就不再理会它们, WDF 驱动在需要时,自己从队列中提取 IO 请求并处理。
针对队列可进行 Pnp/Power 配置,发生 Pnp/Power 事件时可配置以什么方式处理队列中提取的 IO 请求;还可配置一个 Cancel 回调,当上层驱动取消相关请求时被调用。
WDF 的一个主要特点是,所有的东西都被封装成一个对象,而所有的操作都被定义成一个事件(Event)或回调(Callback)。 WDF 对每个框架对象都维护一个引用计数,这样就能有效的控制对象的生命周期。
举例来说: WDF 的一个优点是将 I/O 进行了封装,使得程序员不用直接面对 IRP 而只要处理 WDFREQUEST 对象即可,处理也变得异常简单。 I/O 的对象不再是设备对象,而是一个新的 I/O target 对象。比 DEVICE_OBJECT 使用更广,它不仅可以代表设备,也可以代表驱动、队列等。
队列对象是一个全新事物, WDM 编程中涉及队列的地方不多,但 WDF 却让它成为了不可避免的部件。队列是专门用来管理 WDFREQUEST 对象的,因为 WDFREQUEST 对象是对 IRP 的封装,所以队列归根结底是对 IRP 的管理。队列允许 WDFREQUEST 对象以三种方式发送给驱动处理;并行、
串行、手动。由于对 IRP 的管理、使用失策而导致的驱动问题,是 WDM 程序的一大难点。现在队列的引入,让 IRP 从野马变成了驯良的坐骑。
由于队列比较重要,下面我们分别来讲一讲。三者中,并行队列最好理解了,和 WDM 中的无序方式一样,所有的 IRP 都是并行处理的,也就是说,收到一个 IRP 就立即处理一个 IRP 。串行队列和手动队列可能较难理解,下面举例演示串行队列的使用。
假设有一个用户线程 X 以同步方式向内核发送一个命令,内核框架收到命令后,判断出命令应当归于串行队列1。此时串行队列1中已经有3个排队命令。
下面的图1、2、3很形象很形象的演示了一个同步 IO 请求,在串行队列中是如何处理的。图中的每个灰色小球各代表一个 IO 请求。
图1显示了同步请求的入队过程,线程 X 在发送命令后,一直处于等待状态;图2显示了串行队列是如何处理排序命令的;图3显示了命令被最终处理完成后,线程继续执行。
在图1中:第一步,用户线程向内核驱动发送一个请求, WDF 框架首先获得处理这个请求的机会;第二步,此请求隶属于串行队列, WDF 便将请求排到串行队列的最后面,作为 Request4 ;第三步,线程将一直等待 Request4 ;的完成。

在图2中,第一步,队列中的前三个请求处理完成,轮到“新命令”;第二步,“新命令”处理后, WDF 回向通知用户线程,并返回处理结果。

在图3中:第一步,内核中的串行队列空(或者继续接受新命令);第二步,线程 X 继续执行。
每个设备对象都有一个默认队列,未经说明的请求类型都被送往这个默认队列中,如果把某些请求排列到特殊队列,则需要特别说明。一般总是把默认队列设置为并行队列,这是为了将效率最大化。只将部分需要特别处理以保证安全的对象放置到串行队列中;而手动队列一般放置
一些特殊的请求,这些请求往往是为了等待某个条件被满足已达到同步目的,或等待所需要的数据到来,如从 USB 的中断端口读取数据。
多个队列之间可以互相交流,也就是说,A队列中的请求,可以被送到B队列中进行排队,这种交流存在于任何两个队列之间。在很多情况下,需要用到队列转移,比如 IRP_MJ_DEVICE_IO_CONTROL 类型的请求,一般都可以被并行处理,但控制号 CTL_CODE 为 IOCTL_REQUEST_1 的
DEVICE_IO_CONTROL 请求却必须被串行处理;这时候我们可以把 IRP_MJ_DEVICE_IO_CONTROL 请求设置为在并行队列中排队,这样绝大部分的控制请求都被并行处理,而当并行队列的处理函数遇到控制码为 IOCTL_REQUEST_1 时,放弃对它的处理,并重新将它送到串行队列中排队等待。
5.10 创建 IO 请求
目前框架只对5种 IRP 请求进行 WDFREQUEST 对象封装,但实际上 WDFREQUEST 对象可以封装任何一种 IRP 请求。 WDF 驱动程序除了使用框架通过事件回调传递的 WDFREQUEST 对象外,还可以自己新建任何类型的 WDFREQUEST 对象。
创建 WDFREQUEST 对象有两种方法。第一种方法是创建空对象,然后调用格式化函数,将对象格式化为指定类型的命令,目前框架只提供了4中格式化函数(共5个函数),这样这种方法只能创建4种 WDFREQUEST 对象;第二种方法是直接通过一个指定的 IRP 创建,使用此方法可创建任何类型的
WDFREQUEST 对象,根据 IRP 类型而定。
调用 WdfRequestCreate 创建一个空对象,函数定义如下:
NTSTATUS WdfRequestCreate(
__in_opt
    PWDF_OBJECT_ATTRIBUTES RequestAttributes,
    __in_opt
    WDFIOTARGET IoTarget,
    __out
    WDFREQUEST* Request
);
参数 RequestAttributes 用来设置将被创建的框架对象,这个类型的参数并不是仅针对 WDFREQUEST 对象,所有继承自 WDFOBJECT 的框架对象都使用 WDF_OBJECT_ATTRIBUTES 结构体进行对象配置。
IoTarget 是一个可选参数,仅仅用来做验证,并不会被保存到新建的 WDFREQUEST 对象中,如果此参数不为 NULL ,则 WdfRequestCreate 函数将验证新建的对象,是否可以被发送到此 IO 目标对象,如果证实不可能,则 WDFREQUEST 对象将创建失败,并返回状态值 
STATUS_REQUEST_NOT_ACCEPTED 。这个状态值表明,当前系统中的资源不足以创建一个发送到指定 IO 目标对象的 WDFREQUEST 请求。
在成功创建的情况下,参数 Request 将返回一个 WDFREQUEST 对象句柄。
成功创建一个空对象,并不能即刻备用,它的内部确实是“空”的,所以需要格式化。格式化的意思是往对象内部填入表明“身份”的数据,大家可以联想到磁盘格式化。可用的隔阂四化接口函数有如下四种(5个)。
WdfIoTargetFormatRequestForIoctl
WdfIoTargetFormatRequestForInternalIoctl
WdfIoTargetFormatRequestForInternalIoctlOthers
WdfIoTargetFormatRequestForRead
WdfIoTargetFormatRequestForWrite

第一个函数将 WDFREQUEST 对象格式化为 IRP_MJ_DEVICE_CONTROL 命令;第二、三个函数将 WDFREQUEST 对象格式化为 IRP_MJ_INTERNAL_DEVICE_CONTROL 命令,这两个函数使用了不同的参数;第四个函数将 DFREQUEST 对象格式化为 IRP_MJ_READ 命令;第五个函数将 DFREQUEST 对象格式化
为 IRP_MJ_WRITE 命令。下面以 WdfIoTargetFormatRequestForRead 函数为例进行详解。
NTSTATUS
FORCEINLINE
WdfIoTargetFormatRequestForRead(
    __in
    WDFIOTARGET IoTarget,
    __in
    WDFREQUEST Request,
    __in_opt
    WDFMEMORY OutputBuffer,
    __in_opt
    PWDFMEMORY_OFFSET OutputBufferOffset,
    __in_opt
    PLONGLONG DeviceOffset
)
参数 IoTarget 指明发送到的 IO 目标, WDFIOTARGET 对象是对 DEVICE_OBJECT 对象的封装,最终使用的也恰恰是这个设备对象指针;参数 Request 传入一个前面创建的 WDFIOTARGET 对象句柄,当函数调用成功后,这个对象的内部将焕然一新。
余下的三个参数 OutputBuffer 、 OutputBufferOffset 和 DeviceOffset 与操作有关。 OutputBuffer 代表一个数据缓冲区,用来保存读入的数据。 OutputBufferOffset 用来指示读数据缓冲区的起始偏移值,如果数据缓冲区中已经放置了一些有用的数据,或因其他原因必须保留一部分
缓冲区,则应当利用这个偏移值空出头部,这样读缓冲区的长度将是全部长度减去偏移值。 DeviceOffset 指示目标设备的读偏移值。
上面提到的3个参数,我们称为“与具体操作相关的参数”,其他格式化函数与 WdfIoTargetFormatRequestForRead 函数的区别之处只在于这些参数。我们可以通过函数 WdfRequestGetParameters 获取这些与具体操作相关的参数,这是一个类似于 IO_STATCK_LOCATION 的结构体。
第二种创建 WDFREQUEST 对象的方法是调用 WdfRequestCreateFromIrp ,选择直接从一个已有的 IRP 结构体进行封装。
NTSTATUS
FORCEINLINE
WdfRequestCreateFromIrp(
    __in_opt
    PWDF_OBJECT_ATTRIBUTES RequestAttributes,
    __in
    PIRP Irp,
    __in
    BOOLEAN RequestFreesIrp,
    __out
    WDFREQUEST* Request
)
参数 RequestAttributes 是框架对象属性;参数 Irp 是将要被封装的 IRP 结构体;参数 RequestFreesIrp 用以设置 Irp 的移除属性, RequestFreesIrp 为 TRUE ,则 WDFREQUEST 对象被删除时,将负责调用 IoFreeIrp 释放 Irp ,否则用户必须手动释放它;参数 Request 是生成的 WDFREQUEST 对象。
WDF 驱动创建的 WDFREQUEST 对象,必须最后调用 WdfObjectDelete 方法进行删除。但如果是由框架通过参数传递给 WDF 驱动的,则不可调用 WdfObjectDelete 方法删除,这个权利应当让给框架在其内部进行。
最后提一提发送请求。调用框架接口函数 WdfRequestSend 将一个请求发送到指定的 IO 目标对象。
BOOLEAN
FORCEINLINE
WdfRequestSend(
    __in
    WDFREQUEST Request,
    __in
    WDFIOTARGET Target,
    __in_opt
    PWDF_REQUEST_SEND_OPTIONS Options
)
重点讲一下第三个参数 Options 。他其实是一个结构体,定义如下:
typedef struct _WDF_REQUEST_SEND_OPTIONS {
    //
    // Size of the structure in bytes
    //
    ULONG Size;

    //
    // Bit field combination of values from the WDF_REQUEST_SEND_OPTIONS_FLAGS
    // enumeration
    //
    ULONG Flags;

    //
    // Valid when WDF_REQUEST_SEND_OPTION_TIMEOUT is set
    //
    LONGLONG Timeout;

} WDF_REQUEST_SEND_OPTIONS, *PWDF_REQUEST_SEND_OPTIONS;

参数 Flags 是一系列标志值,
typedef enum _WDF_REQUEST_SEND_OPTIONS_FLAGS {
    WDF_REQUEST_SEND_OPTION_TIMEOUT = 0x0000001,
    WDF_REQUEST_SEND_OPTION_SYNCHRONOUS = 0x0000002,
    WDF_REQUEST_SEND_OPTION_IGNORE_TARGET_STATE = 0x0000004,
    WDF_REQUEST_SEND_OPTION_SEND_AND_FORGET = 0x0000008,
} WDF_REQUEST_SEND_OPTIONS_FLAGS;

其中重要的两个标志是:
WDF_REQUEST_SEND_OPTION_SYNCHRONOUS :同步标志。设置同步标志后,直到所发送的请求被完成, WdfRequestSend 函数才会返回。请求被完成前,调用者一直处于阻塞状态;如果不设置此标志,则采用相反的异步方式发送请求, WdfRequestSend 被调用后将立即返回,而请求完成时,指定的完成函数将被调用。
WDF_REQUEST_SEND_OPTION_TIMEOUT :超时标志。设置超时标志后,框架将把 WDF_REQUEST_SEND_OPTIONS 架构体中的 Timeout 值作为超时值计算超时。比如,为一个请求设置了1秒的超时,如果请求被发送1秒后仍未收到完成通知,则框架将取消此请求。


6. PNP 和 电源模型
几乎没有人能写出完全正确的 PNP/电源处理代码。真的,以为实在是太复杂了,分岔路太多了。 WDF 为 PNP 、 电源、电源策略三者定义的状态值,据说达到近两百种之多——想要实际领略一下,请在 WDF 文档中搜索查看 WDF_DEVICE_PNP_STATE 、 WDF_DEVICE_POWER_STATE 和 WDF_DEVICE_POWER_POLICY_STATE 
三者加起来超过了 270 个枚举量。按照最苛刻的要求,当然也要在驱动中对这些状态都进行处理,哪怕大部分状态只有百万分之一的几率发生。恐怕没有人曾经把这么多的状态都自己处理过吧——用 WDM 代码!
读者可能开始为这270个状态量纠结了。但值得欣慰的是, WDF 将从此让我们好过起来,当遇到一切时和 PNP 、电源相关的问题时——微笑吧,程序员们!蓝屏会远离我们的,而更强壮的驱动代码在朝我们招手。
WDF 对电源和 PNP 的设计方法式是,进行最基本的处理,框架向程序员提供回调接口,如果注册了某个回调,则框架在对应的状态变化时调用此回调,否则就使用默认的处理方式。
下面是 PNP/Power/Power 策略三者的回调函数结构体定义。
// PNP/电源回调
typedef struct _WDF_PNPPOWER_EVENT_CALLBACKS
{
//
    // Size of this structure in bytes
    //
    ULONG Size;

//初始与结束回调函数
//初始: EvtDeviceD0Entry 早于 EvtDevicePrepareHardware 被调用
//结束: EvtDeviceD0Exit 晚于  EvtDeviceReleaseHardware 被调用
    PFN_WDF_DEVICE_D0_ENTRY                 EvtDeviceD0Entry;//进入 D0 电源状态
    PFN_WDF_DEVICE_D0_ENTRY_POST_INTERRUPTS_ENABLED EvtDeviceD0EntryPostInterruptsEnabled;
    PFN_WDF_DEVICE_D0_EXIT                  EvtDeviceD0Exit;//退出 D0 电源状态
    PFN_WDF_DEVICE_D0_EXIT_PRE_INTERRUPTS_DISABLED EvtDeviceD0ExitPreInterruptsDisabled;
    PFN_WDF_DEVICE_PREPARE_HARDWARE         EvtDevicePrepareHardware;
    PFN_WDF_DEVICE_RELEASE_HARDWARE         EvtDeviceReleaseHardware;
//设备被 Remove 后,调用此回调来释放在 EvtDeviceSelfManagedIoCleanup中
//为每个框架设备对象申请的资源
    PFN_WDF_DEVICE_SELF_MANAGED_IO_CLEANUP  EvtDeviceSelfManagedIoCleanup;
//当设备被 Remove 后,此回调被用来处理遗留的 IO REQUEST 对象
    PFN_WDF_DEVICE_SELF_MANAGED_IO_FLUSH    EvtDeviceSelfManagedIoFlush;
//首次进入 D0 状态后,系统为驱动创建的每个框架设备对象调用此函数
//来申请专属于此设备对象的资源
    PFN_WDF_DEVICE_SELF_MANAGED_IO_INIT     EvtDeviceSelfManagedIoInit;
//上面申请的设备资源将被暂停,有三种可能性
//设备休眠、设备拔出或者 PNP 管理器试图重新分配系统资源
    PFN_WDF_DEVICE_SELF_MANAGED_IO_SUSPEND  EvtDeviceSelfManagedIoSuspend;
//资源被暂停后又可以重新使用了。只有当系统从休眠状态醒来时,才会被调用
    PFN_WDF_DEVICE_SELF_MANAGED_IO_RESTART  EvtDeviceSelfManagedIoRestart;
//设备被异常拔除
    PFN_WDF_DEVICE_SURPRISE_REMOVAL         EvtDeviceSurpriseRemoval;
//查询设备是否能够被移除
    PFN_WDF_DEVICE_QUERY_REMOVE             EvtDeviceQueryRemove;
//查询设备是否可被停止
    PFN_WDF_DEVICE_QUERY_STOP               EvtDeviceQueryStop;
//当驱动使用一些特殊文件时,此回调被调用
//特殊文件时:页文件、休眠文件、Dump文件
    PFN_WDF_DEVICE_USAGE_NOTIFICATION       EvtDeviceUsageNotification;
//系统初始化是,或者驱动属下的多个设备对象之间的关系发生变化时,此回调被调用
    PFN_WDF_DEVICE_RELATIONS_QUERY          EvtDeviceRelationsQuery;
}WDF_PNPPOWER_EVENT_CALLBACKS, *PWDF_PNPPOWER_EVENT_CALLBACKS;

//电源策略回调 

typedef struct _WDF_POWER_POLICY_EVENT_CALLBACKS {
    //
    // Size of this structure in bytes
    //
    ULONG Size;

//设备将进入休眠状态了,尚未离开 D0 状态
//实现电源策略: 让设备可以从即将进入的休眠状态中醒来
    PFN_WDF_DEVICE_ARM_WAKE_FROM_S0         EvtDeviceArmWakeFromS0;
//设备从休眠状态中被唤醒,进入 D0 状态
//实现电源策略:为了让设备进入休眠后不再醒来,要在这里做一些设置
    PFN_WDF_DEVICE_DISARM_WAKE_FROM_S0      EvtDeviceDisarmWakeFromS0;
//设备把自己从休眠状态中唤醒,此时已经进入 D0 状态
//但 EvtDeviceDisarmWakeFromS0 还没有调用
//实现电源策略: 能接收并处理唤醒信号
    PFN_WDF_DEVICE_WAKE_FROM_S0_TRIGGERED   EvtDeviceWakeFromS0Triggered;

//下面的三个函数类似前三个,但这里的系统状态是 Sx ,而前面的系统状态是 S0
//上面的唤醒,仅仅是设备唤醒自己;而这里的唤醒,也包含把系统从 Sx 唤醒到 S0
    PFN_WDF_DEVICE_ARM_WAKE_FROM_SX         EvtDeviceArmWakeFromSx;
    PFN_WDF_DEVICE_DISARM_WAKE_FROM_SX      EvtDeviceDisarmWakeFromSx;
    PFN_WDF_DEVICE_WAKE_FROM_SX_TRIGGERED   EvtDeviceWakeFromSxTriggered;

//是 EvtDeviceArmWakeFromSx 的高级版本,提供了更详细的信息。在 WDF 1.7 以后才有
//不应该同时注册这两个回调函数。它多了两个参数,使得函数能够明白自己被调用的原因
    PFN_WDF_DEVICE_ARM_WAKE_FROM_SX_WITH_REASON EvtDeviceArmWakeFromSxWithReason;

} WDF_POWER_POLICY_EVENT_CALLBACKS, *PWDF_POWER_POLICY_EVENT_CALLBACKS;

所有的回调函数都在被动级别上运行,这使我们省去了很多麻烦,使得程序在进行初始化或反初始化时,不用担心运行于分发级别(DISPATCH_LEVEL)而顾虑重重。因为在分发级别上运行的代码有很多限制,常常免不了要使用 WorkItem ,而使用 WorkItem 往往又必须同时实现同步,相当大的麻烦。
上述的回调函数和 WDM 的分发函数之间谈不上一一对应,但也存在着比较强的对应关系。比如函数 EvtDevicePrepareHardware 对应了 WDM 中的 PNP_STOP_DEVICE 分发;函数 EvtDeviceSurpriseRemoval 对应了 WDM 中的 PNP_SUPPRISE_REMOVE 分发;函数 
EvtDeviceD0Entry/EvtDeviceD0Exit 对应了 WDM 中的 PNP_SET_POWER 分发;等等。
在处理 PNP_START_DEVICE 时,免不了要担心一下是第几次调用 StartDevice 函数,考虑电源状态是否已进入 D0 状态。 EvtDevicePrepareHardware 回调却不必有此顾虑,因为框架已在内部做了考虑。

0

阅读 收藏 喜欢 打印举报/Report
后一篇:WDF开发详解
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有