杰瑞科技汇

Windows驱动教程,从零开始怎么学?

Windows 驱动开发终极入门指南

目录

  1. 第一部分:基础认知
    • 什么是驱动程序?
    • 为什么驱动开发很难?
    • 驱动程序类型
    • 开发环境准备
  2. 第二部分:你的第一个驱动程序 - "Hello, World!"
    • 创建驱动项目
    • 编写驱动代码
    • 编译驱动
    • 安装和加载驱动
    • 查看输出
    • 卸载驱动
  3. 第三部分:深入理解 - 驱动的基本结构
    • DriverEntry - 驱动的入口点
    • AddDevice - 添加设备
    • IRP - I/O 请求包
    • Dispatch 函数 - 处理请求
  4. 第四部分:进阶主题与学习资源
    • WDF vs WDM
    • 调试技巧
    • 推荐书籍与文档
    • 社区与论坛

第一部分:基础认知

什么是驱动程序?

驱动程序是操作系统和硬件之间的“翻译官”或“中间人”,它是一段特殊的软件,操作系统通过它来控制和使用特定的硬件设备(如显卡、网卡、硬盘、USB设备等)。

Windows驱动教程,从零开始怎么学?-图1
(图片来源网络,侵删)
  • 用户模式:你平时运行的程序(如 Chrome, Word)都在这里,它们不能直接访问硬件,必须通过系统调用(如 CreateFile, DeviceIoControl)请求内核模式的驱动程序来帮忙。
  • 内核模式:驱动程序运行在这里,它拥有对系统所有资源的最高访问权限,可以直接操作硬件内存和I/O端口。

为什么驱动开发很难?

  • 高权限风险:内核模式的任何一个小错误(如空指针解引用、非法内存访问)都可能导致整个系统崩溃,也就是著名的 “蓝屏死机” (BSOD - Blue Screen of Death)
  • 复杂的API:Windows驱动开发框架提供了大量复杂的函数和数据结构,学习曲线陡峭。
  • 调试困难:你无法像调试普通应用那样直接在IDE里运行和断点调试,你需要使用专门的内核调试器(如 WinDbg),并且通常需要两台机器(一台宿主机调试,一台目标机运行驱动)。
  • 稳定性要求高:驱动程序必须极其稳定,因为它的崩溃会影响整个系统的稳定性和数据安全。

驱动程序类型

  • WDM (Windows Driver Model):较传统的模型,直接处理IRP,比较复杂,但功能强大,现在主要用于一些老旧硬件的驱动。
  • WDF (Windows Driver Framework):微软推出的新一代驱动开发框架,旨在简化驱动开发,它分为两种:
    • KMDF (Kernel-Mode Driver Framework):用于编写内核模式驱动。这是目前的主流和推荐方式,特别是对于新的驱动开发。
    • UMDF (User-Mode Driver Framework):用于编写用户模式驱动,它更安全,崩溃不会导致系统蓝屏,但性能稍差,功能受限,适用于一些非核心的设备,如USB摄像头、打印机等。

开发环境准备

这是开始前最重要的一步。

  1. 操作系统

    • 必须使用 Windows 10/11 专业版/企业版/教育版,家庭版不支持内核调试和驱动签名。
    • 建议在虚拟机(如 VMware, Hyper-V)中进行开发,避免频繁蓝屏损坏你的主系统。
  2. 开发工具

    • Visual Studio:推荐使用 Visual Studio 2025,在安装时,请务必勾选 “使用C++的桌面开发” 工作负载。
    • Windows Driver Kit (WDK):这是驱动开发的SDK,包含了头文件、库、工具和示例。WDK 10 或更新的版本与 Visual Studio 深度集成,安装后会自动配置好开发环境。
  3. 调试环境(强烈推荐)

    Windows驱动教程,从零开始怎么学?-图2
    (图片来源网络,侵删)
    • 两台电脑:一台作为 宿主机,安装VS和WDK;另一台作为 目标机,用于运行和测试驱动。
    • 网络连接:两台机子用网线连接到同一个路由器。
    • 内核调试器:在目标机上启用“测试签名模式”,并配置内核调试(如通过TCP网络)。
    • WinDbg Preview:从Microsoft Store安装,这是强大的内核调试器。

第二部分:你的第一个驱动程序 - "Hello, World!"

我们将创建一个最简单的内核模式驱动,它在加载时打印一条信息到调试输出窗口。

创建驱动项目

  1. 打开 Visual Studio 2025。
  2. 选择 “创建新项目” (Create a new project)
  3. 在模板搜索框中输入 Driver,然后选择 "Kernel Mode Driver (WDM)" 模板,点击“下一步”。
  4. 输入项目名称(如 MyFirstDriver),选择位置,点击“创建”。
  5. 在弹出的窗口中,选择 "Empty (WDM)",然后点击“确定”。

VS会为你生成一个基本的项目结构,包含一个 .c 文件和 .inf 文件。

编写驱动代码

打开 MyFirstDriver.c 文件,你会看到一个名为 DriverEntry 的函数,这是驱动的入口点,类似于C程序中的 main 函数。

修改代码如下:

Windows驱动教程,从零开始怎么学?-图3
(图片来源网络,侵删)
#include <ntddk.h>
// 驱动卸载函数
VOID DriverUnload(PDRIVER_OBJECT pDriverObject) {
    KdPrint(("MyFirstDriver: Driver is unloading.\n"));
}
// 驱动入口函数
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath) {
    UNREFERENCED_PARAMETER(pRegistryPath); // 告诉编译器这个参数我们暂时不用,避免警告
    // 设置卸载函数
    pDriverObject->DriverUnload = DriverUnload;
    KdPrint(("MyFirstDriver: Driver is loaded successfully!\n"));
    return STATUS_SUCCESS;
}

代码解释:

  • #include <ntddk.h>:包含了所有内核模式驱动开发所需的核心定义。
  • DriverEntry:系统加载驱动时调用的第一个函数。
    • pDriverObject:指向驱动对象的结构体指针,我们可以用它来设置驱动的各种属性。
    • pRegistryPath:指向驱动注册表路径的字符串,通常用于读取配置。
  • pDriverObject->DriverUnload = DriverUnload;:告诉系统,当需要卸载这个驱动时,调用 DriverUnload 函数。这是一个好习惯,必须提供卸载函数,否则驱动将无法卸载。
  • DriverUnload:驱动卸载时被调用,我们打印一条卸载信息。
  • KdPrint:这是内核模式下向调试器输出信息的标准宏,当连接了WinDbg时,你可以在它的输出窗口看到这些信息。在发布版本中,KdPrint 默认是无效的。
  • return STATUS_SUCCESS;:告诉系统驱动加载成功,如果返回任何错误码,系统都会加载失败。

编译驱动

F7 或点击 “生成” -> “生成解决方案”,如果一切顺利,你会在项目的 x64\Debug 目录下找到 MyFirstDriver.sys 文件,这就是你的驱动程序。

安装和加载驱动

由于我们没有微软的数字签名,Windows会阻止加载未签名的驱动,我们需要进入 测试模式

  1. 在目标机上:以管理员身份打开 命令提示符PowerShell
  2. 输入以下命令并回车:
    bcdedit /set testsigning on
  3. 重启目标机,重启后,你会在屏幕右下角看到“测试模式”的水印。
  4. 安装驱动
    • 在目标机上,找到 MyFirstDriver.inf 文件(在项目目录下)。
    • 右键点击它,选择 “安装”
    • 系统可能会弹出安全警告,点击“安装”即可。

查看输出

现在驱动已经加载了,但我们怎么看到 KdPrint 的输出呢?答案是使用 WinDbg

  1. 在宿主机上:打开 WinDbg Preview。
  2. 点击 "Debug" -> "Open Executable..."
  3. 在弹出的窗口中,选择 "Kernel Debug" 选项卡。
  4. 在 "Transport" 下拉框中选择 "Network"
  5. 在 "Host name" 输入目标机的 IP地址
  6. 点击 "OK"。
  7. 如果配置正确,WinDbg 会连接到目标机,并显示大量内核信息,在 WinDbg 的命令窗口中输入 !analyze -v 并回车,然后再次输入 g 并回车,让系统继续运行。
  8. 回到步骤4,重新安装一次驱动(或者重启目标机),你会看到 WinDbg 的输出窗口中出现了我们的信息:
    MyFirstDriver: Driver is loaded successfully!

卸载驱动

  1. 以管理员身份打开 设备管理器
  2. 点击 “查看” -> “显示隐藏的设备”。
  3. 展开 “非即插即用驱动程序”。
  4. 找到你的驱动(可能叫 MyFirstDriver 或类似名称),右键点击它,选择 “卸载设备”
  5. 在目标机命令提示符中输入 sc stop MyFirstDriver (如果服务名不同,请用 sc query 查找)。
  6. 重启目标机,回到测试模式:bcdedit /set testsigning off

第三部分:深入理解 - 驱动的基本结构

一个真正的驱动需要处理来自应用程序的请求,这个过程的核心是 IRP (I/O Request Packet)

DriverEntry - 驱动的入口点

我们已经知道了它的作用,它主要做三件事:

  1. 创建设备:创建一个设备对象,应用程序可以通过这个对象来访问驱动。
  2. 创建符号链接:将设备对象映射到 \Device\MyDriver 这样的名字,并创建一个 \??\MyDriver 的符号链接,让用户程序可以通过这个名字打开设备。
  3. 设置派遣函数:告诉系统,当应用程序对设备进行读、写、控制等操作时,应该调用我们驱动的哪个函数来处理。

AddDevice - 添加设备

对于即插即用设备,当硬件被检测到时,PnP管理器会调用 DriverEntry 中设置的 DriverObject->DriverExtension->AddDevice 指向的函数,在这个函数里,你通常会创建设备对象。

IRP - I/O 请求包

当你的应用程序调用 CreateFile, ReadFile, WriteFile, DeviceIoControl 时,操作系统会创建一个IRP结构,并将其发送到你的驱动,IRP包含了请求的所有信息:是读、写还是控制操作?从哪里读?写到哪里?数据是什么?

