VS2013 WDK8.1驱动开发2(最简单的WDM驱动)
本系列博客为学习《Windows驱动开发技术详解》一书的学习笔记。
前言
上次创建了一个最简单的NT驱动程序,这次我们来创建一个最简单的WDM驱动程序。WDM驱动相比于NT驱动程序多了对即插即用功能的支持。
搭建Windows驱动开发环境
- 开发主机安装Microsoft Visual Studio 2013,下载地址点我
 - 开发主机安装Windows Driver Kit (WDK) 8.1,下载地址点我
 - 测试虚拟机环境为Win7 64位
 
VS2013必须配合WDK8.1才可以进行驱动程序的开发,只有安装了WDK8.1后,VS2013中才会出现驱动开发工程的模板,如下图:
创建一个WDM驱动工程
新建一个空的WDM驱动工程,删除附带的Package工程,新建一个WDMDriver.c文件,我们的代码都写在该文件中。
编写WDM驱动程序代码
1. WDM驱动程序的入口函数
    #include <wdm.h>
    /// @brief 初始化驱动程序
    /// @param[in] pDriverObject 驱动对象
    /// @param[in] pRegPath 驱动程序在注册表中的路径
    /// @return 初始化驱动状态
    NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegPath)
    {
        UNREFERENCED_PARAMETER(pRegPath);
        KdPrint(("Enter DriverEntry\n"));
        pDriverObject->DriverExtension->AddDevice = HelloWDMAddDevice;
        pDriverObject->DriverUnload = HelloWDMUnload;
        for (int i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
        {
            pDriverObject->MajorFunction[i] = HelloWDMDispatchRoutine;
        }
        pDriverObject->MajorFunction[IRP_MJ_PNP] = HelloWDMPnp;
        KdPrint(("Leave DriverEntry\n"));
        return STATUS_SUCCESS;
    }
- WDM驱动程序需要包含wdm.h头文件。
 - 代码中我们给AddDevice设置了一个回调函数,NT驱动程序没有此回调函数。该函数的作用是创建设备,该函数由PNP(即插即用)管理器调用,PNP管理器会在指定设备插入系统时调用该函数。
 - 相比于NT驱动程序,WDM驱动程序还多了一个对IRP_MJ_PNP的处理函数,该函数的作用是处理PNP的请求包,例如启动设备、停止设备等请求。
 
2. AddDevice例程
在WDM驱动程序中DriverEntry不再负责创建设备,而是交由AddDevice例程去创建设备。
     /// @brief 设备扩展结构
     typedef struct _DEVICE_EXTENSION
     {
         PDEVICE_OBJECT PDeviceObject; ///< 设备对象
         PDEVICE_OBJECT PNextStackDevice; ///< 下层设备对象指针
         UNICODE_STRING DeviceName; ///< 设备名称
         UNICODE_STRING SymLinkName; ///< 符号链接名
     } DEVICE_EXTENSION, *PDEVICE_EXTENSION;
     /// @brief 添加新设备
     /// @param[in] pDriverObject 从I/O管理器传进来的驱动对象
     /// @param[in] pPhysicalDeviceObject 从I/O管理器传进来的物理设备对象
     /// @return 添加新设备状态
     NTSTATUS HelloWDMAddDevice(IN PDRIVER_OBJECT pDriverObject, IN PDEVICE_OBJECT pPhysicalDeviceObject)
     {
         KdPrint(("Enter HelloWDMAddDevice\n"));
         NTSTATUS status = STATUS_SUCCESS;
         UNICODE_STRING devName = { 0 }; // 设备名称
         UNICODE_STRING symName = { 0 }; // 链接符号名
         PDEVICE_OBJECT pDeviceObject = NULL; // 创建的设备对象
         PDEVICE_EXTENSION pDeviceExt = NULL; // 设备扩展对象
         // 初始化字符串
         RtlInitUnicodeString(&devName, L"\\Device\\HelloWDMDevice");
         RtlInitUnicodeString(&symName, L"\\??\\HelloWDM");
         // 创建设备
         status = IoCreateDevice(
             pDriverObject,
             sizeof(DEVICE_EXTENSION),
             &devName,
             FILE_DEVICE_UNKNOWN,
             0,
             FALSE,
             &pDeviceObject);
         if (!NT_SUCCESS(status))
         {
             return status;
         }
         pDeviceExt = (PDEVICE_EXTENSION)pDeviceObject->DeviceExtension;
         pDeviceExt->PDeviceObject = pDeviceObject;
         pDeviceExt->DeviceName = devName;
         pDeviceExt->SymLinkName = symName;
         // 讲设备对象挂接在设备堆栈上
         pDeviceExt->PNextStackDevice = IoAttachDeviceToDeviceStack(pDeviceObject, pPhysicalDeviceObject);
         status = IoCreateSymbolicLink(&symName, &devName);
         if (!NT_SUCCESS(status))
         {
             IoDeleteSymbolicLink(&pDeviceExt->SymLinkName);
             status = IoCreateSymbolicLink(&symName, &devName);
             if (!NT_SUCCESS(status))
             {
                 return status;
             }
         }
         pDeviceObject->Flags |= DO_BUFFERED_IO | DO_POWER_PAGABLE;
         pDeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;
         KdPrint(("Leave HelloWDMAddDevice\n"));
         return status;
     }
- 和NT驱动一样我们需要声明一个设备扩展结构。
 - AddDevice例程的参数有两个,第一个是驱动对象,第二个是物理设备对象,该对象由I/O管理器创建,物理设备对象的概以后讲。
 - 创建设备对象代码和NT驱动程序类似,只是多了一个将设备对象附加在设备栈上的过程,设备栈的概念以后会讲到。
 - 当系统中插入多个指定硬件时该函数被被多次调用,也就是说会多次创建设备对象,创建多个设备时需要每个设备的设备名称和链接符号都不同,示例代码中使用相同的设备名和链接符号是有问题的,实际编写程序时请注意。
 
3. PNP IRP处理例程
    /// @brief 对即插即用IPR进行处理
    /// @param[in] pDeviceObject 功能设备对象
    /// @param[in] pIrp 请求包
    /// @return 状态
    NTSTATUS HelloWDMPnp(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp)
    {
        KdPrint(("Enter HelloWDMPnp\n"));
        PDEVICE_EXTENSION pDeviceExt = (PDEVICE_EXTENSION)pDeviceObject->DeviceExtension;
        PIO_STACK_LOCATION pStackLoc = IoGetCurrentIrpStackLocation(pIrp);
        unsigned long func = pStackLoc->MinorFunction;
        KdPrint(("PNP Request (%u)\n", func));
        NTSTATUS status = STATUS_SUCCESS;
        switch (func)
        {
        case IRP_MN_REMOVE_DEVICE:
            status = PnpRemoveDevice(pDeviceExt, pIrp);
            break;
        default:
            status = PnpDefaultHandler(pDeviceExt, pIrp);
            break;
        }
        KdPrint(("Leave HelloWDMPnp\n"));
        return status;
    }
- PNP派遣函数的第一个参数是设备对象,系统在回调该方法时会使用我们在AddDevice例程中创建出来的设备对象填写该参数。
 - 通过当前IRP栈位置我们可以知道请求的次功能代码,在上述代码中我们只对移除设备功能代码做了特殊处理,其他PNP功能代码进行默认处理,IRP栈以后会讲到,目前不用在意。
 
PNP功能代码的默认处理函数如下:
    /// @brief 对PNP IRP进行默认处理
    /// @param[in] pDeviceExt 设备对象扩展
    /// @param[in] pIrp I/O请求包
    /// @return 状态
    NTSTATUS PnpDefaultHandler(PDEVICE_EXTENSION pDeviceExt, PIRP pIrp)
    {
        KdPrint(("Enter DefaultPnpHandler\n"));
        // 略过当前堆栈
        IoSkipCurrentIrpStackLocation(pIrp);
        KdPrint(("Leave DefaultPnpHandler\n"));
        // 用下层堆栈的驱动设备处理此IRP
        return IoCallDriver(pDeviceExt->PNextStackDevice, pIrp);
    }
- 代码中我们只是略过当前堆栈,并将IRP转发给下层设备处理。
 
PNP移除设备代码的处理函数如下:
    /// @brief PNP移除设备处理函数
    /// @param[in] pDeviceExt 设备扩展对象
    /// @param[in] pIrp 请求包
    /// @return 状态
    NTSTATUS PnpRemoveDevice(PDEVICE_EXTENSION pDeviceExt, PIRP pIrp)
    {
        KdPrint(("Enter HandleRemoveDevice\n"));
        pIrp->IoStatus.Status = STATUS_SUCCESS;
        NTSTATUS status = PnpDefaultHandler(pDeviceExt, pIrp);
        // 删除符号链接
        IoDeleteSymbolicLink(&pDeviceExt->SymLinkName);
        //调用IoDetachDevice()把设备对象从设备栈中脱开:  
        if (pDeviceExt->PNextStackDevice != NULL)
            IoDetachDevice(pDeviceExt->PNextStackDevice);
        //删除设备对象:  
        IoDeleteDevice(pDeviceExt->PDeviceObject);
        KdPrint(("Leave HandleRemoveDevice\n"));
        return status;
    }
- 这个函数负责了驱动程序的卸载工作。
 - 卸载前我们需要让下层设备先完成这个请求,所以我们调用了DefaultPnpHandler处理方法。
 - 除了删除符号链接删除设备对象外,我们还需要将设备对象从设备栈中脱开。
 
4. 默认IRP处理例程
    /// @brief 对默认IPR进行处理
    NTSTATUS HelloWDMDispatchRoutine(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp)
    {
        UNREFERENCED_PARAMETER(pDeviceObject);
        KdPrint(("Enter HelloWDMDispatchRoutine\n"));
        NTSTATUS status = STATUS_SUCCESS;
        pIrp->IoStatus.Status = status;
        pIrp->IoStatus.Information = 0;
        IoCompleteRequest(pIrp, IO_NO_INCREMENT);
        KdPrint(("Leave WDMDriverDispatchRoutine\n"));
        return STATUS_SUCCESS;
    }
- 代码中我们简单的将IRP设置为完成。
 
5. 卸载驱动例程
WDM驱动的卸载工作在PNP处理函数中的RemoveDevice函数中完成,所以卸载驱动例程不用做任何事。
    /// @brief 驱动程序卸载操作
    void HelloWDMUnload(IN PDRIVER_OBJECT pDriverObject)
    {
        UNREFERENCED_PARAMETER(pDriverObject);
        KdPrint(("Enter HelloWDMUnload\n"));
        KdPrint(("Leave HelloWDMUnload\n"));
    }
3. 编写INF文件
WDM驱动的安装需要使用INF文件,VS2013已经给我们创建了一个默认的INF文件,我们只需修改它就可以。INF文件描述了WDM驱动程序的操作硬件设备的信息和驱动程序的一些信息,INF文件的配置项很多目前我们只需复制如下的内容即可:
;
; chapter01HelloWDM.inf
;
[Version]
Signature="$WINDOWS NT$"
;如果设备是一个标准类别,使用标准类的名称和GUID 否则创建一个自定义的类别名称,并自定义它的GUID  
;自定义的类别在注册表中 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class\ 有显示 
Class=%DeviceClassName%
ClassGuid={24B968AA-9C43-41D4-BFD5-0DED43B29F16}
;INF文件的提供者 
Provider=Tester
DriverVer=
;如果不是标准类别设备,这里的配置必须的  
[ClassInstall32]                      
Addreg=ClassAddReg  
;对应的注册表是 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class\  
[ClassAddReg]    
HKR,,,,%DeviceClassName%              
HKR,,Icon,,"-5"  
;驱动文件需要复制到的目的目录,12表示%windir%/system32/drivers
[DestinationDirs]
DriverFilesName=12
;驱动文件名段
[DriverFilesName]                                   
chapter01-HelloWDM.sys 
[SourceDisksNames.x86]
1 = "HelloWDM Installation Disk"...                
[SourceDisksNames.amd64]
1 = "HelloWDM Installation Disk"...
[SourceDisksFiles]
;源驱动文件在标记为1的磁盘下
DriverFilesName=1
;这里是设置制作商相关的选项,注意这里VS默认生成的标准设备的配置 如:%ManufacturerName%=Standard,NT$ARCH$  
;如果不是标准类别设备这里必须修改,要不然最后加载的时候会出现259错误
;并且指定模型段  
[Manufacturer]
%ManufacturerName%=MyDeviceModel,ntx86,ntamd64               
;设备模型段 
[MyDeviceModel]  
;指定设备描述和设备ID,以及安装段
%DeviceDesc%=DriverInstall.nt, PCI\VEN_8888&DEV_8888 
[MyDeviceModel.ntx86]  
;指定设备描述和设备ID,以及安装段
%DeviceDesc%=DriverInstall.nt, PCI\VEN_8888&DEV_8888  
[MyDeviceModel.ntamd64]  
;指定设备描述和设备ID,以及安装段
%DeviceDesc%=DriverInstall.nt, PCI\VEN_8888&DEV_8888  
;这里需要注意WIN2000及其以上的系统这里有个.NT
[DriverInstall.nt]   
;指定需要拷贝的文件                    
CopyFiles=DriverFilesName  
;指定写注册表的段  
AddReg=InstallAddReg 
[InstallAddReg]    
HKLM, "System\CurrentControlSet\Services\TestWDMSvc\Parameters", "BreakOnEntry", 0x00010001, 0
;注册表中的服务名具体地址是 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services  
[DriverInstall.nt.Services] 
;TestWDMSvc为安装分服务名称   
Addservice = TestWDMSvc, 0x00000002, SysAddService      
;服务的具体选项  
[SysAddService]    
DisplayName = TestWDMSvc                            
ServiceType = 1 ; SERVICE_KERNEL_DRIVER    
StartType = 3 ; SERVICE_DEMAND_START    
ErrorControl = 1 ; SERVICE_ERROR_NORMAL     
ServiceBinary = %12%\chapter01-HelloWDM.sys   
[Strings]
ManufacturerName="MySoft"
DeviceDesc="MyVEN_8888&DEV_8888Device"
DeviceClassName="TestClass"
4. 配置工程属性、编译
配置选项选择Win7Debug x64并且设置测试签名:
编译成功后可以在工程目录x64\Win7Debug文件夹下找到如下文件:
- chapter01-HelloWDM.sys,目标驱动文件
 - chapter01HelloWDM.inf,安装配置文件
 - chapter01-HelloWDM.cer,证书文件
 
5. 虚拟机配置
- 开启测试签名模式
 - 提前开启DebugView
 - 将chapter01-HelloWDM.cer,chapter01-HelloWDM.sys和chapter01HelloWDM.inf拷贝到虚拟机上
 - 安装测试证书chapter01-HelloWDM.cer
 
6. 安装WDM驱动
1. 打开虚拟机的设备管理器点击添加过时硬件
2. 选择手动安装,点击下一步
3. 选择显示所有设备,点击下一步
4. 点击从磁盘安装
5. 点击浏览,选择chapter01HelloWDM.inf文件,点击确定
6. 点击下一步
7. 成功安装驱动
8. DebugView中可以看到调试信息
9. 设备管理器中可以看到我们安装的驱动程序
9. 点击我们的驱动程序,选择卸载驱动,DebugView中可以看到卸载驱动的调试信息
后话
本文完整工程和代码托管在GitHub上点我查看。
其他章节链接
VS2013 WDK8.1驱动开发2(最简单的WDM驱动)
                
还没有人评论...