编写 Windows 文件系统(零)

打算开始一项伟大的任务:在 Windows 下编写一个勉强能用的文件系统。用一系列博客文章记录下这个过程,如果不鸽掉的话。

这篇文章的内容:

测试环境:

  • 宿主机: Windows 10 专业版 21H1 19043.1889
  • 虚拟机: Windows 10 专业工作站版 21H1 19043.928
  • Visual Studio 2022 Community 17.3.5
  • VMWare Workstation 16.1.2

编写驱动代码

  1. 前往 Windows Driver Kit 的下载链接,执行步骤 1 到步骤 3;
    • 步骤 1 中要注意的是 WDK 需要 Spectre 缓解库。如果已经装好了 Visual Studio,那么需要启动 Visual Studio Installer 安装好对应架构的 Spectre 缓解库;
    • 步骤 2 和步骤 3 是不一样的:前者是 SDK,而后者是 WDK!需要先装好 SDK 后,再安装 WDK。如果只装了前者,会发现 VS 里没有后者的集成。
  2. 打开 VS,创建 Kernel Mode Driver, Empty (KMDF) 类型项目,起名为 KmdfHelloWorld,勾选“将解决方案和项目置于同一目录中”;
  3. 在项目中创建 Driver.c 文件,写代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#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 status = STATUS_SUCCESS;
WDF_DRIVER_CONFIG config;

KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "KmdfHelloWorld: DriverEntry\n"));

WDF_DRIVER_CONFIG_INIT(&config, KmdfHelloWorldEvtDeviceAdd);
status = WdfDriverCreate(DriverObject, RegistryPath, WDF_NO_OBJECT_ATTRIBUTES, &config, WDF_NO_HANDLE);
return status;
}

NTSTATUS
KmdfHelloWorldEvtDeviceAdd(
_In_ WDFDRIVER Driver,
_Inout_ PWDFDEVICE_INIT DeviceInit
)
{
UNREFERENCED_PARAMETER(Driver);

NTSTATUS status = STATUS_SUCCESS;
WDFDEVICE hDevice;

KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "KmdfHelloWorld: EvtDeviceAdd\n"));

status = WdfDeviceCreate(&DeviceInit, WDF_NO_OBJECT_ATTRIBUTES, &hDevice);
return status;
}
  1. 选择想要的配置(例如 Debug + x64),然后在项目属性的 Wpp Tracing 一栏中将 Run Wpp Tracing 改为“否”,在 Inf2Cat 一栏中将 Use Local Time 改为“是(/uselocaltime)” ;
  2. 生成解决方案。

配置虚拟机

按照教程上的说法,做 Windows 驱动开发需要两台机器,一台机器开发,一台机器测试。很显然,这是为了避免炸掉开发环境。这里要提一嘴 Linux,Linux 里直接利用系统自带的头文件编译内核模块代码,就可以 insmod 到内核中,将开发和测试两台机器合二为一,确实非常方便,不过一旦写出解引用空指针这样的 bug 就只有重启一条路可走了。可见 Windows 这套还是有它的优势在。

虚拟机的配置比较麻烦,虽然网上有各种教程,我实际配的时候还是踩了一些坑(当然也有可能是我 Windows 版本选的不对)。这里的操作步骤如下:

  1. 按 VMWare 的引导,创建一台 Windows 10 x64 虚拟机,等待它安装完成;
  2. 设置共享文件夹,将 Windows Driver Kit 的 SDK 和 WDK 安装文件都复制过去,安装;
  3. 装好之后,再安装测试目标 MSI。如果上一步是默认安装位置的话,这个 MSI 就在:
    1
    C:\Program Files (x86)\Windows Kits\10\Remote\x64\WDK Test Target Setup x64-x64_en-us.msi
  4. 去控制面板 → 系统和安全 → Windows Defender 防火墙 → 高级设置,点“入站规则”,找到“虚拟机监控(回显请求 ICMPv4-In)”,改成“允许连接”。保险起见 ICMPv6 的那个也改一下;
  5. 虚拟机的网络默认情况下应该是 NAT 模式,去 VMWare 的菜单点“编辑” → 虚拟网络编辑器,找到 NAT 模式的网段,我这里是 192.168.204.0/24;然后,在宿主机和虚拟机分别 ipconfig /all 一下,确认该网段的 IP 地址,我这里是宿主机 192.168.204.1、虚拟机 192.168.204.130
  6. 验证虚拟机和宿主机之间互相 ping 通。

到这一步,虚拟机就准备好被宿主机配置了。

