1. 程式人生 > >VC++/MFC訊息對映機制(4):附:鉤子函式原理

VC++/MFC訊息對映機制(4):附:鉤子函式原理

VC++/MFC訊息對映機制(4):附:鉤子函式原理

若對C++語法不熟悉,建議參閱《C++語法詳解》一書,電子工業出版社出版,該書語法示例短小精悍,對查閱C++知識點相當方便,並對語法原理進行了透徹、深入詳細的講解。

一、鉤子SetWindowsHookEx

注意:本文的鉤子和鉤子函式是兩個概念,請不要搞混淆了。
1、作用(重點):鉤子主要作用是用於攔截訊息。在訊息發出還未到達目的視窗之前,鉤子函式能先於系統捕獲(攔截)到該訊息,這時鉤子函式就可以對攔截到的訊息進行處理。
2、鉤子分類:鉤子按所攔截到的訊息型別進行分類,比如鍵盤鉤子(攔截鍵盤訊息),滑鼠鉤子等。若攔截的是某個執行緒的鉤子則稱為執行緒鉤子,若是攔截的所有執行緒的鉤子則稱為全域性(系統)鉤子。
3、因為鉤子能攔截除了本執行緒之外的其他執行緒的訊息(比如全域性鉤子),這就給木馬病毒用以獲取使用者的鍵盤滑鼠訊息製造了機會。
4、鉤子連結串列:SetWindowsHookEx可以安裝多個型別的鉤子,這就形成了一個鉤子連結串列,該表由系統負責維護,其結構是先進後出原則,即後安裝的鉤子先獲得控制權。鉤子連結串列儲存了鉤子相關的資訊。使用完鉤子之後應進行解除安裝以釋放資源。注意:鉤子在解除安裝時的順序並不遵守這個規則,當鉤子被解除安裝時,系統便釋放其佔用的記憶體,然後更新整個鉤子連結串列。
5、鉤子函式:也被稱為鉤子子程,被鉤子攔截到的訊息在鉤子函式中進行處理,鉤子函式在SetWindowsHookEx函式中指定。鉤子函式是一個由使用者定義的回撥函式,也就是說使用者可以對攔截到的訊息作出任何的處理。
6、鉤子函式處理訊息的步驟:
1)、首先使用函式SetWindowsHookEx安裝需要攔截何種型別的鉤子和鉤子函式到鉤子連結串列中。
2)、若攔截到指定的訊息,就把該訊息交給鉤子連結串列中最開頭的鉤子函式進行處理。
3)、重點:鉤子函式若返回非0值,則表示丟棄對該訊息的處理,並阻止該訊息的傳遞,這樣目標視窗將不會再接收到該訊息,使用此方法可以遮蔽掉某種訊息。
4)、若要把該訊息傳遞給鉤子連結串列中的下一個鉤子函式進行處理,則需要呼叫CallNextHookEx函式。
5)、使用UnhookWindowsHookEx函式可以移除(解除安裝)安裝的鉤子,並釋放相應的資源。

二、與鉤子有關的函式

1、SetWindowsHookEx原型(winuser.h):

HHOOK SetWindowsHookEx(int idHook, HOOKPROC lpfn, HINSTANCE hMod, DWORD dwThreadID)
 示例:SetWindowsHookEx(WH_MOUSE, g, 0, ::GetCurrentThreadId()); 
表示攔截當前執行緒的滑鼠訊息(即WH_MOUSE),並把訊息交給鉤子函式g進行處理。其中::GetCurrentThreadId()表示返回當前執行緒的ID。第3個實參0,表示這是一個執行緒鉤子。
   1)、idHook:需要安裝的鉤子的型別,即需要被攔截的訊息的型別,在MSDN中共有15種鉤子型別,詳見表3.2
   2)、lpfn:是指向鉤子函式的函式指標,表明呼叫SetWindowsHookEx安裝鉤子時,需要傳遞一個其型別為HOOKPROC的鉤子函式地址作為實參,HOOKPROC的定義見後文。
   3)、hMod:由形參lpfn所指向的鉤子函式所在的DLL檔案的控制代碼,若第4個形參dwThreadID指定的是當前程序建立的執行緒,且相應的鉤子函式定義於當前程序的相關程式碼中,則此引數的值必須被設定為NULL(即值0)。
   4)、dwThreadID:需要被攔截訊息的執行緒標識,若指定的是明確的執行緒,則是執行緒專用鉤子,若值為0,則表示安裝的鉤子是全域性(或系統)鉤子,指定全域性鉤子時,鉤子函式必須包含在DLL(動態連結庫)中,。
   5)、返回值:若函式成功,則返回所安裝的鉤子函式的控制代碼(即型別為HHOOK的變數),否則返回NULL。型別HHOOK是一個鉤子控制代碼,其最後定義為一個結構體型別(與視窗控制代碼HWND等其他控制代碼是類似的)

