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

简单的 WDM 回送驱动程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (24投票s)

2009年3月11日

GPL3

8分钟阅读

viewsIcon

63456

downloadIcon

2545

本文档面向首次编写 Windows 内核设备驱动程序并希望通过简单的示例和源代码进行实验的开发人员。

引言

网络上有许多关于 Windows 内核设备驱动程序的文章。我想再添加一篇。本文档面向那些希望从简单的 Windows 内核驱动程序示例开始的开发者。此驱动程序示例无需任何硬件即可安装到 Windows 系统中。它是一个伪示例,一个回送驱动程序。应用程序将任何可用数据写入驱动程序,当应用程序从驱动程序读取数据时,数据将被返回。

背景

本文档面向首次编写 Windows 内核设备驱动程序并希望通过简单的示例和源代码进行实验的开发人员。如果您在其他操作系统上开发过设备驱动程序,那么这可能是最好的入门示例之一。

要更好地理解这些概念,需要扎实的“C”语言基础。

WDM 入门

在 Windows 中,每个驱动程序都以一个 DriverEntry 函数开始。DriverEntry,顾名思义,是驱动程序的入口点。在 DriverEntry 实现中,必须实例化 WDM 对象,我们还向 WDM 提供设备驱动程序的各种回调函数。该驱动程序展示了如何处理读写操作。

内核通常通过发送 I/O 请求包 (IRP) 来运行驱动程序中的代码。例如,Win32 ReadFile 调用会作为读取 IRP 进入设备驱动程序。读取缓冲区的大小和位置作为参数在 IRP 结构中指定。IRP 结构是设备驱动程序的基础。

驱动程序有一个主要的初始化入口点——一个称为 DriverEntry 的例程;它具有标准的函数签名。

NTSTATUS 
 DriverEntry (
 PDRIVER_OBJECT pDriverObject,
 PUNICODE_STRING pRegistryPath 
)

DriverObject 参数向 DriverEntry 例程提供指向驱动程序驱动程序对象的指针,该对象由 I/O 管理器分配。DriverEntry 例程必须使用驱动程序标准例程的入口点填充驱动程序对象。

DriverObject 指针使驱动程序能够访问 DriverObject->HardwareDatabase,它指向一个计数 Unicode 字符串,该字符串指定到注册表 \Registry\Machine\Hardware 树的路径。

RegistryPath 指向的注册表路径字符串的格式为 \Registry\Machine\System\CurrentControlSet\Services\DriverName。驱动程序可以使用此路径存储特定于驱动程序的信息。DriverEntry 例程应保存 Unicode 字符串的副本,而不是指针,因为 I/O 管理器会在 DriverEntry 返回后释放 RegistryPath 缓冲区。

当驱动程序加载时,内核会调用 DriverEntry 例程。之后,它可能会在驱动程序中调用许多其他函数。这些函数通常称为回调函数。当驱动程序初始化时,它会将所有支持的回调函数注册到内核。然后,内核会在适当的情况下调用回调函数。例如,如果驱动程序支持读取操作,则应将读取回调函数注册到内核。每个回调都有一个标准的函数原型,适用于调用它的情况。

标准驱动程序入口点

  • DriverEntry: 驱动程序初始入口点。将所有回调函数注册到内核。
  • Unload: 卸载驱动程序。
  • StartIO: 用于串行处理 IRP 的回调。
  • 中断服务例程 (ISR): 用于处理硬件中断。
  • IRP 处理程序: 用于处理您希望处理的 IRP。

下面的代码片段显示了向内核注册回调函数

// Announce other driver entry points
pDriverObject->DriverUnload = DriverUnload;
 
// This includes Dispatch routines for Create, Write & Read
pDriverObject->MajorFunction[IRP_MJ_CREATE]     =DispatchCreate;
pDriverObject->MajorFunction[IRP_MJ_CLOSE]      =DispatchClose;
pDriverObject->MajorFunction[IRP_MJ_WRITE]      =DispatchWrite;
pDriverObject->MajorFunction[IRP_MJ_READ]       =DispatchRead;

回调函数的原型如下

NTSTATUS
DispatchXXX(
    IN PDEVICE_OBJECT  DeviceObject,
    IN PIRP  Irp
    );

操作系统通过设备对象表示设备。驱动程序使用 IoCreateDeviceIoCreateDeviceSecure 例程创建设备对象。设备对象是部分不透明的。驱动程序不直接设置设备对象的成员,除非另有说明。

IRP 是 I/O 管理器用来与驱动程序通信以及允许驱动程序之间相互通信的基本 I/O 管理器结构。一个包包含两个不同的部分

  • Header,或包的固定部分:I/O 管理器使用它来存储有关原始请求的信息,例如调用者的与设备无关的参数、已打开文件的设备对象的地址等。驱动程序也使用它来存储请求的最终状态等信息。
  • I/O 堆栈位置:标头之后是一组 I/O 堆栈位置,每个位置对应于请求所绑定的分层驱动程序链中的一个驱动程序。每个堆栈位置都包含相应的驱动程序用于确定其任务的参数、函数代码和上下文。有关更多信息,请参阅 IO_STACK_LOCATION 结构。

