驱动基本结构:DriverEntry、DriverUnload、IRP处理、驱动对象与设备对象

说实话,很多初学者一上来就被Windows驱动的各种概念搞懵了。什么驱动对象、设备对象、IRP……听着就头大。我当年刚接触驱动开发时也是这样,翻了好几天的文档才理清楚这些玩意儿到底是怎么串在一起的。

今天咱们就把驱动的基本结构拆开来看。说白了,一个最简单的Windows驱动,核心就四个东西:DriverEntry(入口)、DriverUnload(卸载)、IRP处理(干活)、驱动对象与设备对象(数据结构)。

1. DriverEntry——驱动的“出生证明”

每个驱动都必须有一个DriverEntry函数。这是驱动的入口点,就像main函数之于普通C程序。系统加载驱动时,第一个调用的就是它。

它的原型长这样:

NTSTATUS DriverEntry(
    _In_ PDRIVER_OBJECT  DriverObject,
    _In_ PUNICODE_STRING RegistryPath
);

两个参数:DriverObject是系统为我们创建好的驱动对象指针,RegistryPath指向注册表中该驱动的配置路径。嗯,这里要注意——RegistryPath指向的内存是临时分配的,如果你后面还要用,得自己拷贝一份。

核心任务:DriverEntry里主要干三件事——

  • 注册卸载例程(DriverUnload)
  • 注册IRP分发函数(MajorFunction数组)
  • 创建设备对象(如果需要)

我个人习惯在DriverEntry开头先打印个调试信息,确认驱动确实被加载了。你想想看,驱动加载失败时连个提示都没有,排查起来多痛苦。

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    DbgPrint("[MyDriver] DriverEntry called\n");

    // 注册卸载例程
    DriverObject->DriverUnload = MyDriverUnload;

    // 注册IRP分发函数
    DriverObject->MajorFunction[IRP_MJ_CREATE] = MyDriverCreateClose;
    DriverObject->MajorFunction[IRP_MJ_CLOSE]  = MyDriverCreateClose;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyDriverDeviceControl;

    // 创建设备对象
    NTSTATUS status = CreateDevice(DriverObject);
    if (!NT_SUCCESS(status))
    {
        DbgPrint("[MyDriver] CreateDevice failed: 0x%X\n", status);
        return status;
    }

    return STATUS_SUCCESS;
}

2. DriverUnload——体面地“退场”

驱动卸载时,系统会调用DriverUnload。这里要做的事情正好和DriverEntry相反:释放资源、删除设备对象、清理分配的内存。

我曾经见过一个同事写的驱动,卸载时忘了删除设备对象,结果系统蓝屏了。排查了半天才发现是设备对象残留导致的。

VOID MyDriverUnload(PDRIVER_OBJECT DriverObject)
{
    DbgPrint("[MyDriver] DriverUnload called\n");

    PDEVICE_OBJECT deviceObj = DriverObject->DeviceObject;
    while (deviceObj)
    {
        PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)deviceObj->DeviceExtension;
        if (devExt && devExt->SymbolicLink.Buffer)
        {
            IoDeleteSymbolicLink(&devExt->SymbolicLink);
        }

        PDEVICE_OBJECT nextDevice = deviceObj->NextDevice;
        IoDeleteDevice(deviceObj);
        deviceObj = nextDevice;
    }
}

注意:删除设备对象时,一定要先删除符号链接,再删设备对象。顺序反了会导致符号链接残留,下次加载驱动时创建设备会失败。

3. IRP处理——驱动真正的“工作”

IRP(I/O Request Packet)是Windows驱动中最重要的概念之一。应用程序调用ReadFile、WriteFile、DeviceIoControl时,系统会构造一个IRP,然后发给对应的驱动去处理。

说白了,IRP就是系统与驱动之间的“快递单”。快递单上写着:谁发的、要干什么、数据在哪、结果如何。

IRP处理函数的基本结构:

NTSTATUS MyDriverDeviceControl(
    PDEVICE_OBJECT DeviceObject,
    PIRP Irp
)
{
    PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp);
    ULONG ioctlCode = irpStack->Parameters.DeviceIoControl.IoControlCode;

    DbgPrint("[MyDriver] IOCTL code: 0x%X\n", ioctlCode);

    NTSTATUS status = STATUS_SUCCESS;
    ULONG info = 0;

    switch (ioctlCode)
    {
    case IOCTL_MY_READ:
        // 处理读请求
        status = HandleRead(Irp, irpStack, &info);
        break;

    case IOCTL_MY_WRITE:
        // 处理写请求
        status = HandleWrite(Irp, irpStack, &info);
        break;

    default:
        status = STATUS_INVALID_DEVICE_REQUEST;
        break;
    }

    Irp->IoStatus.Status = status;
    Irp->IoStatus.Information = info;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return status;
}

小技巧:每个IRP处理函数最后一定要调用IoCompleteRequest,否则应用程序会一直挂起等待。我刚开始写驱动时就犯过这个错,调试了一下午才发现是忘了调用这个函数。

4. 驱动对象与设备对象——两个核心数据结构

驱动对象(DRIVER_OBJECT)和设备对象(DEVICE_OBJECT)是驱动开发中最基础的两个结构体。它们之间的关系,我用一张图来说明:

驱动对象与设备对象关系图 DRIVER_OBJECT DriverUnload MajorFunction[0..28] DriverExtension DeviceObject (指向第一个设备对象) NextDevice DEVICE_OBJECT DriverObject DeviceExtension NextDevice (指向下一个设备对象) NextDevice DEVICE_OBJECT DriverObject DeviceExtension NextDevice = NULL 设备扩展 (DeviceExtension) SymbolicLink 自定义数据 缓冲区指针等 驱动对象 设备对象 设备扩展

从图中可以看到:

  • 一个驱动对象可以对应多个设备对象(通过NextDevice链表串联)
  • 每个设备对象都有自己的设备扩展(DeviceExtension),用来存放私有数据
  • 设备对象通过DriverObject指针回指到驱动对象

创建设备对象的代码通常长这样:

NTSTATUS CreateDevice(PDRIVER_OBJECT DriverObject)
{
    PDEVICE_OBJECT deviceObj = NULL;
    UNICODE_STRING deviceName;
    UNICODE_STRING symbolicLink;

    // 设备名称:\Device\MyDriver
    RtlInitUnicodeString(&deviceName, L"\\Device\\MyDriver");

    // 创建设备对象,指定设备扩展大小
    NTSTATUS status = IoCreateDevice(
        DriverObject,
        sizeof(DEVICE_EXTENSION),  // 设备扩展大小
        &deviceName,
        FILE_DEVICE_UNKNOWN,
        0,
        FALSE,
        &deviceObj
    );

    if (!NT_SUCCESS(status))
    {
        return status;
    }

    // 初始化设备扩展
    PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)deviceObj->DeviceExtension;
    RtlInitUnicodeString(&devExt->SymbolicLink, L"\\DosDevices\\MyDriver");

    // 创建符号链接,让应用程序能找到设备
    status = IoCreateSymbolicLink(&devExt->SymbolicLink, &deviceName);
    if (!NT_SUCCESS(status))
    {
        IoDeleteDevice(deviceObj);
        return status;
    }

    // 设置设备对象的标志(可选)
    deviceObj->Flags |= DO_BUFFERED_IO;

    return STATUS_SUCCESS;
}

关键点总结:

  • DriverEntry:驱动的入口,注册卸载例程和IRP分发函数,创建设备对象
  • DriverUnload:驱动的出口,清理资源,删除设备对象和符号链接
  • IRP处理:驱动的核心工作,处理来自应用程序的I/O请求
  • 驱动对象:代表驱动本身,包含函数指针表和设备对象链表
  • 设备对象:代表一个虚拟或物理设备,应用程序通过它来与驱动通信

嗯,到这里驱动的基本结构就讲完了。这些概念是后续所有驱动开发的基础,你想想看——不管是写简单的HelloWorld驱动,还是复杂的硬件驱动,都离不开这四个核心要素。我在实际项目中见过太多因为没理解清楚这些基础概念而踩坑的案例了。

避坑指南:我曾经在项目里遇到过一个诡异的问题——驱动加载成功了,但应用程序就是打不开设备。查了半天才发现,原来是DriverEntry里忘了调用IoCreateDevice创建设备对象。没有设备对象,应用程序当然找不到设备。所以,写完DriverEntry后,一定要检查设备对象是否成功创建。


公众号:蓝海资料掘金营,微信deep3321