Compilation, installation, debugging of Windows NT drivers
Introduction to Windows NT Drivers
Windows drivers are divided into two categories, one is the driver model left over from Windows NT called the traditional Windows NT driver model, and the other is the KMDF (WDM) driver after Windows has added power management. This article first takes the simplest Windows NT driver model as an example to introduce the simple writing, compiling, installation and debugging of Windows drivers.
NT driver code analysis
If you have studied Linux drivers, you will basically understand that the writing of drivers is actually required to declare and define the API specified by the kernel, and you can realize your own defined driver functions by writing according to a certain driver model or example. (Refer to Linux driver: Linux driver development - (Linux kernel GPIO operation library function) gpio(1) ).
In the Linux driver, the kernel stipulates that to add a driver, you need to implement the driver's entry function and exit function, and then declare it with the following macro definition:
module_init(led_init); //驱动入口函数声明
module_exit(led_exit); //驱动退出函数声明
MODULE_LICENSE("GPL"); //驱动遵循的开源规则声明
And there are similar module entry functions and module exit functions in the Windows driver. The difference is that the function entry in the Windows driver is unified by a fixed function name DriverEntry
. You only need to implement this fixed function entry in your own driver code (the Windows kernel will actively call this entry function to initialize the driver after loading the driver). The specific declaration of this function is as follows:
NTSTATUS DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
);
-
parameter
- DriverObject [in]: Pointer to a DRIVER_OBJECT structure that represents the driver's WDM driver object.
- RegistryPath [in]: Pointer to a UNICODE_STRING structure that specifies the path to the driver's Parameters key in the registry.
-
Return Value
If the routine succeeds, it must return STATUS_SUCCESS. Otherwise, it must return one of the error status values defined in ntstatus.h.
Both the Windows NT model driver and the WDM model driver DriverEntry
start to load the driver and complete the initial initialization of the driver through the entry function (this is also the driver entry function that the Windows kernel will always call). So when we write Windows driver, we actually start from the initial DriverEntry
entry function.
Here we first write the simplest Windows NT driver as a model routine for explanation. The entire driver source file consists of two files: the driver source file (helloNt.cpp) and the driver header file (helloNt.h).
Here, it is best to refer to the content of the article " Building Windows Driver Development Environment " to build a Windows driver development environment, and use VS2019 to create a KMDF driver solution project, as shown below:
After the solution is created using VS2019, the following files will be automatically created. Here we want to create the simplest Windows NT driver, so just delete unnecessary files (Device.c, Device.h, Queue.c, Queue.h, helloNt.inf).
After the deletion is complete, only Driver.c and Driver.h remain
It should be noted here that some settings are made on the project properties (platform parameter x64, compilation treats warnings as errors, sets no, links treats warnings as errors, cancels WPP trace operation)
The specific code content of the driver module is as follows:
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);
compile
Here, before compiling, the basic compiling environment has been built according to the article " Building Windows Driver Development Environment ". So we can build the solution directly in VS 2019.
We have set the properties of the project before, here we only need to select the platform debug-X64 we set, and then click "Generate" - "Generate Solution"/"Regenerate Solution" to generate the driver file we need.
Install
For the driver program we wrote ourselves, in order to prevent our operating system from crashing due to the loading and use of the driver program (the Windows driver program will run in the RING-0 kernel space after loading, the program crash in this space will directly cause the Windows operating system to crash, the most common phenomenon is the blue screen). So we need to build a Windows virtual machine environment, and then install and debug the new driver in the virtual machine.
Since the Windows NT driver is actually a Windows service driver, what we write here is not really related to the device, so you can understand that it is a service program running in the Windows kernel space. For Windows NT driver installation, here are two ways:
DriverMonitor
Open and load the installation through the tool- Modify the registry, add driver services, restart the computer and use
net start helloNt
the command to load and start
Modify the registry to install
Use the shortcut key Win+R to open the dialog box and enter regedit
to open the registry of the system after running. In the registry we find the registration location where the service driver loads.
计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\helloNt
Add a new service program item in it, and then add the content to describe it, and note that the ImagePath description content is the loaded driver file (helloNt.sys).
It involves the path of the driver file, and the input content is as follows:
\??\C:\Tools\helloNt\helloNt\helloNt.sys
Driver file storage location:C:\Tools\helloNt\helloNt
After the registry content is added and configured, we restart the computer (restart will make the registry effective).
After the restart is complete, use the shortcut key Win+X, select Windows powershell (administrator) to open the terminal dialog box, and use the command net start helloNt
to start and load the driver.
The driver is now loaded and started. We can open the "System Information" tool to view the status of this service.
You can see that the driver has been started here.
Use the tool DriverMonitor
to open and load the installation
Originally, the DriverMonitor tool was part of the DriverStudio tool set, but the DriverStudio tool set has also been eliminated, so it is actually not easy to find at present. I found a warehouse with this part of the tool on github, so I can git clone
download it from here. ( https://github.com/HotIce0/windows_kernel_driver_learn_note.git )
After downloading and decompressing this tool, open it to see a simple interface:
Select "File", "Open Driver" to open the driver file.
Then select "File" - "Start Driver" to complete the loading and running of the driver.
debugging
The routine here is actually obtained from the book "Detailed Explanation of Windows Driver Development Technology" (Zhang Fan). When the driver is loaded, it is normal, but if we use manual uninstallation to start, there will be problems. Use the command to uninstall the driver net
:
net stop helloNt
It turned out that the system blue screened (Windows crashed)
At this time, a simple debugging of the driver is required to find the problem. Although we have added log printing ( KdPrint
) to the driver, when the system crashes, we cannot see the log printout in time (it has already crashed and looked at a hammer, only to see a blue screen). In this case, we need to use windbg dual-machine debugging to carry out. (For the dual-machine debugging environment, please refer to the article " windbg dual-machine debugging environment construction (virtual machine) ")
After starting windbg on the host machine, establish a connection with the virtual machine. Execute windbg traditional debugging steps:
- Load symbol tables (especially for self-compiled drivers)
- Set a breakpoint at the entry of the driver module, or directly operate to crash the system (the crash will automatically stop at the breakpoint)
- View the current call stack and refer to the source code to infer the cause of the crash
- Modify the source code, recompile, load and verify
Here first break down, and then load the symbol table through the command, or use the GUI operation to load the symbol table.
.srcpath+ "C:\Tools\helloNt\*.pdb"
Since the crash is caused by the uninstall operation, we add a breakpoint here in the uninstall function:
After executing net stop helloNt
the command, windbg will automatically jump to the breakpoint (the symbol table is recorded) and display the corresponding code.
Single-step execution, and then find the call stack of the crash point:
here is a problem when calling DeleteSymbolicLink in Unload(). Review our code here:
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);
}
}
It is found that this is called IoDeleteSymbolicLink(&pLinkName);
, and then we print the address of pLinkName here to see if this address space is normally accessible.
You can also use !analyze -v for analysis (this analysis result is for reference)
The results were found to be basically consistent with our predicted positions. The problem arises in this link variable here. Why does it crash when we unload link variables?
Through the data in the Unload local variable, we can see that the memory here pDevExt->ustrSymLinkName;
is not accessible, that is to say, there has been a problem here. Go back to the source code and check the initialization part to find that this string variable is put in when 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;
A string variable devName is created here, and then this variable is placed in pDevExt. But here it is found that the devName is a local variable of this function, and only the string pointer is put in here, and there will definitely be problems if the pointer that has been released in the subsequent function call is taken out and used.
After finding the problem, let's modify it and change the local variable here to a global variable or a static variable to try:
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;
}
It was found that after changing it to a global variable, the result is still the same, and the memory here still cannot be read normally.
The comparison found that this is due to the read error of this part of the buffer space, modify the code, and move the initialization of this part of the string from the #pragma INITCODE
declared CreateDevice
function to DriverEntry
in:
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;
}
And it can be seen that the loading and unloading of the driver operation has been normal. It shows that our judgment is correct.