从零开始学习Windows WDF驱动程序开发

从零开始学习Windows WDF驱动程序开发

目录:

1. 第一部分 编译安装测试一个简单的WDF驱动程序

   1.1 编译安装测试

   1.2 Windows驱动开发就是要开发出INF文件和SYS文件

   1.3 简单介绍下编译脚本

   1.4 Windows应用程序如何访问一个设备

2. 第二部分 了解WDF框架并且开发WDF驱动程序

   2.1 驱动程序的加载

   2.2 系统检测到新硬件的时候干些什么?

   2.3 WDF中读写和控制设备

3. 第三部分 后记

 

第一部分 编译安装并且测试一个简单的WDF驱动程序。

    对于初学者,我们需要一个简单的例子,就和C语言里面的HelloWorld一样,编译运行,接着打印出"Hello world!"。我们要先建立起对WDF驱动的一个初步而强烈的感性认识,然后再对照着例子来学习WDF的概念,看它的代码是怎么实现的,这样就会有深刻的认识。这就是教育学上所谓的循序渐进。按照这个思路,我们就先要编译安装运行一个简单驱动程序例子。我浏览了下WDF的例子之后,发现Echo这个例子比较适合我们的这个思路。下面就开始编译、安装和运行Echo这个例子。我是在XP下面做的实验,如果在其他操作系统下,也类似。在开始试验之前,读者可以从微软的网站下载WDK开发包,大小约700Mbytes,需要耐心地下才能下完。下文中,如果读者不知道我说的文件或程序在哪个目录下面,请搜索下,我尽量说详细些。

 

1.1 编译安装测试

(1) 编译Echo这个WDF例子。

   在开始菜单中选择X86 Free Build Environment。

   命令行界面出现之后,用cd切换到/WinDDK/7600.16385.1/src/general/echo/kmdf目录下。

   命令行里面运行,"build -ceZ"。

   可以看到生成了echo.inf, echo.sys这两个文件。后面安装驱动程序的时候要用到它俩。

   编译完毕。

(2) 安装。

   拷贝devcon.exe, WdfCoInstaller01009.dll, echo.inf, echo.sys四个文件到同一个目录下。

   在命令行下进入该目录并运行,devcon.exe install echo.inf "root/echo"

   命令行中出现"Driver installed successfully"。

   安装完毕。

(3) 察看。

   在"我的电脑"->右键"属性"->"硬件"->"设备管理器"里面,可以看见Sample Device已经安装好了。如图,因为我安装了2次,所有这里有两个Sample WDF ECHO Driver。如果安装3次,就有3个。

 

(4) 试验。

   在开始菜单中选择X86 Free Build Environment。

   在命令行中用cd切换到D:/WinDDK/7600.16385.1/src/general/echo/exe目录下面。

   在命令行中运行, "build -ceZ"。

   再cd到D:/WinDDK/7600.16385.1/src/general/echo/exe/objfre_wxp_x86/i386下面。

   在命令行中运行, "echoapp.exe"。出现下面的图就说明,程序正确工作了。

   

     使用上述的步骤,可以编译和安装大部分的WDF驱动例子。WDK中提供的这些WDF例子可以作为我们开发驱动程序的原始代码。

 

1.2 Windows驱动开发就是要开发出INF文件和SYS文件

    实验之后,读者可能会问几个问题devcon.exe, WdfCoInstaller01009.dll, echo.inf, echo.sys是分别用来做什么用的?答案如下。

   echo.sys文件是真正的驱动程序,是一个DLL文件,但是以sys作为文件后缀名。

   echo.inf文件是一个存放着安装信息的文本文件。由Windows的SetupAPI调用echo.inf里面的内建命令和配置信息,完成安装过程。过程有点类似于shell程序调用脚本运行。

   WdfCoInstaller01009.dll是Co-Installer, 用于协助安装的程序,以dll的形式提供。

   devcon.exe是一个命令行工具,可以用来显示设备信息、寻找设备、安装和改动设备、重启电脑等。

   devcon.exe和WdfCoInstaller01009.dll是WDK自带的,而echo.inf和echo.sys是需要驱动程序开发者提供的。你要为某个设备开发一个驱动程序,那你最终提供的就是inf和sys文件。所以掌握了inf文件和sys文件的写法,就掌握了驱动程序的开发方法。sys文件的开发实际上就是驱动程序本身的开发,这是本文重点,所以在后面详述。而inf文件,很多应用程序安装的时候也会用到,有兴趣的读者可以在网上google下,有很多中文资料;WDF文档中也有描述和很多例子,所以我在这里不对其展开介绍。(后面如果有时间,我可能会写一篇关于inf的介绍性文章)。读者要知道,安装驱动程序一定是需要inf文件的。

 

