摘译自Win32 Hooks
作者:Kyle Marsh
本文描述钩子及其子MS Win32应用编程接口中的使用,讨论钩子函数、
过滤函数、以及以下类型的钩子:
Hook
Scope
WH_CALLWNDPROC
Thread or System
WH_CBT
Thread or System
WH_DEBUG
Thread or System
WH_GETMESSAGE
Thread or System
WH_JOURNALRECORD
System Only
WH_JOURNALPLAYBACK
System Only
WH_FOREGROUNDIDLE
Thread or System
WH_SHELL
Thread or System
WH_KEYBOARD
Thread or System
WH_MOUSE
Thread or System
WH_MSGFILTER
Thread or System
WH_SYSMSGFILTER
System Only
对于特定类型的钩子,线程范围的钩子首先被调用,然后是系统范围的钩子。通常如果使用线程范围的钩子能够满足要求,则尽量使用线程钩子,原因如下:
(1)应用程序不会导致系统范围的开销;
(2)不会引起所有的事件被序列化;例如,如何应用程序安装了一个系统范围的键盘钩子,则发生到所有应用程序的所有的键盘消息将会流经该应用程序的键盘过滤函数,导致系统的多个输入队列毫无用处。如果过滤函数停止处理键盘事件,则对用户而言系统似乎停滞了,然而实际上没有停。这时候,用户似乎需要使用CTRL+ALT+DEL组合键来解决问题喽。
(3)不需要将过滤函数打包为一个独立的DLL。因为所有的系统级钩子必须置于DLL中。
(4)不需要做DLL中共享数据。系统级过滤函数必须打包为一个DLL,还必须共享其它进程所需的数据。由于共享数据不是DLL的缺省行为,所以必须仔细的设计系统级过滤函数。如果一个过滤函数不能正确地实现数据共享,会导致进程的崩溃。
SetWindowsHookEx函数返回一个到所安装钩子的句柄(HHOOK),应用程序或库在调用UnhookWindowsHookEx函数时必须使用该句柄来标识钩子。如果过滤函数不能安装到钩子,则SetWindowsHookEx函数返回NULL。此外,SetWindowsHookEx函数会将最后的错误设置为如下值之一,以说明函数调用失败的原因:
ERROR_INVALID_HOOK_FILTER:钩子码无效
ERROR_INVALID_FILTER_PROC:过滤函数无效
ERROR_HOOK_NEEDS_HMOD:一个全局钩子却使用NULL实例参数,或者线程级钩子设置为可用于其它应用程序的线程
ERROR_GLOBAL_ONLY_HOOK:一个系统钩子被安装到特定线程
ERROR_INVALID_PARAMETER:线程ID无效
ERROR_JOURNAL_HOOK_SET:已经存在一个journal类型的钩子。此外,如果在屏保运行时应用程序尝试设置journal钩子,也会设置该代码。
ERROR_MOD_NOT_FOUND:用于全局钩子的hInstance参数不是一个库,该代码说明用户在其模块列表中不能定位模块句柄。
其它值:安全不允许设置钩子,或系统内存溢出
Windows在内部保存过滤函数链,且不依赖于过滤函数来存储下一个过滤函数的地址。此外,由于内部存储过滤函数链,使得性能得以很大提升。
UnhookWindowsHookEx函数:从钩子链中移除过滤函数,返回值表示是否钩子被移除。
过滤函数(Filter Functions)
过滤函数是安装到钩子上的函数。由于过滤函数是被Windows而非应用程序调用的,所以通常认为过滤函数是回调函数(callback functions)。所有的过滤函数具有如下形式:
LRESULT CALLBACK FilterFunc( nCode, wParam, lParam )int nCode;
WORD wParam;
DWORD lParam;
所有的过滤函数均应返回LONG型值,FilterFunc是实际过滤函数名的占位符。
过滤函数的参数
过滤函数接受三个参数:ncode(钩子代码)、wParam、lParam。钩子代码是一个整数,用来通知过滤函数一些附加的信息,例如钩子代码能够说明什么样的行为会引起调用钩子。
在Windows 3.1版本之前,钩子代码表示过滤函数是否应该处理事件或调用DEFHookProc。如果钩子代码是负数,过滤函数不应处理事件,而应调用DefHookProc,并直接将三个参数传递过去。Windows在应用程序的辅助下使用这些负数代码来维护过滤函数链。
在Windows 3.1中,如果Windows发送一个负数钩子代码给过滤函数,则过滤函数必须以传递给过滤函数的参数为实参对CallNextHookEx函数进行调用,并返回CallNextHookEx的返回值。
传递给过滤函数的第二个参数wParam是一个WPARAM,第三个参数lParam是一个LPARAM,这两个参数传递过滤函数需要的信息。每种钩子对wParam和lParam附加了不同的意义。例如,安装到WH_KEYBOARD钩子的过滤函数在wParam中接收一个虚拟键代码,而lParam包含一组位域(bit fields)来描述键盘事件发生时的键盘状态;安装到WH_MSGFILTER钩子的过滤函数在wParam中接收一个NULL值,在lParam中接收一个指向消息结构的指针;具体wParam和lParam的意义依赖于引起钩子调用的事件。完整的参数列表及每种类型钩子下的意义,需要参考Win32 SDK中如下的内容:
Hook
Filter function documentation
WH_CALLWNDPROC
CallWndProc
WH_CBT
CBTProc
WH_DEBUG
DebugProc
WH_GETMESSAGE
GetMsgProc
WH_JOURNALRECORD
JournalRecordProc
WH_JOURNALPLAYBACK
JournalPlaybackProc
WH_SHELL
ShellProc
WH_KEYBOARD
KeyboardProc
WH_MOUSE
MouseProc
WH_MSGFILTER
MessageProc
WH_SYSMSGFILTER
SysMsgProc
钩子设置完成后,Windows调用钩子链中的第一个过滤函数,然后Windows的工作结束,调用钩子链中下一个过滤函数的责任必须由过滤函数来保证。为此,Windows提供了CallNextHookEx来完成该项工作。函数CallNextHookEx有四个参数:
(1)第一个参数是SetWindowsHookEx调用的返回值,即当前钩子的句柄;
(2)其它参数分别是nCode、wParam、lParam
Windows在内部存储过滤函数,并跟踪哪个过滤函数被调用。因此在调用CallNextHookEx函数时,Windows确定钩子链中的下一个过滤函数,并对其进行调用。
有时候,过滤函数并不希望将事件传递给钩子链中的其它钩子函数,尤其在钩子允许过滤函数抛弃事件且过滤函数决定这样做时,一定不要调用CallNextHookEx。
由于过滤函数不会以任何特定的顺序被安装,因此在任何时刻都不能确定一个过滤函数在钩子链的什么地方,当然有例外,在过滤函数安装的时候,它是钩子链的第一个函数。同样,不要绝对地肯定你会获得所有出现的事件,因为在你之后安装的过滤函数可能不会将事件传递给你的过滤函数。
系统级的过滤函数必须存在于一个DLL中。在Win16中,允许但不推荐在一个应用程序中将过滤函数安装到一个系统级钩子上,这在Win32中已不可行。WH_JOURNALRECORD和WH_JOURNALPLAYBACK对该规则例外。
系统级钩子的过滤函数运行在多个不同的进程中,因此必须准备好共享任何需要的数据。一个DLL被映射到其每个客户进程的地址空间,在DLL中的全局变量将是实例特定的,除非全局变量被放在共享数据段。例如,在钩子的例子程序中HOOKSDLL.DLL库需要共享两个数据项:
要共享这些数据,HOOKSDLL将其放在一个共享数据段中。以下是HOOKSDLL共享数据的步骤:
// Shared DATA #pragma data_seg(".SHARDATA") static HWND hwndMain = NULL; // Main hwnd. We will get this from the app. static int nLineHeight = 0; // Height of lines in window. #pragma data_seg()
SECTIONS .SHARDATA Read Write Shared
hooksdll.exp: hooksdll.obj hooksdll.def $(implib) -machine:$(CPU) \ -def:hooks.def \ hooksdll.obj \ -out:hooksdll.lib
hooksdll.dll: hooksdll.obj hooksdll.def hooksdll.lib hooksdll.exp $(link) $(linkdebug) \ -base:0x1C000000 \ -dll \ -entry:LibMain$(DLLENTRY) \ -out:hooksdll.dll \ hooksdll.exp hooksdll.obj hooksdll.rbj \ $(guilibsdll)