65.9K
CodeProject 正在变化。 阅读更多。
Home

构建和部署基本的 WDF 内核模式驱动程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (53投票s)

2006 年 2 月 16 日

MIT

25分钟阅读

viewsIcon

474643

downloadIcon

5179

一篇关于为 WDF 内核模式驱动程序基金会开发驱动程序的文章。

引言

2005 年 12 月底,微软发布了新的 Windows 驱动程序基金会(Windows Driver Foundation,WDF)。这是一个用于构建 Windows 设备驱动程序的新框架。它比 Windows 驱动程序模型(Windows Driver Model,WDM)更高级,因此更容易学习,开发设备驱动程序所需的时间也更少。本文将向您展示如何编写、构建和部署一个骨架 WDF 内核模式设备驱动程序。本文不解释驱动程序开发背后的所有底层概念。要了解这些基本概念,请查阅本文中的“相关资料”部分。

本文解释的设备驱动程序没有演示应用程序。这是因为它目前还没有真正做任何事情。您唯一需要的是 DebugView 实用程序。

背景

对于那些不知道 WDF 是什么的人来说:它是自切片面包以来最伟大的发明。直到最近,如果您需要设备驱动程序,都必须使用 Windows 驱动程序模型(WDM)。WDM 提供了一个用于创建设备驱动程序的底层框架。使用此框架,您的驱动程序必须接受 PNP、电源管理和 IO 请求,并根据驱动程序的状态决定如何处理它们。

有几个简单的状态图描述了不同 PNP 状态之间以及不同电源状态之间的转换,以及导致不同状态转换的事件。表面上看,这似乎很简单。只有 6 种 PNP 状态和 6 种系统电源状态,所以应该不会那么难,对吧?

一旦开始编写驱动程序,您就会意识到对 WDM 了解得越多,就越感到困惑。实现 PNP 是可行的,前提是您可以使用别人的取消安全 IRP 队列。否则,您必须自己编写。当您尝试实现电源管理时,真正的恐怖就会降临。通常,您的设备驱动程序必须管理系统电源状态策略和设备策略 IRP。这相当复杂,根据文档,在处理这些 IRP 时绝不能执行任何阻塞活动。这意味着您必须构建一个复杂的、与完成例程连接的状态机。

IO、PNP 和电源 IRP 之间没有系统级同步,因此您的驱动程序在执行其他 IO 活动时可能会同时收到 PNP 和电源 IRP。由此引入的复杂性是如此巨大,如果您不是经验丰富的专业驱动程序编写者,几乎不可能自己编写此代码。

老实说,电源管理是我放弃的地方。我的 OSR USB-FX2 学习套件消失在抽屉里,我无法花费数月空闲时间让驱动程序正常工作。

然后,WDF 发布了。它看起来非常有前景。突然,编写内核模式设备驱动程序的前景再次变得令人兴奋。当我编写第一个设备驱动程序时,关于 KMDF 的文章很少。我能找到的唯一信息来源是 OSR Online 上的两篇文章。那时我就想,为像我这样的 WDF 驱动程序编程初学者编写这样一篇文章会是一个好主意。

相关资料

WDF 是基于 WDM 构建的。了解 WDM 的概念以及一般的驱动程序开发仍然非常有用。

关于现代驱动程序开发的最佳信息来源仍然是 Walter Oney 的书《编程 Windows 驱动程序模型,第二版》。它解释了 WDM 驱动程序中涉及的所有概念,以及一般的 Windows 驱动程序开发。如果您对驱动程序开发非常感兴趣,这本书是“必读”的。

另一个好的信息来源是 OSR Online。它提供了一些驱动程序开发新闻组、免费工具和文章,甚至还有一本您可以免费订阅的印刷杂志。他们还销售学习套件,其中包含示例驱动程序,方便您学习驱动程序开发。

最后但同样重要的是,CodeProject 上有许多关于驱动程序开发的优秀文章。

KMDF 概述

KMDF 代表内核模式驱动程序框架。如果您的设备驱动程序需要在内核模式下运行,那么 KMDF 为您提供了一个优雅的框架,使内核设备驱动程序开发几乎没有痛苦。

