許多開發者認為全域鉤子一定要依賴 DLL 才能將程式碼注入其他處理序。實際上,Windows 提供了四種特殊的鉤子類型——WH_JOURNALPLAYBACK、WH_JOURNALRECORD、WH_KEYBOARD_LL、WH_MOUSE_LL——它們的回呼函數會在「安裝鉤子的執行緒」內執行,因此完全不需要 DLL 就能運作。本文將以低階鍵盤鉤子(WH_KEYBOARD_LL)為例,示範如何在單一執行檔中實作全域鍵盤監聽。
根據 MSDN 文件,這四種鉤子的描述皆為:「This hook is called in the context of the thread that installed it.」(鉤子函數在安裝它的執行緒上下文中被呼叫)。換句話說,系統不會將鉤子程式碼映射到目標處理序的位址空間,而是直接在安裝者執行緒中呼叫回呼。因此:
// kbhook.cpp
#define _WIN32_WINNT 0x0400
#define STRICT
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
DWORD g_main_tid = 0;
HHOOK g_kb_hook = 0;
BOOL CALLBACK con_handler(DWORD)
{
PostThreadMessage(g_main_tid, WM_QUIT, 0, 0);
return TRUE;
}
LRESULT CALLBACK kb_proc(int code, WPARAM w, LPARAM l)
{
if (code < 0) return CallNextHookEx(g_kb_hook, code, w, l);
PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)l;
const char *info = NULL;
if (w == WM_KEYDOWN) info = "key dn";
else if (w == WM_KEYUP) info = "key up";
else if (w == WM_SYSKEYDOWN) info = "sys key dn";
else if (w == WM_SYSKEYUP) info = "sys key up";
if (info)
printf("%s - vkCode [%04x], scanCode [%04x]\n", info, p->vkCode, p->scanCode);
return CallNextHookEx(g_kb_hook, code, w, l);
}
int main(void)
{
g_main_tid = GetCurrentThreadId();
SetConsoleCtrlHandler(con_handler, TRUE);
// 注意:hMod 必須傳 GetModuleHandle(NULL),不可為 NULL
g_kb_hook = SetWindowsHookEx(WH_KEYBOARD_LL, kb_proc, GetModuleHandle(NULL), 0);
if (!g_kb_hook) {
fprintf(stderr, "SetWindowsHookEx failed with error %d\n", GetLastError());
return 1;
}
// 訊息迴圈是必要的,讓系統能呼叫回呼函數
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
UnhookWindowsHookEx(g_kb_hook);
return 0;
}
GetModuleHandle(NULL)(目前模組控制代碼),若傳 NULL 會導致 SetWindowsHookEx 失敗。GetMessage 迴圈。SetConsoleCtrlHandler 捕捉 Ctrl+C,發送 WM_QUIT 讓迴圈正常退出並釋放鉤子。LRESULT CALLBACK kb_proc(int nCode, WPARAM wParam, LPARAM lParam),lParam 指向 KBDLLHOOKSTRUCT 結構,包含虛擬鍵碼、掃描碼等資訊。善用 WH_KEYBOARD_LL 與 WH_MOUSE_LL 可在無需 DLL 的情況下實現全域輸入監聽,程式碼更簡潔、部署更輕鬆。若需更深層的處理序間介入,仍須採用傳統的 DLL 注入式鉤子(WH_KEYBOARD、WH_MOUSE 等)。