环境:vs2017,添加Windows.h头文件。
核心函数:SetWindowsHookEx(),UnhookWindowsHookEx(),CallNextHookEx()
你可能在其他api文档看到SetWindowsHookExA、SetWindowsHookExW这两个函数,他们是编码上的区别,A指ASCII,W指wide-char也就是unicode编码,但系统已经帮我们用宏处理好,只需调用SetWindowsHookEx就行了,自动选择对应编码那个函数。
Hook:建议先百度自行稍微了解
HINSTANCE、HMODULE、HANDLE、HWND是一样的东西,都是各种typedef绕来绕去,区别只在于人类使用时附加的语义,实际上都是一串数字。下面就混着用了。
下面仅以最基本的控制台应用示例。
非常简单的一个例子,运行后鼠标移动即可看到效果:
LRESULT CALLBACK mymouse(int nCode, WPARAM wParam, LPARAM lParam)
{
cout<<"yes"<<endl;
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
int main()
{
HHOOK mouseHook = SetWindowsHookEx(WH_MOUSE_LL, mymouse, 0, 0);
MSG msg;
while (GetMessage(&msg, NULL, NULL, NULL))
{
}
UnhookWindowsHookEx(mouseHook);
return 0;
}
1.mymouse函数:
LRESULT:long result,实际就是个宏,当long
(长整数)就行。
CALLBACK:也是宏,实际上是__stdcall
,函数修饰关键字,详细自行可百度。
WPARAM、LPARAM:还是宏,当整数就行。不同的值有不同含义,具体看msdn。
为什么这样定义?
因为SetWindowsHookEx()的第二个参数就是接受这样的一个函数指针。简单了解使用函数指针
该函数返回0时,消息继续往下传递;返回1时消息不再往下传递。
为什么是return CallNextHookEx()而不是直接return 0或1?
SetWindowsHookEx()把这个钩子放在Hook链头,不调用这个方法其他链节点的Hook不执行。另外该函数的第一个参数没用,直接null就行。
2.HHOOK类型:
代表加进去的钩子,后面解除钩子的函数UnhookWindowsHookEx(),参数就是这个的对象。
3.MSG、while:
为了不让程序结束,用一个while卡住它。
为什么不用while(true)?
看第5点
4.SetWindowsHookEx函数:
原型:
HHOOK WINAPI SetWindowsHookEx(
_In_ int idHook,
_In_ HOOKPROC lpfn,
_In_ HINSTANCE hMod,
_In_ DWORD dwThreadId
);
看到陌生的不用头大,都是关键字和宏罢了,直接讲4个参数。
idHook:一个int值,不同的值对应不同的钩子(鼠标or键盘之类的)。vs中打出WH_
后,看自动补全显示的名字,你就能了解了,不用死记硬背。WH就是window hook的意思。
lpfn:函数指针,类型就是mymouse函数那个类型。每当钩子获取到消息,就会调用该函数指针。
hMod:用于帮助找到真正的地址,下面会讲。
dwThreadId:DWORD也是一个宏,当整数就行。这里指你要把钩子挂到哪个线程中,所有进程的所有线程都可以选择,只不过其他进程的线程不一定挂的上,需要其他手段,不作细说。填0就是尝试把所有线程都给挂上,成了所谓的全局钩子(只是尝试,拒绝挂钩子的线程还是挂不上)。
当挂钩子失败时,该函数返回null。另外msdn讲解参数这段一定要看:
lpfn [in]
Type: HOOKPROC
A pointer to the hook procedure. If the dwThreadId parameter is zero or specifies the identifier of a thread created by a different process, the lpfn parameter must point to a hook procedure in a DLL. Otherwise, lpfn can point to a hook procedure in the code associated with the current process.
如果dwThreadId指定的线程并不是由当前进程创建的,那么子程(就是那个函数,如示例的mymouse)一定要写在dll里(动态链接库)。
原因:由于lpfn是指针和逻辑地址的机制,当前进程的函数地址不适用于其他进程。系统一般需要借助dll模块以确定函数入口地址(low-level除外,只有此时可以不写dll)。
hMod [in]
Type: HINSTANCE
A handle to the DLL containing the hook procedure pointed to by the lpfn parameter. The hMod parameter must be set to NULL if the dwThreadId parameter specifies a thread created by the current process and if the hook procedure is within the code associated with the current process.
当 dwThreadId指定的线程是当前进程创建的线程 且 子程的代码是在当前进程(非dll)写的,那么这个hMod必须填0(也就是null)。
值得一提,在dll中时,这个参数要填为该dll的句柄,可以有两种方法获取来填:
(1)把入口DllMain的参数hModule(HMODULE和HINSTANCE一样的)保存下来,填进去(推荐)
(2)通过函数GetModuleHandle(L"dll name")来获取(加L代表为unicode编码,不用加dll后缀)(不推荐)
绝大多数情况下会写成动态链接库(建议)。上面示例能算上特例,不写在dll中就要:首先是钩子类型不是WH_MOUSE而是WH_MOUSE_LL(LL代表low-level,另一种回调机制,写在dll中则两种都可以),不然没反应,其次第三个参数hMod写成0。
5.GetMessage函数:从线程消息队列获取消息,没有消息将进入等待态直至有消息被插入队列
(1)系统获取消息-----系统分发消息---(Hook链)-----消息抵达线程消息队列。(这下搞懂第一点说的消息是否继续传递和CallNextHookEx了吧)
(2)我们示例的控制台程序创建时默认是没有消息队列的,但调用相关函数时就自动创建消息队列,如GetMessage函数
(3)子程是由系统通过回调机制,把执行SetWindowsHookEx函数的线程叫回来去执行的,也就是示例中的mymouse函数是由主线程去执行的。因此,如果写成while(true),那么线程将无限运行,系统没有机制能够中断它的执行来把它叫过来执行子程。但是,调用GetMessage函数后,线程有了消息队列,而且我们没有往里面插入消息,线程就进入等待态,此时系统便能够控制该线程,让它去执行子程。也就是说,我们需要把这个线程弄成等待态去让系统获取控制权,所以你还可以使用while(true){ Sleep(100); }
达到相同目的。
(4)由于上述原因,对于挂Hook的线程来说,这样一个循环查询消息队列是有必要的。
(5)如果系统发现hook的子程长期得不到执行(你的线程正在繁忙),那么系统会自动删除掉这个hook。这里就要注意了,不要安排耗时任务在该线程上。