2、HOOKPROC(即鉤子函式的原型)定義為(winuser.h)

typedef LRESULT (CALLBACK* HOOKPROC)(int code, WPARAM wParam, LPARAM lParam)
1)、LRESULT的型別為long
2)、CALLBACK是一個stdcall呼叫方式,在此處表明該函式是一個回撥函式。注意:在定義鉤子函式時,此關鍵字不能省略。
3)、code:該值的取值與SetWindowsHookEx函式的第一個形參idHook(即鉤子型別)有關,根據idHook的取值不同(或鉤子型別的不同),code可以取不同的值,比如若idHook的值為WH_CBT時,其code值可取HCBT_CLICKSKIPPED、HCBT_CREATEWND等。其具體的取值詳見MSDN或後面章節的內容。
   4)、wParam和lParam:這兩個引數的取值與code有關聯,此處暫不講解。

3、UnhookWindowsHookEx原型(winuser.h):

UnhookWindowsHookEx(HHOOK hhk)
功能:解除安裝(移除)鉤子hhk,形參hhk可以是SetWindowsHookEx的返回值。

4、CallNextHookEx原型(winuser.h):

CallNextHookEx(HHOOK hhk, int nCode, WPARAM wParam, LPARAM lParam);
   1)、作用:把訊息傳遞給鉤子連結串列中的下一下鉤子函式處理,若不使用該函式,則其他安裝了鉤子的應用程式將不能接收到此鉤子的通知。
   2)、hhk:表示當前的鉤子控制代碼,該控制代碼可以是SetWindowsHookEx的返回值。
   3)、nCode:表示需要傳遞給下一個鉤子函式處理的鉤子型別(即訊息型別)
   4)、wParam和lParam:這兩個形參的值與nCode有關聯。

在這裡插入圖片描述

示例3.4:鉤子函式的使用
#include <afxwin.h>   //編寫MFC程式,必須包含此標頭檔案
HHOOK hk;    //用於儲存安裝鉤子後的鉤子控制代碼。
LRESULT CALLBACK f(HWND h,UINT u,WPARAM w, LPARAM l)  /*過程函式,對於鍵盤訊息,形參w儲存的是鍵盤按下的按鍵ASCII碼。*/
{ switch(u){
case WM_KEYDOWN: {//當按下鍵盤上的按鍵時執行以下訊息,除按下F鍵外,其他按鍵會被鉤子攔截。
			if(w==0x41){::MessageBox(NULL,"AAA","",0);} //按下按鍵A時彈出對話方塊
			if(w==0x42){::MessageBox(NULL,"BBB","",0);}//按下按鍵B時彈出對話方塊
			if(w==0x43){::MessageBox(NULL,"CCC","",0);}
			if(w==0x44){::MessageBox(NULL,"DDD","",0);}
			if(w==0x45){::MessageBox(NULL,"EEE","",0);}
			if(w==0x46){::MessageBox(NULL,"FFF","",0);}//按下按鍵F
			break;	}
	case WM_DESTROY:{	PostQuitMessage(0);		break;}
	default:return ::DefWindowProc(h,u,w,l);	}	return 0; }   //f結束