1.3 简单介绍下编译脚本

    既然要编译程序,就要涉及到编译器和其相关的工程文件。WDK使用Build程序来编译。Build程序要使用makefile和sources文件。用户没有必要从自己写一个makefile和sources,因为WDF例子里面就有很多现成脚本,可以用作参考,改改就能用。

 

    从KMDF的echo例子可以看到,makefile如果不是特别需要的话,就不需要改了。而Sources文件里面的几个地方需要改下。

TARGETNAME=echo            <---改为自己想要的驱动程序名称

INF_NAME=echo              <---改为自己想要的inf文件名称

SOURCES=driver.c  /        <---改为需要编译的c文件名

        device.c  /

        queue.c

 

1.4 Windows应用程序如何访问一个设备

   先了解windows设备树的概念。

  

    当系统启动起来的时候,系统(主要是PnP管理器)会搜集各个硬件的信息,然后生成一个设备树。在Windows的"设备管理器"里面可以看到整个设备树的组织情况。设备树立面的每一个节点都代表了一个设备,叫做设备节点或者devnode。WDF的device对象正是对应着这样的一个节点。每当有硬件加载或者卸载的时候,设备树上相应的节点就会添加或者删除。为了访问一个设备,可以用枚举的方法顺藤摸瓜,在设备树上找到该设备。也可以根据设备注册在系统中的ID直接找到设备。echoapp.exe使用的是后面的方法,代码如下。

    DevicePath = GetDevicePath((LPGUID)&GUID_DEVINTERFACE_ECHO);

    printf("DevicePath: %s/n", DevicePath);

    找到了设备在设备树上的路径,也就找到了设备。Windows下面对设备的操作被抽象为对文件的操作,所以在对设备进行操作之前,先要根据设备路径来创建设备文件对象。以后对设备文件的读写和控制,就代表了对设备的读写和控制。创建设备文件对象的代码如下。

    hDevice = CreateFile(DevicePath,

                         GENERIC_READ|GENERIC_WRITE,

                         FILE_SHARE_READ | FILE_SHARE_WRITE,

                         NULL,

                         OPEN_EXISTING,

                         0,

                         NULL );

    读写和控制,分别是用这三个API函数。ReadFile(), WriteFile(), DeviceIoControl()。当然,最后还要用CloseHandle()关闭设备文件对象。

   好奇的读者,还会进一步问,系统是怎么把这些读写命令传递给目标设备的?这正是WDF框架做的事情,我们下面正式开始讨论WDF框架和WDF驱动程序的开发。

 

第二部分 了解WDF框架并且开发WDF驱动程序

2.1 驱动程序的加载

    驱动程序什么时候被加载?加载时会做些什么?

    通常Windows启动的时候会加载驱动程序,这个时候会调用驱动程序里面的DriverEntry()函数。这是整个驱动程序内部最早运行的函数,它主要是做一些驱动程序运行的准备工作。其中最主要是调用了WdfDriverCreate()函数初始化了一个driver对象。

   WDF使用WDF_DRIVER_CONFIG结构来初始化driver对象。于是初始化好之后,driver对象就有了WDF_DRIVER_CONFIG预定义好的事件响应函数。WDF_DRIVER_CONFIG结构的定义如下:

typedef struct _WDF_DRIVER_CONFIG {

  ULONG  Size;

  PFN_WDF_DRIVER_DEVICE_ADD  EvtDriverDeviceAdd;

  PFN_WDF_DRIVER_UNLOAD  EvtDriverUnload;

  ULONG  DriverInitFlags;

  ULONG  DriverPoolTag;

} WDF_DRIVER_CONFIG, *PWDF_DRIVER_CONFIG;

注意第二个成员变量是一个EvtDriverDeviceAdd事件回调函数指针。当系统监测到有新硬件存在的时候,就会调用该硬件的驱动程序的WDF_DRIVER_CONFIG变量里面的EvtDriverDeviceAdd指针指向的函数。WdfDriverCreate()只初始化driver对象。而在EvtDriverDeviceAdd指向的事件回调函数里面,才是驱动程序的资源(队列、中断等)初始化的地方。这说明系统中有硬件了,才会分配资源;没有硬件,就不会分配相关资源。

    下面是示意代码。

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 = MyEvtDriverUnload;

    status = WdfDriverCreate(

                              DriverObject,

                              RegistryPath,

                              WDF_NO_OBJECT_ATTRIBUTES,

                              &config,        //这是WDF_DRIVER_CONFIG类型的变量

                              WDF_NO_HANDLE

                              );

    if (!NT_SUCCESS(status)) {

        TraceEvents(

                    TRACE_LEVEL_ERROR,

                    DBG_PNP,

                    "WdfDriverCreate failed with status %!STATUS!",

                    status

                    );

    }

    return status;

}