KMDF 背后的理念是,您的驱动程序是一个巨大的 WDM 状态机,接收所有 IO 和系统请求。这个状态机将执行正确的同步操作,并在适当的时间执行您的回调函数。这也意味着您不必为同步电源转换与 IO 和其他事物而绞尽脑汁。您的驱动程序除了保护驱动程序的内部数据结构所需的同步之外,无需执行任何其他同步。

KMDF 的另一个重要特性是许多函数是可选的。如果您没有为某些事件指定回调函数,WDM 状态机将执行默认函数来处理请求。这样,您就不必关心对驱动程序不重要的事情。

特点

KMDF 是基于对象的。功能包含在不同类型的对象中。这些对象可以导出方法、属性和事件,就像 C++ 中的类一样。这确保了功能按对象逻辑分组,并且对象可以像 .NET 类或 MFC 类一样放置在层次结构中,只是您使用的是 C 语法。

所有对象都有一个引用计数,在框架创建时将其设置为 1,并在对象完成时递减。除非引用计数为 0,否则对象永远不会被删除,因此驱动程序可以通过显式引用和取消引用来控制对象的生命周期,超出其自然生命周期。驱动程序还可以为对象提供一个删除回调函数,该函数将在对象删除时执行。

此外,所有对象都可以具有父子关系。当父对象即将被删除时,框架会在删除父对象之前尝试删除子对象。

最后,框架中的每个对象都可以分配一个上下文空间。此上下文空间可用于存储有关对象的附加信息。一个很好的例子是设备对象的设备上下文。上下文空间将存在于对象的整个生命周期内,并且可以通过之前声明的访问宏进行访问。

实现代码

以下部分描述了构建功能驱动程序需要实现的各种函数,以及它们需要完成的工作。

驱动程序入口点

驱动程序入口是驱动程序自然生命开始的地方。对于标准驱动程序,此函数非常简单。它唯一需要做的就是创建驱动程序对象。如果存在任何驱动程序全局变量,则此处是它们必须分配的地方。

驱动程序对象将使用为每个新添加的设备调用的函数进行初始化。

NTSTATUS
DriverEntry(
    IN PDRIVER_OBJECT  DriverObject, 
    IN PUNICODE_STRING  RegistryPath
    )
{
  WDF_DRIVER_CONFIG config;
  NTSTATUS status;

  KdPrint((__DRIVER_NAME "--> DriverEntry\n"));

  WDF_DRIVER_CONFIG_INIT(&config, EvtDeviceAdd);
  status = WdfDriverCreate(
                      DriverObject,
                      RegistryPath,
                      WDF_NO_OBJECT_ATTRIBUTES,
                      &config,
                      WDF_NO_HANDLE);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME "WdfDriverCreate failed with status 0x%08x\n", 
                                                                status));
  }

  KdPrint((__DRIVER_NAME "<-- DriverEntry\n"));

  return status;
}

添加新设备

每次系统检测到新设备连接时,都会调用 `EvtDeviceAdd` 函数。此函数将承担我们骨架驱动程序的大部分工作。创建和初始化设备对象是此函数的责任。

在创建设备之前,驱动程序需要配置设备初始化。这样,系统就会知道这是什么类型的设备以及它将如何运行。一旦完成,就可以创建设备对象及其设备上下文。

每个设备至少需要一个 IO 请求队列才能与用户模式应用程序通信。设备还可以有一个默认 IO 队列。这是接收所有未创建特定队列的 IO 请求的 IO 队列。

队列还可以分配请求处理程序。请求处理程序是针对特定类型的请求执行的函数。框架将不同类型的请求放入不同的队列,然后为这些请求调用正确的 IO 处理程序。

由于这是一个非常简单的设备驱动程序,它只实现了 `EvtIoDefault` IO 处理程序。这是框架用于分派所有未安装特定处理程序的请求的处理程序。此请求处理程序的责任是确定请求类型,然后对其进行明智的处理。

创建 IO 队列后,是时候创建设备接口了。设备现在在系统中拥有一个唯一的接口,用户模式应用程序可以打开一个句柄。

