Windows操作系统是建立在事件驱动机制之上的,系统各部分之间的沟通也都是通过消息的相互传递而实现的。但在通常情况下,应用程序只能处理来自进程内部的消息或是从其他进程发过来的消息,如果需要对在进程外传递的消息进行拦截处理就必须采取一种被称为HOOK(钩子)的技术。钩子是Windows操作系统中非常重要的一种系统接口,用它可以轻松截获并处理在其他应用程序之间传递的消息,并由此可以完成一些普通应用程序难以实现的特殊功能。基于钩子在消息拦截处理中的强大功能,本文即以VC++ 6.0为编程背景对钩子的基本概念及其实现过程展开讨论。为方便理解,在文章最后还给出了一个简单的有关鼠标钩子的应用示例。
钩子的基本原理
钩子的本质是一段用以处理系统消息的程序,通过系统调用,将其挂入到系统。钩子的种类有很多,每一种钩子负责截获并处理相应的消息。钩子机制允许应用程序截获并处理发往指定窗口的消息或特定事件,其监视的窗口即可以是本进程内的也可以是由其他进程所创建的。在特定的消息发出,并在到达目的窗口之前,钩子程序先行截获此消息并得到对其的控制权。此时在钩子函数中就可以对截获的消息进行各种修改处理,甚至强行终止该消息的继续传递。
任何一个钩子都由系统来维护一个指针列表(钩子链表),其指针指向钩子的各个处理函数。最近安装的钩子放在链的开始,最早安装的钩子则放在最后,当钩子监视的消息出现时,操作系统调用链表开始处的第一个钩子处理函数进行处理,也就是说最后加入的钩子优先获得控制权。在这里提到的钩子处理函数必须是一个回调函数(callback function),而且不能定义为类成员函数,必须定义为普通的C函数。在使用钩子时可以根据其监视范围的不同将其分为全局钩子和线程钩子两大类,其中线程钩子只能监视某个线程,而全局钩子则可对在当前系统下运行的所有线程进行监视。显然,线程钩子可以看作是全局钩子的一个子集,全局钩子虽然功能强大但同时实现起来也比较烦琐:其钩子函数的实现必须封装在动态链接库中才可以使用。
钩子的安装与卸载
由于全局钩子具有相当的广泛性而且在功能上完全覆盖了线程钩子,因此下面就主要对应用较多的全局钩子的安装与使用进行讨论。前面已经提过,操作系统是通过调用钩子链表开始处的第一个钩子处理函数而进行消息拦截处理的。因此,为了设置钩子,只需将回调函数放置于链首即可,操作系统会使其首先被调用。在具体实现时由函数SetWindowsHookEx()负责将回调函数放置于钩子链表的开始位置。SetWindowsHookEx()函数原型声明如下:
HHOOK SetWindowsHookEx(int idHook;
HOOKPROC lpfn;
HINSTANCE hMod;
DWORD dwThreadId);
其中:参数idHook 指定了钩子的类型,总共有如下13种:
WH_CALLWNDPROC 系统将消息发送到指定窗口之前的"钩子"
WH_CALLWNDPROCRET 消息已经在窗口中处理的"钩子"
WH_CBT 基于计算机培训的"钩子"
WH_DEBUG 差错"钩子"
WH_FOREGROUNDIDLE 前台空闲窗口"钩子"
WH_GETMESSAGE 接收消息投递的"钩子"
WH_JOURNALPLAYBACK 回放以前通过WH_JOURNALRECORD"钩子"记录的输入消息
WH_JOURNALRECORD 输入消息记录"钩子"
WH_KEYBOARD 键盘消息"钩子"
WH_MOUSE 鼠标消息"钩子"
WH_MSGFILTER 对话框、消息框、菜单或滚动条输入消息"钩子"
WH_SHELL 外壳"钩子"
WH_SYSMSGFILTER 系统消息"钩子"
参数lpfn为指向钩子处理函数的指针,即回调函数的首地址;参数hMod则标识了钩子处理函数所处模块的句柄;第四个参数dwThreadId 指定被监视的线程,如果明确指定了某个线程的ID就只监视该线程,此时的钩子即为线程钩子;如果该参数被设置为0,则表示此钩子为监视系统所有线程的全局钩子。此函数在执行完后将返回一个钩子句柄。
虽然对于线程钩子并不要求其象全局钩子一样必须放置于动态链接库中,但是推荐其也在动态链接库中实现。因为这样的处理不仅可使钩子可为系统内的多个进程访问,也可以在系统中被直接调用,而且对于一个只供单进程访问的钩子,还可以将其钩子处理过程放在安装钩子的同一个线程内,此时SetWindowsHookEx()函数的第三个参数也就是该线程的实例句柄。
在SetWindowsHookEx()函数完成对钩子的安装后,如果被监视的事件发生,系统马上会调用位于相应钩子链表开始处的钩子处理函数进行处理,每一个钩子处理函数在进行相应的处理时都要考虑是否需要把事件传递给下一个钩子处理函数。如果要传递,就通过函数CallNestHookEx()来解决。尽管如此,在实际使用时还是强烈推荐无论是否需要事件传递而都在过程的最后调用一次CallNextHookEx( )函数,否则将会引起一些无法预知的系统行为或是系统锁定。该函数将返回位于钩子链表中的下一个钩子处理过程的地址,至于具体的返回值类型则要视所设置的钩子类型而定。该函数的原型声明如下:
LRESULT CallNextHookEx(HHOOK hhk;int nCode;WPARAM wParam;LPARAM lParam);
其中,参数hhk为由SetWindowsHookEx()函数返回的当前钩子句柄;参数nCode为传给钩子过程的事件代码;参数wParam和lParam 则为传给钩子处理函数的参数值,其具体含义同设置的钩子类型有关。
最后,由于安装钩子对系统的性能有一定的影响,所以在钩子使用完毕后应及时将其卸载以释放其所占资源。释放钩子的函数为UnhookWindowsHookEx(),该函数比较简单只有一个参数用于指定此前由SetWindowsHookEx()函数所返回的钩子句柄,原型声明如下:
BOOL UnhookWindowsHookEx(HHOOK hhk);
鼠标钩子的简单示例
最后,为更清楚展示HOOK技术在VC编程中的应用,给出一个有关鼠标钩子使用的简单示例。在钩子设置时采用的是全局钩子。下面就对鼠标钩子的安装、使用以及卸载等过程的实现进行讲述:
由于本例程需要使用全局钩子,因此首先构造全局钩子的载体--动态链接库。考虑到 Win32 DLL与Win16 DLL存在的差别,在Win32环境下要在多个进程间共享数据,就必须采取一些措施将待共享的数据提取到一个独立的数据段,并通过def文件将其属性设置为读写共享:
#pragma data_seg("TestData") HWND glhPrevTarWnd=NULL; // 窗口句柄 HWND glhHook=NULL; // 鼠标钩子句柄 HINSTANCE glhInstance=NULL; // DLL实例句柄 #pragma data_seg() …… SECTIONS // def文件中将数据段TestData设置为读写共享 TestData READ WRITE SHARED |
在安装全局鼠标钩子时使用函数SetWindowsHookEx(),并设定鼠标钩子的处理函数为MouseProc(),安装函数返回的钩子句柄保存于变量glhHook中:
void StartHook(HWND hWnd) { …… glhHook=(HWND)SetWindowsHookEx(WH_MOUSE,MouseProc,glhInstance,0); } |
鼠标钩子安装好后,在移动、点击鼠标时将会发出鼠标消息,这些消息均经过消息处理函数MouseProc()的拦截处理。在此,每当捕获到系统各线程发出的任何鼠标消息后首先获取当前鼠标所在位置下的窗口句柄,并进一步通过GetWindowText()函数获取到窗口标题。在处理函数完成后,通过CallNextHookEx()函数将事件传递到钩子列表中的下一个钩子处理函数:
LRESULT WINAPI MouseProc(int nCode,WPARAM wParam,LPARAM lParam) { LPMOUSEHOOKSTRUCT pMouseHook=(MOUSEHOOKSTRUCT FAR *) lParam; if(nCode>=0) { HWND glhTargetWnd=pMouseHook->hwnd; //取目标窗口句柄 HWND ParentWnd=glhTargetWnd; while(ParentWnd !=NULL) { glhTargetWnd=ParentWnd; //取应用程序主窗口句柄 ParentWnd=GetParent(glhTargetWnd); } if(glhTargetWnd!=glhPrevTarWnd) { char szCaption[100]; //取目标窗口标题 GetWindowText(glhTargetWnd,szCaption,100); …… } } //继续传递消息 return CallNextHookEx((HHOOK)glhHook,nCode,wParam,lParam); } |
最后,调用UnhookWindowsHookEx()函数完成对钩子的卸载:
void StopHook() { …… UnhookWindowsHookEx((HHOOK)glhHook); } |
现在完成的是鼠标钩子的动态链接库,经过编译后需要经应用程序的调用才能实现对当前系统下各线程间鼠标消息的拦截处理。这部分同普通动态链接库的使用没有任何区别,在将其加载到进程后,首先调用动态链接库的StartHook()函数安装好钩子,此时即可对系统下的鼠标消息实施拦截处理,在动态链接库被卸载即终止鼠标钩子时通过动态链接库中的StopHook()函数卸载鼠标钩子。
经上述编程,在安装好鼠标钩子后,鼠标在移动到系统任意窗口上时,马上就会通过对鼠标消息的拦截处理而获取到当前窗口的标题。实验证明此鼠标钩子的安装、使用和卸载过程是正确的。
小结
钩子,尤其是系统钩子具有相当强大的功能,通过这种技术可以对几乎所有的Windows系统消息和事件进行拦截处理。这种技术广泛应用于各种自动监控系统对进程外消息的监控处理。本文只对钩子的一些基本原理和一般的使用方法做了简要的探讨,感兴趣的读者完全可以在本文所述代码基础之上用类似的方法实现对诸如键盘钩子、外壳钩子等其他类型钩子的安装与使用。本文所述代码在Windows 98下由Microsoft Visual C++ 6.0编译通过。