文章目录
键盘的过滤
8.3 键盘过滤的请求处理
8.3.1 通常的处理
最通常的处理就是直接发送到真实设备,跳过虚拟设备的处理。这和前面串口过滤用过的方法一样。代码如下:
NTSTATUS c2pDispatchGeneral(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
//一般的分发函数,直接skip,然后用 IoCallDriver 将 IRP 发送到真实设备的设备对象
KdPrint(("Other Dispatch!"));
IoSkipCurrentIrpStackLocation(Irp);
return IoCallDriver(((PC2P_DEV_EXT)DeviceObject->DeviceExtension)->LowerDeviceObject,Irp);
}
这里与串口那里有明显的不同。我们不用再遍历一个数组去寻找真实设备的设备对象指针了。而是直接使用了设备拓展中预先已经保留的指针。接下来再是对电源 IRP 的处理。
NTSTATUS c2pPower(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
PC2P_DEV_EXT devExt;
devExt = (PC2P_DEV_EXT)DeviceObject->DeviceExtension;
PoStartNextPowerIrp(Irp);
IoSkipCurrentIrpStackLocation(Irp);
return PoCallDriver(devExt->LowerDeviceObject,Irp);
}
8.3.2 PNP的处理
唯一需要处理的是,当有一个设备被拔出的时候,解除绑定,并删除过滤设备。代码的实现大致如下:
NTSTATUS c2pPnP(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
PC2P_DEV_EXT devExt;
PIO_STACK_LOCATION irpStack;
NTSTATUS status = STATUS_SUCCESS;
KIRQL oldIrql;
KEVENT event;
//获得真实设备
devExt = (PC2P_DEV_EXT)DeviceObject->DeviceExtension;
irpStack = IoGetCurrentIrpStackLocation(Irp);
switch(irpStack->MinorFunction)
{
case IRP_MN_REMOVE_DEVICE:
KbPrint(("IRP_MN_REMOVE_DEVICE\n"));
//首先把请求发下去
IoSkipCurrentIrpStackLocation(Irp);
IoCallDriver(devExt->LowerDeviceObject,Irp);
//解除绑定
IoDetachDevice(devExt->LowerDeviceObject);
//删除我们生成虚拟设备
IoDeleteDevice(DeviceObject);
status = STATUS_SUCCESS;
break;
defaule:
//对于其他类型的 IRP,全部都直接下发即可。
IoSkipCurrentIrpStackLocation(Irp);
status = IoCallDriver(devExt->LowerDeviceObject,Irp);
}
return status;
}
当 PNP 请求过来时,不必担心还有未完成的 IRP。这是因为 Windwos 系统要求卸载设备时,Windows 自己应该已经处理了所有未决的 IRP。上述PNP 即拔出设备时,要求卸载该设备对象。
8.3.3 读的处理
当一个读请求到来时候,只是说 Windwos 要从键盘驱动读取一个键扫描码值,但是在完成之前显然这个值是多少我们不清楚。本章要过滤的目的,就是要获得按下了什么键,所以不得不换一种处理方法,即把这个请求下发之后,再去看这个值是多少。
要完成请求,可以采用如下的步骤。
- 调用 IoCopyCurrentIrpStackLocationToNext 把当前栈空间拷贝到下一个栈空间(这与前面的调用 IoSkipCurrentIrpStackLocation 跳过当前栈空间形成对比)
- 给这个 IRP 设置一个完成函数,即回调函数。如果这个 IRP 完成了,系统就会回调这个函数。
- 调用 IoCallDriver 把请求发送到下一个设备。
另外一个需要解决的问题就是我们前面所需要的一个键计数器。即一个请求来到则加一,完成就减1。这个处理比较简单。完整的读处理请求如下:
NTSTATUS c2pDispatchRead(
IN PDEVICE_OBJECT DriverObject,
IN PIRP Irp
)
{
NTSTATUS status = STATUS_SUCCESS;
PC2P_DEV_EXT devExt;
PIO_STACK_LOCATION currentIrpStack;
KEVENT waitEvent;
KeInitializeEvent(&waitEvent,NotificationEvent,FALSE);
if(Irp->CurrentLocation == 1)//判断是否到达了irp栈的最低端,属于错误处理
{
ULONG ReturnedInformation = 0;
KdPrint(("Dispatch encountered bogus current location\n"));
status = STATUS_INVALID_DEVICE_REQUEST;
Irp->IoStatus.Status = status;
Irp->Iostatus.Information = ReturnedInformation;
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return status;
}
//全局变量键计数器加一
gC2pKeyCount++;
//得到设备拓展,目的为了得到下一个设备的指针。
devExt = (PC2P_DEV_EXT)DeviceObject->DeviceExtension;
//设置回调函数并把IRP传递下去。之后读的处理就结束了,等待请求完成。
currentIrpStack = IoGetCurrentIrpStackLocation(Irp);
IoCopyCurrentIrpStackLocationToNext(Irp);
IoSetCompletionRoutine(Irp,c2pReadComplete,DeviceObject,TRUE,TRUE,TRUE);
return IoCallDriver(devExt->LowerDeviceObject,Irp);
}
8.3.4 读完成的处理
读请求完成之后,应该获得输出缓冲区,按键信息就在输出缓冲区中,全局变量gC2pKeyCount应该减1.此外,就没有其他的事情需要做的。所以相关代码比较简单。大致如下:
//IRP回调函数的原型
NTSTATUS c2pReadComplete(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp,
IN PVOID Context
)
{
PIO_STACK_LOCATION IrpSp;
ULONG buf_len = 0;
PUCHAR buf = NULL;
size_t i;
IrpSp = IoGetCurrentIrpStackLocation(Irp);
//假设这个请求是成功的。
if(NT_SUCCESS(Irp->IoStatus.Status))
{
buf = Irp->AssocitatedIrp.SystemBuffer;
//获取该缓冲区长度
//一般来说,不管返回值多长都保存在Information中
buf_len = Irp->Iostatus.Information;
//...这里读者可以自定义处理,作者只是打印出了扫描码
for(i=0;i<buf_len;++i)
{
DbgPrint("ctrl2cap: %2x\r\n",buf[i]);
}
}
gC2pKeyCount--;
if(Irp->PendingReturned)
{//所有的Irp完成函数里都应包含这一句,作用为告诉系统,我异步返回了,因为上面可能在等待这个IRP的完成,请你在IRP完成的时候告诉我IRP完成了。
IoMarkIrpPending(Irp);
}
return Irp->Iostatus.Status;
}
这里我们得到了输出缓冲区,按键信息当然也在其中。但是这些信息是什么格式保存的?又如何从这些信息里打印出按键的情况呢?在下面的内容中进一步说明。
8.4 从请求中打印出按键信息
在完成函数中能完成的任务有限,这是因为受到中断级别的限制。但是本章在完成函数中仅仅需要读取一个键扫描码,任务比较简单,所以相关知识首先屏蔽。在后面磁盘过滤和文件过滤时我们会注意到不同之处。
请求完成之后,读到的信息在 irp->AssociatedIrl.SystemBuffer 中。这里需要介绍一下这个缓冲区的数据格式。在这个缓冲区中可能含有 n 个 KEYBOARD_INPUT_DATA 结构。该结构定义如下:
typedef struce _KEYBOARD_INPUT_DATA{
// 头文件里的解释是这样的;对于设备 \Devcie\KeyboardPort0,这个值是0;
// 对于\Device\KeyboardPort1,这个值是1;以此类推
USHORT UnitId;
// 扫描码
USHORT MakeCode;
// 一个标志。标志这是一个键按下还是弹起
USHORT Flags;
// 保留
USHORT Reserved;
// 扩展信息
ULONG ExtraInformation;
}KEYBOARD_INPUT_DATA,*PKEYBOARD_INPUT_DATA;
下面是 Flags 可能的值。老实的说,这些值的含义作者也不清楚,我们需要字节结合后面的代码来理解。
#define KEY_MAKE 0
#define KEY_BREAK 1
#define KEY_E0 2
#define KEY_E1 4
#define KEY_TERMSRV_SET_LED 8
#define KEY_TERMSRV_SHADOW 0X10
#define KEY_TERMSRV_VKPACKET 0X20
至于有多少个这样的结构,则取决于输入缓冲区到底多长,实际上,这种结构的个数应该为:
size = buf_len/sizeof(KEYBOARD_INPUT_DATA);
8.4.2 从 KEYBOARD_INPUT_DATA 中得到的键
KEYBOARD_INPUT_DATA 下的 MakeCode 就是扫描码。对于 Flags ,这里的代码只是考虑了 KEY_MAKE(0) 和 KEY_BREAK(非0) 两种可能: 一种表示按下;另一种则表示弹起。相关的代码如下:
KeyData = Irp->AssociatedIrp.SystemBuffer;
numKeys = Irp->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA);
for(i=0;i<numKey;i++)
{
//下面打印按键的信息
DbgPrint("numKeys: %d,numKey");
DbgPrint("ScabCideL %x,KeyData->MakeCode");
DbgPrint("%s\n",KeyData->Flags ? "UP":"Down");
MyPrintKeyStroke((UCHAR)KeyData->MakeCode);
//这是一个小测试,如果发现有 Caps Lock 键,我们就改写Ctrl键。证明键盘按键是可以被拦截修改的,其效果是 Caps Lock 可以起到和 Ctrl 一样的作用。
if(KeyData->MakeCode == CAPS_LOCK)
{
KeyData->MakeCode = LCONTROL;
}
}
应该注意到,有几个键会英雄从扫描码到实际字符的转换。
8.4.3 从MakeCode 到实际字符
本节尽力把按键现实成可以显示的字符。这涉及扫描码和实际的字符是如何对应的。
所谓的实际字符是 ASCII 码。大家都知道大小写的 ASCII 码并不相同,但是键是同一个。即扫描码是相同的,具体是取决于几个键盘的状态(包括 shitf 、 Caps Lock)。因此,这个模块在过滤按键的同时,也必须把这几个控制键的状态保存下来。尤其注意 Shift 键是按下生效,而 Caps Lock 是每按一次切换一次状态。因此过滤方法不同。
//键按下的状态
#define S_SHITF 1
#define S_CAPS 2
#define S_NUM 4
//一个标志,用来保存键盘当前的状态。其中有三个位分别表示
//Caps Lock Num Lock 和 Shitf 是否按下了
static int kb_status = S_NUM;
void __stdcall print_keystroke(UCHAR sch)
{
UCHAR ch = 0;
int off = 0;
if((sch & 0x80 ) == 0) //如果是按下
{
//如果按下了字母或者数字等可见字符
if((sch<0x47) || ((sch>=0x47 && sch<0x54) && (kb_status & S_NUM)))
{
//最终得到的字符必须由 定义的三个键状态决定。所以卸载一张表中
ch = asciiTbl[off+sch];
}
switch(sch)
{
//Caps Lock 与 Num Lock 都是按下两次等于没按过,所以用异或来设置标志
case 0x3A:
kb_status ^= S_CAPS;
break;
//shift 则是左右各一个 使用不同的码。但是作用相同。按下时起作用,弹起则消失作用。所以使用或来设置标志
case 0x2A:
case 0x36:
kb_status |= S_SHIFT;
break;
//Num Lock 键
case 0x45:
kb_status ^= S_NUM;
}
}
else //弹起
{
if (sch == 0xAA || sch == 0xB6)
//即如果按下了 shitf 就 恢复状态
kb_status &= ~S_SHIFT;
}
if(ch >= 20 && ch < 0x7F)
{
DbgPrint(%C \n,ch);
}
}
这里使用了很多位运算,不熟悉二进制的可能看着比较难懂。定义的 4 2 1 用二进制表示分别为 100 010 001 所以是不同的标志位。而初始化的 status = 4(100) 则默认表示开启了数字键盘。后续用异或取反等操作,从这三位二进制数字的角度去看就很容易理解了。三位二进制,每一位取1则代表对应的键被按下。比如100(4) 是 Num Lock 按下。010(2) 是 Caps Lock 按下。001(1) 则是 Shift 按下。对应的也可以组合使用。
明日计划
继续驱动编程学习