VS2013 WDK8.1驱动开发1(最简单的NT驱动)
本系列博客为学习《Windows驱动开发技术详解》一书的学习笔记。
前言
Windows驱动程序分为两类,一类是NT驱动程序,一类是WDM驱动程序。NT驱动程序不支持即插即用功能,WDM驱动程序支持即插即用功能,它们的具体区别我们会在以后进行详细了解。
搭建Windows驱动开发环境
- 开发主机安装Microsoft Visual Studio 2013,下载地址点我
- 开发主机安装Windows Driver Kit (WDK) 8.1,下载地址点我
- 测试虚拟机环境为Win7 64位
VS2013必须配合WDK8.1才可以进行驱动程序的开发,只有安装了WDK8.1后,VS2013中才会出现驱动开发工程的模板,如下图:
创建一个NT式驱动工程
VS2013中没有NT驱动工程模板,但是我们可以通过创建一个WDM驱动工程然后改造它。
1. 创建一个WDM驱动工程
2. 删除工程中的一些文件,将它改造成NT驱动工程
红色部分就是需要删除的文件和工程,删除后如下:
3.在工程中新建main.c源文件
我们后续的代码将全部编写在该文件中。
编写NT驱动程序代码
1. NT驱动程序的入口函数
#include <ntddk.h>
/// @brief 驱动程序入口函数
/// @param[in] pDriverObject 从I/O管理器中传进来的驱动对象
/// @param[in] pRegPath 驱动程序在注册表中的路径
/// @return 初始化驱动状态
NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegPath)
{
NTSTATUS status = STATUS_SUCCESS;
KdPrint(("Enter DriverEntry\n"));
// 注册驱动调用函数入口
// 这些函数不是由驱动程序本身负责调用, 而是由操作系统负责调用
pDriverObject->DriverUnload = HelloNTDriverUnload;
pDriverObject->MajorFunction[IRP_MJ_CREATE] = HelloNTDriverDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloNTDriverDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = HelloNTDriverDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_READ] = HelloNTDriverDispatchRoutine;
// 创建驱动设备对象
status = CreateDevice(pDriverObject);
KdPrint(("Leave DriverEntry\n"));
return status;
}
- NT驱动程序需要包含ntddk.h头文件。
- Windows驱动程序的入口函数是DriverEntry,该函数由内核中的I/O管理器负责调用,该函数有两个参数pDriverObject和pRegPath,pDriverObject是I/O管理器传递进来的驱动对象,代表我们的驱动,pRegPath是一个UNICODE字符串,内容为我们驱动负责的注册表路径。
- KdPrint是一个打印调试信息的宏,如果我们使用Debug方式编译驱动,那么该宏会起作用,如果我们使用Release方式编译驱动,那么该宏不做任何事情。
- 在入口函数中我们给驱动对象pDriverObject注册了一个驱动卸载函数HelloNTDriverUnload,该函数负责驱动程序卸载的工作,该函数不是由我们驱动程序主动去调用,而是在我们驱动被卸载时由操作系统负责调用。
- 在入口函数中我们还给驱动对象pDriverObject的一些派遣方法注册了同一个派遣函数HelloNTDriverDispatchRoutine,派遣函数用于处理系统或者Win32程序发送给驱动程序的请求。
- 入口函数中还有一个CreateDevice方法,该方法是我们自定义的方法,帮助我们在驱动对象上创建设备对象。处理外部的请求和硬件沟通都必须通过设备对象。设备对象是可以创建多个的,可以简单理解为一个驱动程序可以同时管理多个相同的设备,每一个设备对象对应一个设备。
- 驱动程序的入口函数只会在启动驱动程序时运行,之后在执行完所有代码后就会直接退出,这个跟我们Win32程序很不一样。Win32程序的main函数退出后就代表整个进程已经结束,所以GUI程序都会有一个事件循环。然而驱动程序的入口函数的结束并不影响程序中其他一些函数的执行,如驱动卸载函数和派遣函数。在我的理解中这是因为驱动程序不是一个进程。当我们启动一个驱动程序后,驱动程序的代码就会被加载到内存中(内核地址空间),这些代码供其他线程在需要的时候执行,这些线程包括系统的线程或者用户线程,而驱动程序本身没有任何线程。
2. 创建设备对象
设备对象用于处理外部请求以及和硬件进行沟通,入口函数中有一个我们自定义的CreateDevice方法,我们来看看它的实现:
/// @brief 设备扩展结构
typedef struct _DEVICE_EXTENSION
{
PDEVICE_OBJECT PDeviceObject;
UNICODE_STRING DeviceName; ///< 设备名称
UNICODE_STRING SymLinkName; ///< 符号链接名
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
/// @brief 创建设备对象
/// @param[in] pDriverObject 驱动对象
/// @return 状态值
NTSTATUS CreateDevice(IN PDRIVER_OBJECT pDriverObject)
{
UNICODE_STRING devName;
UNICODE_STRING symLinkName;
PDEVICE_OBJECT pDevObj = NULL;
PDEVICE_EXTENSION pDevExt = NULL;
NTSTATUS status;
// 创建设备名称
RtlInitUnicodeString(&devName, L"\\Device\\HelloNTDriverDevice");
// 创建设备
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->PDeviceObject = pDevObj;
pDevExt->DeviceName = devName;
// 创建符号链接
// 设备名称只在内核态中可见
// 符号链接, 链接应用程序和设备名称
RtlInitUnicodeString(&symLinkName, L"\\??\\HelloNTDriver");
pDevExt->SymLinkName = symLinkName;
status = IoCreateSymbolicLink(&symLinkName, &devName);
if (!NT_SUCCESS(status))
{
IoDeleteDevice(pDevObj);
return status;
}
return STATUS_SUCCESS;
}
- 我们首先声明了一个设备扩展结构DEVICE_EXTENSION,该结构用于存储设备对象的一些扩展信息。
- 要创建一个设备对象,必须先指定设备对象的名称,示例中我们构造了名为"\Device\HelloNTDriverDevice"的设备名,关于UNICODE字符串会在之后的章节介绍。
- IoCreateDevice用于创建设备对象,填入的第二个参数是设备扩展结构的大小,这样IoCreateDevice在创建设备对象的时候同时也会分配给我们一块指定大小的内存空间,该空间就是用来保存扩展信息,设备对象的DeviceExtension属性记录该空间的地址。
- 通过设备对象的DeviceExtension属性,我们拿到了设备扩展空间的地址,在接下来的代码中我们在扩展结构中保存了设备名称以及设备对象地址,这样的话设备扩展和设备对象其实是相互指向的,我们只要知道其中一个的地址,就可以知道另一个的地址,这个设计的作用在以后的代码中可以体现出来。
创建设备使用的设备名称只能在内核态使用,我们必须给设备名称关联一个连接符号,连接符号对于应用程序是可见的,这样Win32程序可以通过连接符号来发送请求给设备对象。链接符号的格式为"\\??\\HelloNTDriver",HelloNTDriver这个字符串可以自定义。
当创建多个设备对象时,需要每个设备对象的设备名和链接符号都不同。
3. 卸载驱动例程
当卸载驱动时,系统就会调用我们在入口函数设置的卸载例程,我们来看看它的实现:
// @brief 驱动程序卸载操作
/// @param[in] pDriverObject 驱动对象
void HelloNTDriverUnload(IN PDRIVER_OBJECT pDriverObject)
{
PDEVICE_OBJECT pNextObj = NULL;
KdPrint(("Enter HelloNTDriverUnload\n"));
pNextObj = pDriverObject->DeviceObject;
while (pNextObj != NULL)
{
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pNextObj->DeviceExtension;
// 删除符号链接
UNICODE_STRING linkName = pDevExt->SymLinkName;
IoDeleteSymbolicLink(&linkName);
pNextObj = pNextObj->NextDevice;
// 删除设备对象
IoDeleteDevice(pDevExt->PDeviceObject);
}
KdPrint(("Leave HelloNTDriverUnload\n"));
}
- 在驱动启动时我们通过CreateDevice函数创建了设备对象,因此在卸载例程中我们需要删除设备对象。
- 我们之前说过在驱动程序中可以创建多个设备对象,事实上第一个被创建的设备对象的地址存储在驱动对象的DeviceObject属性中,每个设备对象的NextDevice属性都记录这下一个设备对象地址,最后一个设备对象的NextDevice为NULL,这样就形成了一个链表。在卸载驱动例程中,我们需要遍历所有设备对象进行挨个删除。
- 在删除设备对象的同时还需要删除符号链接。
4. 派遣函数
在入口函数中我们注册了派遣函数HelloNTDriverDispatchRoutine,派遣函数用于处理请求,在这个例子中我们只是简单的返回成功:
/// @brief 对IRP进行处理
/// @param[in] pDriverObject
/// @param[in] pIrp
/// @return
NTSTATUS HelloNTDriverDispatchRoutine(IN PDEVICE_OBJECT pDevObject, IN PIRP pIrp)
{
KdPrint(("Enter HelloNTDriverDispatchRoutine\n"));
NTSTATUS status = STATUS_SUCCESS;
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
KdPrint(("Leave HelloNTDriverDispatchRoutine\n"));
return status;
}
- 派遣函数的第一个参数是设备对象指针,系统在回调该方法时会使用我们在CreateDevice函数中创建出来的设备对象填写该参数,只有在有请求消息需要我们的设备对象处理的时候系统才会回调该方法。
- 派遣函数的第二个参数是请求包指针,IRP的概念会在以后的章节中讲解。
- 示例中我们给请求包设置了成功状态并且设置它为完成状态。
编译NT驱动程序
1. 平台和配置属性
平台选择x64,配置属性选择Win7 Debug,这是因为我会在虚拟机上运行我们的驱动,而我的虚拟机是Win7 64位。在测试我们的驱动程序时我们最好使用虚拟机进行操作,因为驱动程序很容易照成蓝屏。64位的OS只能加载64位的驱动程序,32位的OS只能加载32位的驱动程序。
2. 设置测试签名
64位的Windows系统在加载驱动时要求驱动必须被签名,我们可以在工程属性中设置一个测试签名。
3. 解决编译错误
直接编译后我们会得到如下的编译错误:
main.c(18): error C2220: 警告被视为错误 - 没有生成“object”文件
main.c(18): warning C4100: “pDevObject”: 未引用的形参
main.c(114): warning C4100: “pRegPath”: 未引用的形参
驱动程序对源代码的要求很高,针对这种未引用的形参错误,我们可以使用一个宏解决,在以上两个未引用形参的函数中分别加上如下代码:
UNREFERENCED_PARAMETER(pDevObject);
UNREFERENCED_PARAMETER(pRegPath);
4. 成功编译
成功编译后就可以在工程目录\x64\Win7Debug文件夹下找到如下文件:
- chapter01-1.sys
- chapter01-1.cer
chapter01-1.sys是我们编译出来的目标文件,chapter01-1.cer是测试证书文件。有关测试证书和签名的原理可以查看我之前的两篇博客数字签名和Windows下生成测试数字证书。
安装NT驱动程序
安装NT驱动程序需要使用DriverMonitor工具,读者可自行在互联网上下载。代码中我们使用KdPrint来输出调试信息,DbgView工具可以查看这些调试信息,这个工具也需要读者自行在互联网上下载。或者也可以在CSDN上下载猛戳我。
1. 开启测试签名模式
在虚拟机的C:\windows\system32\目录下打开管理员权限的命令行工具输入如下命令:
bcdedit.exe /set TESTSIGNING ON
重启虚拟机后测试签名模式就开启了,关闭测试签名模式的命令是:
bcdedit.exe /set TESTSIGNING OFF
将chapter01-1.sys和chapter01-1.cer拷贝到虚拟机上,安装测试证书chapter01-1.cer。
2. 开启DbgView
在虚拟机上使用管理员权限打开DbgView工具,并设置成收集内核调试信息。
3. 开启DriverMonitor
在虚拟机上使用管理员权限打开DriverMonitor工具,加载我们的驱动程序,然后点击GO启动我们的驱动程序:
驱动程序启动后我们就可以在DbgView工具中看到调试信息:
点击DriverMonitor工具上的STOP按钮,可以停止我们的驱动程序,驱动程序会运行卸载例程,DbgView中也可以看到相应的调试信息:
后话
本文完整工程和代码托管在GitHub上点我查看。
其他章节链接
VS2013 WDK8.1驱动开发1(最简单的NT驱动)
还没有人评论...