注意WdfDriverCreate()的第四个参数是一个WDF_DRIVER_CONFIG类型的变量config。config的第二个参数是一个EvtDriverDeviceAdd回调函数指针,具体指向的函数是MyEvtDeviceAdd()函数。一旦系统监测到相关硬件的时候,就会调用MyEvtDeviceAdd()函数。并且在MyEvtDeviceAdd()里面做各种具体初始化操作。

 

2.2 系统检测到新硬件的时候干些什么?

    在EvtDriverDeviceAdd指向的MyEvtDeviceAdd()具体要做哪些初始化工作呢?

    让我们简单回想下驱动程序的作用。驱动程序在系统中起到的是承上启下的部分,主要作用有两点。一是承上,为系统中上层的软件(驱动程序或者应用程序)提供API接口。上层软件调用API访问驱动和下面的硬件提供的功能。二是启下,管理底层的硬件,包括初始化、读写和控制等。为了提高访问效率,还会支持中断、DMA等方式。这个观点适用在所有操作系统中,不管是PC上的Linux、Windows, 还是嵌入式的uCos、FreeRTOS、VxWorks等的内部都这样。在有些不使用操作系统的嵌入式系统上也适用。与不使用操作系统的裸奔程序相比,操作系统会给用户提供很多机制和框架,比如内存管理、中断、与用户程序的API接口等。只要使用了操作系统提供的这些框架相关的代码,写出来的驱动程序就有很强的可移植性并且更可靠规范。而开发者付出的代价就是需要去学习操作系统的这些框架。

    综上,在操作系统上运行的驱动程序由三部分的内容组成:承上、启下、操作系统的框架代码。

    回到WDF来,可以看到MyEvtDeviceAdd()函数的初始化工作是利用了操作系统提供的框架代码,为承上启下这两个任务做准备的。主要任务有:

- 创建一个代表设备的对象。                      [注:框架代码]

- 创建I/O队列,用于设备的读写和控制。           [注:启下的框架代码]

- 创建应用程序或者其他驱动访问本驱动程序的接口。[注:承上的框架代码]

- 为支持Windows Management Instrumentation (WMI)做一些初始化工作。[注:承上的框架代码]

- 如果用到了中断,就创建中端对象。              [注:启下的框架代码]

- 如果用到了DMA,就创建DMA对象。                [注:启下的框架代码]

    简单总结下,就是创建设备对象,对下提供I/O队列和中断和DMA用于读写和控制硬件,对上提供应用程序和WMI接口。对于一个初学者,只要先掌握创建设备对象,创建I/O队列,就可以完成一个简单的驱动程序了。一旦建立起这些概念,可以举一返三地掌握其余的内容。

 

2.3 WDF中读写和控制设备

    按照前一节的说明,首先要创建设备对象。示意代码如下:

NTSTATUS

MyEvtDeviceAdd(

    IN WDFDRIVER  Driver,

    IN PWDFDEVICE_INIT  DeviceInit

    )

{

    WDF_PNPPOWER_EVENT_CALLBACKS  pnpPowerCallbacks;

    WDF_OBJECT_ATTRIBUTES  attributes;

    NTSTATUS  status;

    WDFDEVICE  device;

 

    // Initialize the WDF_PNPPOWER_EVENT_CALLBACKS structure.

    WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&pnpPowerCallbacks);

    pnpPowerCallbacks.EvtDevicePrepareHardware = MyEvtDevicePrepareHardware;

    pnpPowerCallbacks.EvtDeviceD0Entry = MyEvtDeviceD0Entry;

    pnpPowerCallbacks.EvtDeviceD0Exit  = MyEvtDeviceD0Exit;

    WdfDeviceInitSetPnpPowerEventCallbacks(

                                           DeviceInit,

                                           &pnpPowerCallbacks

                                           );

    // This driver uses buffered I/O.

    WdfDeviceInitSetIoType(

                           DeviceInit,

                           WdfDeviceIoBuffered

                           );

   

    // Specify the device object's context space by

    // using a driver-defined DEVICE_CONTEXT structure.

    WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(

                                            &attributes,

                                            DEVICE_CONTEXT

                                            );

    // Create the device object.

    status = WdfDeviceCreate(

                             &DeviceInit,

                             &attributes,

                             &device

                             );

    if (!NT_SUCCESS(status)) {

        return status;

    }

    // ... 下面的代码是创建队列,此处省略,下面讲。

}

    上面的代码先是设置好设备对象的各项初始化属性,然后调用WdfDeviceCreate()创建设备对象。其中的MyEvtDevicePrepareHardware()是设备硬件初始化函数,设备的硬件初始化的工作可以放在这里做。MyEvtDeviceD0Entry()和MyEvtDeviceD0Exit()分别是进入和退出D0状态时候的回调函数。

    当一个Windows应用程序使用ReadFile()试图读取来自设备的数据的时候,Windows系统的I/O管理器和WDF的机制相配合,为这一次读操作创建出一个reqeust对象,并且把这个request对象放到设备的驱动程序的I/O queue里面。很快,WDF框架又会调用queue的EvtIoRead()函数,把数据读给应用程序。对于设备的写操作和控制操作,即EvtIoWrite()和EvtIoDeviceControl()回调函数,其过程也是类似的。

    从ReadFile()到EvtIoRead()的这个流程实际上很简单,但是有时候驱动程序会创建多个queue,那选择哪个queue里面的EvtIoRead()呢?

    如果只创建了一个queue,那么用WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE把这个queue设为默认的queue, 所有来在上层的request对象都被会被WDF框架放到这个默认queue中,等待处理。

    如果创建了多个queue, 可以通过WdfDeviceConfigureRequestDispatching()函数来指定某个类型的queue处理某个类型的request。通过这个办法,WDF框架在接收到上层应用程序的request的时候,知道把这个request放到哪个queue里面去。

    下面看一下创建队列的示意代码。注意这段代码还是在MyEvtDeviceAdd()函数里面的,位于创建设备对象的代码的后面。

