分类: WINDOWS
2014-03-07 12:09:59
读写操作
设备对象一共可以有三种读写方式,分别是缓冲区方式读写,直接方式读写,其他方式读写。这三种方式的Flags分别对应为DO_BUFFERED_IO,DO_DIRECT_IO和0。
在驱动程序创建设备对象的时候,需要考虑好该设备是采用何种读写方式。当IoCreateDevice创建完设备后,需要对设备对象的Flags子域进行设置。设置不同的Flags子域会导致以不同的读写方式操作设备。
示例代码:
//创建设备
status = IoCreateDevice(pDriverObj, sizeof(DEVICE_EXTENSION),
&(UNICODE_STRING)ustrDeviceName,
FILE_DEVICE_UNKNOWN, 0, TRUE,
&pDeviceObj);
//判断是否创建成功
if (!NT_SUCCESS(status))
{
return status;
}
//设置读写方式,这里为缓冲区读写方式
pDeviceObj->Flags |= DO_BUFFERED_IO;
1. 缓冲区方式读写设备
读写操作一般是由ReadFile和WriteFile函数引起的。例如,WriteFile要求用户提供一段带有数据的缓冲区,并且说明缓冲区的大小,然后WriteFile将这段内存的数据传入到驱动程序中。
对于缓冲区读写方式来说,操作系统会将用户应用程序提供的缓冲区中的数据复制到内核模式下的地址中。IRP的派遣函数将会对内核模式下的缓冲区进行操作,而不是操作用户模式下的缓冲区。对于ReadFile来说,当IRP请求结束时(一般是由IoCompleteRequest函数结束IRP),这段内存地址会被复制到ReadFile提供的缓冲区中,以此读出在内核中的数据。
这样做的优点是,比较简单的解决了将用户地址传入驱动的问题。缺点是需要在用户模式和内核模式之间复制数据,影响了运行效率。在少量内存操作时,可以使用该方法。
以“缓冲区”方式读写设备时,操作系统会分配一段内核模式下的内存。这段内存大小等于ReadFile或者WriteFile指定的字节数。并且ReadFile或者WriteFile创建的IRP的AssociatedIrp.SystemBuffer子域会记录这段内存地址。
另外,在派遣函数中,我们还可以通过IO_STACK_LOCATION中的Parameters.Read.Length子域知道ReadFile请求多少字节。通过中的Parameters.Write.Length子域知道WriteFile写入多少字节。
然而,WriteFile和ReadFile指定对设备操作多少字节,并不意味着操作了这么多字节。在派遣函数中,应该设置IRP的子域IoStatus.Information。这个子域记录设备实际操作了多少字节。
而用户模式下的ReadFile和WriteFile分别通过各自的第四个参数得到真实操作了多少字节。
示例代码:
驱动:
NTSTATUS FuckRead(IN PDEVICE_OBJECT pDeviceObj, IN PIRP pIrp)
{
KdPrint(("进入IRP_MJ_READ派遣函数!\n"));
PIO_STACK_LOCATION
pIrpStackLoc = IoGetCurrentIrpStackLocation(pIrp);
//readLength 和 ReadFile函数中的第三个参数数值相同
//是想要读取的字节数
ULONG readLength = pIrpStackLoc->Parameters.Read.Length;
//pIrp->IoStatus.Information的值就是ReadFile函数返回的第四个参数的值
//是实际读取的字节数
pIrp->IoStatus.Information = readLength;
pIrp->IoStatus.Status = STATUS_SUCCESS;
//填充内核模式下的缓冲区
RtlFillMemory(pIrp->AssociatedIrp.SystemBuffer, readLength, 'A');
//完成IRP
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
KdPrint(("离开IRP_MJ_READ派遣函数!\n"));
return STATUS_SUCCESS;
}
应用程序:
#include
#include
int main(void)
{
HANDLE hDevice;
hDevice = CreateFile("\\\\.\\HelloDDK",GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hDevice == INVALID_HANDLE_VALUE)
{
DWORD dwError = GetLastError();
printf("%d\n", dwError);
}
//多分配一个字节,使得printf可以读到'\0'结束
char readBuffer[11] = {0};
DWORD ulLength;
ReadFile(hDevice, readBuffer, 10, &ulLength, NULL);
printf("%s\n", readBuffer);
CloseHandle(hDevice);
getchar();
return 0;
}
2
直接方式读写设备
这种方式需要在创建完设备对象后,在设置设备属性的时候,对Flags子域设置为DO_DIRECT_IO。
和缓冲区方式读写设备不同,直接方式读写设备,操作系统会将用户模式下的缓冲区锁住。然后操作系统将这段缓冲区在内核模式地址再次映射一遍。这样,用户模式的缓冲区和内核模式的缓冲区指向的是同一区域的物理内存。
操作系统先将用户模式的地址锁住后,操作系统用内存描述符表(MDL数据结构)记录这段内存。用户模式的这段缓冲区在虚拟内存上是连续的,但是在物理内存上可能是离散的。如下图所示:
MDL记录这段虚拟内存,这段虚拟内存的大小存储在mdl->ByteCount里,这段虚拟内存的第一个页地址是mdl->StartVa,这段虚拟内存的首地址对于第一个页地址偏移量为mdl->ByteOffset。因此,这段虚拟内存的首地址应该是mdl->StartVa +
mdl->ByteOffest。DDK提供了几个宏,方便我们得到这几个数值:
#define MmGetMdlByteCount(Mdl) ((Mdl)->ByteCount)
#define MmGetMdlByteOffsetMdl) ((Mdl)->ByteOffset)
#define MmGetMdlVirtualAddress (Mdl) ((PVOID) ((PCHAR) ((Mdl)->StartVa) + (Mdl)->ByteOffset))
我们通过IRP的pIrp->MdlAddress得到MDL数据结构,这个结构描述了被锁住的缓冲区内存。通过DDK的三个宏MmGetMdlByteCount,MmGetMdlVirtualAddress,MmGetMdlByteOffset可以得到锁住缓冲区的长度,虚拟内存地址,偏移量。
示例代码:
NTSTATUS FuckRead(IN PDEVICE_OBJECT pDeviceObj, IN PIRP pIrp)
{
KdPrint(("进入IRP_MJ_READ派遣函数!\n"));
//得到当前IO堆栈
NTSTATUS status = STATUS_SUCCESS;
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
//获取指定的读字节数
ULONG ulReadLen = stack->Parameters.Read.Length;
KdPrint(("ulReadLen:%d\n", ulReadLen));
//得到锁定缓冲区的长度
ULONG mdl_length = MmGetMdlByteCount(pIrp->MdlAddress);
//得到锁定缓冲区的偏移量
ULONG mdl_offset= MmGetMdlByteOffset(pIrp->MdlAddress);
//得到锁定缓冲区的首地址,用户模式下地址
PVOID mdl_address = MmGetMdlVirtualAddress(pIrp->MdlAddress);
KdPrint(("mdl_address:0x%08X\n", mdl_address));
KdPrint(("mdl_length:%d\n", mdl_length));
KdPrint(("mdl_offset:%d\n", mdl_offset));
//mdl的长度应该和要读取的长度相等,否则操作设为不成功。
if (mdl_length != ulReadLen)
{
pIrp->IoStatus.Information = 0;
status = STATUS_UNSUCCESSFUL;
}
else
{
//用MmGetSystemAddressForMdlSafe得到MDL在内核模式下的映射
PVOID kernel_address = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority);
KdPrint(("kernel_address:0x%08X", kernel_address));
//填充内存
RtlFillMemory(kernel_address, mdl_length, 'B');
pIrp->IoStatus.Information = mdl_length;
}
//设置完成状态
pIrp->IoStatus.Status = status;
//结束IRP请求
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
KdPrint(("离开IRP_MJ_READ派遣函数!\n"));
return status;
}
3. 其他方式读写设备
在调用IoCreateDevice创建设备后,对pDevObj->Flags即不设置DO_BUFFERED_IO,也不设置DO_DIRECT_IO,此时采用的读写方式就是其他读写方式。
在使用其他方式读写设备时,派遣函数直接读写应用程序提供的缓冲区地址。在驱动程序中,直接操作应用程序的缓冲区是很危险的。只有驱动程序与应用程序运行在相同线程上下文的情况下,才能使用这种方式。
用这种方式读写时,ReadFile和WriteFile提供的缓冲区内存地址,可以在派遣函数中通过pIrp->UserBuffer字段得到。需要读取的字节数可以从I/O堆栈中的stack->Parameters.Read.Length字段得到。
使用用户模式的内存时要格外小心,因为ReadFile有可能把空指针地址或者非法地址传递给驱动程序。因此,驱动程序使用用户模式地址前,需要探测这段内存是否可读写。探测可读写,可以使用ProbeForWrite函数和try块。
示例代码:
NTSTATUS FuckRead(IN PDEVICE_OBJECT pDeviceObj, IN PIRP pIrp)
{
KdPrint(("进入IRP_MJ_READ派遣函数!\n"));
NTSTATUS status = STATUS_SUCCESS;
//得到当前堆栈
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
//得到要读取数据的长度
ULONG ulReadLength = stack->Parameters.Read.Length;
//得到用户模式下数据的地址
PVOID user_address = pIrp->UserBuffer;
KdPrint(("user_address:0x%08X\n", user_address));
__try
{
KdPrint(("进入__try块!\n"));
//测试用户模式下的地址是否可写
ProbeForWrite(user_address, ulReadLength, 4);
RtlFillMemory(user_address, ulReadLength, 'C');
KdPrint(("离开__try块!\n"));
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
KdPrint(("进入__except块!\n"));
status = STATUS_UNSUCCESSFUL;
ulReadLength = 0;
}
//设置完成状态
pIrp->IoStatus.Status = status;
//设置操作字节数
pIrp->IoStatus.Information = ulReadLength;
//结束IRP请
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
KdPrint(("离开IRP_MJ_READ派遣函数!\n"));
return status;
}