當前位置:編程學習大全網 - 熱門推薦 - 系統鉤子是什麽

系統鉤子是什麽

壹、 介紹

本文將討論在.NET應用程序中全局系統鉤子的使用。為此,我開發了壹個可重用的類庫並創建壹個相應的示例程序(見下圖)。

妳可能註意到另外的關於使用系統鉤子的文章。本文與之類似但是有重要的差別。這篇文章將討論在.NET中使用全局系統鉤子,而其它文章僅討論本地系統鉤子。這些思想是類似的,但是實現要求是不同的。

二、 背景

如果妳對Windows系統鉤子的概念不熟悉,讓我作壹下簡短的描述:

壹個系統鉤子允許妳插入壹個回調函數-它攔截某些Windows消息(例如,鼠標相聯系的消息)。

壹個本地系統鉤子是壹個系統鉤子-它僅在指定的消息由壹個單壹線程處理時被調用。

壹個全局系統鉤子是壹個系統鉤子-它當指定的消息被任何應用程序在整個系統上所處理時被調用。

已有若幹好文章來介紹系統鉤子概念。在此,不是為了重新收集這些介紹性的信息,我只是簡單地請讀者參考下面有關系統鉤子的壹些背景資料文章。如果妳對系統鉤子概念很熟悉,那麽妳能夠從本文中得到妳能夠得到的任何東西。

關於MSDN庫中的鉤子知識。

Dino Esposito的《Cutting Edge-Windows Hooks in the .NET Framework》。

Don Kackman的《在C#中應用鉤子》。

本文中我們要討論的是擴展這個信息來創建壹個全局系統鉤子-它能被.NET類所使用。我們將用C#和壹個DLL和非托管C++來開發壹個類庫-它們壹起將完成這個目標。

三、 使用代碼

在我們深入開發這個庫之前,讓我們快速看壹下我們的目標。在本文中,我們將開發壹個類庫-它安裝全局系統鉤子並且暴露這些由鉤子處理的事件,作為我們的鉤子類的壹個.NET事件。為了說明這個系統鉤子類的用法,我們將在壹個用C#編寫的Windows表單應用程序中創建壹個鼠標事件鉤子和壹個鍵盤事件鉤子。

這些類庫能用於創建任何類型的系統鉤子,其中有兩個預編譯的鉤子-MouseHook和KeyboardHook。我們也已經包含了這些類的特定版本,分別稱為MouseHookExt和KeyboardHookExt。根據這些類所設置的模型,妳能容易構建系統鉤子-針對Win32 API中任何15種鉤子事件類型中的任何壹種。另外,這個完整的類庫中還有壹個編譯的HTML幫助文件-它把這些類歸檔化。請確信妳看了這個幫助文件-如果妳決定在妳的應用程序中使用這個庫的話。

MouseHook類的用法和生命周期相當簡單。首先,我們創建MouseHook類的壹個實例。

mouseHook = new MouseHook();//mouseHook是壹個成員變量

接下來,我們把MouseEvent事件綁定到壹個類層次的方法上。

mouseHook.MouseEvent+=new MouseHook.MouseEventHandler(mouseHook_MouseEvent);

// ...

private void mouseHook_MouseEvent(MouseEvents mEvent, int x, int y){

string msg =string.Format("鼠標事件:{0}:({1},{2}).",mEvent.ToString(),x,y);

AddText(msg);//增加消息到文本框

}

為開始收到鼠標事件,簡單地安裝下面的鉤子即可。

mouseHook.InstallHook();

為停止接收事件,只需簡單地卸載這個鉤子。

mouseHook.UninstallHook();

妳也可以調用Dispose來卸載這個鉤子。

在妳的應用程序退出時,卸載這個鉤子是很重要的。讓系統鉤子壹直安裝著將減慢系統中的所有的應用程序的消息處理。它甚至能夠使壹個或多個進程變得很不穩定。因此,請確保在妳使用完鉤子時壹定要移去妳的系統鉤子。我們確定在我們的示例應用程序會移去該系統鉤子-通過在Form的Dispose方法中添加壹個Dispose調用。

protected override void Dispose(bool disposing) {

if (disposing) {

if (mouseHook != null) {

mouseHook.Dispose();

mouseHook = null;

}

// ...

}

}

使用該類庫的情況就是如此。該類庫中有兩個系統鉤子類並且相當容易擴充。

四、 構建庫

這個庫***有兩個主要組件。第壹部分是壹個C#類庫-妳可以直接使用於妳的應用程序中。該類庫,反過來,在內部使用壹個非托管的C++ DLL來直接管理系統鉤子。我們將首先討論開發該C++部分。接下來,我們將討論怎麽在C#中使用這個庫來構建壹個通用的鉤子類。就象我們討論C++/C#交互壹樣,我們將特別註意C++方法和數據類型是怎樣映射到.NET方法和數據類型的。

妳可能想知道為什麽我們需要兩個庫,特別是壹個非托管的C++ DLL。妳還可能註意到在本文的背景壹節中提到的兩篇參考文章,其中並沒有使用任何非托管的代碼。為此,我的回答是,"對!這正是我寫這篇文章的原因"。當妳思考系統鉤子是怎樣實際地實現它們的功能時,我們需要非托管的代碼是十分重要的。為了使壹個全局的系統鉤子能夠工作,Windows把妳的DLL插入到每個正在運行的進程的進程空間中。既然大多數進程不是.NET進程,所以,它們不能直接執行.NET裝配集。我們需要壹種非托管的代碼代理- Windows可以把它插入到所有將要被鉤住的進程中。

首先是提供壹種機制來把壹個.NET代理傳遞到我們的C++庫。這樣,我們用C++語言定義下列函數(SetUserHookCallback)和函數指針(HookProc)。

int SetUserHookCallback(HookProc userProc, UINT hookID)

typedef void (CALLBACK *HookProc)(int code, WPARAM w, LPARAM l)

SetUserHookCallback的第二個參數是鉤子類型-這個函數指針將使用它。現在,我們必須用C#來定義相應的方法和代理以使用這段代碼。下面是我們怎樣把它映射到C#。

private static extern SetCallBackResults

SetUserHookCallback(HookProcessedHandler hookCallback, HookTypes hookType)

protected delegate void HookProcessedHandler(int code, UIntPtr wparam, IntPtr lparam)

public enum HookTypes {

JournalRecord = 0,

JournalPlayback = 1,

// ...

KeyboardLL = 13,

MouseLL = 14

};

首先,我們使用DllImport屬性導入SetUserHookCallback函數,作為我們的抽象基鉤子類SystemHook的壹個靜態的外部的方法。為此,我們必須映射壹些外部數據類型。首先,我們必須創建壹個代理作為我們的函數指針。這是通過定義上面的HookProcessHandler 來實現的。我們需要壹個函數,它的C++簽名為(int,WPARAM,LPARAM)。在Visual Studio .NET C++編譯器中,int與C#中是壹樣的。也就是說,在C++與C#中int就是Int32。事情並不總是這樣。壹些編譯器把C++ int作為Int16對待。我們堅持使用Visual Studio .NET C++編譯器來實現這個工程,因此,我們不必擔心編譯器差別所帶來的另外的定義。

接下來,我們需要用C#傳遞WPARAM和LPARAM值。這些確實是指針,它們分別指向C++的UINT和LONG值。用C#來說,它們是指向uint和int的指針。如果妳還不確定什麽是WPARAM,妳可以通過在C++代碼中單擊右鍵來查詢它,並且選擇"Go to definition"。這將會引導妳到在windef.h中的定義。

//從windef.h:

typedef UINT_PTR WPARAM;

typedef LONG_PTR LPARAM;

因此,我們選擇System.UIntPtr和System.IntPtr作為我們的變量類型-它們分別相應於WPARAM和LPARAM類型,當它們使用在C#中時。

現在,讓我們看壹下鉤子基類是怎樣使用這些導入的方法來傳遞壹個回叫函數(代理)到C++中-它允許C++庫直接調用妳的系統鉤子類的實例。首先,在構造器中,SystemHook類創建壹個到私有方法InternalHookCallback的代理-它匹配HookProcessedHandler代理簽名。然後,它把這個代理和它的HookType傳遞到C++庫以使用SetUserHookCallback方法來註冊該回叫函數,如上面所討論的。下面是其代碼實現:

public SystemHook(HookTypes type){

_type = type;

_processHandler = new HookProcessedHandler(InternalHookCallback);

SetUserHookCallback(_processHandler, _type);

}

InternalHookCallback的實現相當簡單。InternalHookCallback在用壹個catch-all try/catch塊包裝它的同時僅傳遞到抽象方法HookCallback的調用。這將簡化在派生類中的實現並且保護C++代碼。記住,壹旦壹切都準備妥當,這個C++鉤子就會直接調用這個方法。

[MethodImpl(MethodImplOptions.NoInlining)]

private void InternalHookCallback(int code, UIntPtr wparam, IntPtr lparam){

try { HookCallback(code, wparam, lparam); }

catch {}

}

我們已增加了壹個方法實現屬性-它告訴編譯器不要內聯這個方法。這不是可選的。至少,在我添加try/catch之前是需要的。看起來,由於某些原因,編譯器在試圖內聯這個方法-這將給包裝它的代理帶來各種麻煩。然後,C++層將回叫,而該應用程序將會崩潰。

現在,讓我們看壹下壹個派生類是怎樣用壹個特定的HookType來接收和處理鉤子事件。下面是虛擬的MouseHook類的HookCallback方法實現:

protected override void HookCallback(int code, UIntPtr wparam, IntPtr lparam){

if (MouseEvent == null) { return; }

int x = 0, y = 0;

MouseEvents mEvent = (MouseEvents)wparam.ToUInt32();

switch(mEvent) {

case MouseEvents.LeftButtonDown:

GetMousePosition(wparam, lparam, ref x, ref y);

break;

// ...

}

MouseEvent(mEvent, new Point(x, y));

}

首先,註意這個類定義壹個事件MouseEvent-該類在收到壹個鉤子事件時激發這個事件。這個類在激發它的事件之前,把數據從WPARAM和 LPARAM類型轉換成.NET中有意義的鼠標事件數據。這樣可以使得類的消費者免於擔心解釋這些數據結構。這個類使用導入的 GetMousePosition函數-我們在C++ DLL中定義的用來轉換這些值。為此,請看下面幾段的討論。

在這個方法中,我們檢查是否有人在聽這壹個事件。如果沒有,不必繼續處理這壹事件。然後,我們把WPARAM轉換成壹個MouseEvents枚舉類型。我們已小心地構造了MouseEvents枚舉來準確匹配它們在C ++中相應的常數。這允許我們簡單地把指針的值轉換成枚舉類型。但是要註意,這種轉換即使在WPARAM的值不匹配壹個枚舉值的情況下也會成功。 mEvent的值將僅是未定義的(不是null,只是不在枚舉值範圍之內)。為此,請詳細分析System.Enum.IsDefined方法。

接下來,在確定我們收到的事件類型後,該類激活這個事件,並且通知消費者鼠標事件的類型及在該事件過程中鼠標的位置。

最後註意,有關轉換WPARAM和LPARAM值:對於每個類型的事件,這些變量的值和意思是不同的。因此,在每壹種鉤子類型中,我們必須區別地解釋這些值。我選擇用C++實現這種轉換,而不是盡量用C#來模仿復雜的C++結構和指針。例如,前面的類就使用了壹個叫作GetMousePosition的 C++函數。下面是C++ DLL中的這個方法:

bool GetMousePosition(WPARAM wparam, LPARAM lparam, int amp; x, int amp; y) {

MOUSEHOOKSTRUCT * pMouseStruct = (MOUSEHOOKSTRUCT *)lparam;

x = pMouseStruct->pt.x;

y = pMouseStruct->pt.y;

return true;

}

不是盡量映射MOUSEHOOKSTRUCT結構指針到C#,我們簡單地暫時把它回傳到C++層以提取我們需要的值。註意,因為我們需要從這個調用中返回壹些值,我們把我們的整數作為參考變量傳遞。這直接映射到C#中的int*。但是,我們可以重載這個行為,通過選擇正確的簽名來導入這個方法。

private static extern bool InternalGetMousePosition(UIntPtr wparam,IntPtr lparam, ref int x, ref int y)

通過把integer參數定義為ref int,我們得到通過C++參照傳遞給我們的值。如果我們想要的話,我們還可以使用out int。

五、 限制

壹些鉤子類型並不適合實現全局鉤子。我當前正在考慮解決辦法-它將允許使用受限制的鉤子類型。到目前為止,不要把這些類型添加回該庫中,因為它們將導致應用程序的失敗(經常是系統範圍的災難性失敗)。下壹節將集中討論這些限制背後的原因和解決辦法。

HookTypes.CallWindowProcedure

HookTypes.CallWindowProret

HookTypes.ComputerBasedTraining

HookTypes.Debug

HookTypes.ForegroundIdle

HookTypes.JournalRecord

HookTypes.JournalPlayback

HookTypes.GetMessage

HookTypes.SystemMessageFilter

六、 兩種類型的鉤子

在本節中,我將盡量解釋為什麽壹些鉤子類型被限制在壹定的範疇內而另外壹些則不受限制。如果我使用有點偏差術語的話,請原諒我。我還沒有找到任何有關這部分題目的文檔,因此,我編造了我自己的詞匯。另外,如果妳認為我根本就不對,請告訴我好了。

當Windows調用傳遞到SetWindowsHookEx()的回調函數時它們會因不同類型的鉤子而被區別調用。基本上有兩種情況:切換執行上下文的鉤子和不切換執行上下文的鉤子。用另壹種方式說,也就是,在放鉤子的應用程序進程空間執行鉤子回調函數的情況和在被鉤住的應用程序進程空間執行鉤子回調函數的情況。

鉤子類型例如鼠標和鍵盤鉤子都是在被Windows調用之前切換上下文的。整個過程大致如下:

1. 應用程序X擁有焦點並執行。

2. 用戶按下壹個鍵。

3. Windows從應用程序X接管上下文並把執行上下文切換到放鉤子的應用程序。

4. Windows用放鉤子的應用程序進程空間中的鍵消息參數調用鉤子回調函數。

5. Windows從放鉤子的應用程序接管上下文並把執行上下文切換回應用程序X。

6. Windows把消息放進應用程序X的消息排隊。

7. 稍微壹會兒之後,當應用程序X執行時,它從自己的消息排隊中取出消息並且調用它的內部按鍵(或松開或按下)處理器。

8. 應用程序X繼續執行...

例如CBT鉤子(window創建,等等。)的鉤子類型並不切換上下文。對於這些類型的鉤子,過程大致如下:

1. 應用程序X擁有焦點並執行。

2. 應用程序X創建壹個窗口。

3. Windows用在應用程序X進程空間中的CBT事件消息參數調用鉤子回調函數。

4. 應用程序X繼續執行...

這應該說明了為什麽某種類型的鉤子能夠用這個庫結構工作而壹些卻不能。記住,這正是該庫要做的。在上面第4步和第3步之後,分別插入下列步驟:

1. Windows調用鉤子回調函數。

2. 目標回調函數在非托管的DLL中執行。

3. 目標回調函數查找它的相應托管的調用代理。

4. 托管代理被以適當的參數執行。

5. 目標回調函數返回並執行相應於指定消息的鉤子處理。

第三步和第四步因非切換鉤子類型而註定失敗。第三步將失敗,因為相應的托管回調函數不會為該應用程序而設置。記住,這個DLL使用全局變量來跟蹤這些托管代理並且該鉤子DLL被加載到每壹個進程空間。但是這個值僅在放鉤子的應用程序進程空間中設置。對於另外其它情況,它們全部為null。

Tim Sylvester在他的《Other hook types》壹文中指出,使用壹個***享內存區段將會解決這個問題。這是真實的,但是也如Tim所指出的,那些托管代理地址對於除了放鉤子的應用程序之外的任何進程是無意義的。這意味著,它們是無意義的並且不能在回調函數的執行過程中調用。那樣會有麻煩的。

因此,為了把這些回調函數使用於不執行上下文切換的鉤子類型,妳需要某種進程間的通訊。

我已經試驗過這種思想-使用非托管的DLL鉤子回調函數中的進程外COM對象進行IPC。如果妳能使這種方法工作,我將很高興了解到這點。至於我的嘗試,結果並不理想。基本原因是很難針對各種進程和它們的線程(CoInitialize(NULL))而正確地初始化COM單元。這是壹個在妳可以使用 COM對象之前的基本要求。

我不懷疑,壹定有辦法來解決這個問題。但是我還沒有試用過它們,因為我認為它們僅有有限的用處。例如,CBT鉤子可以讓妳取消壹個窗口創建,如果妳希望的話。可以想像,為使這能夠工作將會發生什麽。

