第2章 开始内核开发

安装 Visual Studio 与 WDK,创建最小 WDM 驱动项目,实现 DriverEntry 与 DriverUnload,完成签名、部署、加载、卸载与调试跟踪的完整生命周期。

学习目标

  • 能安装配置 Visual Studio + WDK,创建并构建一个最小 WDM 驱动项目,产出 .sys 内核模块
  • 能改出 DriverEntry 中 DbgPrint 输出自定义字符串的效果,并在 DebugView 中观察到自己的日志
  • 能回答:为什么 64 位 Windows 加载未签名驱动会失败?整条生命周期中是哪一步出了问题?

为什么写内核驱动要先走通一整条流水线

写一个驱动,就像把一块钢锭送进工厂流水线,最后变成一台能用的机器再回收拆解。原料要按对图纸,铸造要控好温度,出厂要盖章,运到现场装好才能通电运行,用完还得按规程拆解回收。

这条流水线任何一环出岔,机器都跑不起来——或者更糟,跑起来后失控伤人。驱动和系统共用同一片电路,一处短路就可能让整台机器烧掉。

这一章带你从写第一行代码,走到把成品装进机器、再安全拆出来。把这条流水线完整走一遍,后面所有功能都是在这条线上加料。

驱动生命周期:七步流水线

(Windows Driver Model)是本章用的驱动模型。一个 WDM 驱动从写代码到卸载要依次走过七步:编写、构建、签名、部署、加载、运行、卸载。前四步在用户模式工具链里完成,后三步发生在内核里。

流水线的起点和终点是两个回调函数。 在驱动加载时被内核调用,负责创建设备对象、注册、设置派遣函数。DriverUnload 在卸载时被调用,负责删除设备、释放内存。这一进一出,定义了驱动的生命周期边界。

中间的“运行”阶段,驱动通过 (I/O Request Packet)接收工作。用户程序调用 DeviceIoControl 时,内核构造 IRP 派发给驱动的派遣函数,驱动处理完标记完成,请求才返回用户模式。“签名”这一步则是 64 位 Windows 的强制要求——开发期用 模式加载自签证书签名的驱动。下面的流水线图把七步串起来看。

动手:驱动生命周期流水线

下面的可视化把七步流水线画成一条可点击的管线。点任一阶段看详情;打开“注入故障”开关,高亮三个最常见的失败点:架构错配(构建)、未签名(签名)、忘记卸载(卸载)。

猜一猜:开启“注入故障”后,“签名”阶段会标红——未签名的驱动会在第 5 步加载时发生什么?动手看看,再对照下面的代码。

⚡ 驱动生命周期流水线
内核驱动生命周期:从代码到卸载点击任一阶段查看详情;开启"注入故障"高亮常见失败点1编写代码2编译构建3签名4部署5加载6运行7卸载
编写代码 · DriverEntry.c

编写 DriverEntry 与 DriverUnload 的 C 代码。DriverEntry 是驱动的入口函数(类似 main),接收 DriverObject 指针和注册表路径两个参数。在这里注册 Unload 回调、创建设备对象、设置派遣函数。这是整条生命周期的起点——代码错误会在后续阶段被放大。

代码对照:DriverEntry 与 DriverUnload

这一节给出最小可运行 WDM 驱动的完整代码。它做三件事:在 DriverEntry 里创建设备对象和符号链接、注册 Unload 和派遣函数;在 DriverUnload 里删除设备和符号链接;在 DispatchDefault 里完成所有 IRP。三个 CodeTabs 分别对应流水线的“加载入口”“卸载出口”“运行处理”。

段一:DriverEntry —— 加载时内核第一个调用的函数

DriverEntry 是内核调用驱动的地方。它先用 IoCreateDevice 建一个设备对象(驱动的“身份证”),再用 IoCreateSymbolicLink 给它起一个用户模式能访问的名字(\\.\HelloDriver)。然后把 DriverUnload 和 DispatchDefault 填进 DriverObject 的函数指针表。最后返回 STATUS_SUCCESS——返回任何错误码,驱动都不会被加载。

#include <ntddk.h>
 
#define DEVICE_NAME  L"\\Device\\HelloDriver"
#define SYMLINK_NAME L"\\??\\HelloDriver"
 
void DriverUnload(PDRIVER_OBJECT);
NTSTATUS DispatchDefault(PDEVICE_OBJECT, PIRP);
 
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(RegistryPath);
    DbgPrint("[HelloDriver] DriverEntry\n");
 
    UNICODE_STRING devName = RTL_CONSTANT_STRING(DEVICE_NAME);
    PDEVICE_OBJECT dev;
    NTSTATUS st = IoCreateDevice(DriverObject, 0, &devName,
        FILE_DEVICE_UNKNOWN, 0, FALSE, &dev);
    if (!NT_SUCCESS(st)) return st;
 
    UNICODE_STRING link = RTL_CONSTANT_STRING(SYMLINK_NAME);
    IoCreateSymbolicLink(&link, &devName);
 
    DriverObject->DriverUnload = DriverUnload;
    for (int i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++)
        DriverObject->MajorFunction[i] = DispatchDefault;
    return STATUS_SUCCESS;
}