NTSTATUS
EvtDeviceAdd(
    IN WDFDRIVER  Driver,
    IN PWDFDEVICE_INIT  DeviceInit
    )
{
  NTSTATUS status;
  WDFDEVICE device;
  PDEVICE_CONTEXT devCtx = NULL;
  WDF_OBJECT_ATTRIBUTES attributes;
  WDF_PNPPOWER_EVENT_CALLBACKS pnpPowerCallbacks;
  WDF_IO_QUEUE_CONFIG ioQConfig;

  UNREFERENCED_PARAMETER(Driver);

  KdPrint((__DRIVER_NAME "--> EvtDeviceAdd\n"));

  /*set the callback functions that will be 
  executed on PNP and Power events*/
  WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&pnpPowerCallbacks);
  pnpPowerCallbacks.EvtDevicePrepareHardware = 
                               EvtDevicePrepareHardware;
  pnpPowerCallbacks.EvtDeviceD0Entry = EvtDeviceD0Entry;
  pnpPowerCallbacks.EvtDeviceD0Exit = EvtDeviceD0Exit;
  WdfDeviceInitSetPnpPowerEventCallbacks(DeviceInit, 
                                    &pnpPowerCallbacks);

  WdfDeviceInitSetIoType(DeviceInit, WdfDeviceIoBuffered);

  /*initialize storage for the device context*/
  WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attributes, 
                                          DEVICE_CONTEXT);

  /*create a device instance.*/
  status = WdfDeviceCreate(&DeviceInit, 
           &attributes, &device);  
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfDeviceCreate failed with status 0x%08x\n", status));
    return status;
  }
  
  devCtx = GetDeviceContext(device);

  /*create the default IO queue. this one will 
  be used for all requests*/
  WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&ioQConfig,
                                WdfIoQueueDispatchSequential);
  ioQConfig.EvtIoDefault = EvtDeviceIoDefault;
  status = WdfIoQueueCreate(device, &ioQConfig,
                            WDF_NO_OBJECT_ATTRIBUTES,
                            &devCtx->IoDefaultQueue);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfIoQueueCreate failed with status 0x%08x\n", status));
    return status;
  }

  status = WdfDeviceCreateDeviceInterface(device, 
                                     &GUID_DEV_IF_BASIC, NULL);
  if(!NT_SUCCESS(status))
  {
    KdPrint((__DRIVER_NAME
      "WdfDeviceCreateDeviceInterface failed with status 0x%08x\n", 
                                                           status));
    return status;
  }

  KdPrint((__DRIVER_NAME "<-- EvtDeviceAdd\n"));
  return status;
}

如果您已经阅读了代码,您可能会对清理代码以及此代码中没有任何清理代码感到疑惑。在开发我的第一个 WDF 驱动程序时,我也做了同样的事情。这就是您第一次看到父子对象模型的美妙之处。

让我们假设由于某种原因,设备接口无法注册。在这种情况下,`EvtDeviceAdd` 函数将返回错误,框架不会为设备构建设备堆栈。然而,一个 IO 队列和一个设备对象仍将被创建。

IO 队列是设备对象的子项。这意味着当设备对象被删除时,它将自动被删除。设备对象是驱动程序对象的子项,当驱动程序卸载时,它将自动被删除。当系统检测到没有更多设备连接到计算机需要由该驱动程序处理时,驱动程序将被卸载。

所以你看:一切都将得到妥善清理,而无需编写一行清理代码。是不是很棒?

硬件初始化

如果 `EvtDeviceAdd` 函数成功,`EvtDevicePrepareHardware` 将是框架调用的下一个函数。`EvtDevicePrepareHardware` 函数的任务是确保驱动程序可以访问硬件。这意味着它必须映射 PCI 内存范围,打开 USB 接口,或执行设备访问设备硬件所需的任何其他活动。

由于骨架驱动程序是“仅软件”驱动程序,因此它无需注册此函数,但它仍然这样做。这样,当添加一些实际功能时,它可以稍后进行修改。

此函数在设备进入未初始化 D0 状态时执行,即它已通电,但 `EvtDeviceD0Entry` 函数尚未执行。