虽然高级驱动程序可能会检查 IRP 中的 Cancel 布尔值,但即使该值为 TRUE,该驱动程序也不能假定 IRP 将以 STATUS_CANCELLED 状态由低级驱动程序完成。

只有在创建了设备对象后,内核才会调用所有 IRP 句柄或回调函数。我们已经看到,所有 IRP 句柄原型都需要一个由 IoCreateDevice 函数创建的设备对象。

驱动程序将在 DriverEntry 函数退出之前创建设备对象。IoCreateDevice 创建一个设备对象并返回指向该对象的指针。调用者负责在不再需要该对象时通过调用 IoDeleteDevice 来删除它。

IoCreateDevice 只能用于创建未命名设备对象,或创建已由 INF 文件设置了安全描述符的命名设备对象。否则,驱动程序必须使用 IoCreateDeviceSecure 来创建命名设备对象。由 IoCreateDevice 创建的设备在用户空间中不可见,并且会创建符号链接供 Windows 应用程序访问设备。IoCreateSymbolicLink 例程在设备的 对象名称和用户可见的设备名称之间建立符号链接。

DriverEntry 代码解释完毕。下一部分将描述回调函数及其实现细节。

卸载回调实现

WDM 驱动程序需要卸载例程,而非 WDM 驱动程序则可以选择提供。驱动程序的卸载例程(如果提供)应命名为 XxxUnload,其中 Xxx 是特定于驱动程序的前缀。驱动程序的 DriverEntry 例程必须将卸载例程的地址存储在 DriverObject->DriverUnload 中。(如果未提供例程,则此指针必须为 NULL。)

当驱动程序被替换、设备驱动程序服务被移除或驱动程序初始化失败时,操作系统会卸载驱动程序。

当满足以下所有条件时,I/O 管理器会调用驱动程序的卸载例程:

  • 驱动程序创建的任何设备对象都不再有引用。换句话说,与底层设备相关的任何文件都不能打开,也不能对驱动程序的任何设备对象有未完成的 IRP。
  • 没有其他驱动程序附加到此驱动程序。
  • 驱动程序已调用 IoUnregisterPlugPlayNotification 以取消注册之前注册的所有即插即用通知。

请注意,如果驱动程序的 DriverEntry 例程返回失败状态,则不会调用卸载例程。在这种情况下,I/O 管理器将简单地释放驱动程序占用的内存空间。

PnP 管理器和 I/O 管理器都不会在系统关机时调用卸载例程。必须执行关机处理的驱动程序应注册 DispatchShutdown 例程。

下面是卸载例程的代码片段

VOID DriverUnload (PDRIVER_OBJECT   pDriverObject) 
{
    PDEVICE_OBJECT    pNextObj;
     
    // Loop through each device controlled by Driver
    pNextObj = pDriverObject->DeviceObject;
     
    while (pNextObj != NULL) {
        PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pNextObj->DeviceExtension;

        //...
        //  UNDO whatever is done in Driver Entry
        //

        // ... delete symbolic link name
        IoDeleteSymbolicLink(&pDevExt->symLinkName);
        pNextObj = pNextObj->NextDevice;

        // then delete the device using the Extension
        IoDeleteDevice( pDevExt->pDevice );
    }
    return;
}

创建回调实现

驱动程序的 DispatchCreate 例程应命名为 XxxDispatchCreate,其中 Xxx 是特定于驱动程序的前缀。驱动程序的 DriverEntry 例程必须将 DispatchCreate 例程的地址存储在 DriverObject->MajorFunction [IRP_MJ_CREATE] 中。

所有Dispatch 例程的输入参数均包含在 Irp 指向的 IRP 结构中。附加参数包含在驱动程序关联的 I/O 堆栈位置中,该位置由 IO_STACK_LOCATION 结构描述,可以通过调用 IoGetCurrentIrpStackLocation 获取。

NTSTATUS DispatchCreate (PDEVICE_OBJECT   pDevObj, PIRP pIrp) 
{
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;    
 
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;     // no bytes xfered
    IoCompleteRequest( pIrp, IO_NO_INCREMENT );
    return STATUS_SUCCESS;
}

关闭回调实现

驱动程序的 DispatchClose 例程应命名为 XxxDispatchClose,其中 Xxx 是特定于驱动程序的前缀。驱动程序的 DriverEntry 例程必须将 DispatchClose 例程的地址存储在 DriverObject->MajorFunction [IRP_MJ_CLOSE] 中。

NTSTATUS DispatchClose (IN PDEVICE_OBJECT pDevObj,IN PIRP pIrp)
{
    ULONG i,thNum;
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj->DeviceExtension;
 
    // . . .
    // Deallocates memory, Deregister functions, ISR
    // IF allocated in either DriverEntry, DistpatchCreate, …etc.
    // . . .
 
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;     // no bytes xfered
    IoCompleteRequest( pIrp, IO_NO_INCREMENT );
    return STATUS_SUCCESS;
}

读取回调实现

驱动程序的 DispatchRead 例程应命名为 XxxDispatchRead,其中 Xxx 是特定于驱动程序的前缀。驱动程序的 DriverEntry 例程必须将 DispatchRead 例程的地址存储在 DriverObject->MajorFunction [IRP_MJ_READ] 中。

