VS2013 WDK8.1驱动开发8(设备读写操作)
本系列博客为学习《Windows驱动开发技术详解》一书的学习笔记。
前言
从上节内容我们知道,Win32API ReadFile和WriteFile会分别产生IRP_MJ_READ和IRP_MJ_WRITE这两种IRP,本节我们会实现这两种IRP的派遣函数。
缓冲区方式读写操作
缓冲区设备
在创建设备的时候我们需要考虑设备需要采用何种读写方式,在之前的代码中我们默认都是采用缓冲区读写的方式:
// 创建设备对象
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;
设备对象的Flags被设置为DO_BUFFERED_IO意思就是采用缓冲区的方式进行读写设备。
我们知道应用程序调用ReadFile或WriteFile函数时都要提供一段缓冲区,前面我们也说过使用ReadFile或WriteFile函数时操作系统会调用相应的驱动对象的派遣函数。应用程序提供的缓冲区在用户模式地址空间,驱动程序直接使用这段内存是不安全的,因为随着进程的切换,驱动程序访问的内存地址必然是错误的。如果我们设置设备对象为缓冲区方式读写,那么在进行读写操作的时候操作系统会在内核地址空间分配一个相同大小的缓冲区给驱动程序进行操作,写操作前会复制应用程序缓冲区的内容到内核地址空间中,读操作完成后会复制内核地址空间缓冲区的内容到应用程序的缓冲区中。
内核地址空间的缓冲区地址保存在IRP的AssociatedIrop.SystemBuffer中,IO_STACK_LOCATION中的Parameters.Read.Length记录ReadFile请求多少字节,Parameters.Write.Length记录WriteFile请求多少字节。
如下图:
缓冲区设备模拟文件读写
我们可以把我们创建的设备对象模拟成一个文件进行读写,先定义如下的设备扩展结构:
/// @brief 设备扩展结构
typedef struct _DEVICE_EXTENSION
{
PDEVICE_OBJECT PDeviceObject;
UNICODE_STRING DeviceName; ///< 设备名称
UNICODE_STRING SymLinkName; ///< 符号链接名
unsigned char* PFileBuffer; ///< 模拟的文件缓冲区
ULONG FileLength; ///< 文件长度
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
PFileBuffer用于模拟文件缓冲区,FileLength用于记录文件长度。在创建设备对象时要分配该缓冲区,在卸载驱动时要释放该缓冲区。
...
// 分配空间
pDevExt->PFileBuffer = (unsigned char*)ExAllocatePool(NonPagedPool, MAX_FILE_LENGTH);
pDevExt->FileLength = 0;
...
// 释放空间
if (NULL != pDevExt->PFileBuffer)
{
ExFreePool(pDevExt->PFileBuffer);
pDevExt->PFileBuffer = NULL;
}
...
我们先完成一个写派遣函数如下:
/// @brief 缓冲区写例程
/// @param[in] pDevObject
/// @param[in] pIrp
/// @return
NTSTATUS BufferedWriteRoutine(IN PDEVICE_OBJECT pDevObject, IN PIRP pIrp)
{
KdPrint(("\nEnter BufferedWriteRoutine\n"));
NTSTATUS status = STATUS_SUCCESS;
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObject->DeviceExtension;
PIO_STACK_LOCATION pIoStack = IoGetCurrentIrpStackLocation(pIrp);
// 获取要写入的字节数以及偏移
ULONG writeLength = pIoStack->Parameters.Write.Length;
ULONG writeOffset = (ULONG)pIoStack->Parameters.Write.ByteOffset.QuadPart;
ULONG completeLength = 0;
if ((writeLength + writeOffset) > MAX_FILE_LENGTH)
{
status = STATUS_FILE_INVALID;
completeLength = 0;
goto SAFE_EXIT;
}
// 写入数据
memcpy(pDevExt->PFileBuffer + writeOffset, pIrp->AssociatedIrp.SystemBuffer, writeLength);
if ((writeLength + writeOffset) > pDevExt->FileLength)
{
pDevExt->FileLength = writeOffset + writeLength;
}
status = STATUS_SUCCESS;
completeLength = writeLength;
SAFE_EXIT:
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = completeLength;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
KdPrint(("Leave BufferedWriteRoutine\n"));
return status;
}
在写派遣函数中我们从pIrp->AssociatedIrp.SystemBuffer取出内容复制到我们自己的缓冲区,写的长度和偏移地址在pIoStack->Parameters.Write参数中。
我们再完成一个读派遣函数如下:
/// @brief 缓冲区读例程
/// @param[in] pDevObject
/// @param[in] pIrp
/// @return
NTSTATUS BufferedReadRoutine(IN PDEVICE_OBJECT pDevObject, IN PIRP pIrp)
{
KdPrint(("\nEnter BufferedReadRoutine\n"));
NTSTATUS status = STATUS_SUCCESS;
PIO_STACK_LOCATION pStack = IoGetCurrentIrpStackLocation(pIrp);
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObject->DeviceExtension;
// 获取需要读设备的字节数和偏移
ULONG readLength = pStack->Parameters.Read.Length;
ULONG readOffset = (ULONG)pStack->Parameters.Read.ByteOffset.QuadPart;
ULONG completeLength = 0;
if (readOffset + readLength > MAX_FILE_LENGTH)
{
status = STATUS_FILE_INVALID;
completeLength = 0;
goto SAFE_EXIT;
}
memcpy(pIrp->AssociatedIrp.SystemBuffer, pDevExt->PFileBuffer + readOffset, readLength);
status = STATUS_SUCCESS;
completeLength = readLength;
SAFE_EXIT:
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = completeLength;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
KdPrint(("Leave BufferedReadRoutine\n"));
return STATUS_SUCCESS;
}
在读派遣函数中我们复制我们自己缓冲区中的内容到pIrp->AssociatedIrp.SystemBuffer中,复制的长度和偏移在pStack->Parameters.Read参数中。
在DriverEntry中注册派遣函数:
/// @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"));
UNREFERENCED_PARAMETER(pRegPath);
// 注册驱动调用函数入口
// 这些函数不是由驱动程序本身负责调用, 而是由操作系统负责调用
pDriverObject->DriverUnload = HelloNTDriverUnload;
pDriverObject->MajorFunction[IRP_MJ_CREATE] = HelloNTDriverDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloNTDriverDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = BufferedWriteRoutine;
pDriverObject->MajorFunction[IRP_MJ_READ] = BufferedReadRoutine;
// 创建驱动设备对象
status = CreateDevice(pDriverObject);
KdPrint(("Leave DriverEntry\n"));
return status;
}
Win32程序代码:
// 写设备, 触发IRP_MJ_WRITE
UCHAR writeBuffer[10] = { 0 };
ULONG writeLen = 0;
memset(writeBuffer, 0xAB, 10);
BOOL iRet = WriteFile(hDevice, writeBuffer, 10, &writeLen, NULL);
if (TRUE == iRet)
{
printf("Writed Length: %u\n", writeLen);
}
// 读设备, 触发IRP_MJ_READ
UCHAR readBuffer[10] = { 0 };
ULONG readedLen = 0;
iRet = ReadFile(hDevice, readBuffer, 10, &readedLen, NULL);
if (TRUE == iRet)
{
printf("Readed Length : %u\n", readedLen);
for (ULONG i = 0; i < readedLen; i++)
{
printf("%02X ", readBuffer[i]);
}
printf("\n");
}
运行结果:
直接方式读写操作
除了缓冲区方式读写设备外,另外一种方式是直接方式读写设备。这种方式创建完设备对象后,在设置设备属性的时候,设置为DO_DIRECT_IO:
// 创建设备对象
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_DIRECT_IO;
直接方式读写设备,操作系统会将用户模式下的缓冲区锁住,然后操作系统将这段缓冲区在内核模式地址再次映射一遍。映射后的虚拟内存地址记录在MDL数据结构中,这段虚拟内存的第一个页地址是mdl->StartVa,这段虚拟内存的首地址对于第一个页地址的偏移量是mdl->ByteOffset,这段虚拟内存的大小是mdl->ByteCount。通过IRP的pTrp->MdlAddress可以得到MDL数据结构。
如下图:
完成一个直接方式读例程如下:
/// @brief 直接读例程
/// @param[in] pDevObject
/// @param[in] pIrp
/// @return
NTSTATUS DirectReadRoutine(IN PDEVICE_OBJECT pDevObject, IN PIRP pIrp)
{
KdPrint(("\nEnter DirectReadRoutine\n"));
NTSTATUS status = STATUS_SUCCESS;
PIO_STACK_LOCATION pStack = IoGetCurrentIrpStackLocation(pIrp);
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObject->DeviceExtension;
// 获取需要读设备的字节数和偏移
ULONG readLength = pStack->Parameters.Read.Length;
ULONG readOffset = (ULONG)pStack->Parameters.Read.ByteOffset.QuadPart;
ULONG completeLength = 0;
if (readOffset + readLength > MAX_FILE_LENGTH)
{
status = STATUS_FILE_INVALID;
completeLength = 0;
goto SAFE_EXIT;
}
// 获取MDL地址
PMDL pMdl = pIrp->MdlAddress;
PVOID mdlAddress = (PVOID)((PCHAR)(pMdl->StartVa) + pMdl->ByteOffset);
ULONG mdlLength = pMdl->ByteCount;
if (mdlLength < readLength)
{
status = STATUS_FILE_INVALID;
completeLength = 0;
goto SAFE_EXIT;
}
memcpy(mdlAddress, pDevExt->PFileBuffer + readOffset, readLength);
status = STATUS_SUCCESS;
completeLength = readLength;
SAFE_EXIT:
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = completeLength;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
KdPrint(("Leave DirectReadRoutine\n"));
return STATUS_SUCCESS;
}
完成一个直接方式写例程如下:
/// @brief 直接写例程
/// @param[in] pDevObject
/// @param[in] pIrp
/// @return
NTSTATUS DirectWriteRoutine(IN PDEVICE_OBJECT pDevObject, IN PIRP pIrp)
{
KdPrint(("\nEnter DirectWriteRoutine\n"));
NTSTATUS status = STATUS_SUCCESS;
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObject->DeviceExtension;
PIO_STACK_LOCATION pIoStack = IoGetCurrentIrpStackLocation(pIrp);
// 获取要写入的字节数以及偏移
ULONG writeLength = pIoStack->Parameters.Write.Length;
ULONG writeOffset = (ULONG)pIoStack->Parameters.Write.ByteOffset.QuadPart;
ULONG completeLength = 0;
if ((writeLength + writeOffset) > MAX_FILE_LENGTH)
{
status = STATUS_FILE_INVALID;
completeLength = 0;
goto SAFE_EXIT;
}
// 获取MDL地址
PMDL pMdl = pIrp->MdlAddress;
PVOID mdlAddress = (PVOID)((PCHAR)(pMdl->StartVa) + pMdl->ByteOffset);
ULONG mdlLength = pMdl->ByteCount;
if (mdlLength < writeLength)
{
status = STATUS_FILE_INVALID;
completeLength = 0;
goto SAFE_EXIT;
}
// 写入数据
memcpy(pDevExt->PFileBuffer + writeOffset, mdlAddress, writeLength);
if ((writeLength + writeOffset) > pDevExt->FileLength)
{
pDevExt->FileLength = writeOffset + writeLength;
}
status = STATUS_SUCCESS;
completeLength = writeLength;
SAFE_EXIT:
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = completeLength;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
KdPrint(("Leave DirectWriteRoutine\n"));
return status;
}
程序测试运行结果如下:
后话
本文完整工程和代码托管在GitHub上猛戳我。
其他章节链接
VS2013 WDK8.1驱动开发3(手动加载NT驱动程序)
VS2013 WDK8.1驱动开发8(设备读写操作)
还没有人评论...