NTSTATUS
EvtDevicePrepareHardware(
    IN WDFDEVICE    Device,
    IN WDFCMRESLIST ResourceList,
    IN WDFCMRESLIST ResourceListTranslated
    )
{
  NTSTATUS status = STATUS_SUCCESS;

  KdPrint((__DRIVER_NAME "--> EvtDevicePrepareHardware\n"));

  UNREFERENCED_PARAMETER(Device);
  UNREFERENCED_PARAMETER(ResourceList);
  UNREFERENCED_PARAMETER(ResourceListTranslated);

  KdPrint((__DRIVER_NAME "<-- EvtDevicePrepareHardware\n"));

  return status;
}

设备 D0 状态进入

`EvtDeviceD0Entry` 函数负责启动驱动程序应该执行的活动。例如:如果设备是执行周期性操作的纯软件设备,则必须启动计时器。如果设备是 USB 设备,则必须启动低级 USB IO 目标,并可能加载设备固件。

请注意,要执行的操作可能因之前的电源状态而异。除非设备是首次连接或从 D3 电源状态恢复,否则不需要加载固件等操作。一个真实的驱动程序会在这里有一个 `switch` 语句,为不同类型的 D0 进入执行不同的操作。

与 `EvtDevicePrepareHardware` 函数一样,此函数目前不执行任何操作。

NTSTATUS
EvtDeviceD0Entry(
    IN WDFDEVICE  Device,
    IN WDF_POWER_DEVICE_STATE  PreviousState
    )
{
  NTSTATUS status = STATUS_SUCCESS;

  KdPrint((__DRIVER_NAME "--> EvtDeviceD0Entry\n"));

  KdPrint((__DRIVER_NAME "<-- EvtDeviceD0Entry\n"));

  return status;
}

设备 D0 状态退出

`EvtDeviceD0Exit` 函数执行与 `EvtDeviceD0Entry` 函数相反的操作。它的职责是停止或暂停当前的 IO 操作,保存设备状态,并将设备带入低功耗状态。

然后可以使用保存的设备状态信息将设备恢复到其进入低功耗状态之前的状态。

NTSTATUS
EvtDeviceD0Exit(
    IN WDFDEVICE  Device,
    IN WDF_POWER_DEVICE_STATE  TargetState
    )
{
  NTSTATUS status = STATUS_SUCCESS;

  KdPrint((__DRIVER_NAME "--> EvtDeviceD0Exit\n"));

  KdPrint((__DRIVER_NAME "<-- EvtDeviceD0Exit\n"));

  return status;
}

处理 IO 请求

当创建默认 IO 队列时,只为其注册了一个 IO 处理程序:`EvtDeviceIoDefault`。使用下面所示的实现,此函数将接受任何请求,并将失败,因为尚未实现任何 IO 功能。

要实际处理请求,处理程序需要检查请求对象,然后根据请求类型执行某些操作。在这种情况下,为驱动程序接受的请求注册处理程序更有意义。这样,请求将自动传递给正确的处理程序。

VOID 
EvtDeviceIoDefault(
    IN WDFQUEUE  Queue,
    IN WDFREQUEST  Request
    )
{
  KdPrint((__DRIVER_NAME "--> EvtDeviceIoDefault\n"));

  WdfRequestComplete(Request, STATUS_NOT_IMPLEMENTED);

  KdPrint((__DRIVER_NAME "<-- EvtDeviceIoDefault\n"));
}

中断请求级别和可分页性

中断请求级别(IRQL)是内核例程执行的优先级。对于大多数代码,IRQL 为 `PASSIVE` 或 `DISPATCH`。如果代码以 IRQL = `DISPATCH` 级别运行,则保证更及时的执行,但这需要付出代价。

在 `DISPATCH` 级别运行的代码绝不能阻塞,也绝不能产生页故障。如果代码在 `DISPATCH` 级别执行,系统无法处理页故障,因此页故障将导致错误检查(蓝屏死机)。

为了确保您不违反此规则,您必须检查 WDF 框架将执行您的代码的 IRQL。然后,您必须确保遵循适用于指定 IRQL 的规则。

我使用可分页性是因为可以告诉编译器将代码放入内存的分页或非分页部分。实际上,除非您告诉编译器另行处理,否则代码总是放置在非分页内存中。这可以通过 `alloc_text` `pragma` 来完成。

此 `pragma` 语句告诉编译器将指定函数的编译代码放入特定的内存段。

#pragma alloc_text(INIT, DriverEntry)
#pragma alloc_text(PAGE, EvtDeviceAdd)

