钩子和DLL






4.89/5 (76投票s)
2001年4月1日
11分钟阅读

1685339

13303
关于如何设置和使用全局钩子函数,存在很多困惑。本文旨在澄清其中一些问题。
引言
关于如何设置和使用全局钩子函数,存在很多困惑。本文旨在澄清其中一些问题。
值得指出的是,Flounders 通常不赞成使用钩子,但这些钩子被认为是可接受的。
请注意,如果您只是从自己的进程中挂钩操作,则不会出现以下描述的任何问题。这仅在您想要获取系统范围的事件时才会发生。
这里的关键问题是地址空间。当全局 DLL 执行时,它在挂钩事件的进程的上下文中执行。这意味着它甚至为其自己的变量看到的地址也是目标进程上下文中的地址。由于这是一个 DLL,它为每个使用它的进程提供其数据的私有副本,这意味着您在 DLL 中全局声明的变量(例如在文件级别声明的变量)中设置的任何值都是私有变量,并且不会从原始 DLL 的上下文中继承任何内容。它们将被重新初始化,通常这意味着它们将为零。
最近的一篇帖子甚至提出了在 DLL 中存储回调地址的想法。这是不可能的。好吧,存储它并非不可能,但使用它是不可能的。您存储的是一堆位。即使您按照下面的说明创建一个所有 DLL 实例都可见的共享内存变量,这一堆位(您认为是地址)实际上也只在存储它的进程的上下文中作为地址有意义。对于所有其他进程,这仅仅是一堆位,如果您尝试将其用作地址,您将调用正在被拦截事件的进程中的某个地址,这完全无用。它很可能会导致应用程序崩溃。
这种独立地址空间的概念很难理解。让我用一张图片来加以说明。
这里有三个进程。您的进程显示在左侧。DLL 包含代码、数据和一个共享段,我们稍后将讨论如何实现。现在,当钩子 DLL 执行以拦截进程 A 的事件时,它被映射到进程 A 的地址空间,如图所示。代码是共享的,因此进程 A 中的地址引用与您的进程中的地址相同的页面。 巧合的是,它们恰好被重新定位到进程 A 中相同的虚拟地址,这意味着进程 A 看到的地址。进程 A 也获得其自己的私有副本的数据段,因此进程 A 看到的“数据”中的任何内容都完全是进程 A 私有的,并且不能影响任何其他进程(或受其他进程影响!)。但是,使这一切奏效的技巧是共享数据段,此处以红色显示。您的进程引用的页面与进程 A 中引用的内存页面完全相同。请注意,巧合的是,这些页面恰好在进程 A 的地址空间中以与您的进程中完全相同的虚拟地址出现。如果您同时调试您的进程和进程 A(您可以通过运行两个 VC++ 副本来实现!),如果您查看共享数据段中的
&something
,并在您的进程中查看它,然后在进程 A 中查看相同的&something
,您会看到完全相同的数据,甚至在相同的地址。如果您使用调试器更改,或观察程序更改something
的值,您可以转到另一个进程,检查它,并看到新值也出现在那里。
但这里有一个关键点:相同的地址是巧合。它绝对、肯定不保证。看看进程 B。当事件在进程 B 中被挂钩时,DLL 被映射进来。但它在您的进程和进程 A 中占用的地址在进程 B 的地址空间中不可用。因此,所发生的一切是代码被重新定位到进程 B 中的一个不同地址。代码很满意;它实际上不关心它在哪个地址执行。数据地址被调整以引用数据的新位置,甚至共享数据也被映射到一组不同的地址,因此它的引用方式不同。如果您在进程 B 中运行调试器并查看共享区域中的&something
,您会发现something
的地址不同,但something
的内容相同;在您的进程或进程 A 中更改内容将立即使更改在进程 B 中可见,尽管进程 B 在不同的地址看到它。它是相同的物理内存位置。虚拟内存是您作为程序员看到的地址与实际构成您计算机的物理内存页面之间的映射。
虽然我将相似的放置称为巧合,但这种“巧合”有点刻意;Windows 尽可能尝试将 DLL 映射到与相同 DLL 的其他实例相同的虚拟位置。它会尝试。但它可能无法成功。
如果你懂得一点点(足够危险的程度),你可能会说,啊哈!我可以重新基址我的DLL,让它加载到一个不会冲突的地址,这样我就可以忽略这个特性了。这是一个“一知半解是危险的”典型例子。你不能保证这能在你的电脑上运行的每个可执行文件上都有效!因为这是一个全局钩子DLL,它可以被Word、Excel、Visio、VC++以及六千个你从未听说过但你总有一天会运行或你的客户可能会运行的应用程序调用。所以,算了吧。不要尝试重新基址。你最终会失败的。通常是在最糟糕的时候,面对你最重要的客户(例如,你的产品评论员,或者你对其他可能遇到的bug已经感到紧张的头号客户……) 假定共享数据段是“可移动的”。如果你不理解这段话,说明你懂得还不足以危险,你可以忽略它。
这种重定位还有其他含义。在DLL中,如果你在你的进程中存储了一个指向回调函数的指针,那么DLL在进程A或进程B中执行它将是毫无意义的。该地址确实会导致控制转移到它所指定的位置,但是这种转移会发生在进程A或进程B的地址空间中,这是相当无用的,更不用说几乎肯定是致命的。
这还意味着您不能在您的 DLL 中使用任何 MFC。它不能是 MFC DLL,也不能是 MFC 扩展 DLL。为什么?因为它会调用 MFC 函数。它们在哪里?嗯,它们在您的地址空间中。不在用 Visual Basic 编写的进程 A 的地址空间中,也不在用 Java 编写的进程 B 的地址空间中。因此,您必须编写一个纯 C DLL,我也建议忽略整个 C 运行时库。您应该只使用 API。使用 lstrcpy
代替 strcpy
或 tcscpy
,使用 lstrcmp
代替 strcmp
或 tcscmp
,等等。 
关于 DLL 如何与其控制服务器通信,有许多解决方案。一种解决方案是使用 ::PostMessage
或 ::SendMessage
(请注意,我这里指的是原始 API 调用,而不是 MFC 调用!)。只要可能使用 ::PostMessage
,就优先使用它而不是 ::SendMessage
,因为您可能会遇到讨厌的死锁。如果您的进程最终停止,系统中的每个其他进程都会停止,因为所有进程都阻塞在永远不会返回的 ::SendMessage
上,而您刚刚导致整个系统瘫痪,这可能会导致用户认为关键的应用程序中数据严重丢失。这绝不是好事。
您还可以在共享内存区域中使用信息队列,但我将把这个主题视为本文的范围之外。
在 ::SendMessage
或 ::PostMessage
中,您无法传回指针(我们将忽略将相对指针传回共享内存区域的问题;这也超出了本文的范围)。这是因为您生成的任何指针都将引用 DLL 中的地址(重定位到钩子进程中)或钩子进程(进程 A 或进程 B)中的地址,因此在您的进程中将完全无用。您只能在 WPARAM
或 LPARAM
中传回与地址空间无关的信息。
我强烈建议为此目的使用注册窗口消息(请参阅我关于消息管理的文章)。您可以在发送或发布消息的窗口的MESSAGE_MAP
中使用ON_REGISTERED_MESSAGE
宏。
现在获取该窗口的 HWND
是主要要求。幸运的是,这很容易。
您要做的第一件事是创建共享数据段。这是通过使用 #pragma data_seg
声明完成的。选择一个好的助记数据段名称(长度不得超过 8 个字符)。为了强调该名称是任意的,我在这里使用了自己的名称。我发现在教学中,如果我使用像 .SHARE
或 .SHR
或 .SHRDATA
这样的好名称,学生会认为该名称具有重要意义。它没有。
#pragma data_seg(".JOE") HANDLE hWnd = NULL; #pragma dta_seg() #pragma comment(linker, "/section:.JOE,rws")
您在命名数据段的 #pragma
范围内声明的任何变量都将分配给该数据段,前提是它们已初始化。如果您未能提供初始化程序,则变量将分配给默认数据段,并且 #pragma
将不起作用。
目前看来,这排除了在共享数据段中使用 C++ 对象数组,因为您无法初始化用户定义对象的 C++ 数组(它们的默认构造函数应该完成此操作)。这似乎是一个根本性的限制,是形式上的 C++ 要求与要求存在初始化程序的 Microsoft 扩展之间的交互。
#pragma comment
会使链接器在链接步骤中添加所示的命令行开关。您可以进入 VC++ 的 Project | Settings 并更改链接器命令行,但如果您移动代码,这很难记住(常见的失败是忘记将设置更改为 All Configurations,因此可以愉快地调试,但在发布配置中却失败。所以我发现最好将命令直接放在源文件中。请注意,后面的文本必须符合链接器命令开关的语法。这意味着您所示的文本中不能有任何空格,否则链接器将无法正确解析它。
您通常提供某种机制来设置窗口句柄,例如
void SetWindow(HWND w)
{
hWnd = w;
}
尽管这通常与设置钩子本身相结合,我将在下面展示。
示例:鼠标钩子
头文件 (myhook.h)
函数setMyHook
和clearMyHook
必须在这里声明,但这在我的文章《终极DLL头文件》中有所解释。
#define UWM_MOUSEHOOK_MSG \ _T("UMW_MOUSEHOOK-" \ "{B30856F0-D3DD-11d4-A00B-006067718D04}")
源文件 (myhook.cpp)
#include "stdafx.h" #include "myhook.h" #pragma data_seg(".JOE") HWND hWndServer = NULL; #pragma data_seg() #pragma comment("linker, /section:.JOE,rws") HINSTANCE hInstance; UINT HWM_MOUSEHOOK; HHOOK hook; // Forward declaration static LRESULT CALLBACK msghook(int nCode, WPARAM wParam, LPARAM lParam);
/**************************************************************** * DllMain * Inputs: * HINSTANCE hInst: Instance handle for the DLL * DWORD Reason: Reason for call * LPVOID reserved: ignored * Result: BOOL * TRUE if successful * FALSE if there was an error (never returned) * Effect: * Initializes the DLL. ****************************************************************/ BOOL DllMain(HINSTANCE hInst, DWORD Reason, LPVOID reserved) { switch(Reason) { /* reason */ //********************************************** // PROCESS_ATTACH //********************************************** case DLL_PROCESS_ATTACH: // Save the instance handle because we need it to set the hook later hInstance = hInst; // This code initializes the hook notification message UWM_MOUSEHOOK = RegisterWindowMessage(UWM_MOUSEHOOK_MSG); return TRUE; //********************************************** // PROCESS_DETACH //********************************************** case DLL_PROCESS_DETACH: // If the server has not unhooked the hook, unhook it as we unload if(hWndServer != NULL) clearMyHook(hWndServer); return TRUE; } /* reason */
/**************************************************************** * setMyHook * Inputs: * HWND hWnd: Window whose hook is to be set * Result: BOOL * TRUE if the hook is properly set * FALSE if there was an error, such as the hook already * being set * Effect: * Sets the hook for the specified window. * This sets a message-intercept hook (WH_GETMESSAGE) * If the setting is successful, the hWnd is set as the * server window. ****************************************************************/ __declspec(dllexport) BOOL WINAPI setMyHook(HWND hWnd) { if(hWndServer != NULL) return FALSE; hook = SetWindowsHookEx( WH_GETMESSAGE, (HOOKPROC)msghook, hInstance, 0); if(hook != NULL) { /* success */ hWndServer = hWnd; return TRUE; } /* success */ return FALSE; } // SetMyHook
/**************************************************************** * clearMyHook * Inputs: * HWND hWnd: Window whose hook is to be cleared * Result: BOOL * TRUE if the hook is properly unhooked * FALSE if you gave the wrong parameter * Effect: * Removes the hook that has been set. ****************************************************************/ __declspec(dllexport) BOOL clearMyHook(HWND hWnd) { if(hWnd != hWndServer) return FALSE; BOOL unhooked = UnhookWindowsHookEx(hook); if(unhooked) hWndServer = NULL; return unhooked; }
/**************************************************************** * msghook * Inputs: * int nCode: Code value * WPARAM wParam: parameter * LPARAM lParam: parameter * Result: LRESULT * * Effect: * If the message is a mouse-move message, posts it back to * the server window with the mouse coordinates * Notes: * This must be a CALLBACK function or it will not work! ****************************************************************/ static LRESULT CALLBACK msghook(int nCode, WPARAM wParam, LPARAM lParam) { // If the value of nCode is < 0, just pass it on and return 0 // this is required by the specification of hook handlers if(nCode < 0) { /* pass it on */ CallNextHookEx(hook, nCode, wParam, lParam); return 0; } /* pass it on */ // Read the documentation to discover what WPARAM and LPARAM // mean. For a WH_MESSAGE hook, LPARAM is specified as being // a pointer to a MSG structure, so the code below makes that // structure available LPMSG msg = (LPMSG)lParam; // If it is a mouse-move message, either in the client area or // the non-client area, we want to notify the parent that it has // occurred. Note the use of PostMessage instead of SendMessage if(msg->message == WM_MOUSEMOVE || msg->message == WM_NCMOUSEMOVE) PostMessage(hWndServer, UWM_MOUSEMOVE, 0, 0); // Pass the message on to the next hook return CallNextHookEx(hook, nCode, wParam, lParam); } // msghook
服务器应用程序
在头文件中,将此添加到类的保护部分
afx_msg LRESULT OnMyMouseMove(WPARAM,LPARAM);
在应用程序文件中,将此添加到文件开头某处
UINT UWM_MOUSEMOVE = ::RegisterWindowMessage(UWM_MOUSEMOVE_MSG);
在MESSAGE_MAP
中,将以下行添加到魔术//{AFX_MSG
注释之外
ON_REGISTERED_MESSAGE(UWM_MOUSEMOVE, OnMyMouseMove)
在您的应用程序文件中,添加以下函数
LRESULT CMyClass::OnMyMouseMove(WPARAM, LPARAM) { // ...do stuff here return 0; }
我写了一个小示例应用程序来演示这个,但由于我第 n+1 次做全局钩子函数感到无聊,所以我给它设计了一个漂亮的界面。猫咪从窗户向外看,观察着鼠标。但要小心!靠近猫咪足够近,它就会抓住鼠标!
您可以下载并构建此项目。真正的关键是 DLL 子项目;其余的都是使用它的装饰性内容。
此示例中还展示了其他几种技术,包括各种绘图技术、ClipCursor
和SetCapture
的使用、区域选择、屏幕更新等,因此对于Windows编程各个方面的初学者来说,除了演示钩子函数的使用外,它还有其他价值。
这些文章中表达的观点是作者的观点,不代表,也不被微软认可。