教程:编写 Hello World Windows 驱动程序(Kernel-Mode 驱动程序框架)
本文介绍如何使用 Kernel-Mode 驱动程序框架(KMDF)编写小型 通用 Windows 驱动程序,然后在单独的计算机上部署和安装驱动程序。
先决条件
按照步骤安装 Windows 驱动程序工具包(WDK)。 安装 WDK 时,需要包括 Windows 调试工具。
安装 Visual Studio 2022。 安装 Visual Studio 2022 时,选择使用 C++ 进行桌面开发工作负荷,然后在单独组件下添加:
- MSVC v143 - VS 2022 C++ ARM64/ARM64EC Spectre 缓解库(最新版本)
- MSVC v143 - VS 2022 C++ x64/x86 Spectre 缓解库(最新版本)
- 带有 Spectre 缓解库的适用于最新 v143 生成工具的 C++ ATL (ARM64/ARM64EC)
- 带有 Spectre 缓解库的适用于最新 v143 生成工具的 C++ ATL (x86 & x64)
- 带有 Spectre 缓解库的适用于最新 v143 生成工具的 C++ MFC (ARM64/ARM64EC)
- 带有 Spectre 缓解库的适用于最新 v143 生成工具的 C++ MFC (x86 & x64)
- Windows 驱动程序工具包
创建和生成驱动程序
打开 Microsoft Visual Studio。 在“文件”菜单上,选择新建 > 项目。
在“创建新项目 对话框中,选择左侧下拉列表中的 C++,在中间下拉列表中选择 Windows,然后选择右侧下拉列表中的 驱动程序。
从项目类型列表中选择“内核模式驱动程序,空(KMDF)”。 选择下一步。
提示
如果在 Visual Studio 中找不到驱动程序项目模板,WDK Visual Studio 扩展未正确安装。 若要解决此问题,请启动 Visual Studio Installer,选择 “修改”,在 “单个组件”选项卡中添加 Windows 驱动程序工具包,然后选择 “修改”。
在 配置新项目 对话框中,在 项目名称 字段中输入“KmdfHelloWorld”。
注意
创建新的 KMDF 或 UMDF 驱动程序时,必须选择包含 32 个字符或更少字符的驱动程序名称。 此长度限制在 wdfglobals.h 中定义。
在 位置 字段中,输入要创建新项目的目录。
选中“将解决方案和项目置于同一目录中”,然后选择“创建”。
Visual Studio 创建一个项目和一个解决方案。 可以在 解决方案资源管理器 窗口中看到它们。 (如果“解决方案资源管理器”窗口不可见,请从 视图 菜单中选择 解决方案资源管理器。该解决方案有一个名为 KmdfHelloWorld 的驱动程序项目。
在 解决方案资源管理器 窗口中,选择并按住(或右键单击)KmdfHelloWorld 解决方案,然后选择 配置管理器。 为驱动程序项目选择配置和平台。 例如,选择“调试”和“x64”。
在解决方案资源管理器窗口中,再次选择并按住(或右键选择) KmdfHelloWorld 项目,选择添加,然后选择新项。
在“添加新项 对话框中,选择 C++文件。 对于“名称”,请输入“Driver.c”。
注意
文件扩展名 .c,而不是 .cpp。
选择“” 添加 “”。 Driver.c 文件在 源文件下添加,如下所示。
编写第一个驱动程序代码
创建空的 Hello World 项目并添加了 Driver.c 源文件后,你将通过实现两个基本事件回调函数编写驱动程序运行所需的最基本的代码。
在 Driver.c 中,首先包括以下头文件:
#include <ntddk.h> #include <wdf.h>
提示
如果无法添加
Ntddk.h
,请打开“配置”->“C/C++”->“常规”->“其他包含目录”并添加C:\Program Files (x86)\Windows Kits\10\Include\<build#>\km
,将<build#>
替换为 WDK 安装中的相应目录。Ntddk.h 包含所有驱动程序的核心 Windows 内核定义,Wdf.h 包含基于 Windows 驱动程序框架(WDF)的驱动程序定义。
接下来,为要使用的两个回调提供声明:
DRIVER_INITIALIZE DriverEntry; EVT_WDF_DRIVER_DEVICE_ADD KmdfHelloWorldEvtDeviceAdd;
使用以下代码编写 DriverEntry:
NTSTATUS DriverEntry( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath ) { // NTSTATUS variable to record success or failure NTSTATUS status = STATUS_SUCCESS; // Allocate the driver configuration object WDF_DRIVER_CONFIG config; // Print "Hello World" for DriverEntry KdPrintEx(( DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "KmdfHelloWorld: DriverEntry\n" )); // Initialize the driver configuration object to register the // entry point for the EvtDeviceAdd callback, KmdfHelloWorldEvtDeviceAdd WDF_DRIVER_CONFIG_INIT(&config, KmdfHelloWorldEvtDeviceAdd ); // Finally, create the driver object status = WdfDriverCreate(DriverObject, RegistryPath, WDF_NO_OBJECT_ATTRIBUTES, &config, WDF_NO_HANDLE ); return status; }
DriverEntry 是所有驱动程序的入口点,如
Main()
适用于许多用户模式应用程序。 DriverEntry 的工作是初始化驱动程序范围的结构和资源。 在此示例中,你在 DriverEntry中打印了“Hello World”,将驱动程序对象配置为注册 EvtDeviceAdd 回调的入口点,接着创建驱动程序对象并返回。驱动程序对象充当你可能在驱动程序中创建的所有其他框架对象的父对象,其中包括设备对象、I/O 队列、计时器、旋转锁等。 有关框架对象的详细信息,请参阅 框架对象简介。
提示
对于 DriverEntry,强烈建议将名称保留为“DriverEntry”,以帮助进行代码分析和调试。
接下来,使用以下代码编写 KmdfHelloWorldEvtDeviceAdd:
NTSTATUS KmdfHelloWorldEvtDeviceAdd( _In_ WDFDRIVER Driver, _Inout_ PWDFDEVICE_INIT DeviceInit ) { // We're not using the driver object, // so we need to mark it as unreferenced UNREFERENCED_PARAMETER(Driver); NTSTATUS status; // Allocate the device object WDFDEVICE hDevice; // Print "Hello World" KdPrintEx(( DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "KmdfHelloWorld: KmdfHelloWorldEvtDeviceAdd\n" )); // Create the device object status = WdfDeviceCreate(&DeviceInit, WDF_NO_OBJECT_ATTRIBUTES, &hDevice ); return status; }
当系统检测到设备到达时,系统会调用 EvtDeviceAdd。 其作业是初始化该设备的结构和资源。 在此示例中,你只需输出 EvtDeviceAdd的“Hello World”消息,创建了设备对象并返回。 在其他编写驱动程序中,你可以为硬件创建 I/O 队列、为设备特定信息设置 设备上下文 存储空间,或者执行准备设备所需的其他任务。
提示
对于设备添加回调,请注意以驱动程序名称为前缀对回调命名的方式 (KmdfHelloWorldEvtDeviceAdd)。 通常,我们建议以这种方式命名驱动程序的函数,以将它们与其他驱动程序的函数区分开来。 DriverEntry 是完全应该这样命名的唯一一项。
您的完整 Driver.c 现在如下所示:
#include <ntddk.h> #include <wdf.h> DRIVER_INITIALIZE DriverEntry; EVT_WDF_DRIVER_DEVICE_ADD KmdfHelloWorldEvtDeviceAdd; NTSTATUS DriverEntry( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath ) { // NTSTATUS variable to record success or failure NTSTATUS status = STATUS_SUCCESS; // Allocate the driver configuration object WDF_DRIVER_CONFIG config; // Print "Hello World" for DriverEntry KdPrintEx(( DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "KmdfHelloWorld: DriverEntry\n" )); // Initialize the driver configuration object to register the // entry point for the EvtDeviceAdd callback, KmdfHelloWorldEvtDeviceAdd WDF_DRIVER_CONFIG_INIT(&config, KmdfHelloWorldEvtDeviceAdd ); // Finally, create the driver object status = WdfDriverCreate(DriverObject, RegistryPath, WDF_NO_OBJECT_ATTRIBUTES, &config, WDF_NO_HANDLE ); return status; } NTSTATUS KmdfHelloWorldEvtDeviceAdd( _In_ WDFDRIVER Driver, _Inout_ PWDFDEVICE_INIT DeviceInit ) { // We're not using the driver object, // so we need to mark it as unreferenced UNREFERENCED_PARAMETER(Driver); NTSTATUS status; // Allocate the device object WDFDEVICE hDevice; // Print "Hello World" KdPrintEx(( DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "KmdfHelloWorld: KmdfHelloWorldEvtDeviceAdd\n" )); // Create the device object status = WdfDeviceCreate(&DeviceInit, WDF_NO_OBJECT_ATTRIBUTES, &hDevice ); return status; }
保存 Driver.c.
此示例演示了驱动程序的基本概念:它们是一个“回调集合”,一旦初始化,就处于待命状态并等待系统在需要时调用。 系统调用可以是新的设备到达事件、用户模式应用程序的 I/O 请求、系统电源关闭事件、来自另一个驱动程序的请求,或者在用户意外拔出设备时意外删除事件。 幸运的是,就“Hello World”而言,只需操心驱动程序和设备的创建。
接下来,你将生成驱动程序。
生成驱动程序
在解决方案资源管理器窗口中,选择并按住(或右键选择)解决方案“KmdfHelloWorld”(1 个项目),然后选择配置管理器。 为驱动程序项目选择配置和平台。 对于本练习,请选择调试和 x64。
在 解决方案资源管理器 窗口中,长按(或右键单击)KmdfHelloWorld,然后选择 属性。 在“Wpp 跟踪”>“所有选项”中,将“运行 Wpp 跟踪”设置为“否”。 依次选择“应用”、“确定” 。
若要生成驱动程序,请从 生成 菜单中选择 生成解决方案。 Visual Studio 在“输出”窗口中显示生成进度。 (如果 输出 窗口不可见,请从 视图 菜单中选择 输出。验证解决方案是否已成功生成后,可以关闭 Visual Studio。
若要查看生成的驱动程序,请在文件资源管理器中转到 KmdfHelloWorld 文件夹,然后 x64\Debug\KmdfHelloWorld。 该文件夹包括:
- KmdfHelloWorld.sys - 内核模式驱动程序文件
- KmdfHelloWorld.inf -- 安装驱动程序时 Windows 使用的信息文件
- KmdfHelloWorld.cat - 安装程序用来验证驱动程序的测试签名的目录文件
提示
如果在生成驱动程序时看到 DriverVer set to a date in the future
,请更改驱动程序项目设置,以便 Inf2Cat 设置 /uselocaltime
。 为此,请使用 配置属性 ->Inf2Cat ->常规 ->使用本地时间。 现在,Stampinf 和 Inf2Cat 都使用本地时间。
部署驱动程序
通常在测试和调试驱动程序时,调试器和驱动程序在单独的计算机上运行。 运行调试器的计算机称为 主计算机,运行驱动程序的计算机称为 目标计算机。 目标计算机也称为“测试计算机”。
到目前为止,你已使用 Visual Studio 在主计算机上生成驱动程序。 现在需要配置目标计算机。
按照预配计算机以便进行驱动程序部署和测试 (WDK 10) 中的说明进行操作。
提示
按照步骤使用网络电缆自动预配目标计算机时,请记下端口和密钥。 稍后将在调试步骤中使用它们。 在此示例中,我们将使用 50000 作为端口,1.2.3.4 作为密钥。
在实际驱动程序调试方案中,我们建议使用 KDNET 生成的密钥。 有关如何使用 KDNET 生成随机密钥的详细信息,请参阅 调试驱动程序 - 分步实验室(Sysvad 内核模式) 主题。
在主计算机上,在 Visual Studio 中打开解决方案。 可以在 KmdfHelloWorld 文件夹中双击解决方案文件KmdfHelloWorld.sln。
在“解决方案资源管理器”窗口中,选择并按住(或右键单击)KmdfHelloWorld 项目,然后选择“属性”。
在“KmdfHelloWorld 属性页”窗口中,转到“配置属性”>“驱动程序安装”>“部署”,如下所示。
选中“部署前删除以前的驱动程序版本”。
对于“目标设备名称”,请选择配置用于测试和调试的计算机名。 在本练习中,我们使用名为 MyTestComputer 的计算机。
选择 硬件 ID 驱动程序更新,然后输入驱动程序的硬件 ID。 在本练习中,硬件 ID 为 Root\KmdfHelloWorld。 选择“确定”。
注意
在本练习中,硬件 ID 并不用于标识真实的硬件。 它标识了虚构设备,该设备位于设备树中,作为根节点的子节点。 对于真实的硬件,不要选择“硬件 ID 驱动程序更新”,而要选择“安装并验证”。 你将在驱动程序的信息(INF)文件中看到硬件 ID。 在 解决方案资源管理器 窗口中,转到 KmdfHelloWorld > 驱动程序文件,然后双击 KmdfHelloWorld.inf。 硬件 ID 位于 [Standard.NT$ARCH$] 下。
[Standard.NT$ARCH$] %KmdfHelloWorld.DeviceDesc%=KmdfHelloWorld_Device, Root\KmdfHelloWorld
在“生成”菜单上,选择“部署解决方案”。 Visual Studio 会自动将安装和运行驱动程序所需的文件复制到目标计算机。 部署可能需要一两分钟。
部署驱动程序时,驱动程序文件将复制到测试计算机上的 %Systemdrive%\drivertest\drivers 文件夹。 如果在部署过程中出现问题,可以检查文件是否已复制到测试计算机。 验证 .inf、.cat、测试证书和 .sys 文件以及任何其他必需的文件是否存在于 %systemdrive%\drivertest\drivers 文件夹中。
有关部署驱动程序的详细信息,请参阅 将驱动程序部署到测试计算机。
安装驱动程序
将 Hello World 驱动程序部署到目标计算机后,现在将安装驱动程序。 如果你之前使用“自动”选项通过 Visual Studio 预配了目标计算机,则在预配过程中,Visual Studio 会将目标计算机设置为运行测试签名驱动程序。 现在,只需使用 DevCon 工具安装驱动程序。
在主计算机上,导航到 WDK 安装中的“工具”文件夹并找到 DevCon 工具。 例如,查看以下文件夹:
C:\Program Files (x86)\Windows Kits\10\Tools\x64\devcon.exe
将 DevCon 工具复制到远程计算机。
在目标计算机上,导航到包含驱动程序文件的文件夹,然后运行 DevCon 工具来安装驱动程序。
devcon 工具的一般语法如下,用于安装驱动程序:
devcon 安装 <INF 文件><硬件 ID>
安装此驱动程序所需的 INF 文件为 KmdfHelloWorld.inf。 INF 文件包含用于安装驱动程序二进制文件的硬件 ID,KmdfHelloWorld.sys。 请注意,位于 INF 文件中的硬件 ID 是 Root\KmdfHelloWorld。
以管理员身份打开命令提示符窗口。 导航到包含生成的驱动程序 .sys 文件的文件夹,然后输入以下命令:
devcon 安装 kmdfhelloworld.inf root\kmdfhelloworld
如果收到有关无法识别 devcon 的错误消息,请尝试将路径添加到 devcon 工具。 例如,如果将它复制到目标计算机上名为 C:\Tools的文件夹,请尝试使用以下命令:
c:\tools\devcon install kmdfhelloworld.inf root\kmdfhelloworld
将显示一个对话框,指示测试驱动程序是未签名的驱动程序。 选择“仍然安装此驱动程序”以继续。
调试驱动程序
在目标计算机上安装 KmdfHelloWorld 驱动程序后,将从主计算机远程附加调试器。
在主计算机上,以管理员身份打开命令提示符窗口。 切换到 WinDbg.exe 目录。 我们将使用作为 Windows 套件安装一部分的 Windows 驱动程序工具包(WDK)中 WinDbg.exe 的 x64 版本。 下面是 WinDbg.exe的默认路径:
C:\Program Files (x86)\Windows Kits\10\Debuggers\x64
使用以下命令启动 WinDbg 以连接到目标计算机上的内核调试会话。 端口和密钥的值应与用于预配目标计算机的值相同。 我们将为端口使用 50000,将为密钥使用 1.2.3.4,正如在部署步骤中使用的值一样。 k 标志指示这是内核调试会话。
WinDbg -k net:port=50000,key=1.2.3.4
在 调试 菜单上,选择 中断。 主计算机上的调试程序将中断目标计算机。 在 调试器命令 窗口中,可以看到内核调试命令提示符:kd>。
此时,可以通过在 kd> 提示符处输入命令来试验调试器。 例如,可以尝试以下命令:
若要让目标计算机再次运行,请从“调试”菜单中选择“执行”,或者按“g”,然后按“Enter”。
若要停止调试会话,请从调试菜单中选择分离调试器。
重要
请确保使用“go”命令让目标计算机在退出调试器之前再次运行,否则目标计算机仍将对鼠标和键盘输入无响应,因为它仍在与调试器通信。
有关驱动程序调试过程的详细分步演练,请参阅 调试通用驱动程序 - 分步实验室 (Echo Kernel-Mode)。
有关远程调试的详细信息,请参阅 使用 WinDbg进行远程调试。