在大多数情况下,您只会使用两个部分:`INIT` 和 `PAGE`。

`INIT` 部分在初始化阶段分页。之后,它被丢弃。这种优化并不是很大,因为 `DriverEntry` 函数不包含太多代码,但一旦完成,就没有必要将其保留在内存中。

`PAGE` 部分根据系统分页算法和代码使用模式进行分页进出。

将代码放入分页内存是一项重要的优化,因为它只在驱动程序实际使用时才需要存在于物理内存中。如果不需要代码,这会使物理内存可用于其他用途。

为了优化系统资源的使用,您应该将所有不需要显式锁定的函数放入可分页内存中。但是,为了防止系统崩溃,请在将函数放入可分页部分之前检查它们的 IRQL。WDF 文档对此非常清楚,除了 IO 请求事件处理程序的情况。

文档规定这些在 IRQL <= `DISPATCH` 级别调用,除非设备对象是在 `ExecutionLevel` 属性设置为 `WdfExecutionLevelPassive` 的情况下创建的。由于我们没有这样做,乍一看似乎我们不能允许 `EvtDeviceIoDefault` 函数是可分页的。然而,WDF DDK 中的示例似乎正是这样做的。经过一番搜索,NTDEV 列表上有人好心地向我解释说,这取决于驱动程序的类型及其使用方式。

我们的设备驱动程序是一个顶级功能驱动程序,将直接从用户模式程序调用。根据 DDK 文档,文件系统驱动程序和最高级别驱动程序的调度例程将在非任意线程上下文的 IRQL = `PASSIVE` 级别调用。对于我们的驱动程序,这意味着所有函数都可以是可分页的。

创建项目文件

现在您已经拥有驱动程序中所有函数的源代码,是时候准备驱动程序项目以构建设备驱动程序二进制文件了。由于您使用的是标准 DDK 构建工具,因此您需要提供一个 makefile 和一个 sources 文件。

makefile 仅用于将构建过程重定向到 DDK 附带的通用 makefile。您基本上可以从 DDK 示例部分的项目中复制 makefile。

需要 sources 文件来告诉构建过程它必须编译哪些文件以及如何进行编译。驱动程序的 sources 文件如下所示:

TARGETNAME=basic                           This will be the name of 
                                                    the final driver
TARGETTYPE=DRIVER                          This will be the type of 
                                            binary that will be built

MSC_WARNING_LEVEL=/W4                      most strict warning level.

INCLUDES = .\                              Specify additional include 
                                                          directories

NO_BINPLACE=1                              Specify that the BinPlace 
                                             utility will not be used

KMDF_VERSION=1                             This driver uses the first 
                                                      version of KMDF

SOURCES=Driver.c DeviceIO.c Power.c Device.c  List of files that make 
                                                up the complete driver

!include $(WDF_ROOT)\project.mk            Include the default 
                                                  WDF project settings.

这些选项大多不言自明。所有宏和变量的完整列表可以在 DDK 文档集中找到。值得一提的一个宏是 `MSC_WARNING_LEVEL` 宏。此宏定义了用于编译代码的警告级别。默认警告级别为 3。此级别主要用于生产设置,并显示大多数警告。

级别 4 将显示所有可能被视为偏离 C 标准的警告。首次使用级别 4 编译时,您可能会看到数百甚至数千条警告闪过。由于 DDK 默认将其视为错误,因此这些警告将被视为错误。这些错误大部分是由 DDK 头文件本身引起的。要消除这些错误,您必须将它们封装在 `#pragma warning` 指令之间。

#pragma warning(push,3)
#pragma warning(disable:4115)  // named typedef in parenthesis
#pragma warning(disable:4200)  // nameless struct/union
#pragma warning(disable:4201)  // nameless struct/union
#pragma warning(disable:4214)  // bit field types other than int

#include "ntddk.h"
#include "wdf.h"

#pragma warning(pop)

剩下的错误是您自己的错误,您可能需要使用更多的 `pragma` 指令才能获得干净的编译。C 语言允许您以微妙的方式自作自受。由于使用 /W4 时导致的错误而引起的任何问题都将极难找到和调试,因此最好从一开始就预防它们。