现在,回到宿主机,去 VS 项目的项目属性中 Driver Install → Deployment 一栏,可以看到一个 Target Device Name,下面是空着的。点击“…”按钮进入 Configure Devices 对话框,我们需要在这里把虚拟机配成一个能接受 VS 自动部署的状态。

  1. 点 Add New Device,Network host name 填写虚拟机 IP,Provisioning Options 选第一项,然后下一步;
  2. 调整和确认 Kernel Mode 下面的设置,主要是 Host IP 需要改为能和虚拟机通信的那个网段的 IP(可以从下拉列表里选),Port Number 和 Key 记下备用;
  3. 确保虚拟机处于正常开启状态,然后下一步,这时应该能马上看到“Copying required files”,如果不是的话那么很大概率等一会就会出错;
    • 我之前的出错原因是没有安装测试目标的 MSI,这时候会显示连接某个端口超时没有响应。
  4. 等待自动配置完成(“完成”按钮亮起),期间虚拟机可能会重启多次,并且变为 WDKRemoteUser 账户。这时你可能会看到有些步骤失败了,但是不要紧,可以点击下面的日志文件的链接,打开日志文件查看具体失败细节。我遇到了以下几个问题:
    • Installing desktop driver test framework 失败,原因是虚拟机上 C:\WDTFInstallText.log 这个文件找不到。在虚拟机中以管理员权限打开命令提示符,把上面显示的指令重新输一遍:
      1
      msiexec.exe /i "%SystemDrive%\DriverTest\Setup\WDTF_Desktop_Kit_Product-x64_en-us.msi" /qb- KITTARGET=1 /l*v "%SystemDrive%\DriverTest\Logs\WDTF_Desktop_Kit_Product-x64_en-us.msi_install.log"
      不报错,就认为是可以了;
    • Installing TAEF service 失败,原因是在虚拟机上执行指令时,用了宿主机上 WDK 的路径,显然是自动部署脚本写出锅了。把路径改对,去虚拟机上执行即可:
      1
      C:\Program Files (x86)\Windows Kits\10\Testing\Runtimes\TAEF\X64\Wex.Services.exe /install:Te.Service
  5. 手动执行完失败了的指令后,点上一步回去,再下一步重新跑一遍。尽管该失败的还是失败,但应该也只有上面这两个失败了。
  6. 点下一步,如果看到“Status: Configured for driver testing”,即表示虚拟机配置成功。

再回到项目属性中的 Deployment 那一栏:

  1. 选好 Target Device Name,勾选“Remove previous driver versions before deployment”,在 Driver Install Options 下面选择 Hardware ID Driver Update,然后输入 Root\KmdfHelloWorld,然后点确定结束设置;
  2. 在生成菜单中选择“部署解决方案”,驱动会被部署到虚拟机的 C:\DriverTest\Drivers 目录下,其中会包含 cat、cer、inf、sys 四个文件。

安装驱动并观测输出

就好像在 Linux 下用 dmesg 观察 printk 等函数的输出一样,Windows 下也可以用 DebugView 查看 KdPrintEx 等函数的输出。在虚拟机中以管理员权限启动 DebugViewer,它就会开始收集内核打印的调试信息。

现在,按照教程中“安装驱动程序”一节的指示:

  1. 去虚拟机中 Windows Kits 安装目录下的 Tools 目录,找到对应架构的 DevCon 工具。Tools 目录下可能唯独没有 x64 目录,需要再点进去一级:
    1
    C:\Program Files (x86)\Windows Kits\10\Tools\10.0.22621.0\x64\devcon.exe
  2. 保持 DebugView 打开状态,执行下列命令安装驱动程序:
    1
    devcon.exe install C:\DriverTest\Drivers\KmdfHelloWorld.inf Root\KmdfHelloWorld
  3. 在 DebugView 中可以看到调试输出: 在虚拟机的设备管理器中也能看到驱动对应的设备:

这样一来,我们就走通了开发 Windows 内核驱动的全流程。不过,实际开发过程中,可能还需要用调试器调试驱动,这里一并记录一下。

调试虚拟机

有两种调试方案,它们的区别在于打开调试器的地方不同,但是进入调试之后的操作基本都一样。

Visual Studio

在菜单中选择“调试” → 附加到进程,选择 Windows Kernel Mode Debugger 和之前设好的虚拟机,这时下面会显示一个 Kernel 进程,点击附加即可。

附加成功后,按上方的暂停按钮中断虚拟机运行(int 3),然后进行各种调试。

WinDbg

前往 Windows Kits 目录下的 Debuggers 目录,启动对应架构的 windbg.exe。这里需要用上我们之前记下的调试用端口号和密钥:

1
.\windbg.exe -k net:port=50365,key=TQZ6HOHHNX9O.6Y7GTHZJ7OF3.HNO3FYOM25W.0TNVZ9TNVS4

参考资料