Windows驱动开发(8) - 派遣函数
1、IRP和派遣函数
驱动程序的主要功能是负责处理I/O请求,其中大部分I/O请求是在派遣函数中处理的。用户模式下所有对驱动程序的I/O请求,全部由操作系统转化为一个叫做IRP的数据结构,不同的IRP数据会被“派遣”到不同的派遣函数(Dispatch Function)中。
1.1 IRP
IRP(I/O request packet)有两个属性,一个是MajorFunction,另外一个是MinorFunction。操作系统根据MajorFunction将IRP 派遣到不同的派遣函数中;在派遣函数中还可以继续判断IRP属于那种MinorFunction。
在DriverEntry的驱动对象pDriverObject中,有个函数指针数组MajorFunction,系统默认这些IRP类型与IoInvalidateDeviceRequest函数关联。
1.2 IRP类型
- IRP_MJ_CREATE:创建设备
- IRP_MJ_CLOSE:关闭设备
- IRP_MJ_CLEANUP:清除工作
- IRP_MJ_DEVICE_CONTROL:DeviceIOControl函数会产生此IRP
- IRP_MJ_PNP:即插即用消息
- IRP_MJ_POWER:处理电源消息
- IRP_MJ_QUERY_INFORMATION:获取信息
- IRP_MJ_READ:读取设备内容
- IRP_MJ_SET_INFORMATION:设置信息
- IRP_MJ_SHUTDOWN:关闭系统
- IRP_MJ_SYSTEM_CONTROL:系统控制
- IRP_MJ_WRITE:对设备进行写入操作
1.3 结束IRP请求
VOID IoCompleteRequest(
IN PIRP Irp;
IN CCHAR PriorityBoost
);
- Irp:需要被结束的IRP
- PriorityBoost:线程恢复时的优先级别
1.4 获取IO堆栈
PIO_STACK_LOCATION IoGetCurrentIrpStackLocation(
IN PIRP Irp
);
2、缓冲区方式读写操作
驱动程序所创建的设备的一般有三种读写方式:缓冲区方式,直接方式,其他方式;对应DO_BUFFERED_IO, DO_DIRECT_IO和 0。
对设备对象的Flags子域设置不同的Flags,会导致以不同的方式操作设备。
DO_BUFFERED_IO的意思是OS将应用程序提供缓冲区的数据复制到内核模式下的地址中来避免由于内核模式下进程切合切换而引起的读写错误问题。一般通过ReadFile,WriteFile来读写。优点是简单的解决了将用户地址地址传入驱动的问题,缺点是需要数据在两个模式下复制,影响效率。
在派遣函数中,通过IO_STACK_LOCATION中的ULONG ulReadLength = stack->Parameters.Read.Length;
来知道ReadFile请求多少字节。再设置 pIrp->IoStatus.Information = ulReadLength;
来记录设备实际操作了多少字节。
3、直接方式读写操作
操作系统将用户模式下的缓冲区锁住,然后操作系统将这段缓冲区在内核模式地址再映射一遍。这样,用户模式的缓冲区和内核模式的缓冲区指向的是同一区域的物理地址。
操作系统将用户模式的地址锁定后,用内存描述符MDL记录这段内存。
这段虚拟内存的大小存储在mdl->ByteCount
里,第一个页地址是mdl->StartVa
,首地址对于第一个页地址的偏移量是mdl->ByteOffset
,首地址是mdl->StartVa + mdl->ByteOffset
。DDK提供了几个宏方便得到这些值。
#define MmGetMdlByteCount(Mdl) ((Mdl)->ByteCount)
#define MmGetMdlByteOffset(Mdl) ((Mdl)->ByteOffset)
#define MmGetMdlVirtualAddress(Mdl) \
((PVOID) ((PCHAR) ((Mdl)->StartVa) + (Mdl)->ByteOffset))
ReadFile的第三个参数为希望读的长度,等于stack->Parameters.Read.Length.派遣函数设置pRrp->IoStatus.Infomation告诉ReadFile实际读了多少字节,对应第四个参数。
pIrp->MdlAddress记录了MDL数据结构
NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,
IN PIRP pIrp)
{
KdPrint(("Enter HelloDDKRead\n"));
//扩展设备
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
NTSTATUS status = STATUS_SUCCESS;
//当前I/O堆栈
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
//获取指定的的读字节数
ULONG ulReadLength = stack->Parameters.Read.Length;
KdPrint(("ulReadLength:%d\n",ulReadLength));
//得到锁到的缓冲区的长度
ULONG mdl_length = MmGetMdlByteCount(pIrp->MdlAddress);
//得到锁到的缓冲区的首地址
PVOID mdl_address = MmGetMdlVirtualAddress(pIrp->MdlAddress);
//得到锁到的缓冲区的偏移量
ULONG mdl_offset = MmGetMdlByteOffset(pIrp->MdlAddress);
KdPrint(("mdl_address:0X%08X\n",mdl_address));
KdPrint(("mdl_length:%d\n",mdl_length));
KdPrint(("mdl_offset:%d\n",mdl_offset));
if (mdl_length!=ulReadLength)
{
//MDL的长度应该和读长度相等,否则该操作应该设为不成功
pIrp->IoStatus.Information = 0;
status = STATUS_UNSUCCESSFUL;
}else
{
//用MmGetSystemAddressForMdlSafe得到MDL在内核模式下的映射
PVOID kernel_address = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority);
KdPrint(("kernel_address:0X%08X\n",kernel_address));
memset(kernel_address,0XAA,ulReadLength);
pIrp->IoStatus.Information = ulReadLength; // bytes xfered
}
pIrp->IoStatus.Status = status;
IoCompleteRequest( pIrp, IO_NO_INCREMENT );
KdPrint(("Leave HelloDDKRead\n"));
return status;
}
4、IO设备控制操作
应用程序可以通过DeviceIOControl操作设备,使操作系统创建一个IRP_MJ_DEVICE_CONTROL类型的IRP,然后将这个IRP转发到派遣函数中。
4.1 DeviceIOControl
BOOL WINAPI DeviceIoControl(
HANDLE hDevice, // 已打开的设备
DWORD dwIoControlCode, //控制码
LPVOID lpInBuffer, //输入缓冲区
DWORD nInBufferSize, //输入缓冲区大小
LPVOID lpOutBuffer, //输出缓冲区
DWORD nOutBufferSize, //输出缓冲区大小
LPDWORD lpBytesReturned, //实际返回字节数
LPOVERLAPPED lpOverlapped //是否OVERLAP操作
);
其中lpBytesReturned对应派遣函数中的IRP结构中的pIrp->IoStatus.Information。
DeviceIOControl的第二个参数是I/O控制码(IOCTL),是一个32位的无符号整数。
宏CTL_CODE可以用来定义IOCTL。
CTL_CODE(DeviceType, Function, Method, Acces)
- DeviceType:设备类型,要与创建设备时的类型匹配。
- Function:驱动程序定义的IOCTL码,其中0x0000~0x7FFF为微软保留,0x800~0xFFFF为程序员自己定义。
- Method:操作模式
- METHOD_BUFFERED:缓冲区方式操作
- METHOD_IN_DIRECT:直接写方式操作
- METHOD_OUT_DIRECT:直接读方式操作
- METHOD_NEITHER:其他方式操作
- Acces:访问权限,一般用FILE_ANY_ACCESS
4.2 缓冲内存模式IOCTL
输入缓冲区大小:stack->Parameters.DeviceIoControl.InputBufferLength
输出缓冲区大小:stack->Parameters.DeviceIoControl.OutputBufferLength
输入缓冲区:pIrp->AssociatedIrp.SystemBuffer
输出缓冲区:pIrp->AssociatedIrp.SystemBuffer
4.3 直接内存模式IOCTL之METHOD_IN_DIRECT
输入缓冲区大小:stack->Parameters.DeviceIoControl.InputBufferLength
输出缓冲区大小:stack->Parameters.DeviceIoControl.OutputBufferLength
输入缓冲区:MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority)
输出缓冲区:pIrp->AssociatedIrp.SystemBuffer
4.4 直接内存模式IOCTL之METHOD_OUT_DIRECT
输入缓冲区大小:stack->Parameters.DeviceIoControl.InputBufferLength
输出缓冲区大小:stack->Parameters.DeviceIoControl.OutputBufferLength
输入缓冲区:MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority)
输出缓冲区:pIrp->AssociatedIrp.SystemBuffer
METHOD_IN_DIRECT与METHOD_OUT_DIRECT区别在于打开设备的权限
1) 只读权限打开设备,METHOD_IN_DIRECT的IOCTL操作成功,而METHOD_OUT_DIRECT的操作失败
2) 读写权限打开设备,METHOD_IN_DIRECT与METHOD_OUT_DIRECT的IOCTL操作都成功
4.5 其他内存模式IOCTL
输入缓冲区大小:stack->Parameters.DeviceIoControl.InputBufferLength
输出缓冲区大小:stack->Parameters.DeviceIoControl.OutputBufferLength
输入缓冲区:ProbeForRead(stack->Parameters.DeviceIoControl.Type3InputBuffer)
输出缓冲区:ProbeForWrite(pIrp->UserBuffer)