如果您仔细查看上面的代码,您会注意到我不仅禁用了某些警告,还将 DDK 头文件的警告级别从 4 更改为 3。这是因为某些版本的 *ntddk.h* 存在错误。头文件本身禁用了某些警告,然后将其恢复到默认状态,而不是将其设置为以前的状态。因此,DDK 头文件以警告级别 3 编译,在这种情况下不会有问题。我希望微软能承认这一点,并让我知道这个 bug 在新的 WDK (Windows Vista DDK) 中已解决。

最后要提到的是您会在代码不同位置找到的 `UNREFERENCED_PARAMETER` 宏。此宏封装了函数体未使用的参数。这样,它们就不会被未使用,也不会导致编译器警告。

使用构建环境

设备驱动程序是使用构建实用程序构建的。DDK 可以为每个当前的 Windows 平台安装一个构建实用程序,从 Windows 2000 到 Windows 2003。对于旧版本,您必须安装旧版本的 DDK,但那样您将无法为 WDF 框架构建。

当前支持的硬件架构是 i386、x64(i386 指令集的 AMD 64 位扩展)和 ia64(Itanium)。

当我开始撰写本文时(使用 KMDF 1.0),Windows 2000 不是 WDF 支持的平台,这意味着您只能为 Windows XP 或更高版本制作 KMDF 驱动程序。然而,就在几天前,微软宣布他们已经改变了不支持 Windows 2000 的决定。从 KMDF 的下一个版本(KMDF 1.1)开始,您也将能够为 Windows 2000 构建 KMDF 驱动程序。

要构建驱动程序,您首先必须选择您的驱动程序支持的最低平台。与 WDM 一样,为特定平台构建的驱动程序将与新平台向上兼容。在驱动程序编译之前,必须设置正确的环境变量。为此,打开构建实用程序,并“cd”到 KMDF 文件夹。如果 DDK 以默认设置安装,那应该是“C:WindDDK\WDF\KMDF”。然后,执行命令“set_wdf_env.cmd”。

重要的是,您的驱动程序项目路径以及 DDK 路径不得包含任何空格。如果包含,您将收到各种奇怪的消息,这些消息与关于空格的警告完全不符。相反,您会收到关于未知内部命令“JVC”的错误。

现在您已完成所有这些操作,就可以构建驱动程序了。为此,只需 `cd` 到驱动程序项目所在的文件夹并执行 `build` 命令。这将启动编译过程,结果要么是干净的构建,要么是一个或多个编译错误。在前一种情况下,您已准备好部署和测试驱动程序。在后一种情况下,您必须阅读错误列表,纠正错误,然后重复该过程。

使用 INF 文件部署驱动程序

到目前为止,您已经构建了一个 WDF 设备驱动程序。剩下的唯一事情就是安装和测试它。为了安装,您需要一个 INF 文件。INF 文件包含系统安装和启动您的设备驱动程序所需的信息。

DDK 提供了一个名为 `geninf.exe` 的工具,可以作为向导来为您的驱动程序创建 INF 文件。此工具的问题在于它不能为您完成所有工作。您必须手动编辑一些内容。这样做有点麻烦,因为生成的文件看起来有点模糊。

更好的解决方案是手动为该驱动程序创建 INF 文件。这是一个非常简单的驱动程序,因此 INF 也可以相对简单。这样,您也知道 INF 文件中不同部分的含义,以便在出现问题或需要添加内容时可以自行修复。

版本部分

INF 文件中的第一个文本块是 `Version` 部分。此部分是您放置公司名称和其他内容的地方。此部分中最重要的两个键是 `Signature` 和 `Class`。

`Signature` 指定此 INF 文件可以用于的平台。您基本上可以在 NT 系列平台(NT4、W2K、XP、2K3)、9x 系列(95、98、Me)或两者之间进行选择。由于 WDF 仅在 NT 系列上运行,因此无需为 9x 平台提供信息。根据 DDK 的说法,signature 参数实际上并未被使用,但它向阅读 INF 文件的人提供了指示。

`Class` 定义了设备的类型。在这里,您告诉系统您的设备是 PCI 桥接器、USB 主控制器或任何类型的预定义设备。我们的设备不是预定义类型,因此您必须指定自定义类和类 GUID。您可以使用 `guidgen.exe` 生成 GUID。

如果您的驱动程序通过 WHQL 认证,`CatalogFile` 变量将包含驱动程序的目录。如果驱动程序未签名,则该文件可能为空。

[Version] 
Signature = "$Windows NT$" 
Class=Sample
ClassGUID={C3077FCD-9C3C-482f-9317-460712F23EFA}
Provider=%MFGNAME% 
CatalogFile=basic.cat
DriverVer= 01/21/2005
;copyright Bruno van Dooren

标准部分

在 `Version` 部分之后,是许多部分,这些部分告诉系统哪些设备将由设备驱动程序支持。一个驱动程序可以支持多个设备,但在这种情况下,列表只包含一个设备。

[Manufacturer]
%MFGNAME%=DeviceList

首先,INF 文件列出了组成安装包的不同磁盘。除非您打算通过软盘分发驱动程序,否则无需指定多个磁盘。

[SourceDisksNames]
1=%INST_DISK_NAME%

然后,INF 文件必须声明此包中可用的文件列表、可以在哪个磁盘上找到它们以及它们在安装磁盘上的相对路径。在此列表中,文件假定位于安装磁盘的根目录中。如果您希望驱动程序在不弹出对话框要求该文件的情况下安装,您要么必须将其放在那里,要么将其放在子文件夹中并在该列表中声明相对路径。

[SourceDisksFiles]
basic.sys=1,objchk_wxp_x86\i386\,
WdfCoinstaller01000.dll=1,,

`DestinationDirs` 部分指定了不同 `CopyFiles` 部分的目标文件夹。所有在 `DestinationDirs` 部分中没有键的文件操作都将在 `DefaultDestDir` 文件夹中完成。WDM 和 WDF 驱动程序必须进入 `%windir%\system32\drivers`。`%windir%` 的编号为 10。`%windir%\system32` 的编号为 11。由于 `DriverInstall` 的 `CopyFiles` 名称未在此部分中列出,因此其文件将被复制到默认目标。

[DestinationDirs]
DefaultDestDir=10,System32\drivers              
ClassInstall32_CopyFiles=11
CoInstaller_CopyFiles = 11

`DeviceList` 部分指定了支持的设备列表。设备识别字符串的格式取决于设备类型。有关不同设备类型格式的详细信息,请参阅 DDK 文档。

如果您没有 USB-FX2 学习套件,您可以指定“Root\WdfBasic”作为设备类型(当然不带引号)。系统将加载您的驱动程序作为纯软件驱动程序。

[DeviceList]
%DEV_DESCRIPTION%=DriverInstall,USB\VID_0547&PID_1002

类安装

驱动程序类的安装非常简单,因为它不需要安装任何文件。因此,安装程序部分非常简短。它包含一个注册表项的安装,用于指定族名。设备管理器中显示的图标是默认图标之一。

[ClassInstall32]
AddReg=ClassInstall32_AddReg
CopyFiles=ClassInstall32_CopyFiles

[ClassInstall32_AddReg]
HKR,,,,"Sample device drivers"
HKR,,Icon,,101

[ClassInstall32_CopyFiles]

驱动程序安装

驱动程序安装从设备列表中指定的部分开始。平台标识符(nt、ntx86、ntia64 或 ntamd64)指定了安装程序的目标平台。通过平台标识符,可以轻松地为其他平台添加安装部分。由于 amd64 将成为未来的常见平台,这一点很快就会变得更加重要。

主安装部分应列出驱动程序版本和包含要安装文件列表的部分名称。文件列表按名称列出文件。这就是为什么之前声明了 `SourceDisksFiles` 部分,以便安装程序服务知道在哪里可以找到这些文件。值为 2 的文件标志表示该文件对安装至关重要。

[DriverInstall.ntx86]
DriverVer=27/12/2005,1.0.0.1
CopyFiles=DriverCopyFiles

[DriverCopyFiles]
basic.sys,,,2

仅仅将文件复制到系统上是不够的。系统应该知道哪个文件包含驱动程序入口点,以及驱动程序如何启动。这在 `DriverInstall.xxxx.Services` 部分中声明。此部分对于 PNP 驱动程序是强制性的。

[DriverInstall.ntx86.Services]
AddService=wdf_basic,2,DriverService

