《驱动程序开发技术》大作业——过滤键盘驱动姓名:***学号:**********班级:计科普0902摘要Kbdclass.sys是键盘的类驱动,无论是USB键盘,还是PS/2键盘都要经过它的处理;在键盘类驱动之下,和实际硬件打交道的驱动叫做“端口驱动”,比如:i8042prt.sys是ps/2键盘的端口驱动,Kbdhid.sys是USB键盘的端口驱动。
键盘中断导致键盘中断服务例程被执行,导致最终i8042prt的I8042KeyboardInterruptService被执行。
在I8042KeyboardInterruptService中,从端口读取扫描码,放到一个KEYBOARD_INPUT_DATA 结构中。
并把这个结构放到i8042prt的输入队列中。
最后会调用内核api函数KeInsertQueueDpc。
在这个调用中会调用上层KbdClass.sys中处理输入的回调函数KeyboardClassServiceCallback,取走i8042prt的输入数据队列里的数据。
利用驱动分层机制,使用过滤驱动捕获键盘的扫描码并保存下来;应用程序定时访问驱动程序取回扫描码,转换成相应的按键名称并显示;通过应用程序设定按键映射,应用程序将指令传送给驱动程序,以实现将指定的按键消息转换成其他按键。
关键词:过滤键盘;驱动分层;映射;扫描码过滤键盘驱动一、主要设计思路利用驱动分层机制,使用过滤驱动捕获键盘的扫描码并保存下来;应用程序定时访问驱动程序取回扫描码,转换成相应的按键名称并显示;通过应用程序设定按键映射,应用程序将指令传送给驱动程序,以实现将指定的按键消息转换成其他按键。
键盘过滤驱动是工作在异步模式下的。
系统为了得到一个按键操作,首先要发送一个IRP_MJ_READ消息到驱动的设备栈,驱动收到这个IRP后,会一直保持这个IRP为未确定(pending)态,因为当时并没有按键操作。
直到一个键被真正的按下,驱动此时就会立刻完成这个IRP,并将刚按下的键的相关数据做为该IRP的返回值。
在该IRP带着对应的数据返回后,操作系统将这些值传递给对应的事件系统来处理,然后系统紧接着又会立刻发送一个IRP_MJ_READ请求,等待下次的按键操作,重复以上的步骤。
为了实现截获键盘消息,需要在过滤驱动程序中创建一个挂接到物理键盘设备上层的过滤驱动设备。
系统发送的IRP_MJ_READ消息会首先到达过滤驱动设备,这样就可以有机会给IRP_MJ_READ设置指定的完成例程,然后将消息下传给物理键盘设备。
当有按键动作发生时,IRP_MJ_READ消息在完成后就会调用指定的完成例程,这时就可以在完成例程中读出键盘动作的内容,或者修改这些信息,以实现按键的映射。
表.2:截获键盘消息标准的按键扫描码和ASCII码没有直接的对应关系,大部分按键的扫描码为一个字节;部分功能键为两个字节,且都以0xE0为高字节。
但实验中发现,IRP中返回的按键信息和标准的扫描码并不全等。
在KEYBOARD_INPUT_DATA结构中,MakeCode字段仅包含了一个字节的编码,还要同时参照Flags字段的内容才能判断出按键的扫描码。
下表是KEYBOARD_INPUT_DATA结构中两个字段的内容与其所代表的按键动作的对应关系。
当Flags=0或1时,说明按下的按键是扫描码为一个字节的按键;若Flags=2或3,则说明按下的是扫描码为两个字节的按键,而MakeCode中只保留扫描码的低字节。
表.3:按键动作的对应关系若使用指定的内容改写返回值中的KEYBOARD_INPUT_DATA结构,就可以改变按键的作用,实现按键映射的功能。
除了IRP_MJ_READ以外,对于其他发送给键盘设备的消息,到达过滤驱动设备时就可以不做处理,直接下传给键盘设备,以保证系统的正常工作。
在完成例程中将每次捕获得到的扫描码保存起来,应用程序每隔一定的时间(100ms)读取一次并将其清空,再根据扫描码查表得到相应按键的名称,这样就可以做到在应用程序中实时的显示键盘动作。
用户在应用程序中设定好按键映射的对应关系后,可以通过IRP_MJ_DEVICE_CONTROL消息将映射关系发送给过滤驱动程序,还是在完成例程中实现按键的映射替代。
二、模块的划分、实现及说明具体实现分为驱动程序和应用程序两大部分。
驱动程序用C和WindowsXP DDK实现,应用程序通过VC++ 6.0基于MFC实现。
在调试和测试中使用了DriverMonitor和Dbgview等工具。
下面分别介绍其中主要部分的实现:(一)驱动程序部分1.DEVICE_EXTENSION的定义2.DriverEntry主要任务是填写MajorFunction数组、设置卸载例程,并调用CreateDevice函数。
对所关心的一些消息分别设置回调函数,为其他消息设置通用处理函数。
3.DispatchGeneral对于不关心的那些消息,返回成功值并传递给下一层的键盘设备,本层不作处理。
4.CreateDevice建立设备对象,初始化DEVICE_EXTENSION结构,建立符号链接,将过滤驱动设备挂接到物理键盘设备之上。
5.DispatchRead在收到IRP_MJ_READ的IRP后,为其设置指定的完成例程ReadComplete,然后再将IRP 发送给物理键盘设备。
6.ReadComplete在IRP完成时会调用ReadComplete例程,此时用KEYBOARD_INPUT_DATA结构读取IRP中的返回值,得到按键事件的扫描码并保存。
同时若符合按键映射规则,则改写IRP中的返回值,实现按键的替换。
7.DeviceIOControl实现与应用程序之间的通信。
当应用程序通过DeviceIoControl读扫描码时,利用IRP 中的rmation字段返回一个ULONG类型的扫描码。
当应用程序设定按键映射规则时,同样使用DeviceIoControl通过系统内存缓冲区传递两个ULONG型的扫描码到pDevExt->SetCode数组中。
(二)应用程序部分使用MFC实现应用界面,在程序启动时打开过滤驱动设备。
开辟新的工作线程,实现定时(100ms)读取捕获到的键盘扫描码,将扫描码翻译成按键名称,并按时间顺序将按键动作显示在ListBox中。
设定按键映射规则时,通过DeviceIoControl将两个DWORD类型的扫描码通过系统缓冲IO的方式传递给驱动程序。
三、遇到的问题及解决方法键盘过滤驱动的卸载问题。
在使用常规的驱动卸载步骤时,会发生系统蓝屏重启的故障。
通过分析和查阅相关资料,终于得出了解决的办法。
由于IRM_MJ_READ是异步的,在给IRP_MJ_READ设置了完成例程的情况下,该IRP完成后会调用过滤驱动所指定的完成例程,使得有了处理返回数据的机会。
但也正是因为这样,当动态御载了键盘过滤驱动,也就卸载掉了完成例程,而之后的再次按键动作在完成了这个IRP后还是会调用那个已经不存在了的完成例程,因而引发错误。
同理可知,在安装过滤驱动时,就已经有一个IRP在键盘设备驱动中等待按键了,而该IRP并没有被设置完成例程。
因此可知,在驱动运行之后的第一个按键动作是不会被截获的。
在实验中的确证实了这个推想。
动态卸载的实现,是在设备扩展对象中加入一个IrpPendingCount计数器,用来记录正在运行中的(pending状态)IRP数量。
当收到一个IRM_MJ_READ时计数器加1,在完成一个IRP之后计数器减1。
在驱动卸载例程中,先用IoDetachDevice解除过滤驱动的绑定,使得之后的IRP不会再被设置完成例程;然后要等待到计数器为0,即已被设置了完成例程的IRP都已完成之后才卸载设备和驱动对象。
因此,在进入驱动卸载例程之后,还需要等待一次额外的按键操作才能正常的完成驱动程序的卸载。
// 解除过滤驱动,释放设备对象与物理驱动之间的绑定关系IoDetachDevice(KeyDevice);//等待计数器为0while(pDevExt->IrpPendingCount){}//删除过滤设备IoDeleteDevice(KeyFilterDevice);//删除符号链接pLinkName = pDevExt->ustrSymLinkName;IoDeleteSymbolicLink(&pLinkName);return;四、程序运行结果及使用说明图.1:使用DriverMonitor打开和加载驱动程序图.2:设备对象KeyFilterDriver已被正确创建,并挂接到了键盘设备上图.3:显示经过翻译的按键动作输入按键映射规则,如要将按键A替换成B,则在映射码中输入A的扫描码:0x1E,在目标码中输入B的扫描码:0x30,点击设置,之后的按键A就会被替换成按键B。
关闭应用程序后,在DriverMonitor中卸载驱动,驱动卸载正常(需要一次额外的按键操作)。
图.4:在DriverMonitor中卸载驱动,驱动卸载正常五、总结对于设备驱动这门课程我感觉自己学的不太好,对于一些知识点比较模糊,估计是自己花在这门课程的时间相对较少,这次的大作业对我来说难度不小,但是通过同学和老师的帮助以及在网上的反复查找使我对Windows驱动程序的基本结构有了大致的了解,对IRP的传递和操作方式有了一定的认识和理解,对内核模式和应用模式之间的数据交换有了初步的认识。
在以后的学习生活当中,我会改掉自己学习中的一些坏习惯,对于模糊的知识要尽快去弄清楚,不要一而再再而三的拖下去。