Windows WDF驱动开发简单helloworld入门

helloworld.h

/*
预处理:用来避免头文件被重复包含
还可以用#pragma once 防止头文件被重复包含,保证头文件只被编译一次,可移植性差

第一种:
    #ifndef __SOMEFILE_H__
    #define __SOMEFILE_H__
    ... ... // 声明、定义语句
    #endif
第二种:
    #pragma once
    ... ... // 声明、定义语句
*/
#ifndef __HELLOWORLD__ //预处理
#define __HELLOWORLD__ //预处理

// 包含驱动所需的头文件
#include <ntddk.h>
#include <wdf.h>

// 这是一个结构体的定义,用以描述驱动程序的设备拓展。它保存了我们自定义所需的一些信息,有助于更加方便的编程。
typedef struct _DEVICE_EXTENSION {
    PDEVICE_OBJECT DeviceObject;
    UNICODE_STRING DeviceName;
    UNICODE_STRING SymbolicLink;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

// 相关函数的声明,这些函数的具体实现存在于定义文件中
NTSTATUS DriverEntry(
    IN PDRIVER_OBJECT DriverObject,
    IN PUNICODE_STRING RegPath
);

VOID DriverUnload(
    IN PDRIVER_OBJECT DriverObject
);

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

#endif //预处理

helloworld.c

// 包含指定的声明文件。为每个定义文件写一个声明文件是一个不错的选择
#include "helloworld.h"

// 这些是预处理。在驱动开发中,需要指定每一个函数是分页内存还是非分页内存。
// INIT 标识是指定函数在驱动加载时使用,是初始化相关函数,驱动成功加载以后可以从内存卸载。
// PAGE 标识是指此函数在驱动运行时可以被交换到磁盘上,如果不指定,编译器默认是非分页内存。
// 一般情况下,我们不需要考虑这些问题,但有些特殊情况,代码是不予许被交换到磁盘上的,否则导致系统蓝屏或重启。
// 注:函数的声明必须在这些指定内存分配的预处理器之前,否则无法通过编译。
#pragma alloc_text(INIT,DriverEntry)
#pragma alloc_text(PAGE,DefaultDispatch)
#pragma alloc_text(PAGE,DriverUnload)

// 是DriverEntry函数的具体实现。DriverEntry是驱动程序的入口函数。有操作系统内核的I/O管理器调用。
NTSTATUS DriverEntry(
    IN PDRIVER_OBJECT Driver,
    IN PUNICODE_STRING RegPath
) 
{
    // 函数相关变量的定义
    // C语言中变量必须定义在函数体的开始处,否则出现编译错误。
    // C++ 语言没有这种限制
    NTSTATUS status;
    PDEVICE_OBJECT deviceObject;
    PDEVICE_EXTENSION deviceExtension;
    UNICODE_STRING symbolicLink;
    UNICODE_STRING deviceName;
    ULONG i;

    // KdPrint 和 DbgPrint是一个函数,KdPrint 是 DbgPrint的宏定义凡是,用以打印调试信息,好处在于
    // 调试版本编译时,KdPrint会打印调试信息,
    // 发布版本编译时,KdPrint将会被全部移除。
    KdPrint(("Enter HelloWorld DriverEntry\n"));

    // 一个宏,经常被用来指定参数未被引用,可以避免不必要的警告
    // 做到开发驱动程序不出警告是基础,因为驱动程序会导致系统出现各种各样的问题
    UNREFERENCED_PARAMETER(RegPath);

    // 对一个Unicode字符串进行初始化,Windows内核大量使用Unicode字符串,其具体操作有一系列函数(Rtl系列,微软推荐的运行时函数)
    RtlInitUnicodeString(&deviceName, L"\\Device\\helloworld");

    // 宏IRP_MJ_MAXIMUM_FUNCTION代表驱动程序最大的派遣函数指针数,这里使用默认的派遣函数初始化他们。然后紧跟着下面修改我们不打算使用默认的派遣函数指针。
    // 类似于定义数组,初始化数组
    // 派遣函数:又被称为回调函数。在驱动程序中这些派遣函数是我们主要工作重点
    for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++)
    {
        Driver->MajorFunction[i] = DefaultDispatch;
    }

    // 卸载函数,这个派遣函数需要单独提供,如果不打算对驱动程序进行卸载,这个函数可以不用提供。
    Driver->DriverUnload = DriverUnload;

    // 提供给操作系统的创建,关闭,读写的派遣函数,还有更多的派遣函数需要提供,这里为了简单明了用DefaultDispatch替换
    Driver->MajorFunction[IRP_MJ_CREATE] = DefaultDispatch;
    Driver->MajorFunction[IRP_MJ_CLOSE] = DefaultDispatch;
    Driver->MajorFunction[IRP_MJ_READ] = DefaultDispatch;
    Driver->MajorFunction[IRP_MJ_WRITE] = DefaultDispatch;

    // 使用IoCreateDevice函数创建一个设备对象,其名称为helloworld,其设备类型为FILE_DEVICE_UNKNOWN,是一种独占设备,在运行时,只能被一个程序所使用。
    status = IoCreateDevice(Driver, sizeof(DEVICE_EXTENSION), &deviceName, FILE_DEVICE_UNKNOWN, 0, TRUE, &deviceObject);

    // 判断设备是否创建成功,并进行必要的失败处理,这对于驱动程序的健壮性起着不容忽视的作用。
    if (!NT_SUCCESS(status)) {
        return status;
    }

    // 设备标识。有BUFFERED_IO 和 DO_BUFFERED_IO 两种,代表两种不同的缓冲区处理方式
    deviceObject->Flags = DO_BUFFERED_IO;

    // 初始化了一个Unicode字符串,同时也初始化了声明文件中定义过的设备拓展的结构体,设备拓展中保存了我们自定义所需的一些信息。
    deviceExtension = (PDEVICE_EXTENSION)deviceObject->DeviceExtension;
    deviceExtension->DeviceObject = deviceObject;
    deviceExtension->DeviceName = deviceName;
    RtlInitUnicodeString(&symbolicLink, L"\\??\\helloworld");
    deviceExtension->SymbolicLink = symbolicLink;

    //使用IoCreateSymbolicLink函数创建设备符号链接,这个符号链接名,主要用来与应用程序进行通信
    status = IoCreateSymbolicLink(&symbolicLink, &deviceName);
    //对创建结果进行必要的失败处理,如果创建失败,就删除已创建的设备对象
    if (!NT_SUCCESS(status)) {
        IoDeleteDevice(deviceObject);
        return status;
    }

    KdPrint(("End HelloWorld DriverEntry\n"));
    return status;
}

// DriverUnload函数的具体实现,功能是删除设备对象和设备符号链接,如果DriverEntry函数分配了其他资源,也要在这里释放
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject) {
    PDEVICE_OBJECT deviceObject;
    UNICODE_STRING linkName;
    KdPrint(("Enter HelloWorld DriverUnload\n"));

    // 由驱动对象指针得到设备对象指针
    deviceObject = DriverObject->DeviceObject;

    // 遍历所有已经穿件的设备对象和设备符号链接,并将其删除
    while (NULL != deviceObject)
    {
        PDEVICE_EXTENSION deviceExtension = (PDEVICE_EXTENSION)deviceObject->DeviceExtension;

        linkName = deviceExtension->SymbolicLink;
        IoDeleteSymbolicLink(&linkName);
        deviceObject = deviceObject->NextDevice;
        IoDetachDevice(deviceExtension->DeviceObject);
    }
    KdPrint(("End of HelloWorld DriverUnload"));
}

// DefaultDispatch函数的具体实现,功能是直接完成IRP(Input/Output Request Package,输入输出请求包)
NTSTATUS DefaultDispatch(
    IN PDEVICE_OBJECT DeviceObject,
    IN PIRP Irp
)
{
    NTSTATUS status;
    KdPrint(("Enter HelloWorld DefaultDispatch"));

    // 指定参数未被引用,避免不必要的警告
    UNREFERENCED_PARAMETER(DeviceObject);

    // 设置IRP的状态为成功
    status = STATUS_SUCCESS;

    // 因为打算直接完成IRP,所以操作信息的长度为空,这里将字节处理长度信息设置为0
    Irp->IoStatus.Status = status;
    Irp->IoStatus.Information = 0;

    // 使用IoCompleteRequest函数直接完成IRP
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    KdPrint(("End of HelloWorld DefaultDispatch\n"));

    return status;
}

驱动开发所需要的工具

Windbg:和VM配合实现双机联合调试,完成双机调试功能,可以结合《软件调试》这本书对Windbg有较为深入的认识。

DebugView: 可以捕获程序中由TRACE(debug版本)和OutputDebugString输出的信息。

InstDrv:安装驱动程序的软件,也可以自己编写。

DriverMonitor:用于查看驱动程序中的各种打印信息,帮助我们进行相应的信息显示与具体的设计。

DeviceTree:设备树,查看驱动对象和设备对象。

PcHunter(XueTr):较高权限的内核对象查看工具,相当于任务管理器的加强版。

WinObj:驱动对象和设备对象的普通查看工具



Windows驱动开发      Windows wdf驱动开发 WDF

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!