[DriverService]
ServiceType=1                           ;;kernel mode driver
StartType=3                             ;;start on demand
ErrorControl=1                          ;;normal error handling.
ServiceBinary=%10%\system32\drivers\basic.sys

`hw` 部分是可选的,可用于配置驱动程序特定的注册表项。它放在这里是为了演示原理。

[DriverInstall.ntx86.hw]
AddReg=DriverHwAddReg

[DriverHwAddReg]
HKR,,SampleInfo,,"Basic registry key"

CoInstaller 安装

前面创建的驱动程序是一个 WDF 驱动程序。因此,它需要 WDF 库才能运行。为了确保驱动程序可以加载到内存中,INF 文件需要一个 `CoInstaller` 部分来与驱动程序文件一起安装 WDF 库。指定 `CoInstaller` 是通过“CoInstallers”部分完成的。它指定了一个注册表部分和一个 `CopyFiles` 部分,就像 `DriverInstall` 部分和 `ClassInstall32` 部分一样。

[DriverInstall.ntx86.CoInstallers]
AddReg=CoInstaller_AddReg
CopyFiles=CoInstaller_CopyFiles

[CoInstaller_CopyFiles]
WdfCoinstaller01000.dll,,,2

AddReg` 部分指定了一个 `REG_MULTI_SZ` 类型的注册表项。它指定了包含 `CoInstaller` 入口点的文件以及入口点的名称。

[CoInstaller_AddReg]
HKR,,CoInstallers32,0x00010000, 
      "WdfCoinstaller01000.dll,WdfCoInstaller"

Wdf` 部分是 INF 格式的新增功能,用于告知系统哪个驱动程序使用哪个版本的 KMDF,并用于指定 WDF 安装部分。它将在安装后由 `CoInstaller` 读取。

指定 WDF 版本是必要的,因为可以使用一个 INF 文件安装多个驱动程序,并且每个驱动程序可能针对不同的 KMDF 版本编写。

WDF 安装部分包含(至少)`KmdfLibraryVersion` 键。这是驱动程序构建时所针对的 KMDF 版本。

[DriverInstall.ntx86.Wdf]
KmdfService = basic, basic_wdfsect

[basic_wdfsect]
KmdfLibraryVersion = 1.0

字符串部分

`Strings` 部分是 INF 文件中的最后一个部分。它包含安装文件中使用的变量列表。

[Strings]
MFGNAME="Driver factory"
INSTDISK=" Installation Disc"
DEV_DESCRIPTION="Basic WDF device"
INST_DISK_NAME="Basic WDF device driver installation disk"

安装驱动程序

安装驱动程序可以通过多种方式完成。最简单的方式可用于安装 PNP 设备。将安装包复制到硬盘(可能通过安装程序),然后插入设备。如果设备不支持热插拔,可能需要关机并重启。另一种解决方案是使用控制面板中的“添加硬件”小程序。无论哪种方式,请按照安装向导的指示,浏览到 INF 文件所在的位置,然后继续安装。您可能希望在设备安装期间运行调试监视器。一旦设备启动,您就可以在输出窗口中看到调试语句滚动显示。

结论

您现在拥有一个功能齐全的设备驱动程序,可以构建和安装。它实际上没有任何作用,但这并非本文的目的。例如,此驱动程序可以用作构建 USB 驱动程序的基础。现在,您还了解了使用 WDF 内核模式设备基础编写、构建和安装驱动程序所涉及的不同步骤。

如果您对驱动程序开发(无论是 WDF 还是 WDM)非常感兴趣,请尽可能多地阅读。正如我在本文开头所说,本网站上的其他驱动程序开发文章,尤其是 Oney 的书,都值得您花费时间(和金钱)阅读。由于 WDF 是基于 WDM 构建的,了解其背后的概念是值得的。

如果您没有带寄存器级文档的硬件设备,购买 OSR Online 的学习套件也是一个好主意。我使用了 OSR USB-FX2 设备,因为它可以在笔记本电脑上使用,具有优势。

历史

本文已发布以下版本:

  • 1.0:本文的初始版本。
  • 1.1:根据 Vishal Manan 的解释,更正了 WDF `Coinstaller` 部分的解释。
© . All rights reserved.