驱动开发之 用DeviceIoControl实现应用程序与驱动程序通信
1.
readfile和writefile可以实现应用程序与驱动程序通信,另外一个Win32 API 是DeviceIoControl。
应用程序自定义一中IO控制码,然后调用DeviceIoControl函数,IO管理器会产生一个MajorFunction 为IRP_MJ_DEVICE_CONTROL,MinorFunction 为自己定义的控制码的IRP,系统就调用相应的处理IRP_MJ_DEVICE_CONTROL的派遣函数,你在派遣函数中判断MinorFunction ,是自定义的控制码你就进行相应的处理。
2.
首先介绍一下DeviceIoControl函数
BOOL WINAPI DeviceIoControl( _In_ HANDLE hDevice, //已经打开的设备句柄 _In_ DWORD dwIoControlCode,//自定义的控制码,稍后介绍怎么定义 _In_opt_ LPVOID lpInBuffer, //输入缓冲区 _In_ DWORD nInBufferSize, //输入缓冲区的大小 _Out_opt_ LPVOID lpOutBuffer, //输出缓冲区 _In_ DWORD nOutBufferSize, //输出缓冲区的大小 _Out_opt_ LPDWORD lpBytesReturned, //实际返回的字节数,对应驱动程序中pIrp->IoStatus.Information。 _Inout_opt_ LPOVERLAPPED lpOverlapped //重叠操作结构指针。同步设为NULL,DeviceIoControl将进行阻塞调用;否则,应在编程时按异步操作设计);例如:
UCHAR InputBuffer[10];UCHAR OutputBuffer[10];//将输入缓冲区全部置成0XBBmemset(InputBuffer,0xBB,10);DWORD dwOutput;//输入缓冲区作为输入,输出缓冲区作为输出BOOL bRet = DeviceIoControl(hDevice, IOCTL_TEST, InputBuffer, 10, &OutputBuffer, 10, &dwOutput, (LPOVERLAPPED)NULL);
3.
定义IO控制码
#define IOCTL_Device_Function CTL_CODE(DeviceType, Function, Method, Access)
IOCTL_Device_Function:生成的IRP的MinorFunction
DeviceType:设备对象的类型。设备类型可参考:http://blog.csdn.net/liyun123gx/article/details/38058965
Function :自定义的IO控制码。自己定义时取0x800到0xFFF,因为0x0到0x7FF是微软保留的。
Method :数据的操作模式。
METHOD_BUFFERED:缓冲区模式
METHOD_IN_DIRECT:直接写模式
METHOD_OUT_DIRECT:直接读模式
METHOD_NEITHER :Neither模式
Access:访问权限,可取值有:
FILE_ANY_ACCESS:表明用户拥有所有的权限
FILE_READ_DATA:表明权限为只读
FILE_WRITE_DATA:表明权限为可写
也可以 FILE_WRITE_DATA | FILE_READ_DATA:表明权限为可读可写,但还没达到FILE_ANY_ACCESS的权限。
例如:#define IOCTL_TEST CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
4.下面介绍不同的数据操作模式
(1).METHOD_BUFFERED:缓冲区模式
用户提供的输入缓冲区的内容被复制到IRP中的pIrp->AssociatedIrp.SystemBuffer内存地址,复制的长度是DeviceIoControl指定的输入字节数。
驱动程序输出数据时,还可以向pIrp->AssociatedIrp.SystemBuffer中写入,操作系统会将此地址的数据复制到DeviceIoControl的输出缓冲区。
复制的字节数通过设置pIrp->IoStatus.Information来指定。
派遣函数中通过下面代码得到输入缓冲区输出缓冲的大小以及IOCTL
//得到当前堆栈PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);//得到输入缓冲区大小ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength;//得到输出缓冲区大小ULONG cbout = stack->Parameters.DeviceIoControl.OutputBufferLength;//得到IOCTL码ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
通过操作pIrp->AssociatedIrp.SystemBuffer来进行数据的输入输出
UCHAR* InputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;for (ULONG i=0;i<cbin;i++){ KdPrint(("%X\n",InputBuffer[i]));}//操作输出缓冲区,输出缓冲区和输入缓冲区是一个缓冲区UCHAR* OutputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;memset(OutputBuffer,0xAA,cbout);//设置实际操作输出缓冲区长度 pIrp->IoStatus.Information = cbout;(2) METHOD_IN_DIRECT与METHOD_OUT_DIRECT 直接内存模式
与缓冲模式相同,用户提供的输入缓冲区的内容被复制到IRP中的pIrp->AssociatedIrp.SystemBuffer内存地址,复制的长度是DeviceIoControl指定的输入字节数。
直接内存模式中,操作系统会将DeviceIoControl指定的输出缓冲区锁定,然后在内核模式地址下重新映射一段地址。
派遣函数中IRP中的pIrp->MdlAddress记录DeviceIoControl指定的输出缓冲区。派遣函数应该使用MmGetSystemAddressForMdlSafe将这段内存映射到内核模式下的内存地址。
得到输入输出缓冲区的大小以及IOCTL的方式与缓冲区模式相同。
另外需要注意CTL_CODE设置的权限问题,若以只读方式打开设备,METHOD_IN_DIRECT的IOCTL操作会失败。
派遣函数中处理直接内存模式:
//显示输入缓冲区数据UCHAR* InputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;for (ULONG i=0;i<cbin;i++){ KdPrint(("%X\n",InputBuffer[i]));}//pIrp->MdlAddress为DeviceIoControl输出缓冲区地址相同KdPrint(("User Address:0X%08X\n",MmGetMdlVirtualAddress(pIrp->MdlAddress)));UCHAR* OutputBuffer = (UCHAR*)MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority);//InputBuffer被映射到内核模式下的内存地址,必定在0X80000000-0XFFFFFFFF之间memset(OutputBuffer,0xAA,cbout);
(3)METHOD_NEITHER :Neither模式
因为此模式直接访问用户模式地址,这是很危险的,所以此模式很少被用到。
使用用户模式地址必须保证调用DeviceIoControl 的线程与派遣函数运行在同一个线程上下文中。
派遣函数得到输入缓冲区的方式与前两种不同,此模式是通过IO堆栈的stack->Parameters.DeviceIoControl.Type3InputBuffer;得到输入缓冲区。
驱动通过pIrp->UserBuffer得到输出缓冲区。
得到输入输出缓冲区的长度与IOCTL的方式与前两种相同。
由于驱动程序的派遣函数不能保证传递进来的用户地址是合法地址,所以要对传入的用户模式地址进行可读写判断。这就需要ProbeForRead函数和ProbeForWrite函数与_try _execpt 结合使用。
下面是驱动派遣函数中Neither模式
//显示输入缓冲区数据UCHAR* UserInputBuffer = (UCHAR*)stack->Parameters.DeviceIoControl.Type3InputBuffer;KdPrint(("UserInputBuffer:0X%0X\n",UserInputBuffer));//得到用户模式地址PVOID UserOutputBuffer = pIrp->UserBuffer;KdPrint(("UserOutputBuffer:0X%0X\n",UserOutputBuffer));__try{ KdPrint(("Enter __try block\n")); //判断指针是否可读 ProbeForRead(UserInputBuffer,cbin,4); //显示输入缓冲区内容 for (ULONG i=0;i<cbin;i++) { KdPrint(("%X\n",UserInputBuffer[i])); } //判断指针是否可写 ProbeForWrite(UserOutputBuffer,cbout,4); //操作输出缓冲区 memset(UserOutputBuffer,0xAA,cbout); //如果在上面引发异常,所以以后语句不会被执行! pIrp->IoStatus.Information = cbout; KdPrint(("Leave __try block\n"));}__except(EXCEPTION_EXECUTE_HANDLER){ KdPrint(("Catch the exception\n")); KdPrint(("The program will keep going\n")); status = STATUS_UNSUCCESSFUL;}pIrp->IoStatus.Information = cbout;