1. 鉤子回調函數開始執行。

2. 調用非托管的鉤子DLL中的相應的鉤子回調函數。

3. 執行必須被路由回到主鉤子應用程序。

4. 該應用程序必須決定是否允許這壹創建。

5. 調用必須被路由回仍舊在運行中的鉤子回調函數。

6. 在非托管的鉤子DLL中的鉤子回調函數從主鉤子應用程序接收到要采取的行動。

7. 在非托管的鉤子DLL中的鉤子回調函數針對CBT鉤子調用采取適當的行動。

8. 完成鉤子回調函數的執行。

這不是不可能的,但是不算好的。我希望這會消除在該庫中的圍繞被允許的和受限制的鉤子類型所帶來的神秘。

七、 其它

庫文檔:我們已經包含了有關ManagedHooks類庫的比較完整的代碼文檔。當以"Documentation"構建配置進行編譯時,這被經由Visual Studio.NET轉換成標準幫助XML。最後,我們已使用NDoc來把它轉換成編譯的HTML幫助(CHM)。妳可以看這個幫助文件,只需簡單地在該方案的解決方案資源管理器中點擊Hooks.chm文件或通過查找與該文相關的可下載的ZIP文件。

增強的智能感知:如果妳不熟悉Visual Studio.NET怎樣使用編譯的XML文件(pre-NDoc output)來為參考庫的工程增強智能感知,那麽讓我簡單地介紹壹下。如果妳決定在妳的應用程序中使用這個類庫,妳可以考慮復制該庫的壹個穩定構建版本到妳想參考它的位置。同時,還要把XML文檔文件 (SystemHooks\ManagedHooks\bin\Debug\Kennedy.ManagedHooks.xml)復制到相同的位置。當妳添加壹個參考到該庫時,Visual Studio.NET將自動地讀該文件並使用它來添加智能感知文檔。這是很有用的,特別是對於象這樣的第三方庫。

單元測試:我相信,所有的庫都應有與之相應的單元測試。既然我是壹家公司(主要負責針對.NET環境軟件的單元測試)的合夥人和軟件工程師,任何人不會對此感到驚訝。因而,妳將會在名為ManagedHooksTests的解決方案中找到壹個單元測試工程。為了運行該單元測試,妳需要下載和安裝 HarnessIt-這個下載是我們的商業單元測試軟件的壹個自由的試用版本。在該單元測試中,我對這給予了特殊的註意-在此處,方法的無效參數可能導致 C++內存異常的發生。盡管這個庫是相當簡單的,但該單元測試確實能夠幫助我在壹些更為微妙的情況下發現壹些錯誤。

非托管的/托管的調試:有關混合解決方案(例如,本文的托管的和非托管的代碼)最為技巧的地方之壹是調試問題。如果妳想單步調試該C++代碼或在C++代碼中設置斷點,妳必須啟動非托管的調試。這是壹個Visual Studio.NET中的工程設置。註意,妳可以非常順利地單步調試托管的和非托管的層,但是,在調試過程中,非托管的調試確實嚴重地減慢應用程序的裝載時間和執行速度。

八、 最後警告

很明顯,系統鉤子相當有力量;然而,使用這種力量應該是有責任性的。在系統鉤子出了問題時,它們不僅僅垮掉妳的應用程序。它們可以垮掉在妳的當前系統中運行的每個應用程序。但是到這種程度的可能性壹般是很小的。盡管如此,在使用系統鉤子時,妳還是需要再三檢查妳的代碼。

我發現了壹項可以用來開發應用程序的有用的技術-它使用系統鉤子來在微軟的虛擬PC上安裝妳的喜愛的開發操作系統的壹個拷貝和Visual Studio.NET。然後,妳就可以在此虛擬的環境中開發妳的應用程序。用這種方式,當妳的鉤子應用程序出現錯誤時,它們將僅退出妳的操作系統的虛擬實例而不是妳的真正的操作系統。我已經不得不重啟動我的真正的OS-在這個虛擬OS由於壹個鉤子錯誤崩潰時,但是這並不經常。

  • 上一篇:神奇寶貝第二部
  • 下一篇:手機來電顯示683什麽意思
  • copyright 2024編程學習大全網