在学习wdk驱动的时候,关于通讯这块介绍真的很少,加上《天书夜读》没有公开代码,所以实现起来真的很困难,所以在学习的过程中做了些小结,附上源码,希望对初学者有一些帮助。目录如下,如果有用过虚拟机,第三章可以忽略。时间比较宝贵的直接看第五章。word文档里面有超链接,阅读也方便一点。
从某种程度上说,这个算是自己第一次发帖,所以大家多多指教。
附件如下: 《天书夜读》Windows驱动编程基础教程.rar Comm.rar dioc.rar fhlj1.rar irpcl.rar List.rar 总.rar 总的word文件:WDK下驱动和应用层序通信.rar
第一章 VC6.0 +WDK(7600.16385.1) 开发驱动的环境配置 1
第二章 设备和符号链接生成以及删除 2
第三章 虚拟机运行 3
第四章 应用程序向驱动程序发送消息 7
第五章 驱动程序主动发消息给应用程序 10
第一章 VC6.0 +WDK(7600.16385.1) 开发驱动的环境配置
(1)第一步:打开VC建立一个Win32 Application工程,然后选择An empty Project。
(2)第二步:随便找一个驱动程序,主要是.C,.h及rc资源文件,加入第一步建立的工程
(3) 第三步: 设置VC编译驱动的环境,这才是重点。打开VC的Tool->Options->Directories里设置Include,Lib,及Exectuable File。
设置Include路径:
C:/WINDDK/7600.16385.1/INC
C:/WinDDK/7600.16385.1/INC/wdf
C:/WinDDK/7600.16385.1/INC/wdf/kmdf/1.9
C:/WINDDK/7600.16385.1/INC/CRT
C:/WINDDK/7600.16385.1/INC/DDK
C:/WINDDK/7600.16385.1/INC/API (这里可能有些不需要,也可能少了,根据自己的情况在INC目录下继续添加就OK了)
设置LIB路径:
C:/WINDDK/7600.16385.1/LIB
C:/WINDDK/7600.16385.1/LIB/WDF/KMDF/I386/1.9
C:/WINDDK/7600.16385.1/LIB/CRT/I386
C:/WINDDK/7600.16385.1//LIB/WNET/I386
设置Executable Files:
C:/WINDDK/7600.16385.1/BIN/X86/X86
(这里要注意,一定要在bin/x86目录下,确切的找到Cl.exe文件,然后添加它所在的目录,DDK与WDK这个文件所在的路径并不是一样的,上面的是我所安装的WDK中cl.exe所在的路径,这里要根据不同情况选择正确的路径)
(4)第四步:
这里要设置工程的属性:
打开VC->Project菜单,找到C/C++选项卡,在Preprocesser definitions里输入如下字符串:
Release下输入:
_X86_=1,i386=1,STD_CALL,WIN32=100,_WIN32_WINNT=0x0501,WINVER=0x0501,NDEBUG
Debug下输入:
_X86_=1,i386=1,STD_CALL,WIN32=100,_WIN32_WINNT=0x0501,WINVER=0x0501,_DEBUG
在Code Generation里的Calling convention用_STDCALL
Debug版有个特殊编译选项 /GZ (注意是大写的,小写的不要删!) 删掉,否则有链接错误,chkesp链接不上什么的。
(5)第五步:
设置link选项卡,将输出文件名改为sys扩展名,
在General的Objcet/Library modules,填写驱动调用的几个lib:
Kernel32.lib ntoskrnl.lib hal.lib usbd.lib wmilib.lib wdfdriverentry.lib wdfldr.lib
然后进入Output在Entry-point symbol:填DriverEntry
下面就是往Project Options手动增加一些链接选项,复制如下进去:
/machine:IX86 /driver /subsystem:native /SECTION:INIT,d /IGNORE:4198,4010,4037,4039,4065,4070,4078,4087,4089,4221
删除如下选项:
/subsystem:windows
/machine代表目标机器类型,/driver代表驱动,/subsystem:native 也是代表驱动。
(这里,我在配置的时候,只是将/subsystem:windows改为:/subsystem:native没有再进行其它设置,也可成功生成驱动文件,至于有没有问题,暂时不知道)
http://hi.baidu.com/hell74111/blog/item/847374a9648fd8b8cb130c0a.html
源文档 <http://liong.blog.51cto.com/374966/265242 >
那么如果我们要编译64位驱动呢?这里大家注意一下,64位有两种,一个是AMD64,一个是IA64,要选定你的目标平台,目前AMD64平台最为流行.
其实编译成64位也很简单,只要把32位的配置稍微改一下就行了,我们以编译AMD64位驱动为例(你会复制VC的工程 Configurations吧,把32位的Relase,Debug各复制一份改名为ReleaseAMD64和DebugAMD64,然后在复制品的基础上修改):
1.要修改两个lib路径:
C:/2003DDK/LIB/CRT/I386改为:C:/2003DDK/LIB/CRT/amd64
C:/2003DDK/LIB/WNET/I386 改为:C:/2003DDK/lib/wnet/amd64
2.修改编译器路径:
C:/2003DDK/BIN/X86
改为:
C:/2003DDK/bin/win64/x86/amd64
C:/2003DDK/BIN/X86 //这句也要存在
3.修改C/C++里的Preprocesser definitions为:
ReleaseAMD64下输入:
_AMD64_,AMD64,STD_CALL,WIN32=100,_WIN32_WINNT=0x0501,WINVER=0x0501,NDEBUG
DebugAMD64下输入:
_AMD64_,AMD64,STD_CALL,WIN32=100,_WIN32_WINNT=0x0501,WINVER=0x0501,_DEBUG
4.需要修改link页里的Project Options,手动改一下:将/machine:IX86改为:/machine:amd64
5.理论上到这里设置就OK了,不过还是有一个链接错误unresolved external symbol __security_cookie ,只要在链接的lib里加入bufferoverflowK.lib (为什么是bufferoverflowK.lib?看这里:http://support.microsoft.com/kb/894573 )就可以了。我修改的工程范例下载:下载地址。
以上的设置大部分都可以保存在VC工程文件(*.dsp)里,除了设置的include路径,lib路径和编译器路径,这些路径是全局的,在打开另一些工程也会留下了,就会给编译应用程序带来麻烦,还需要一个一个改回去,而目录切换器就是为了快速切换这些路径而开发的。
PS:你在建好一个工程文是, 目录已经保存下来了, 再创建一个新工程时,就不用再设置目录了, 但工程的属性,还需要设置, 这个时候可以用比较工具,把.dsp文件和以前创建工程的.dsp文件比较,把改动之间复制过去即可, 不用再一项一项重新设置了。
第二章 设备和符号链接生成以及删除
代码:fhlj1.c
本代码和hello world差不多,不过如果不写DriverUnload,会发现代码每次卸载之后,就不能再次安装,原因就是设备没有删除,所有DriverUnload的职责就是删掉符号链接和设备。
本文生成的设备文件DosDevices\\MyCDOSL可以被应用程序打开,这个是实现通信的基础。当时上面的代码还没有具备这样的功能,应用程序通过CreateFile无法打开,原因是该驱动没有对自己创建的设备做出的irp做出处理。
代码:irpcl.c
上面的代码黄色部分,就是我们的分发函数,对于自己创建的设备,要自己做出处理,如果写成
// 所有处理都直接通过
NTSTATUS MyPassThrough(IN PDEVICE_OBJECT DeviceObject,
IN PIRP irp)
{
IoSkipCurrentIrpStackLocation(irp);
if(device==DeviceObject)
{
//return STATUS_SUCCESS;
}
return IoCallDriver(DeviceObject,irp);
}
系统马上会出现蓝屏,原因就是即使调用IoCallDriver,系统会重新调用MyPassThrough,irp仍然没人去处理它,这样可以说是出现了死递归,这说明自己创建的设备需要自己做出处理,。
第三章 虚拟机运行
这里突然出入虚拟机的共享目录的安装,是因为驱动确实不能在真是的系统中测试,反复的蓝屏和重启会让人吐血了,双机调试到不用,记得输出调试信息,然后虚拟机运行就行了,这个主要介绍vmware tool 的安装,方便文件交换。
本文以 VMware Workstation 7.1.4_385536 汉化版为例,讲解在虚拟机中创建 VMware 共享文件夹(vmware shared folders)的具体步骤与方法。
一、安装 VMware Tools
启动好 Windows XP 客户机,点击“虚拟机”菜单,选择“安装 VMware Tools”项,如下图所示:
客户机系统会自动安装,一直点击“下一步”,安装完成后点击“确定”按钮,重新启动客户机系统, VMware Tools 就安装好了。
二、在 VMware 虚拟机中设置共享文件夹
当我们在 VMware 中创建好一台新的虚拟机,安装好虚拟操作系统后,比如安装好 Windows XP 以后,会出现一个“摘要”窗口。具体请查看使用 VMware Workstation 创建虚拟机的详细方法〔图解〕中的最后一个示例图。
在示例图中,点击“命令”下面的“编辑虚拟机设置”项,出现“虚拟机设置”窗口,点击“选项”,如下图所示:
按照上图中红色椭圆划定的位置,设置好后,点击“添加”按钮,点击“下一步”,点击“浏览”按钮,设置好文件夹,如下图所示:
点击“下一步”,选择“启用该共享”项,然后点击“完成”按钮,就创建好了一个共享文件夹。如下图所示:
按照上面讲解的步骤,可以创建多个共享文件夹。创建好以后点击“确定”按钮。
三、在 Windows XP 客户机中访问共享文件夹
在桌面上点击“我的电脑”,选择“工具”菜单中的“映射网络驱动器”项,出现下面的窗口:
点击“浏览”按钮,选择上面设置的 F 共享文件夹。如下图所示:
点击“确定”按钮,返回到“映射网络驱动器”中,点击“完成”按钮。就在“我的电脑”中创建了一个网络驱动器。如下图所示:
只要点击红色椭圆内的网络驱动器,就能访问共享文件夹了。
按照第二步、第三步的方法,我们可以创建并能访问多个共享文件夹。
四、提示
如果想删除一个网络驱动器,我们只要在相关的网络驱动器上,比如在红色椭圆内的网络驱动器 F 上单击鼠标右键,在出现的菜单中,选择“断开”项即可。
第四章 应用程序向驱动程序发送消息
实现一个简单的需求,应用程序把字符串发给驱动,驱动返回同样的字符串,如图
驱动代码:dioc.c
应用层代码:open.c
写出这个通信的代码必须对iocrate
本例中,最重要的对DeviceIoControl有一定了解,这里用看雪的一帖说明:
http://bbs.pediy.com/showthread.php?p=446641
驱动程序和客户应用程序经常需要进行数据交换,但我们知道驱动程序和客户应用程序可能不在同一个地址空间,因此操作系统必须解决两者之间的数据交换。
驱动层和应用层通信,主要是靠DeviceIoControl函数,下面是该函数的原型:
BOOL DeviceIoControl (
HANDLE hDevice, // 设备句柄
DWORD dwIoControlCode, // IOCTL请求操作代码
LPVOID lpInBuffer, // 输入缓冲区地址
DWORD nInBufferSize, // 输入缓冲区大小
LPVOID lpOutBuffer, // 输出缓冲区地址
DWORD nOutBufferSize, // 输出缓冲区大小
LPDWORD lpBytesReturned, // 存放返回字节数的指针
LPOVERLAPPED lpOverlapped // 用于同步操作的Overlapped结构体指针
);
dwIoControlCode
要进行操作的控制码。驱动程序可以通过CTL_CODE宏来组合定义一个控制码,并在IRP_MJ_DEVICE_CONTROL的实现中进行控制码的操作。在驱动层,irpStack->Parameters.DeviceIoControl.IoControlCode表示了这个控制码。
IOCTL请求有四种缓冲策略,下面一一介绍。
1、 输入输出缓冲I/O(METHOD_BUFFERED)
2、 直接输入缓冲输出I/O(METHOD_IN_DIRECT)
3、 缓冲输入直接输出I/O(METHOD_OUT_DIRECT)
4、 上面三种方法都不是(METHOD_NEITHER)
为了对这些类型更详细的描述,请看msdn上的解释,我抄录如下:
"缓冲"方法(METHOD_BUFFERED)
备注:在下面的讨论中,"输入"表示数据从用户模式的应用程序到驱动程序,"输出"表示数据从驱动程序到应用程序。
对于读取请求,I/O 管理器分配一个与用户模式的缓冲区大小相同的系统缓冲区。IRP 中的 SystemBuffer 字段包含系统地址。UserBuffer 字段包含初始的用户缓冲区地址。当完成请求时,I/O 管理器将驱动程序已经提供的数据从系统缓冲区复制到用户缓冲区。对于写入请求,会分配一个系统缓冲区并将 SystemBuffer 设置为地址。用户缓冲区的内容会被复制到系统缓冲区,但是不设置 UserBuffer。对于 IOCTL 请求,会分配一个容量大小足以包含输入缓冲区或输出缓冲区的系统缓冲区,并将 SystemBuffer 设置为分配的缓冲区地址。输入缓冲区中的数据复制到系统缓冲区。UserBuffer 字段设置为用户模式输出缓冲区地址。内核模式驱动程序应当只使用系统缓冲区,且不应使用 UserBuffer 中存储的地址。
对于 IOCTL,驱动程序应当从系统缓冲区获取输入并将输出写入到系统缓冲区。当完成请求时,I/O 系统将输出数据从系统缓冲区复制到用户缓冲区。
"直接"方法(METHOD_IN/OUT_DIRECT)
对于读取和写入请求,用户模式缓冲区会被锁定,并且会创建一个内存描述符列表 (MDL)。MDL 地址会存储在 IRP 的 MdlAddress 字段中。SystemBuffer 和 UserBuffer 均没有任何含义。但是,驱动程序不应当更改这些字段的值。
对于 IOCTL 请求,如果在 METHOD_IN_DIRECT 和 METHOD_OUT_DIRECT 中同时有一个输出缓冲区,则分配一个系统缓冲区(SystemBuffer 又有了地址)并将输入数据复制到其中。如果有一个输出缓冲区,且它被锁定,则会创建 MDL 并设置 MdlAddress。UserBuffer 字段没有任何含义。
"两者都不"方法(METHOD_NEITHER)
对于读取和写入请求,UserBuffer 字段被设置为指向初始的用户缓冲区。不执行任何其他操作。SystemAddress 和 MdlAddress 没有任何含义。对于 IOCTL 请求,I/O 管理器将 UserBuffer 设置为初始的用户输出缓冲区,而且,它将当前 I/O 栈位置的 Parameters.DeviceIoControl.Type3InputBuffer 设置为用户输入缓冲区。利用该 I/O 方法,由驱动程序来确定如何处理缓冲区:分配系统缓冲区或创建 MDL。
通常,驱动程序在访问用户数据时不应当将 UserBuffer 字段用作地址,即使当用户缓冲区被锁定时也是如此。这是由于在调用驱动程序时,在系统中可能看不到调用用户的地址空间。(对于该规则的一个例外是,在最高层驱动程序将 IRP 向下传递到较低层的驱动程序之前,它可能需要使用 UserBuffer 来复制数据。)如果使用"直接"或"两者都不"方法,在创建 MDL 之后,驱动程序可以使用 MmGetSystemAddressForMdl 函数来获取有效的系统地址以访问用户缓冲区。 在驱动层,依传输类型的不同,输入缓冲区的位置亦不同,见下表。
传输类型 位置
METHOD_IN_DIRECT irp->AssociatedIrp.SystemBuffer
METHOD_OUT_DIRECT irp->AssociatedIrp.SystemBuffer
METHOD_BUFFERED irp->AssociatedIrp.SystemBuffer
METHOD_NEITHER irpStack->Parameters.DeviceIoControl.Type3InputBuffer
在驱动层,依传输类型的不同,输出缓冲区的位置亦不同,见下表。
传输类型 位置
METHOD_IN_DIRECT irp->MdlAddress
METHOD_OUT_DIRECT irp->MdlAddress
METHOD_BUFFERED irp->AssociatedIrp.SystemBuffer
METHOD_NEITHER irp->UserBuffer
整个帖子重要的就是加了黑色的部分,就是对应的缓冲机制。 所以只要确定了传输方式后,就可以根据各自的位置来读取和写入数据,从而实现应用层和驱动的通信。
下面看驱动层对ioctl控制码的处理代码:Comm.c (不知道还可不可以编译,我把原来的头文件给去掉了,附加在Comm.c上)
第五章 驱动程序主动发消息给应用程序
解决这个问题提一下我原本的一些想法,我希望驱动绑定卷设备之后,对于所有的对文件系统的写操作irp,通过发送这个irp的操作文件的路径给应用程序,应用程序对这个路径作分析,然后决定是否让这个irp通过。
这样一个需求是第四章这样一种通讯方式无法解决的,他需要驱动获取某个irp的时候,获得他的路径,主动发送给应用程序,这时候这个irp不能做处理,要等应用层的消息过来,然后决定irp时候通过。
对于很多对驱动和应用程序的通讯方法,都是停留于第四章这样子介绍,不过《天书夜读》到时提了一下,可惜谭文这本书没有附加光盘。原文描述如下:
驱动主动通知应用和应用通知驱动的通道是同一个。只是方向反过来。应用程序需要开启一个线程调用DeviceIoControl,(调用ReadFile亦可)。而驱动在没有消息的时候,则阻塞这个IRP的处理。等待有信息的时候返回。
有的读者可能听说过在应用层生成一个事件,然后把事件传递给驱动。驱动有消息要通知应用的时候,则设置这个事件。但是实际上这种方法和上述方法本质相同:应用都必须开启一个线程去等待(等待事件)。而且这样使应用和驱动之间交互变得复杂(需要传递事件句柄)。这毫无必要。
让应用程序简单的调用DeviceIoControl就可以了。当没有消息的时候,这个调用不返回。应用程序自动等待(相当于等待事件)。有消息的时候这个函数返回。并从缓冲区中读到消息。
实际上,驱动内部要实现这个功能,还是要用事件的。只是不用在应用和驱动之间传递事件了。
驱动内部需要制作一个链表。当有消息要通知应用的时候,则把消息放入链表中(请参考前面的“使用LIST_ENTRY”),并设置事件(请参考前面的“使用事件”)。在DeviceIoControl的处理中等待事件。下面是一个例子:这个例子展示的是驱动中处理DeviceIoControl的控制码为MY_DVC_OUT_CODE的部分。实际上驱动如果有消息要通知应用,必须把消息放入队列尾并设置事件g_my_notify_event。MyGetPendingHead获得第一条消息。请读者用以前的知识自己完成其他的部分。
NTSTATUS MyDeviceIoCtrlOut(PIRP irp,ULONG out_len)
{
MY_NODE *node;
ULONG pack_len;
// 获得输出缓冲区。
PVOID buffer = irp->AssociatedIrp.SystemBuffer;
// 从队列中取得第一个。如果为空,则等待直到不为空。
while((node = MyGetPendingHead()) == NULL)
{
KeWaitForSingleObject(
&g_my_notify_event,// 一个用来通知有请求的事件
Executive,KernelMode,FALSE,0);
}
// 有请求了。此时请求是node。获得PACK要多长。
pack_len = MyGetPackLen(node);
if(out_len < pack_len)
{
irp->IoStatus.Information = pack_len; // 这里写需要的长度
irp->IoStatus.Status = STATUS_INVALID_BUFFER_SIZE;
IoCompleteRequest (irp,IO_NO_INCREMENT);
return irp->IoStatus.Status;
}
// 长度足够,填写输出缓冲区。
MyWritePackContent(node,buffer);
// 头节点被发送出去了,可以删除了
MyPendingHeadRemove ();
// 返回成功
irp->IoStatus.Information = pack_len; // 这里写填写的长度
irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest (irp,IO_NO_INCREMENT);
return irp->IoStatus.Status;
}
这个函数的处理要追加到MyDeviceIoControl中。如下:
NTSTATUS MyDeviceIoControl(
PDEVICE_OBJECT dev,
PIRP irp)
{
…
if(code == MY_DVC_OUT_CODE)
return MyDeviceIoCtrlOut(dev,irp);
…
}
在这种情况下,应用可以循环调用DeviceIoControl,来取得驱动驱动通知它的信息。
如果这个代码是正确的,同时看这份文档的人和我一样是新手,自然有两个问题,可惜毕竟这本书没有光盘,使得弄清楚这个问题变得困难了。所以先脱离一下原来的目的,回答下面两个问题
1 那些加了黄色的函数是怎么实现的?
这相当于队列的基本功能,实现代码List.c,可以和open.c通讯,运行结果如下:(List.c里面实现了自旋锁)
2 事件驱动,线程同步处理
然而链表什么的我们先放下,看懂这个代码的关键,就是KeWaitForSingleObject这个函数了。
KeWaitForSingleObject内核用来等待事件的。寒假独钓里面添加代码,就可以实现如下如下功能:
这样就可以在应用程序里面获取想要过滤的路径给应用程序。在《寒假独钓》第四章代码的基础上,修改如下:
#define MY_DVC_IN_CODE (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN,0xa01,METHOD_BUFFERED,FILE_READ_DATA|FILE_WRITE_DATA)//设备的宏定义
#define MY_DVC_OUT_CODE (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN,0xa00,METHOD_BUFFERED,FILE_READ_DATA|FILE_WRITE_DATA)//设备的宏定义
#define maxn 1000//最大路径名
KEVENT event;
WCHAR path[maxn];
LONG len;
NTSTATUS MyDeviceIoControl(PDEVICE_OBJECT dev,
PIRP irp
)
{
ULONG in_len,out_len,code;
PVOID in_buffer,out_buffer;
PIO_STACK_LOCATION irpsp;
KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL);
irpsp=IoGetCurrentIrpStackLocation(irp);
code=irpsp->Parameters.DeviceIoControl.IoControlCode;
in_len=irpsp->Parameters.DeviceIoControl.InputBufferLength;
out_len=irpsp->Parameters.DeviceIoControl.OutputBufferLength;
in_buffer=irp->AssociatedIrp.SystemBuffer;
out_buffer=irp->AssociatedIrp.SystemBuffer;
if(irp->MdlAddress)
{
out_buffer = MmGetSystemAddressForMdlSafe(irp->MdlAddress, NormalPagePriority);
}
if(code==MY_DVC_IN_CODE)
{
DbgPrint("in_buffer %s\n",in_buffer);
if(in_buffer&&path)
{
wcscpy(out_buffer, path);
}
DbgPrint("out_buffer %ws\n",out_buffer);
irp->IoStatus.Information=out_len;
irp->IoStatus.Status=STATUS_SUCCESS;
}
else
{
irp->IoStatus.Information=0;
irp->IoStatus.Status=STATUS_SUCCESS;
//irp->IoStatus.Status=STATUS_INVALID_PARAMETER;
}
IoCompleteRequest(irp,IO_NO_INCREMENT);
return irp->IoStatus.Status;
}
SF_RET OnSfilterIrpPre(
IN PDEVICE_OBJECT dev,
IN PDEVICE_OBJECT next_dev,
IN PVOID extension,
IN PIRP irp,
OUT NTSTATUS *status,
PVOID *context)
{
// 获得当前调用栈
PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);
PFILE_OBJECT file = irpsp->FileObject;
// 我仅仅过滤文件请求。 FileObject不存在的情况一律passthru.
if(file == NULL||((file->FileName).Buffer)==NULL)
return SF_IRP_PASS;
if((irpsp->MajorFunction)==IRP_MJ_CREATE)
{
PWCHAR tmp=(file->FileName).Buffer;
len=wcslen(tmp);
if(len>=maxn)return SF_IRP_PASS;
wcscpy(path,tmp);
//RtlCopyMemory(path, tmp, wcslen(tmp));
DbgPrint("PRE IRP: file name = %ws %d",path,len);
KeSetEvent( &event, IO_NO_INCREMENT, FALSE );
}
return SF_IRP_PASS;
}
这样修改就可以直线书中所说的将驱动将irp的路径主动通知应用程序,然而做到这里还不够,回顾一下我最开始的想法:由应用程序判断路径是否要过滤,然后发结果给驱动,驱动由这个结果决定改irp是否下发成功。其实上面那个代码实现这个功能很近了,这里就不给代码了,只说一下,在OnSfilterIrpPre里面多初始化一个事件(和前面那个事件不一样,就是有两个事件)在这个函数里面要return之前,用KeWaitForSingleObject让他停下来,那么事件触发设置在MyDeviceIoControl里面,那么应用程序判断路径之后想发了结果过来,就触发这第二个事件,然后OnSfilterIrpPre就可以根据返回的结果继续执行了。这个想法OnSfilterIrpPre和MyDeviceIoControl都有两个事件的交互,加多一个全局变量判断是处于哪个事件就ok了。
由于这部分的代码比较多和乱,就不发上来了,按照前面按部就班看下来理解这个想法是不难的,虽然我说得比较乱。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
上传的附件: