引言
Windows操作系統是建立在事件驅動機制之上的,系統各部分之間的溝通也都是通過消息的相互傳遞而實現的。但在通常情況下,應用程序只能處理來自進程內部的消息或是從其他進程發過來的消息,如果需要對在進程外傳遞的消息進行攔截處理就必須採取一種被稱爲HOOK(鉤子)的技術。鉤子是Windows操作系統中非常重要的一種系統接口,用它可以輕鬆截獲並處理在其他應用程序之間傳遞的消息,並由此可以完成一些普通應用程序難以實現的特殊功能。基於鉤子在消息攔截處理中的強大功能,本文即以VC++ 6.0爲編程背景對鉤子的基本概念及其實現過程展開討論。爲方便理解,在文章最後還給出了一個簡單的有關鼠標鉤子的應用示例。
鉤子的基本原理
鉤子的本質是一段用以處理系統消息的程序,通過系統調用,將其掛入到系統。鉤子的種類有很多,每一種鉤子負責截獲並處理相應的消息。鉤子機制允許應用程序截獲並處理髮往指定窗口的消息或特定事件,其監視的窗口即可以是本進程內的也可以是由其他進程所創建的。在特定的消息發出,並在到達目的窗口之前,鉤子程序先行截獲此消息並得到對其的控制權。此時在鉤子函數中就可以對截獲的消息進行各種修改處理,甚至強行終止該消息的繼續傳遞。
任何一個鉤子都由系統來維護一個指針列表(鉤子鏈表),其指針指向鉤子的各個處理函數。最近安裝的鉤子放在鏈的開始,最早安裝的鉤子則放在最後,當鉤子監視的消息出現時,操作系統調用鏈表開始處的第一個鉤子處理函數進行處理,也就是說最後加入的鉤子優先獲得控制權。在這裏提到的鉤子處理函數必須是一個回調函數(callback function),而且不能定義爲類成員函數,必須定義爲普通的C函數。在使用鉤子時可以根據其監視範圍的不同將其分爲全局鉤子和線程鉤子兩大類,其中線程鉤子只能監視某個線程,而全局鉤子則可對在當前系統下運行的所有線程進行監視。顯然,線程鉤子可以看作是全局鉤子的一個子集,全局鉤子雖然功能強大但同時實現起來也比較煩瑣:其鉤子函數的實現必須封裝在動態鏈接庫中纔可以使用。
鉤子的安裝與卸載
由於全局鉤子具有相當的廣泛性而且在功能上完全覆蓋了線程鉤子,因此下面就主要對應用較多的全局鉤子的安裝與使用進行討論。前面已經提過,操作系統是通過調用鉤子鏈表開始處的第一個鉤子處理函數而進行消息攔截處理的。因此,爲了設置鉤子,只需將回調函數放置於鏈首即可,操作系統會使其首先被調用。在具體實現時由函數SetWindowsHookEx()負責將回調函數放置於鉤子鏈表的開始位置。SetWindowsHookEx()函數原型聲明如下:
HHOOK SetWindowsHookEx(int idHook;
HOOKPROC lpfn;
HINSTANCE hMod;
DWORD dwThreadId);
其中:參數idHook 指定了鉤子的類型,總共有如下13種:
WH_CALLWNDPROC 系統將消息發送到指定窗口之前的"鉤子"
WH_CALLWNDPROCRET 消息已經在窗口中處理的"鉤子"
WH_CBT 基於計算機培訓的"鉤子"
WH_DEBUG 差錯"鉤子"
WH_FOREGROUNDIDLE 前臺空閒窗口"鉤子"
WH_GETMESSAGE 接收消息投遞的"鉤子"
WH_JOURNALPLAYBACK 回放以前通過WH_JOURNALRECORD"鉤子"記錄的輸入消息
WH_JOURNALRECORD 輸入消息記錄"鉤子"
WH_KEYBOARD 鍵盤消息"鉤子"
WH_MOUSE 鼠標消息"鉤子"
WH_MSGFILTER 對話框、消息框、菜單或滾動條輸入消息"鉤子"
WH_SHELL 外殼"鉤子"
WH_SYSMSGFILTER 系統消息"鉤子"
參數lpfn爲指向鉤子處理函數的指針,即回調函數的首地址;參數hMod則標識了鉤子處理函數所處模塊的句柄;第四個參數dwThreadId 指定被監視的線程,如果明確指定了某個線程的ID就只監視該線程,此時的鉤子即爲線程鉤子;如果該參數被設置爲0,則表示此鉤子爲監視系統所有線程的全局鉤子。此函數在執行完後將返回一個鉤子句柄。
在SetWindowsHookEx()函數完成對鉤子的安裝後,如果被監視的事件發生,系統馬上會調用位於相應鉤子鏈表開始處的鉤子處理函數進行處理,每一個鉤子處理函數在進行相應的處理時都要考慮是否需要把事件傳遞給下一個鉤子處理函數。如果要傳遞,就通過函數CallNestHookEx()來解決。儘管如此,在實際使用時還是強烈推薦無論是否需要事件傳遞而都在過程的最後調用一次CallNextHookEx( )函數,否則將會引起一些無法預知的系統行爲或是系統鎖定。該函數將返回位於鉤子鏈表中的下一個鉤子處理過程的地址,至於具體的返回值類型則要視所設置的鉤子類型而定。該函數的原型聲明如下:
LRESULT CallNextHookEx(HHOOK hhk;int nCode;WPARAM wParam;LPARAM lParam);
其中,參數hhk爲由SetWindowsHookEx()函數返回的當前鉤子句柄;參數nCode爲傳給鉤子過程的事件代碼;參數wParam和lParam 則爲傳給鉤子處理函數的參數值,其具體含義同設置的鉤子類型有關。
最後,由於安裝鉤子對系統的性能有一定的影響,所以在鉤子使用完畢後應及時將其卸載以釋放其所佔資源。釋放鉤子的函數爲UnhookWindowsHookEx(),該函數比較簡單隻有一個參數用於指定此前由SetWindowsHookEx()函數所返回的鉤子句柄,原型聲明如下:
BOOL UnhookWindowsHookEx(HHOOK hhk);
鼠標鉤子的簡單示例 最後,調用UnhookWindowsHookEx()函數完成對鉤子的卸載:
最後,爲更清楚展示HOOK技術在VC編程中的應用,給出一個有關鼠標鉤子使用的簡單示例。在鉤子設置時採用的是全局鉤子。下面就對鼠標鉤子的安裝、使用以及卸載等過程的實現進行講述:
由於本例程需要使用全局鉤子,因此首先構造全局鉤子的載體--動態鏈接庫。考慮到 Win32 DLL與Win16 DLL存在的差別,在Win32環境下要在多個進程間共享數據,就必須採取一些措施將待共享的數據提取到一個獨立的數據段,並通過def文件將其屬性設置爲讀寫共享:
#pragma data_seg("TestData") HWND glhPrevTarWnd=NULL; // 窗口句柄 HWND glhHook=NULL; // 鼠標鉤子句柄 HINSTANCE glhInstance=NULL; // DLL實例句柄 #pragma data_seg() …… SECTIONS // def文件中將數據段TestData設置爲讀寫共享 TestData READ WRITE SHARED |
在安裝全局鼠標鉤子時使用函數SetWindowsHookEx(),並設定鼠標鉤子的處理函數爲MouseProc(),安裝函數返回的鉤子句柄保存於變量glhHook中:
void StartHook(HWND hWnd) { …… glhHook=(HWND)SetWindowsHookEx(WH_MOUSE,MouseProc,glhInstance,0); } |
鼠標鉤子安裝好後,在移動、點擊鼠標時將會發出鼠標消息,這些消息均經過消息處理函數MouseProc()的攔截處理。在此,每當捕獲到系統各線程發出的任何鼠標消息後首先獲取當前鼠標所在位置下的窗口句柄,並進一步通過GetWindowText()函數獲取到窗口標題。在處理函數完成後,通過CallNextHookEx()函數將事件傳遞到鉤子列表中的下一個鉤子處理函數:
LRESULT WINAPI MouseProc(int nCode,WPARAM wParam,LPARAM lParam) { LPMOUSEHOOKSTRUCT pMouseHook=(MOUSEHOOKSTRUCT FAR *) lParam; if(nCode>=0) { HWND glhTargetWnd=pMouseHook->hwnd; //取目標窗口句柄 HWND ParentWnd=glhTargetWnd; while(ParentWnd !=NULL) { glhTargetWnd=ParentWnd; //取應用程序主窗口句柄 ParentWnd=GetParent(glhTargetWnd); } if(glhTargetWnd!=glhPrevTarWnd) { char szCaption[100]; //取目標窗口標題 GetWindowText(glhTargetWnd,szCaption,100); …… } } //繼續傳遞消息 return CallNextHookEx((HHOOK)glhHook,nCode,wParam,lParam); } |
void StopHook() { …… UnhookWindowsHookEx((HHOOK)glhHook); } |
現在完成的是鼠標鉤子的動態鏈接庫,經過編譯後需要經應用程序的調用才能實現對當前系統下各線程間鼠標消息的攔截處理。這部分同普通動態鏈接庫的使用沒有任何區別,在將其加載到進程後,首先調用動態鏈接庫的StartHook()函數安裝好鉤子,此時即可對系統下的鼠標消息實施攔截處理,在動態鏈接庫被卸載即終止鼠標鉤子時通過動態鏈接庫中的StopHook()函數卸載鼠標鉤子。
經上述編程,在安裝好鼠標鉤子後,鼠標在移動到系統任意窗口上時,馬上就會通過對鼠標消息的攔截處理而獲取到當前窗口的標題。實驗證明此鼠標鉤子的安裝、使用和卸載過程是正確的。
小結
鉤子,尤其是系統鉤子具有相當強大的功能,通過這種技術可以對幾乎所有的Windows系統消息和事件進行攔截處理。這種技術廣泛應用於各種自動監控系統對進程外消息的監控處理。本文只對鉤子的一些基本原理和一般的使用方法做了簡要的探討,感興趣的讀者完全可以在本文所述代碼基礎之上用類似的方法實現對諸如鍵盤鉤子、外殼鉤子等其他類型鉤子的安裝與使用。本文所述代碼在Windows 98下由Microsoft Visual C++ 6.0編譯通過。