Windows NT 驱动介绍
Windows 驱动分为两类,一类是从 Windows NT 遗留下来的驱动模型称为传统的 Windows NT 驱动程序模型,另一类是 Windows 添加了电源管理后的 KMDF (WDM)驱动程序。本文这里首先以最简单的 Windows NT 驱动模型为例介绍 Windows 驱动的简单编写、编译、安装及调试。
NT 驱动代码分析
如果有学习过 Linux 驱动的同学,基本上会了解到驱动的编写其实都是内核规定好的 API 需要去声明和定义的,根据一定的驱动模型或者范例来进行编写即可实现自己定义的驱动功能。(可参考 Linux 驱动:Linux驱动开发——(Linux内核GPIO操作库函数)gpio(1))。
在 Linux 驱动中内核规定要添加驱动,需要自己实现驱动的入口函数和退出函数,然后用如下的宏定义进行声明:
module_init(led_init); //驱动入口函数声明
module_exit(led_exit); //驱动退出函数声明
MODULE_LICENSE("GPL"); //驱动遵循的开源规则声明
而在 Windows 驱动中也有类似的模块入口函数和模块退出函数。区别是在 Windows 驱动中函数入口都统一由一个固定的函数名称 DriverEntry
,你只需要在自己的驱动代码中实现这个固定的函数入口(Windows 内核会在加载驱动后主动调用这个入口函数进行驱动的初始化操作),该函数的具体声明如下:
NTSTATUS DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
);
参数
- DriverObject [in]:指向 DRIVER_OBJECT 结构的指针,该结构表示驱动程序的 WDM 驱动程序对象。
- RegistryPath [in]:指向 UNICODE_STRING 结构的指针,该结构指定注册表中驱动程序 的 Parameters 键 的路径。
- 返回值
如果例程成功,则必须返回STATUS_SUCCESS。 否则,它必须返回 ntstatus.h 中定义的错误状态值之一。
Windows NT 模型驱动和 WDM 模型驱动都是由 DriverEntry
入口函数开始加载驱动和完成驱动最初的初始化操作的(这也是 Windows 内核固定会调用的驱动入口函数)。所以我们编写 Windows 驱动其实也是从最初的 DriverEntry
入口函数开始。
这里首先编写一个最简单的 Windows NT 驱动作为模型例程进行讲解,整个驱动源文件有两个文件组成:驱动源文件(helloNt.cpp)和驱动头文件(helloNt.h)。
这里最好参考《Windows 驱动开发环境搭建》这篇文章的内容已经搭建好了 Windows 驱动开发环境,并且使用 VS2019 创建 KMDF 驱动解决方案项目,如下图:
使用 VS2019 创建完成解决方案后会自动创建如下文件,这里由于我们想要创建一个最简单的 Windows NT 驱动,所以删除不需要的文件即可(Device.c、Device.h、Queue.c、Queue.h、helloNt.inf)。
删除完成后只保留 Driver.c 和 Driver.h
这里需要注意,将项目属性进行一些设置(平台参数 x64、编译将警告视为错误设置为否、链接将警告视为错误设置为否、取消 WPP trace 运行)
驱动模块具体代码内容如下:
Driver.c
#include "driver.h"
#ifdef ALLOC_PRAGMA
#pragma alloc_text (INIT, CreateDevice)
#pragma alloc_text (PAGE, HelloDDKDispatchRoutine)
#pragma alloc_text (PAGE, HelloDDKUnload)
#endif
NTSTATUS
DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
/*++
Routine Description:
DriverEntry initializes the driver and is the first routine called by the
system after the driver is loaded. DriverEntry specifies the other entry
points in the function driver, such as EvtDevice and DriverUnload.
Parameters Description:
DriverObject - represents the instance of the function driver that is loaded
into memory. DriverEntry must initialize members of DriverObject before it
returns to the caller. DriverObject is allocated by the system before the
driver is loaded, and it is released by the system after the system unloads
the function driver from memory.
RegistryPath - represents the driver specific path in the Registry.
The function driver can use the path to store driver related data between
reboots. The path does not store hardware instance specific data.
Return Value:
STATUS_SUCCESS if successful,
STATUS_UNSUCCESSFUL otherwise.
--*/
{
NTSTATUS status;
KdPrint(("Enter DriverEntry\n"));
//注册其他驱动调用函数入口
DriverObject->DriverUnload = HelloDDKUnload;
DriverObject->MajorFunction[IRP_MJ_CREATE] = HelloDDKDispatchRoutine;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloDDKDispatchRoutine;
DriverObject->MajorFunction[IRP_MJ_WRITE] = HelloDDKDispatchRoutine;
DriverObject->MajorFunction[IRP_MJ_READ] = HelloDDKDispatchRoutine;
//创建驱动设备对象
status = CreateDevice(DriverObject);
KdPrint(("DriverEntry end\n"));
return status;
}
/************************************************************************
* 函数名称:CreateDevice
* 功能描述:初始化设备对象
* 参数列表:
pDriverObject:从I/O管理器中传进来的驱动对象
* 返回 值:返回初始化状态
*************************************************************************/
#pragma INITCODE
NTSTATUS CreateDevice(
IN PDRIVER_OBJECT pDriverObject)
{
NTSTATUS status;
PDEVICE_OBJECT pDevObj;
PDEVICE_EXTENSION pDevExt;
//创建设备名称
UNICODE_STRING devName;
RtlInitUnicodeString(&devName, L"\\Device\\MyDDKDevice");
//创建设备
status = IoCreateDevice(pDriverObject,
sizeof(DEVICE_EXTENSION),
&devName,
FILE_DEVICE_UNKNOWN,
0, TRUE,
&pDevObj);
if (!NT_SUCCESS(status))
return status;
pDevObj->Flags |= DO_BUFFERED_IO;
pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
pDevExt->pDevice = pDevObj;
pDevExt->ustrDeviceName = devName;
//创建符号链接
UNICODE_STRING symLinkName;
RtlInitUnicodeString(&symLinkName, L"\\??\\HelloDDK");
pDevExt->ustrSymLinkName = symLinkName;
status = IoCreateSymbolicLink(&symLinkName, &devName);
if (!NT_SUCCESS(status))
{
IoDeleteDevice(pDevObj);
return status;
}
return STATUS_SUCCESS;
}
/************************************************************************
* 函数名称:HelloDDKUnload
* 功能描述:负责驱动程序的卸载操作
* 参数列表:
pDriverObject:驱动对象
* 返回 值:返回状态
*************************************************************************/
#pragma PAGEDCODE
VOID HelloDDKUnload(IN PDRIVER_OBJECT pDriverObject)
{
PDEVICE_OBJECT pNextObj;
KdPrint(("Enter DriverUnload\n"));
pNextObj = pDriverObject->DeviceObject;
while (pNextObj != NULL)
{
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
pNextObj->DeviceExtension;
//删除符号链接
UNICODE_STRING pLinkName = pDevExt->ustrSymLinkName;
IoDeleteSymbolicLink(&pLinkName);
pNextObj = pNextObj->NextDevice;
IoDeleteDevice(pDevExt->pDevice);
}
}
/************************************************************************
* 函数名称:HelloDDKDispatchRoutine
* 功能描述:对读IRP进行处理
* 参数列表:
pDevObj:功能设备对象
pIrp:从IO请求包
* 返回 值:返回状态
*************************************************************************/
#pragma PAGEDCODE
NTSTATUS HelloDDKDispatchRoutine(IN PDEVICE_OBJECT pDevObj,
IN PIRP pIrp)
{
KdPrint(("Enter HelloDDKDispatchRoutine\n"));
NTSTATUS status = STATUS_SUCCESS;
// 完成IRP
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = 0; // bytes xfered
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
KdPrint(("Leave HelloDDKDispatchRoutine\n"));
return status;
}
Driver.h
#pragma once
#ifdef __cplusplus
extern "C"
{
#endif
#include <ntddk.h>
#include <NTDDK.h>
#ifdef __cplusplus
}
#endif
#define arraysize(p) (sizeof(p)/sizeof((p)[0]))
typedef struct _DEVICE_EXTENSION {
PDEVICE_OBJECT pDevice;
UNICODE_STRING ustrDeviceName;
UNICODE_STRING ustrSymLinkName;
} DEVICE_EXTENSION, * PDEVICE_EXTENSION;
NTSTATUS CreateDevice(IN PDRIVER_OBJECT pDriverObject);
VOID HelloDDKUnload(IN PDRIVER_OBJECT pDriverObject);
NTSTATUS HelloDDKDispatchRoutine(IN PDEVICE_OBJECT pDevObj,
IN PIRP pIrp);
编译
这里在编译前,首先已经根据文章《Windows 驱动开发环境搭建》完成了基本的编译环境搭建。所以我们直接在 VS 2019 中将解决方案进行构建即可。
前面我们已经对项目的属性进行了设置,这里只要选取我们设置的平台 debug-X64,然后点击“生成”——“生成解决方案”/“重新生成解决方案”即可生成我们需要的驱动文件。
安装
对于我们自己编写的驱动程序,为了防止驱动程序的加载使用导致我们的操作系统崩溃(Windows 驱动程序加载后就会运行在 RING-0 内核空间中,这个空间中的程序崩溃会直接导致 Windows 操作系统崩溃,最常见的现象就是蓝屏)。所以我们需要搭建 Windows 虚拟机环境,然后在虚拟机中进行新驱动的安装调试。
由于 Windows NT 驱动程序实际上就是 Windows 服务驱动程序,我们这里编写的并没有真正与设备进行相关,所以你可以理解它是一个运行在 Windows 内核空间的服务程序。对于 Windows NT 驱动程序的安装,这里可以给出两种方式:
- 通过工具
DriverMonitor
进行打开加载安装 - 修改注册表,添加驱动服务,重启计算机使用
net start helloNt
命令进行加载启动
修改注册表进行安装
使用快捷键 Win+R 打开对话框后输入 regedit
运行后打开系统的注册表。在注册表中我们找到服务驱动程序加载的注册位置。
计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\helloNt
在其中添加一个新的服务程序项目,然后加入一下内容进行描述,注意其中的 ImagePath 描述内容为加载的驱动程序文件(helloNt.sys)。
其中涉及到驱动文件的路径,输入内容如下:
\??\C:\Tools\helloNt\helloNt\helloNt.sys
驱动文件存放位置:C:\Tools\helloNt\helloNt
注册表内容都添加配置完成后,我们重启计算机(重启才会使得注册表生效)。
重启完成后,使用快捷键 Win+X,选择 Windows powershell(管理员)打开终端对话框,使用命令 net start helloNt
启动加载驱动程序。
这时候驱动程序就已经加载启动了。我们可以打开“系统信息”工具来查看这个服务的状态。
可以看到这里已经启动了这个驱动程序。
使用工具 DriverMonitor
进行打开加载安装
本来这个 DriverMonitor 工具是 DriverStudio 工具集的一部分,但是这个 DriverStudio 工具集也已经被淘汰了,所以目前其实这个并不好找。我在 github 上找到一个仓库中存放有这部分的工具,所以可以从这里使用 git clone
进行下载使用。(https://github.com/HotIce0/windows_kernel_driver_learn_note.git)
将这个工具下载下来解压后,打开可以看到一个简单的界面:
选择“File”,“Open Driver” 打开驱动文件。
然后选择“File”-“Start Driver”,即可完成驱动的加载和运行。
调试
这里的例程其实是从书籍《Windows 驱动开发技术详解》(张帆)中得到的,驱动加载的时候都是正常的,但是我们如果使用手动卸载启动时就会出现问题,使用 net
命令卸载驱动:
net stop helloNt
结果发现系统蓝屏了(Windows 崩溃了)
这时候就需要对驱动进行简单调试来找到出问题的地方。虽然我们在驱动中添加了 log 打印(KdPrint
),但是当系统崩溃后,我们并不能及时看到 log 的打印输出(已经崩溃了还看个锤子,只能看到蓝屏)。针对这种情况就需要我们使用 windbg 双机调试来进行了。(双机调试环境请参考文章《windbg 双机调试环境搭建(虚拟机)》)
在宿主机上启动 windbg 后,与虚拟机建立连接。执行 windbg 传统调试步骤:
- 加载符号表(尤其是自己编译的驱动程序的符号表)
- 设置驱动模块入口断点,或者直接操作使系统崩溃(崩溃会自动停止在断点位置)
- 查看当前调用堆栈,参考源代码推断崩溃原因
- 修改源码,重新编译,加载验证
这里首先 break 下来,然后通过命令加载符号表,或者使用 GUI 操作加载符号表。
.srcpath+ "C:\Tools\helloNt\*.pdb"
由于是卸载操作引起的崩溃,所以我们在卸载函数这里添加断点:
执行 net stop helloNt
命令后,windbg 会自动跳转到断点(记载了符号表)也会显示对应的代码。
单步执行,然后发现崩溃点的调用栈:
这里发现是在调用 Unload() 中的 DeleteSymbolicLink 的时候出现的问题。回顾我们这里的代码:
VOID HelloDDKUnload(IN PDRIVER_OBJECT pDriverObject)
{
PDEVICE_OBJECT pNextObj;
KdPrint(("Enter DriverUnload\n"));
pNextObj = pDriverObject->DeviceObject;
while (pNextObj != NULL)
{
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
pNextObj->DeviceExtension;
UNICODE_STRING pLinkName = pDevExt->ustrSymLinkName;
IoDeleteSymbolicLink(&pLinkName);
pNextObj = pNextObj->NextDevice;
IoDeleteDevice(pDevExt->pDevice);
}
}
发现这里调用了 IoDeleteSymbolicLink(&pLinkName);
,然后我们在这里打印 pLinkName 的地址看一下这个地址空间是否正常可访问。
也可以使用 !analyze -v 进行分析(这个分析结果可供参考)
发现结果和我们预测的位置基本一致。那问题就出现在这个链接变量这里了。为什么我们卸载链接变量会崩溃呢?
通过 Unload 局部变量中的数据可以看到这里的 pDevExt->ustrSymLinkName;
内存是不可以访问的,也就是说在这里的时候已经出现问题了。回到源代码中查看初始化部分发现这个字符串变量是在 CreateDevice 的时候放进去的:
//创建设备名称
UNICODE_STRING devName;
RtlInitUnicodeString(&devName,L"\\Device\\MyDDKDevice");
//创建设备
status = IoCreateDevice( pDriverObject,
sizeof(DEVICE_EXTENSION),
&(UNICODE_STRING)devName,
FILE_DEVICE_UNKNOWN,
0, TRUE,
&pDevObj );
if (!NT_SUCCESS(status))
return status;
pDevObj->Flags |= DO_BUFFERED_IO;
pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
pDevExt->pDevice = pDevObj;
pDevExt->ustrDeviceName = devName;
这里创建了一个字符串变量 devName,然后将这个变量放到了 pDevExt 中。但是这里发现这个 devName 是这个函数的局部变量,这里放进去的只是字符串指针,而在后面的函数调用中取出了已经释放空间的指针进行使用肯定会有问题。
找到问题后,我们修改一下,把这里的局部变量修改成全局变量或者静态变量试一下:
UNICODE_STRING gdevName;
UNICODE_STRING gsymLinkName;
/************************************************************************
* 函数名称:CreateDevice
* 功能描述:初始化设备对象
* 参数列表:
pDriverObject:从I/O管理器中传进来的驱动对象
* 返回 值:返回初始化状态
*************************************************************************/
#pragma INITCODE
NTSTATUS CreateDevice(
IN PDRIVER_OBJECT pDriverObject)
{
NTSTATUS status;
PDEVICE_OBJECT pDevObj;
PDEVICE_EXTENSION pDevExt;
//创建设备名称
//UNICODE_STRING devName;
RtlInitUnicodeString(&gdevName, L"\\Device\\MyDDKDevice");
//创建设备
status = IoCreateDevice(pDriverObject,
sizeof(DEVICE_EXTENSION),
&gdevName,
FILE_DEVICE_UNKNOWN,
0, TRUE,
&pDevObj);
if (!NT_SUCCESS(status))
return status;
pDevObj->Flags |= DO_BUFFERED_IO;
pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
pDevExt->pDevice = pDevObj;
pDevExt->ustrDeviceName = gdevName;
//创建符号链接
//UNICODE_STRING symLinkName;
RtlInitUnicodeString(&gsymLinkName, L"\\??\\HelloDDK");
pDevExt->ustrSymLinkName = gsymLinkName;
status = IoCreateSymbolicLink(&gsymLinkName, &gdevName);
if (!NT_SUCCESS(status))
{
IoDeleteDevice(pDevObj);
return status;
}
return STATUS_SUCCESS;
}
发现修改成全局变量后,结果还是一样的,这里的 memory 还是无法正常 read。
比较发现这里是由于这部分的 buffer 空间 read error,修改代码,将这部分的字符串初始化从这个 #pragma INITCODE
声明的 CreateDevice
函数中移到 DriverEntry
中:
NTSTATUS
DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
{
NTSTATUS status;
KdPrint(("Enter DriverEntry\n"));
UNICODE_STRING devName;
UNICODE_STRING symLinkName;
RtlInitUnicodeString(&devName, L"\\Device\\MyDDKDevice");
RtlInitUnicodeString(&symLinkName, L"\\??\\HelloDDK");
//注册其他驱动调用函数入口
DriverObject->DriverUnload = HelloDDKUnload;
DriverObject->MajorFunction[IRP_MJ_CREATE] = HelloDDKDispatchRoutine;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloDDKDispatchRoutine;
DriverObject->MajorFunction[IRP_MJ_WRITE] = HelloDDKDispatchRoutine;
DriverObject->MajorFunction[IRP_MJ_READ] = HelloDDKDispatchRoutine;
//创建驱动设备对象
status = CreateDevice(DriverObject, devName, symLinkName);
KdPrint(("DriverEntry end\n"));
return status;
}
/************************************************************************
* 函数名称:CreateDevice
* 功能描述:初始化设备对象
* 参数列表:
pDriverObject:从I/O管理器中传进来的驱动对象
* 返回 值:返回初始化状态
*************************************************************************/
#pragma INITCODE
NTSTATUS CreateDevice(
IN PDRIVER_OBJECT pDriverObject,
IN UNICODE_STRING devName,
IN UNICODE_STRING symLinkName)
{
NTSTATUS status;
PDEVICE_OBJECT pDevObj;
PDEVICE_EXTENSION pDevExt;
//创建设备名称
//UNICODE_STRING devName;
//RtlInitUnicodeString(&devName, L"\\Device\\MyDDKDevice");
//创建设备
status = IoCreateDevice(pDriverObject,
sizeof(DEVICE_EXTENSION),
&devName,
FILE_DEVICE_UNKNOWN,
0, TRUE,
&pDevObj);
if (!NT_SUCCESS(status))
return status;
pDevObj->Flags |= DO_BUFFERED_IO;
pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
pDevExt->pDevice = pDevObj;
pDevExt->ustrDeviceName = devName;
//创建符号链接
//UNICODE_STRING symLinkName;
//RtlInitUnicodeString(&symLinkName, L"\\??\\HelloDDK");
pDevExt->ustrSymLinkName = symLinkName;
status = IoCreateSymbolicLink(&symLinkName, &devName);
if (!NT_SUCCESS(status))
{
IoDeleteDevice(pDevObj);
return status;
}
return STATUS_SUCCESS;
}
并且可以看到加载和卸载驱动操作已经正常了。说明我们的判断是正确的。