//鉤子函式g
LRESULT CALLBACK g(int code, WPARAM w, LPARAM l){  //鉤子函式的返回型別CALLBACK不能省略。
	if(w==0x41) {					//攔截按鍵A的訊息
::MessageBox(NULL,"A--","",0);  //彈出訊息框
return 1;}     //返回非零值表示阻塞該訊息傳遞到目標視窗
	if(w==0x42) {::MessageBox(NULL,"B--","",0);//攔截按鍵B的訊息,
return 0;}    //返回0,表示不阻塞該訊息傳遞到目標視窗
	if(w==0x43){::MessageBox(NULL,"C--","",0);//攔截按鍵C的訊息
UnhookWindowsHookEx(hk); //移除(解除安裝)鉤子連結串列中安裝的鉤子函式g(即本鉤子函式)。
return 0;}    //不阻塞訊息
	if(w==0x44) {			//攔截按鍵D的訊息
CallNextHookEx(hk, code,w, l);/*把攔截到的訊息傳遞給鉤子連結串列中的下一個鉤子函式(本例為gg函式)進行處理*/
return 0; } 
	return 0;}   //不阻塞其他按鍵訊息傳遞到目標視窗
//鉤子函式gg
LRESULT CALLBACK gg(int code, WPARAM w, LPARAM l){
if(w==0x44) {::MessageBox(NULL,"D--","",0);return 1;}  /*攔截按鍵D的訊息,並阻塞該訊息傳遞到目標視窗*/
	if(w==0x45) {::MessageBox(NULL,"E--","",0);return 1;}
return 1; } //阻塞其他按鍵訊息傳遞到目標視窗,gg結束
//建立視窗
class B:public CFrameWnd{public:B(){Create("HH","H",WS_OVERLAPPEDWINDOW);} 
BOOL CreateEx(       //重定義CWnd::CreateEx建立自定義的視窗
DWORD dwExStyle, LPCTSTR lpszClassName,	LPCTSTR lpszWindowName, 
DWORD dwStyle,int x, int y, int nWidth, int nHeight,
				HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam)
{	HINSTANCE hs=AfxGetModuleState()->m_hCurrentInstanceHandle;
		WNDCLASSEX w;  
		memset(&w,0,sizeof(w)); 	 //把結構體w的所有成員的值初始化為。
		w.hInstance=hs;  	w.cbSize=sizeof(w);  	w.lpszClassName="HH"; 	
		w.lpfnWndProc=f;  //設定過程函式為f,本示例需要該過程函式進行演示
		::RegisterClassEx(&w); 
		HWND h=::CreateWindow("HH","H",WS_OVERLAPPEDWINDOW,0,0,350,280,0,0,hs,0);  
		::ShowWindow(h,1);  ::UpdateWindow(h);    //過載CreateEx建立的視窗應在此處顯示
		return 1;}  };  //類B結束
class A:public CWinApp{public:
	BOOL InitInstance(){	m_pMainWnd=new B(); 
		//安裝鉤子攔截鍵盤訊息,後安裝的鉤子函式先執行,即鉤子函式g先於gg執行。
		hk=::SetWindowsHookEx(WH_KEYBOARD,gg,0,::GetCurrentThreadId());
		hk=::SetWindowsHookEx(WH_KEYBOARD,g,0,::GetCurrentThreadId());
		return 1;		}	};   //類A結束
A ma;

程式執行結果分析
重要:每次重新按鍵時,都需要關閉彈出的所有訊息框,然後再按鍵,因為彈出的訊息框也會接收到鍵盤訊息,此訊息同樣會被鉤子函式攔截到,本示例主要用於說明鉤子函式的幾個原理,關閉訊息框可避免複雜性。
情形1:未被鉤子攔截的訊息,只會彈出類似情形1的訊息框,被鉤子攔截到的訊息會彈出類似情形2含“C–”字元的訊息框。
在這裡插入圖片描述
情形2:驗證鉤子函式執行順序:後安裝的鉤子函式g先於先安裝的鉤子函式gg執行。因此本示例把攔截到的按鍵訊息傳遞到鉤子函式g去執行,
驗證方式,請重新執行程式(重要),然後按以下方式按鍵盤進行驗證(此處僅對函式g和gg的執行順序進行討論,其他情形的討論詳見後文)
1)、按下鍵盤ABD,都會彈出的類似情形二的訊息框,證明鉤子函式g被執行。
2)、然後關閉所有訊息框,按鍵盤E,彈出類似情形1的訊息框,說明執行的是過程函式f,鉤子函式gg未被執行。
3)、以上步驟順序無關緊要,按下按鍵時彈出的訊息框也不止一個。
4)、最好不要按鍵盤C,因為C會解除安裝掉鉤子函式g ,從而使g不能再接收到由鉤子攔截到的訊息。
5)、注意:若沒有使用CallNextHookEx函式把訊息傳遞給鉤子連結串列中的下一個鉤子函式,下一個鉤子函式不會接收到訊息,這也是為什麼按下鍵盤E,沒有執行鉤子函式gg彈出訊息框的原因。
情形3:驗證鉤子函式返回NULL(值0)未阻塞訊息(圖3.2)。
重新執行程式(重要),按以下方式按鍵盤進行驗證(不要連續按著鍵盤不放,因為這樣會連續重複傳送訊息):
按下鍵B,此時執行鉤子函式g中if(w==0x42)之後的程式,彈出兩個內容為“B–”的訊息框(按下和彈起按鍵各彈出一個)。因為此時過程函式g返回0表示未阻塞按鍵B的訊息傳遞到目標視窗,因此把這兩個訊息框關閉之後(此時鉤子函式g才執行完畢),執行過程函式f中的語句,並彈出一個內容為“BBB”的訊息框。
在這裡插入圖片描述

