[src: ]
动机
很多游戏在窗口失焦时会自动暂停,绝大多数情况下这没问题,也是玩家期待的行为,因为游戏大概率是全屏或最大化运行,没有玩家希望因为一个弹窗而导致角色死亡。但是如果游戏的输入操作都已经被自动化(如游戏内机制、脚本),或者游戏本身就并不需要太多输入,甚至双屏且大脑具有超线程的用户希望同时激活两个窗口。这个【自动暂停】显然是不利的,强制保持意味着进行其他工作必然会暂停该游戏。(桌面环境只能一个窗口处于激活状态)
把行为类似这个游戏的程序称为【独占程序】,与之相对的,失焦不会暂停的程序(如浏览器、视频播放器)称为【非独占程序】。
解决方案
我们不能对操作系统下手,要解决这个问题,只能改变【独占程序】的行为。让【独占程序】不再独占需要对消息处理函数 下手,用 写过窗口程序的朋友应该熟悉这个函数,该函数处理发送到窗口的消息。用户的所有操作都以操作系统桌面发消息的形式与程序交互,只要拦截了可能导致暂停的消息,那么就去掉了这个程序的“独占性”。
而对 下手的做法就是一种挂钩(),而处理拦截的代码,被称为钩子(hook)。
以下来自 wiki条目:
钩子编程(),也称作“挂钩”,是计算机程序设计术语,指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码,被称为钩子(hook)。
可以见得,并不是只有拦截 消息处理函数 的行为是挂钩。
而拦截了 也不是只能处理这里【独占程序】的问题,与消息处理有关的其他问题也有可能通过这种方法解决(如自定义×按钮的行为、自定义组合键、分离输入)
细节
因为知道挂钩 Win32 API 已经有一些可以拿来主义的成品了,如,顺着这个思路考虑。函数是实现了窗口功能的程序员提供的,本身并不是 ,没法直接挂钩这个函数,只能挂钩任何与 相关的 Win32 API函数,如,,,任何对这些函数的调用将被拦截,每次调用传入的 参数都会被修改后传入原版 Win32 API 函数,这样就能完成任务。这个方法很绕而且不干净,遇到了一些问题后我放弃了这个方法。
后来才知道 能在运行时直接修改,这时我才从圈子里转出来,挂钩消息处理函数并不需要挂钩 。挂钩 的代码大概像下面一样。
WNDPROC originalWndProc = NULL;
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
if (message == WM_NCACTIVATE)
return 0;
return CallWindowProc(originalWndProc, hWnd, message, wParam, lParam);
}
void Hook() {
LONG_PTR lp = SetWindowLongPtr(hWindow, GWLP_WNDPROC, (LONG_PTR)WndProc);
// Changes WNDPROC of the specified window
originalWndProc = (WNDPROC)lp;
}
这里仅仅屏蔽了,根据实际情况可能还需要屏蔽更多消息。
实施
因为我使用了 进行了一些输入自动化和窗口管理功能,很自然地,我希望用 解决这个问题。遗憾的是,结果一番调查发现 无法实现此功能。
辅助工具
使用 的自带工具 Spy++或者其他类似工具可以获取窗口信息,这对于实施本节的内容非常有帮助。
侵入式修改
Wiki提到可以通过修改可执行程序来执行自己添加的代码,这要借助调试器找到 的入口点并参考前文作出修改。注意:找入口点也可以直接借助 Spy++或其他类似工具。这涉及逆向的内容,虽然麻烦但是有效。
运行时修改
操作系统的事件钩子对这个问题没有帮助。不过我们可以直接向进程注入代码达到目的,或者进行DLL注入,因为DLL注入用在这里很合适,所以这里给出被注入DLL本身的完整源代码:
// Hook.dll
WNDPROC originalWndProc = NULL;
// export is unnecessary
LRESULT __declspec(dllexport) CALLBACK
WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
if (message == WM_NCACTIVATE)
return 0;
if (message == WM_ACTIVATEAPP && wParam == FALSE)
return 0;
return CallWindowProc(originalWndProc, hWnd, message, wParam, lParam);
}
BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam) {
DWORD pid = GetCurrentProcessId();
DWORD hWnd_pid = 0;
GetWindowThreadProcessId(hWnd, &hWnd_pid);
if (pid == hWnd_pid) {
*(HWND *)lParam = hWnd;
return FALSE;
}
return TRUE;
}
HWND GetCurrentHWND() {
HWND hWnd = 0;
EnumWindows(EnumWindowsProc, (LPARAM)&hWnd);
return hWnd;
}
void Hook() {
HWND hWindow = NULL;
while (hWindow == NULL) {
hWindow = GetCurrentHWND();
// FindWindow is an alternative
Sleep(100);
}
LONG_PTR lp = SetWindowLongPtr(hWindow, GWLP_WNDPROC, (LONG_PTR)WndProc);
// Changes WNDPROC of the specified window
originalWndProc = (WNDPROC)lp;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call,
LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
DisableThreadLibraryCalls(hModule);
// This can reduce the size of the working set for some applications.
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)Hook, hModule,
NULL, NULL);
// Run Hook in new thread
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
以上代码通过枚举所有窗口来获取当前进程对应的窗口句柄,也可以通过Spy++或其他类似工具取得窗口类名和标题,再借由 获取窗口句柄。
要做的工作还没有结束,还要将编译好 DLL 注入进程。
DLL注入
这节实在没什么可写的(毕竟作者也所知甚少),就直接把wiki放在在这里了,里面搜罗了很多方法。值得一提的是,除了注册表方法(全局),可实施性和通用性都比较好的方法应该就是 的代码注入方法了,也很适合用在这里。
我把 wiki 的示例代码增补成了一个命令行工具 ,和前文的 源码放在了同一个 repo里,非常粗糙的程序,小心取用。
实际上,要求程序加载指定 DLL,也可以不在运行时注入 DLL,通过修改 PE文件的导入表( table)解决问题,这有一定的侵入性但某些情况下可能有优势。
尽管类似,但是挂钩 Win32 API 比本文的问题复杂得多
参考
发表回复