WDM 驱动程序开发的简单演示
使用三个伪驱动程序的 WDM 驱动程序编程简介。
引言
许多文章都在研究应用程序层的问题,如皮肤对话框、MFC、ATL、线程、进程、注册表等。很难找到任何带有完整源代码的驱动程序相关文章。根本原因在于大多数驱动程序都是为特定硬件开发的。没有领域知识,您将永远不想接触它。我相信许多软件工程师第一次涉及内核模式编程时都会感到恐惧,而且几乎没有资源可以指导他们从 DDK 研究到编程阶段的整个过程。因此,我决定分享一些我在 Windows 驱动程序编程方面的经验。本演示重点介绍了 WDM 驱动程序架构的快速入门,并将介绍 Windows 附带的两种 I/O 模式:直接 I/O 和缓冲 I/O,如何与驻留在系统内核空间的驱动程序通信,以及向其读写数据。
您无需具备任何与硬件相关的背景知识即可阅读演示程序,演示驱动程序均为伪驱动程序。也就是说,这些驱动程序在没有计算机中物理设备的情况下安装。
本演示程序中定义的成员函数可以用作您以后进行驱动程序开发的模板。
背景
您可能是一位经验丰富的软件工程师,并希望涉足内核编程。
创建您的 WDM 驱动程序:伪驱动程序教程
在开始之前,需要对成员例程和结构进行声明。最重要的驱动程序所需数据结构是 - DEVICE_EXTENSION
!
typedef struct tagDEVICE_EXTENSION { PDEVICE_OBJECT DeviceObject; // device object this driver creates PDEVICE_OBJECT NextDeviceObject; // next-layered device object in this // device stack DEVICE_CAPABILITIES pdc; // device capability IO_REMOVE_LOCK RemoveLock; // removal control locking structure LONG handles; // # open handles PVOID DataBuffer; // Internal Buffer for Read/Write I/O UNICODE_STRING Device_Description; // Device Description SYSTEM_POWER_STATE SysPwrState; // Current System Power State DEVICE_POWER_STATE DevPwrState; // Current Device Power State PIRP PowerIrp; // Current Handling Power-Related IRP } DEVICE_EXTENSION, *PDEVICE_EXTENSION;
下面的代码段演示了创建有效 WDM 驱动程序的开始。
WDM 驱动程序中有强制性和可选成员。一个有效的 WDM 驱动程序应包含以下成员例程,DriverEntry
最重要的任务是向内核注册所有成员例程。
// NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath ) { RtlInitUnicodeString( &Global_sz_Drv_RegInfo, RegistryPath->Buffer); // Initialize function pointers DriverObject->DriverUnload = DriverUnload; DriverObject->DriverExtension->AddDevice = AddDevice; DriverObject->MajorFunction[IRP_MJ_CREATE] = PsdoDispatchCreate; DriverObject->MajorFunction[IRP_MJ_CLOSE] = PsdoDispatchClose; DriverObject->MajorFunction[IRP_MJ_READ] = PsdoDispatchRead; DriverObject->MajorFunction[IRP_MJ_WRITE] = PsdoDispatchWrite; DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = PsdoDispatchDeviceControl; DriverObject->MajorFunction[IRP_MJ_POWER] = PsdoDispatchPower; DriverObject->MajorFunction[IRP_MJ_PNP] = PsdoDispatchPnP; return STATUS_SUCCESS; } //
WDM 驱动程序内的正常操作工作流程
下面的代码段演示了 AddDevice
例程中的工作流程:AddDevice
例程最重要的任务是创建一个设备对象,并将其附加到现有的设备堆栈中。
NTSTATUS AddDevice( IN PDRIVER_OBJECT DriverObject, IN PDEVICE_OBJECT PhysicalDeviceObject ) { ULONG DeviceExtensionSize; PDEVICE_EXTENSION p_DVCEXT; PDEVICE_OBJECT ptr_PDO; NTSTATUS status; RtlInitUnicodeString( &Global_sz_DeviceName, L""); //Get DEVICE_EXTENSION required memory space DeviceExtensionSize = sizeof(DEVICE_EXTENSION); //Create Device Object status = IoCreateDevice( DriverObject, DeviceExtensionSize, &Global_sz_DeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &ptr_PDO ); if (NT_SUCCESS(status)) { ptr_PDO->Flags &= ~DO_DEVICE_INITIALIZING; ptr_PDO->Flags |= DO_BUFFERED_IO; //For Buffered I/O //ptr_PDO->Flags |= DO_DIRECT_IO; //For Direct I/O p_DVCEXT = ptr_PDO->DeviceExtension; p_DVCEXT->DeviceObject = ptr_PDO; RtlInitUnicodeString( /* //Other initialization tasks go here */ //Store next-layered device object //Attach device object to device stack p_DVCEXT->NextDeviceObject = IoAttachDeviceToDeviceStack(ptr_PDO, PhysicalDeviceObject); } return status; }
下面的代码段展示了如何支持 IRP_MJ_CREATE
,当客户端应用程序尝试连接到底层伪驱动程序时会发送此 IRP。在继续之前,请先查看下面的图表以了解连接过程。
通常,您会使用 CreateFile
/fopen
Win32 API 来连接到底层设备。此时 Win32 子系统会提交 IRP_MJ_CREATE
并要求驱动程序连接到目标设备!
NTSTATUS PsdoDispatchCreate( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) { PIO_STACK_LOCATION p_IO_STK; PDEVICE_EXTENSION p_DVCEXT; NTSTATUS status; p_IO_STK = IoGetCurrentIrpStackLocation(Irp); p_DVCEXT = DeviceObject->DeviceExtension; status = IoAcquireRemoveLock(&p_DVCEXT->RemoveLock, p_IO_STK->FileObject); if (NT_SUCCESS(status)) { CompleteRequest(Irp, STATUS_SUCCESS, 0); return STATUS_SUCCESS; } else { IoReleaseRemoveLock(&p_DVCEXT->RemoveLock, p_IO_STK->FileObject); CompleteRequest(Irp, status, 0); return status; } }
下面的代码段展示了如何支持 IRP_MJ_CLOSE
,当客户端应用程序尝试关闭与底层伪驱动程序的连接时会发送此 IRP。在继续之前,请先查看下面的图表以了解关闭过程。
通常,您会使用 CloseHandle
/fclose
Win32 API 来关闭与底层设备的连接。此时 Win32 子系统会提交 IRP_MJ_CLOSE
并要求驱动程序关闭与目标设备的连接!
NTSTATUS PsdoDispatchClose( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) { PIO_STACK_LOCATION p_IO_STK; PDEVICE_EXTENSION p_DVCEXT; p_IO_STK = IoGetCurrentIrpStackLocation(Irp); p_DVCEXT = DeviceObject->DeviceExtension; IoReleaseRemoveLock(&p_DVCEXT->RemoveLock, p_IO_STK->FileObject); CompleteRequest(Irp, STATUS_SUCCESS, 0); return STATUS_SUCCESS; }
I/O 支持:缓冲 I/O 模式
Windows 内核中有三种 I/O 模式:缓冲模式、直接模式和无模式。现在,我们将讨论缓冲 I/O,本文不涉及在用户线程占用内存空间进行处理时的数据传输的无模式,这可能会很危险!如果客户端应用程序要向驱动程序读写数据,驱动程序不会直接引用数据源的内存地址。系统内核会在内核中分配一个大小相同的另一个数据缓冲区。在所有数据传输到目标位置之前,都必须将数据复制到此区域。通常,您会调用 ReadFile
/WriteFile
或 fread
/fwrite
来发出读/写请求。
下面的代码段演示了读请求的 I/O 处理工作流程。正如我们所见,注册用于读取的例程是 DriverEntry
中的 PsdoDispatchRead
,此成员例程会将数据从驱动程序的内部成员 - DataBuffer
中读取到客户端应用程序。
NTSTATUS PsdoDispatchRead( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) { PVOID Buf; //Buffer provided by user program ULONG BufLen; //Buffer length for user provided buffer LONGLONG Offset;//Buffer Offset PVOID DataBuf; //Buffer provided by Driver ULONG DataLen; //Buffer length for Driver Data Buffer ULONG ByteTransferred; PIO_STACK_LOCATION p_IO_STK; PDEVICE_EXTENSION p_DVCEXT; DbgPrint("IRP_MJ_READ : Begin\r\n"); //Get I/o Stack Location & Device Extension p_IO_STK = IoGetCurrentIrpStackLocation(Irp); p_DVCEXT = DeviceObject->DeviceExtension; //Get User Output Buffer & Length BufLen = p_IO_STK->Parameters.Read.Length; Offset = p_IO_STK->Parameters.Read.ByteOffset.QuadPart; Buf = (PUCHAR)(Irp->AssociatedIrp.SystemBuffer) + Offset; //Get Driver Data Buffer & Length DataBuf = p_DVCEXT->DataBuffer; if (DataBuf == NULL) DataLen = 0; else DataLen = 1024; IoAcquireRemoveLock(&p_DVCEXT->RemoveLock, Irp); DbgPrint("Output Buffer Length : %d\r\n", BufLen); DbgPrint("Driver Data Length : %d\r\n", DataLen); // if (BufLen <= DataLen) { ByteTransferred = BufLen; } else { ByteTransferred = DataLen; } RtlCopyMemory( Buf, DataBuf, ByteTransferred); IoReleaseRemoveLock(&p_DVCEXT->RemoveLock, Irp); CompleteRequest(Irp, STATUS_SUCCESS, ByteTransferred); DbgPrint("IRP_MJ_READ : End\r\n"); return STATUS_SUCCESS; }
下面的代码段演示了支持从应用程序到驱动程序进行正常写数据 I/O 请求的可能任务项。
NTSTATUS PsdoDispatchWrite( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) { PVOID Buf; //Buffer provided by user program ULONG BufLen; //Buffer length for user provided buffer LONGLONG Offset;//Buffer Offset PVOID DataBuf; //Buffer provided by Driver ULONG DataLen; //Buffer length for Driver Data Buffer ULONG ByteTransferred; PIO_STACK_LOCATION p_IO_STK; PDEVICE_EXTENSION p_DVCEXT; NTSTATUS status; DbgPrint("IRP_MJ_WRITE : Begin\r\n"); //Get I/o Stack Location & Device Extension p_IO_STK = IoGetCurrentIrpStackLocation(Irp); p_DVCEXT = DeviceObject->DeviceExtension; //Get User Input Buffer & Length BufLen = p_IO_STK->Parameters.Write.Length; Offset = p_IO_STK->Parameters.Read.ByteOffset.QuadPart; Buf = (PUCHAR)(Irp->AssociatedIrp.SystemBuffer) + Offset; //Get Driver Data Buffer & Length DataBuf = p_DVCEXT->DataBuffer; DataLen = 1024; IoAcquireRemoveLock(&p_DVCEXT->RemoveLock, Irp); DbgPrint("Input Buffer Length : %d\r\n", BufLen); DbgPrint("Driver Data Length : %d\r\n", DataLen); if (BufLen <= DataLen) { ByteTransferred = BufLen; } else { ByteTransferred = DataLen; } ByteTransferred = BufLen; RtlZeroMemory( p_DVCEXT->DataBuffer, 1024); RtlCopyMemory( DataBuf, Buf, ByteTransferred); IoReleaseRemoveLock(&p_DVCEXT->RemoveLock, Irp); CompleteRequest(Irp, STATUS_SUCCESS, ByteTransferred); DbgPrint("IRP_MJ_WRITE : End\r\n"); return STATUS_SUCCESS; }
I/O 支持:直接 I/O 模式
下面的图表展示了在客户端应用程序和驱动程序之间传输数据时如何支持直接 I/O 模式。在直接 I/O 模式下,内存管理器会创建 MDL(内存描述符列表)来引用用户提供的缓冲区所占用的物理地址,所有数据都可以通过 MDL 从内核环境直接引用。
在 DDK 中,提供了一些 MMXxx
例程来帮助您获取映射到用户提供的缓冲区物理地址的 MDL。
下面的代码段包含支持直接 I/O 模式下数据读取的语句。这是通过 Mmxxx
例程实现的,请仔细阅读,您也可以在 zip 文件中找到完整代码。在此模式下,您将使用的最重要的 MmXxx
应该是 - MmGetSystemAddressForMdlSafe
,它可以获取引用用户缓冲区物理地址的 MDL。
NTSTATUS PsdoDispatchRead( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) { PVOID Buf; //Buffer provided by user program ULONG BufLen; //Buffer length for user provided buffer ULONG Offset;//Buffer Offset PVOID DataBuf; //Buffer provided by Driver ULONG DataLen; //Buffer length for Driver Data Buffer ULONG ByteTransferred; PIO_STACK_LOCATION p_IO_STK; PDEVICE_EXTENSION p_DVCEXT; DbgPrint("IRP_MJ_READ : Begin\r\n"); //Get I/o Stack Location & Device Extension p_IO_STK = IoGetCurrentIrpStackLocation(Irp); p_DVCEXT = DeviceObject->DeviceExtension; //Get User Output Buffer & Length Buf = MmGetSystemAddressForMdlSafe( Irp->MdlAddress, HighPagePriority); if (Buf == NULL) { DbgPrint("Can't get Virtual Address from MDL\r\n"); return STATUS_INSUFFICIENT_RESOURCES; } BufLen = MmGetMdlByteCount(Irp->MdlAddress); Offset = MmGetMdlByteOffset(Irp->MdlAddress); //Get Driver Data Buffer & Length DataBuf = p_DVCEXT->DataBuffer; if (DataBuf == NULL) DataLen = 0; else DataLen = 1024; IoAcquireRemoveLock(&p_DVCEXT->RemoveLock, Irp); DbgPrint("Output Buffer Length : %d\r\n", BufLen); DbgPrint("Offset for Buffer in the Memory Page: %d\r\n", Offset); DbgPrint("Driver Data Length : %d\r\n", DataLen); // if (BufLen <= DataLen) { ByteTransferred = BufLen; } else { ByteTransferred = DataLen; } RtlCopyMemory( Buf, DataBuf, ByteTransferred); IoReleaseRemoveLock(&p_DVCEXT->RemoveLock, Irp); CompleteRequest(Irp, STATUS_SUCCESS, ByteTransferred); DbgPrint("IRP_MJ_READ : End\r\n"); return STATUS_SUCCESS; }
下面的代码段演示了从用户应用程序写入驱动程序的可能工作流程。
NTSTATUS PsdoDispatchWrite( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) { PVOID Buf; //Buffer provided by user program ULONG BufLen; //Buffer length for user provided buffer ULONG Offset;//Buffer Offset PVOID DataBuf; //Buffer provided by Driver ULONG DataLen; //Buffer length for Driver Data Buffer ULONG ByteTransferred; PIO_STACK_LOCATION p_IO_STK; PDEVICE_EXTENSION p_DVCEXT; NTSTATUS status; DbgPrint("IRP_MJ_WRITE : Begin\r\n"); //Get I/o Stack Location & Device Extension p_IO_STK = IoGetCurrentIrpStackLocation(Irp); p_DVCEXT = DeviceObject->DeviceExtension; //Get User Input Buffer & Length Buf = MmGetSystemAddressForMdlSafe( Irp->MdlAddress, HighPagePriority); if (Buf == NULL) { DbgPrint("Can't get Virtual Address from MDL\r\n"); return STATUS_INSUFFICIENT_RESOURCES; } BufLen = MmGetMdlByteCount(Irp->MdlAddress); Offset = MmGetMdlByteOffset(Irp->MdlAddress); //Get Driver Data Buffer & Length DataBuf = p_DVCEXT->DataBuffer; DataLen = 1024; IoAcquireRemoveLock(&p_DVCEXT->RemoveLock, Irp); DbgPrint("Input Buffer Length : %d\r\n", BufLen); DbgPrint("Offset for Buffer in the Memory Page: %d\r\n", Offset); DbgPrint("Driver Data Length : %d\r\n", DataLen); if (BufLen <= DataLen) { ByteTransferred = BufLen; } else { ByteTransferred = DataLen; } ByteTransferred = BufLen; RtlZeroMemory( p_DVCEXT->DataBuffer, 1024); RtlCopyMemory( DataBuf, Buf, ByteTransferred); IoReleaseRemoveLock(&p_DVCEXT->RemoveLock, Irp); CompleteRequest(Irp, STATUS_SUCCESS, ByteTransferred); DbgPrint("IRP_MJ_WRITE : End\r\n"); return STATUS_SUCCESS; }
源代码 zip 包内容
zip 文件包含以下子文件夹
- Application:包含伪驱动程序的客户端应用程序。
- bin:包含伪驱动程序的安装/卸载实用程序。
- BufferedIO_PW:这里是使用缓冲 I/O 模式进行读写的伪驱动程序所在。
- DirectIO_PW:这里是使用直接 I/O 模式进行读写的伪驱动程序所在。
- IOCTL_PW:这里是仅支持用户定义 I/O 控制代码的伪驱动程序所在。
- ShareFiles:这是 PnP、电源管理、I/O 完成的通用共享库。
- Install:包含安装/卸载实用程序的源代码。(安装实用程序直接引用自 DDK 示例,我将不提供冗余副本,仅提供卸载实用程序的源代码。)
如何构建伪驱动程序?
- 将软件包解压缩到您喜欢的某个文件夹,我们称之为 ROOT_OF_SOURCE。
- 选择 开始->程序->开发工具包->Windows DDK xxxx.xxxx->构建环境->免费构建。(这是针对不包含调试信息的免费版本。)
- 进入 ROOT_OF_SOURCE\SharedFiles 子文件夹,输入 build -cefw,一切顺利,将生成共享库。
- 进入 ROOT_OF_SOURCE\BufferedIO_PW 子文件夹,输入 build -cefw,它将创建伪驱动程序 - BufferDrv.sys。如果添加了任何新功能插件,请将此文件复制到 ROOT_OF_SOURCE\BufferedIO_PW\Install,以供后续驱动程序安装使用。
- 进入 ROOT_OF_SOURCE\DirectIO_PW 子文件夹,输入 build -cefw,它将创建伪驱动程序 - DirectDrv.sys。如果添加了任何新功能插件,请将此文件复制到 ROOT_OF_SOURCE\DirectIO_PW\Install,以供后续驱动程序安装使用。
- 进入 ROOT_OF_SOURCE\IOCTL_PW 子文件夹,输入 build -cefw,它将创建伪驱动程序 - PseudoDrv.sys。如果添加了任何新功能插件,请将此文件复制到 ROOT_OF_SOURCE\IOCTL_PW\Install,以供后续驱动程序安装使用。
将伪驱动程序安装到系统(XP)
- 解压缩源文件,启动 DOS 命令行控制台。
- 进入 bin 子文件夹。
- 执行 DevInst.bat,它将自动将伪驱动程序安装到您的系统中。
从系统(XP)中卸载伪驱动程序
- 进入 bin 子文件夹。
- 执行 DevRemove.bat,它将自动从您的系统中卸载所有与驱动程序相关的资源。
执行客户端应用程序
您可以进入 ROOT_OF_SOURCE\Application 子文件夹,执行 bufferclient.exe、directclient.exe 和 clientapp.exe 来验证三个伪驱动程序是否已成功安装。
已知问题
- 伪驱动程序的安装/卸载在 Windows 2000 上不起作用,根本原因可能是 Setup API 在 Windows 2000 上不起作用,不允许安装没有物理硬件的驱动程序。有谁能帮忙解决吗?非常感谢。
- 如果您想在 Windows 2000 中安装/卸载伪驱动程序,您需要从设备管理器中启动“添加新硬件向导”,然后选择安装新硬件->显示所有硬件->从磁盘安装->"ROOT_OF_SOURCE\BufferedIO_PW\Install",然后单击“确定”按钮。添加新硬件向导将安装缓冲 I/O 伪驱动程序。(这是为缓冲 I/O 演示驱动程序安装准备的。对于直接 I/O,请将源目录设置为“ROOT_OF_SOURCE\DirectIO_PW\Install”)。
- 如果驱动程序在卸载后重新安装,则需要重新启动。我不知道为什么会发生这种情况,希望有人能告诉我。非常感谢。
伪驱动程序的未来发展方向
- 修复上述问题。
- 将在伪驱动程序中添加 WMI 支持。
历史
- 于 2002/02/10 开始创建三个伪驱动程序,于 2003/12/28 完成(其余时间用于此),并在我的书出版后于 2004/10/20 发布。