情形4:驗證鉤子函式返回非零值阻塞訊息(圖3.3)。
重新執行程式(重要),按以下方式按鍵盤進行驗證(不要連續按著鍵盤不放):
按下鍵A,此時執行鉤子函式g中if(w==0x41)之後的程式,彈出兩個內容為“A–”的訊息框(按下和彈起按鍵各彈出一個)。因為此時過程函式g返回1表示阻塞按鍵A的訊息傳遞到目標視窗,因此把這兩個訊息框關閉之後,沒有執行過程函式f中的語句,程式不會彈出一個內容為“AAA”的訊息框。
在這裡插入圖片描述

情形5:解除安裝鉤子。重新執行程式(重要),然後按以下方式按鍵盤進行驗證(不要連續按著鍵盤不放,因為這樣會連續重複傳送訊息):
1)、首先按下鍵F,彈出內容為“FFF”的訊息框。此時執行的是鉤子函式g,因為F未與g中的任何if語句匹配,因此鉤子函式g返回0,不阻塞該訊息,執行過程函式f彈出訊息框。
2)、關閉彈出的訊息框之後按下鍵C,此時彈出三個訊息框,並解除安裝鉤子函式g,訊息框分別由鉤子函式g和過程函式f產生。
3)、按下鍵C解除安裝鉤子函式g之後(需關閉完所有訊息框),由鉤子攔截到的鍵盤訊息現在由鉤子函式gg負責處理(因為解除安裝鉤子函式之後鉤子連結串列會被更新,更新之後的鉤子連結串列只有鉤子函式gg了),因此按下D和E時會彈出由gg產生的訊息框,除此之外的其他按鍵都沒有反應,因為gg對其他按鍵返回的都是非零值,其訊息被阻塞了。
情形7:傳遞鉤子函式。重新執行程式(重要),然後按以下方式按鍵盤進行驗證(不要連續按著鍵盤不放,因為這樣會連續重複傳送訊息):
1)、按下鍵D,彈出三個訊息框,分別由鉤子函式gg和過程函式f產生。此時鉤子函式g把接收到的按鍵D訊息傳遞給鉤子連結串列中的下一個鉤子函式gg進行處理,因此首先由gg產生兩個訊息框,因為鉤子函式g此時返回值0,未阻塞訊息,所以再把此按鍵訊息傳遞給過程函式f處理,因此關閉這兩個訊息框之後(此時鉤子函式g才執行完畢),彈出一個由過程函式f產生的訊息框。
2)、按下鍵E,彈出一個由過程函式f產生的內容為“EEE”的訊息框。因為此時處理由鉤子攔截到的鍵盤訊息的鉤子函式是g,但g並未對該訊息進行處理,也未呼叫CallNextHookEx函式把訊息傳遞給鉤子連結串列中的下一個鉤子函式gg進行處理,因此鉤子函式gg不能接收到該按鍵訊息,最後g返回0,未阻塞該訊息,因此最後把該按鍵訊息交給過程函式f處理,從而產生一個內容為“EEE”的訊息框。

本文作者:黃邦勇帥(原名:黃勇)