Dispatch 函数 - 处理请求

DriverEntry 中最重要的任务之一就是设置 DriverObject 中的 MajorFunction 数组。

// 在 DriverEntry 中添加
pDriverObject->MajorFunction[IRP_MJ_CREATE] = MyCreateClose; // 处理 CreateFile
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = MyCreateClose; // 处理 CloseHandle
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyDeviceControl; // 处理 DeviceIoControl
pDriverObject->MajorFunction[IRP_MJ_READ] = MyRead; // 处理 ReadFile
pDriverObject->MajorFunction[IRP_MJ_WRITE] = MyWrite; // 处理 WriteFile

每个 MajorFunction 都指向一个你编写的函数(如 MyDeviceControl),这个函数接收一个 IRP 指针,你需要分析它,完成相应的操作,然后标记IRP为完成并返回状态码。

示例:一个简单的 DeviceIoControl 处理函数

NTSTATUS MyDeviceControl(PDEVICE_OBJECT pDeviceObject, PIRP pIrp) {
    UNREFERENCED_PARAMETER(pDeviceObject);
    // 获取IO_STACK_LOCATION结构,它包含了控制码和输入输出缓冲区信息
    PIO_STACK_LOCATION pStack = IoGetCurrentIrpStackLocation(pIrp);
    ULONG ControlCode = pStack->Parameters.DeviceIoControl.IoControlCode;
    PVOID pInputBuffer = pIrp->AssociatedIrp.SystemBuffer;
    PVOID pOutputBuffer = pIrp->AssociatedIrp.SystemBuffer;
    ULONG InputBufferLength = pStack->Parameters.DeviceIoControl.InputBufferLength;
    ULONG OutputBufferLength = pStack->Parameters.DeviceIoControl.OutputBufferLength;
    NTSTATUS status = STATUS_SUCCESS;
    ULONG bytesReturned = 0;
    switch (ControlCode) {
        case IOCTL_MY_IOCTL_GET_DATA: {
            // 示例:向应用程序返回一个字符串
            const char* dataToSend = "Hello from Kernel Driver!";
            ULONG dataLen = (ULONG)strlen(dataToSend) + 1; // +1 for null terminator
            if (OutputBufferLength < dataLen) {
                status = STATUS_BUFFER_TOO_SMALL;
                break;
            }
            RtlCopyMemory(pOutputBuffer, dataToSend, dataLen);
            bytesReturned = dataLen;
            break;
        }
        default:
            status = STATUS_INVALID_DEVICE_REQUEST;
            break;
    }
    // 完成IRP
    pIrp->IoStatus.Status = status;
    pIrp->IoStatus.Information = bytesReturned; // 返回给调用者的字节数
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return status;
}

第四部分:进阶主题与学习资源

WDF vs WDM

  • WDM:是“裸”的API,直接与操作系统内核交互,你需要手动管理内存、IRP、同步等,非常灵活,但非常复杂,容易出错。
  • KMDF:是WDM的封装,它帮你处理了大量的底层细节,
    • 自动处理IRP的完成。
    • 提供了内存管理、同步(锁)等高级抽象。
    • 提供了PnP和电源管理的事件回调,而不是让你手动处理。
    • 除非你有特殊需求(如维护旧驱动),否则请始终选择KMDF。 它能让你更专注于业务逻辑,而不是底层陷阱。

调试技巧

  • WinDbg 是你的好朋友:学会使用 !analyze -v 分析蓝屏转储,使用 k 查看调用栈,使用 dt 查看结构体内容。
  • ETW (Event Tracing for Windows):比 KdPrint 更现代、性能更好的追踪框架,可以在不连接调试器的情况下收集内核日志。
  • Driver Verifier:Windows自带的一个工具,可以对你的驱动进行各种压力测试,发现内存泄漏、同步问题等,强烈建议在开发时始终开启。

推荐书籍与文档

  • 官方文档 (最重要)
  • 经典书籍
    • 《Windows Internals》:理解Windows工作原理的“圣经”,虽然不专门讲驱动,但对理解驱动运行的底层环境至关重要。
    • 《Writing WDF Drivers》:微软官方的WDF教程,非常系统。
    • 《Windows NT Device Driver Development》:一本经典的WDM驱动书籍,虽然稍老,但原理讲得非常透彻。
  • 在线资源
    • OSR Online:一个老牌的Windows驱动开发社区,有大量文章、论坛和工具。
    • GitHub:寻找开源的驱动项目,学习别人的代码是进步的捷径。

社区与论坛

  • Microsoft Q&A:微软官方的问答社区,有微软工程师和专家回答问题。
  • OSR Online Forums:非常活跃的驱动开发社区,老司机很多。

Windows驱动开发是一条充满挑战但回报丰厚的道路,它要求你具备扎实的C语言功底、深入的系统知识以及严谨的编程态度。

从今天开始,不要害怕,动手去写你的第一个驱动,然后尝试让它与应用程序通信,遇到蓝屏,不要沮丧,那是系统在告诉你哪里出了问题,学会使用调试器,分析每一个崩溃。

祝你学习顺利!

分享:
扫描分享到社交APP
上一篇
下一篇