NTSTATUS DispatchRead (PDEVICE_OBJECT     pDevObj,PIRP pIrp) 
{     
    NTSTATUS status = STATUS_SUCCESS;
    PVOID userBuffer;
    ULONG xferSize,i,thNum ;
 
    // The stack location contains the user buffer info
    PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation( pIrp );
 
    // Dig out the Device Extension from the Device object
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj->DeviceExtension;
 
    // Determine the length of the request
    xferSize = pIrpStack->Parameters.Read.Length;
    userBuffer = pIrp->AssociatedIrp.SystemBuffer;
      
    //
    // copy to user buffer from system buffer
    //
 
    // Now complete the IRP
    pIrp->IoStatus.Status = status;
    pIrp->IoStatus.Information = xferSize;    // bytes xfered
    IoCompleteRequest( pIrp, IO_NO_INCREMENT );
    return status;
}

写入回调实现

驱动程序的 DispatchWrite 例程应命名为 XxxDispatchWrite,其中 Xxx 是特定于驱动程序的前缀。驱动程序的 DriverEntry 例程必须将 DispatchWrite 例程的地址存储在 DriverObject->MajorFunction [IRP_MJ_WRITE] 中。

NTSTATUS DispatchWrite (IN PDEVICE_OBJECT pDevObj,IN PIRP pIrp) 
{     
    NTSTATUS status = STATUS_SUCCESS;
    PVOID userBuffer;
    ULONG xferSize;
    ULONG i,thNum;
 
    // The stack location contains the user buffer info
    PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation( pIrp );

    // Dig out the Device Extension from the Device object
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
 
    // Determine the length of the request
    xferSize = pIrpStack->Parameters.Write.Length;
    // Obtain user buffer pointer
    userBuffer = pIrp->AssociatedIrp.SystemBuffer;
 
    // Allocate the new buffer and copy from user Buffer. 
 
    // Now complete the IRP
    pIrp->IoStatus.Status = status;
    pIrp->IoStatus.Information = xferSize;    // bytes xfered
    IoCompleteRequest( pIrp, IO_NO_INCREMENT );
    return status;
}

用户应用程序

用户应用程序是一个简单的 Win32 控制台应用程序;它打开一个文件,写入一个缓冲区,然后从驱动程序读取该缓冲区,最后关闭文件。与 Linux 不同,在 Windows 中打开文件和设备是不同的。

Windows 使用 CreateFileReadFileWriteFileCloseHandle 调用来打开或创建设备文件、向设备写入、从设备读取,最后关闭句柄。

下面是一个代码片段

int main() {
    HANDLE hDevice;
    BOOL status;
 
    hDevice = CreateFile("\\\\.\\LBK",
                    GENERIC_READ | GENERIC_WRITE,
                    0,          // share mode none
                    NULL, // no security
                    OPEN_EXISTING,
                    FILE_ATTRIBUTE_NORMAL,
                    NULL );           // no template
 
    if (hDevice == INVALID_HANDLE_VALUE) {
        printf("Failed to obtain file handle to device: "
              "%s with Win32 error code: %d\n",
              "LBK1", GetLastError() );
        return 1;
    }
 
    status = WriteFile(hDevice, outBuffer, outCount, &bW, NULL);
    if (!status) {
        printf("Failed on call to WriteFile - error: %d\n",
              GetLastError() );
        return 2;
    }
 
    . . .
 
    status = ReadFile(hDevice, inBuffer, inCount, &bR, NULL);
    if (!status) {
        printf("Failed on call to ReadFile - error: %d\n",
              GetLastError() );
        return 4;
    }
 
    status = CloseHandle(hDevice);
    if (!status) {
        printf("Failed on call to CloseHandle - error: %d\n",
               GetLastError() );
        return 6;
    }
    printf("Succeeded in closing device...exiting normally\n");
    return 0;
}

运行应用程序

给出的示例代码使用 DDK 控制台构建。如果安装了 DDK,请打开 DDK 构建控制台,然后转到项目 HOME 目录。使用 **build –cEZ** 构建驱动程序以及应用程序。

驱动程序构建完成后,应将其复制到 $WINDOWS$\system32\drivers 文件夹。在服务路径中创建一个注册表项。示例中附带了一个 .reg 文件。通过双击 .reg 文件,注册表将被更新。最好在更新 Windows 注册表后重新启动系统。不重新启动系统也可以工作,但 Windows 文档指出,在更新注册表后,应重新启动系统以使更改生效。

更新注册表后,启动服务。网络上有许多可用于在 Windows 中启动服务的实用工具。通过一个简单的 Windows 命令也可以实现这一点

net start <service-name> # in our case service is driver name

现在驱动程序已准备好与用户应用程序通信。运行用户应用程序 Testor.exe。它将在 Testor 可执行文件夹中。

历史

这是第一个版本。我将确保每次更新示例时,都会将其上传到 The Code Project。这是一个简单的 WDM 示例。我将来还会发布更多 Windows 示例。

© . All rights reserved.