段二:DriverUnload —— 卸载时必须与入口对称

DriverUnload 是 DriverEntry 的镜像:入口创建了什么,卸载就要删什么。这里删除符号链接和设备对象。如果忘了删除,这些内核对象会一直挂着,下次加载同名驱动会冲突。注意 DbgPrint 的输出可以在 DebugView 或 WinDbg 里看到——这是开发期跟踪驱动行为的主要手段。

// 复用 DriverEntry 中的 DEVICE_NAME / SYMLINK_NAME
void DriverUnload(PDRIVER_OBJECT DriverObject)
{
    DbgPrint("[HelloDriver] DriverUnload\n");
 
    UNICODE_STRING link = RTL_CONSTANT_STRING(SYMLINK_NAME);
    IoDeleteSymbolicLink(&link);
    IoDeleteDevice(DriverObject->DeviceObject);
}

段三:DispatchDefault —— 运行期处理 IRP

驱动加载成功后就驻留内核等待工作。每次用户模式调用 DeviceIoControl,内核构造一个 IRP,根据请求类型查 MajorFunction 表派发给对应函数。这里的 DispatchDefault 把所有请求都标记为成功并完成。真实驱动会按 IO_STACK_LOCATION 里的 IoControlCode 分发到不同的处理分支。

NTSTATUS DispatchDefault(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
 
    DbgPrint("[HelloDriver] IRP major=%u\n", stack->MajorFunction);
    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

容易踩的坑

小结

  • WDM 驱动生命周期七步:编写、构建、签名、部署、加载、运行、卸载
  • DriverEntry 是入口,注册 Unload 和派遣函数,返回 STATUS_SUCCESS 才加载成功
  • DriverUnload 必须与入口对称,删除设备和符号链接,否则资源泄漏
  • IRP 是驱动接收工作的单位,用户模式用 DeviceIoControl 触发
  • 64 位强制签名;开发期用测试签名模式加载自签驱动

练习

问题 1: 为什么 64 位 Windows 加载未签名驱动会失败?测试签名模式解决了其中的什么问题?

问题 2(独立实现题): 修改上面的 DriverEntry,在 DbgPrint 里输出你自己的名字(如 “Hello from <你的名字>”),并额外打印 RegistryPath 指向的注册表路径字符串内容。说明如何在 DebugView 中验证输出确实来自你的驱动。

提示:PUNICODE_STRING 的字符串内容在 RegistryPath->Buffer,长度(字节)在 RegistryPath->Length;可以用 %wZ 格式符直接打印 UNICODE_STRING。

问题 3: 如果你 sc stop 一个没有注册 DriverUnload 的驱动会发生什么?为什么?如何恢复?

知识点对照

2.1 安装工具

安装内核开发工具先装 Visual Studio 与 Windows SDK,再安装 WDK,最后在 VS 中勾选驱动程序开发负载。

2.2 创建一个驱动程序项目

创建一个驱动程序项目通常用 Visual Studio 的驱动程序模板,自动生成工程文件与 DriverEntry 骨架。

2.3 DriverEntry和Unload例程

DriverEntry和Unload例程是驱动的两个入口:前者在加载时初始化,后者在卸载时对称释放资源。

2.4 部署驱动程序

部署驱动程序先用 sc create 注册服务并指向 .sys 路径,再 sc start 启动并确认加载成功。

2.5 简单的跟踪

简单的跟踪用 DbgPrint 输出调试信息,配合 DebugView 或 WinDbg 即可查看内核日志。

2.7 总结

本章总结了从安装工具到部署第一个驱动的完整流程,为后续章节的深入学习打下坚实基础,这也是本章的核心要点。

术语表

名词解释

本章出现的专业名词,用大白话再讲一遍。

WDM

Windows Driver Model,Windows 驱动的经典模型。它规定了一个驱动从加载到卸载怎么组织代码:入口函数 DriverEntry、卸载回调 DriverUnload、以及处理 I/O 请求的派遣函数。可以理解为“写 Windows 驱动的一套老规则”,本章用它把生命周期看透。

DriverEntry

驱动的入口函数,类似 C 程序的 main。驱动被加载时内核第一个调用它,它接收 DriverObject 指针和注册表路径。在这里创建设备、注册卸载和派遣函数。返回 STATUS_SUCCESS 才算加载成功,返回错误码驱动就不会被加载。

DriverUnload

驱动卸载时内核调用的回调函数,是 DriverEntry 的镜像。入口创建了什么(设备、符号链接、回调、内存),卸载就要删什么。忘了实现或注册它,驱动将无法卸载,只能重启系统才能清掉。

IRP

I/O Request Packet,I/O 请求包。内核把用户请求(读、写、设备控制等)打包成的标准数据结构,派发给驱动的派遣函数处理。驱动处理完必须标记 IRP 完成,请求才能返回用户模式。可以理解为“内核递给驱动的一张工单”。

测试签名

开发期临时关闭 64 位 Windows 签名强制的模式。用 bcdedit /set testsigning on 开启并重启后,系统允许加载用自签证书签名的驱动。它只影响开发机,发布到生产环境仍必须用正式 EV 证书走 Microsoft 仪表板签名。

讨论

评论区加载中…