NTSTATUS

MyEvtDeviceAdd(

    IN WDFDRIVER  Driver,

    IN PWDFDEVICE_INIT  DeviceInit

    )

{

    // ... 前面是创建设备对象的代码。

    // 这是一个默认queue

    WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(

         &queueConfig,

        WdfIoQueueDispatchSequential

        );

    // 自定义了队列的读操作和写操作

    queueConfig.EvtIoRead   = EchoEvtIoRead;

    queueConfig.EvtIoWrite  = EchoEvtIoWrite;

    // Fill in a callback for destroy, and our QUEUE_CONTEXT size

    WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attributes, QUEUE_CONTEXT);

    attributes.EvtDestroyCallback = EchoEvtIoQueueContextDestroy;

    // 创建队列

    status = WdfIoQueueCreate(

        Device,

        &queueConfig,

        &attributes,

        &queue

        );

}

    最后来看下具体的读写操作,通常就是对内存或者寄存器的读写。以Echo这个例子里面的EvtIoRead()函数中的代码为例。

    // 得到request对象的memory

    Status = WdfRequestRetrieveOutputMemory(Request, &memory);

    if( !NT_SUCCESS(Status) ) {

        KdPrint(("EchoEvtIoRead Could not get request memory buffer 0x%x/n",Status));

        WdfVerifierDbgBreakPoint();

        WdfRequestCompleteWithInformation(Request, Status, 0L);

        return;

    }

    // 复制到输出memory

    Status = WdfMemoryCopyFromBuffer( memory, // destination

                             0,      // offset into the destination memory

                             queueContext->Buffer,

                             Length );

    if( !NT_SUCCESS(Status) ) {

        KdPrint(("EchoEvtIoRead: WdfMemoryCopyFromBuffer failed 0x%x/n", Status));

        WdfRequestComplete(Request, Status);

        return;

    // 指明传输已经结束

    WdfRequestSetInformation(Request, (ULONG_PTR)Length)

    上面的代码就是把设备里面的数据queueContext->Buffer拷贝给输出内存(这个输出是指输出给应用程序的)memory中。最后通知系统传输已经结束了。EvtIoWrite()和EvtIoDeviceControl()之类的回调函数还是类似的,读者可以再次举一返三。比如,USB bulk驱动的话,就可以在EvtIoRead()和EvtIoWrite()中间作bulk read和bulk write的操作。

    我们可以发现,WDF驱动的结构并不复杂。最开始只要关注WdfDriverCreate(),WdfDeviceCreate(), WdfIoQueueCreate(), EvtIoRead(), EvtIoWrite(), EvtIoDeviceControl()就可以了。后面有了实际的需要再往里面添加各个功能,比如为了加快速度采用中断/DMA, 为了同步而采用同步对象。

 

第三部分 后记

 

这篇文章本来是为我的USB设备开发驱动程序的时候整理的一些思路。WDK自带的WDF文档其实是最好的资料,不过太长了,不好好静下心来看上几天,估计是看不出门道的。希望我的这篇文章能过对WDF的初学者有帮助,能快速地让他们掌握WDF的全貌,快速地进入实际的项目开发中,而不是在工具的学习上花费太多时间。欢迎加我的QQ和我讨论。

你可能感兴趣的:(windows驱动开发)