熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> .NET編程 >> 正文

深入探討.NET中的鉤子技術

2022-06-13   來源: .NET編程 

  一 介紹
  
    本文將討論在NET應用程序中全局系統鉤子的使用為此我開發了一個可重用的類庫並創建一個相應的示例程序(見下圖)
  
  
  
    你可能注意到另外的關於使用系統鉤子的文章本文與之類似但是有重要的差別這篇文章將討論在NET中使用全局系統鉤子而其它文章僅討論本地系統鉤子這些思想是類似的但是實現要求是不同的
  
    二 背景
  
    如果你對Windows系統鉤子的概念不熟悉讓我作一下簡短的描述
  
    ·一個系統鉤子允許你插入一個回調函數它攔截某些Windows消息(例如鼠標相聯系的消息)
  
    ·一個本地系統鉤子是一個系統鉤子它僅在指定的消息由一個單一線程處理時被調用
  
    ·一個全局系統鉤子是一個系統鉤子它當指定的消息被任何應用程序在整個系統上所處理時被調用
  已有若干好文章來介紹系統鉤子概念在此不是為了重新收集這些介紹性的信息我只是簡單地請讀者參考下面有關系統鉤子的一些背景資料文章如果你對系統鉤子概念很熟悉那麼你能夠從本文中得到你能夠得到的任何東西
  
    ·關於MSDN庫中的鉤子知識
  
    ·Dino Esposito的《Cutting EdgeWindows Hooks in the NET Framework》
  
    ·Don Kackman的《在C#中應用鉤子》
  
    本文中我們要討論的是擴展這個信息來創建一個全局系統鉤子它能被NET類所使用我們將用C#和一個DLL和非托管C++來開發一個類庫它們一起將完成這個目標
  
    三 使用代碼
  
    在我們深入開發這個庫之前讓我們快速看一下我們的目標在本文中我們將開發一個類庫它安裝全局系統鉤子並且暴露這些由鉤子處理的事件作為我們的鉤子類的一個NET事件為了說明這個系統鉤子類的用法我們將在一個用C#編寫的Windows表單應用程序中創建一個鼠標事件鉤子和一個鍵盤事件鉤子
  
    這些類庫能用於創建任何類型的系統鉤子其中有兩個預編譯的鉤子MouseHook和KeyboardHook我們也已經包含了這些類的特定版本分別稱為MouseHookExt和KeyboardHookExt根據這些類所設置的模型你能容易構建系統鉤子針對Win API中任何種鉤子事件類型中的任何一種另外這個完整的類庫中還有一個編譯的HTML幫助文件它把這些類歸檔化請確信你看了這個幫助文件如果你決定在你的應用程序中使用這個庫的話
  
    MouseHook類的用法和生命周期相當簡單首先我們創建MouseHook類的一個實例
  
  mouseHook = new MouseHook();//mouseHook是一個成員變量
  
    接下來我們把MouseEvent事件綁定到一個類層次的方法上
  
  mouseHookMouseEvent+=new MouseHookMouseEventHandler(mouseHook_MouseEvent);
  //
  private void mouseHook_MouseEvent(MouseEvents mEvent int x int y){
   string msg =stringFormat(鼠標事件:{}:({}{})mEventToString()xy);
   AddText(msg);//增加消息到文本框
  }
  
    為開始收到鼠標事件簡單地安裝下面的鉤子即可
  
  mouseHookInstallHook();
  
    為停止接收事件只需簡單地卸載這個鉤子
  
  mouseHookUninstallHook();
  
    你也可以調用Dispose來卸載這個鉤子
  
    在你的應用程序退出時卸載這個鉤子是很重要的讓系統鉤子一直安裝著將減慢系統中的所有的應用程序的消息處理它甚至能夠使一個或多個進程變得很不穩定因此請確保在你使用完鉤子時一定要移去你的系統鉤子我們確定在我們的示例應用程序會移去該系統鉤子通過在Form的Dispose方法中添加一個Dispose調用
  
  protected override void Dispose(bool disposing) {
   if (disposing) {
    if (mouseHook != null) {
     mouseHookDispose();
     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 =
   JournalPlayback =
   //
   KeyboardLL =
   MouseLL =
  };
  
    首先我們使用DllImport屬性導入SetUserHookCallback函數作為我們的抽象基鉤子類SystemHook的一個靜態的外部的方法為此我們必須映射一些外部數據類型首先我們必須創建一個代理作為我們的函數指針這是通過定義上面的HookProcessHandler來實現的我們需要一個函數它的C++簽名為(intWPARAMLPARAM)在Visual Studio NET C++編譯器中int與C#中是一樣的也就是說在C++與C#中int就是Int事情並不總是這樣一些編譯器把C++ int作為Int對待我們堅持使用Visual Studio NET C++編譯器來實現這個工程因此我們不必擔心編譯器差別所帶來的另外的定義
  
    接下來我們需要用C#傳遞WPARAM和LPARAM值這些確實是指針它們分別指向C++的UINT和LONG值用C#來說它們是指向uint和int的指針如果你還不確定什麼是WPARAM你可以通過在C++代碼中單擊右鍵來查詢它並且選擇Go to definition這將會引導你到在windefh中的定義
  
  //從windefh:
  typedef UINT_PTR WPARAM;
  typedef LONG_PTR LPARAM;
  
    因此我們選擇SystemUIntPtr和SystemIntPtr作為我們的變量類型它們分別相應於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在用一個catchall try/catch塊包裝它的同時僅傳遞到抽象方法HookCallback的調用這將簡化在派生類中的實現並且保護C++代碼記住一旦一切都准備妥當這個C++鉤子就會直接調用這個方法
  
  [MethodImpl(MethodImplOptionsNoInlining)]
  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 = y = ;
    MouseEvents mEvent = (MouseEvents)wparamToUInt();
    switch(mEvent) {
     case MouseEventsLeftButtonDown:
      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只是不在枚舉值范圍之內)為此請詳細分析SystemEnumIsDefined方法
  
    接下來在確定我們收到的事件類型後該類激活這個事件並且通知消費者鼠標事件的類型及在該事件過程中鼠標的位置
  
    最後注意有關轉換WPARAM和LPARAM值對於每個類型的事件這些變量的值和意思是不同的因此在每一種鉤子類型中我們必須區別地解釋這些值我選擇用C++實現這種轉換而不是盡量用C#來模仿復雜的C++結構和指針例如前面的類就使用了一個叫作GetMousePosition的C++函數下面是C++ DLL中的這個方法
  
  bool GetMousePosition(WPARAM wparam LPARAM lparam int & x int & y) {
   MOUSEHOOKSTRUCT * pMouseStruct = (MOUSEHOOKSTRUCT *)lparam;
   x = pMouseStruct>ptx;
   y = pMouseStruct>pty;
   return true;
  }
  
    不是盡量映射MOUSEHOOKSTRUCT結構指針到C#我們簡單地暫時把它回傳到C++層以提取我們需要的值注意因為我們需要從這個調用中返回一些值我們把我們的整數作為參考變量傳遞這直接映射到C#中的int*但是我們可以重載這個行為通過選擇正確的簽名來導入這個方法
  
  private static extern bool InternalGetMousePosition(UIntPtr wparamIntPtr lparam ref int x ref int y)
  
    通過把integer參數定義為ref int我們得到通過C++參照傳遞給我們的值如果我們想要的話我們還可以使用out int 

  五 限制
  
    一些鉤子類型並不適合實現全局鉤子我當前正在考慮解決辦法它將允許使用受限制的鉤子類型到目前為止不要把這些類型添加回該庫中因為它們將導致應用程序的失敗(經常是系統范圍的災難性失敗)下一節將集中討論這些限制背後的原因和解決辦法
  
  HookTypesCallWindowProcedure
  HookTypesCallWindowProret
  HookTypesComputerBasedTraining
  HookTypesDebug
  HookTypesForegroundIdle
  HookTypesJournalRecord
  HookTypesJournalPlayback
  HookTypesGetMessage
  HookTypesSystemMessageFilter
  
    六 兩種類型的鉤子
  
    在本節中我將盡量解釋為什麼一些鉤子類型被限制在一定的范疇內而另外一些則不受限制如果我使用有點偏差術語的話請原諒我我還沒有找到任何有關這部分題目的文檔因此我編造了我自己的詞匯另外如果你認為我根本就不對請告訴我好了
  
    當Windows調用傳遞到SetWindowsHookEx()的回調函數時它們會因不同類型的鉤子而被區別調用基本上有兩種情況切換執行上下文的鉤子和不切換執行上下文的鉤子用另一種方式說也就是在放鉤子的應用程序進程空間執行鉤子回調函數的情況和在被鉤住的應用程序進程空間執行鉤子回調函數的情況
  
    鉤子類型例如鼠標和鍵盤鉤子都是在被Windows調用之前切換上下文的整個過程大致如下
  
     應用程序X擁有焦點並執行
  
     用戶按下一個鍵
  
     Windows從應用程序X接管上下文並把執行上下文切換到放鉤子的應用程序
  
     Windows用放鉤子的應用程序進程空間中的鍵消息參數調用鉤子回調函數
  
     Windows從放鉤子的應用程序接管上下文並把執行上下文切換回應用程序X
  
     Windows把消息放進應用程序X的消息排隊
  
     稍微一會兒之後當應用程序X執行時它從自己的消息排隊中取出消息並且調用它的內部按鍵(或松開或按下)處理器
  
     應用程序X繼續執行
  
    例如CBT鉤子(window創建等等)的鉤子類型並不切換上下文對於這些類型的鉤子過程大致如下
  
     應用程序X擁有焦點並執行
  
     應用程序X創建一個窗口
  
     Windows用在應用程序X進程空間中的CBT事件消息參數調用鉤子回調函數
  
     應用程序X繼續執行
  
    這應該說明了為什麼某種類型的鉤子能夠用這個庫結構工作而一些卻不能記住這正是該庫要做的在上面第步和第步之後分別插入下列步驟
  
     Windows調用鉤子回調函數
  
     目標回調函數在非托管的DLL中執行
  
     目標回調函數查找它的相應托管的調用代理
  
     托管代理被以適當的參數執行
  
     目標回調函數返回並執行相應於指定消息的鉤子處理
  
    第三步和第四步因非切換鉤子類型而注定失敗第三步將失敗因為相應的托管回調函數不會為該應用程序而設置記住這個DLL使用全局變量來跟蹤這些托管代理並且該鉤子DLL被加載到每一個進程空間但是這個值僅在放鉤子的應用程序進程空間中設置對於另外其它情況它們全部為null
  
    Tim Sylvester在他的《Other hook types》一文中指出使用一個共享內存區段將會解決這個問題這是真實的但是也如Tim所指出的那些托管代理地址對於除了放鉤子的應用程序之外的任何進程是無意義的這意味著它們是無意義的並且不能在回調函數的執行過程中調用那樣會有麻煩的
  
    因此為了把這些回調函數使用於不執行上下文切換的鉤子類型你需要某種進程間的通訊
  
    我已經試驗過這種思想使用非托管的DLL鉤子回調函數中的進程外COM對象進行IPC如果你能使這種方法工作我將很高興了解到這點至於我的嘗試結果並不理想基本原因是很難針對各種進程和它們的線程(CoInitialize(NULL))而正確地初始化COM單元這是一個在你可以使用COM對象之前的基本要求
  
    我不懷疑一定有辦法來解決這個問題但是我還沒有試用過它們因為我認為它們僅有有限的用處例如CBT鉤子可以讓你取消一個窗口創建如果你希望的話可以想像為使這能夠工作將會發生什麼
  
     鉤子回調函數開始執行
  
     調用非托管的鉤子DLL中的相應的鉤子回調函數
  
     執行必須被路由回到主鉤子應用程序
  
     該應用程序必須決定是否允許這一創建
  
     調用必須被路由回仍舊在運行中的鉤子回調函數
  
     在非托管的鉤子DLL中的鉤子回調函數從主鉤子應用程序接收到要采取的行動
  
     在非托管的鉤子DLL中的鉤子回調函數針對CBT鉤子調用采取適當的行動
  
     完成鉤子回調函數的執行
  
    這不是不可能的但是不算好的我希望這會消除在該庫中的圍繞被允許的和受限制的鉤子類型所帶來的神秘
  
    七 其它
  
    ·庫文檔我們已經包含了有關ManagedHooks類庫的比較完整的代碼文檔當以Documentation構建配置進行編譯時這被經由Visual StudioNET轉換成標准幫助XML最後我們已使用NDoc來把它轉換成編譯的HTML幫助(CHM)你可以看這個幫助文件只需簡單地在該方案的解決方案資源管理器中點擊Hookschm文件或通過查找與該文相關的可下載的ZIP文件
  
    ·增強的智能感知如果你不熟悉Visual StudioNET怎樣使用編譯的XML文件(preNDoc output)來為參考庫的工程增強智能感知那麼讓我簡單地介紹一下如果你決定在你的應用程序中使用這個類庫你可以考慮復制該庫的一個穩定構建版本到你想參考它的位置同時還要把XML文檔文件 (SystemHooks\ManagedHooks\bin\Debug\KennedyManagedHooksxml)復制到相同的位置當你添加一個參考到該庫時Visual StudioNET將自動地讀該文件並使用它來添加智能感知文檔這是很有用的特別是對於象這樣的第三方庫
  
    ·單元測試我相信所有的庫都應有與之相應的單元測試既然我是一家公司(主要負責針對NET環境軟件的單元測試)的合伙人和軟件工程師任何人不會對此感到驚訝因而你將會在名為ManagedHooksTests的解決方案中找到一個單元測試工程為了運行該單元測試你需要下載和安裝HarnessIt這個下載是我們的商業單元測試軟件的一個自由的試用版本在該單元測試中我對這給予了特殊的注意在此處方法的無效參數可能導致C++內存異常的發生盡管這個庫是相當簡單的但該單元測試確實能夠幫助我在一些更為微妙的情況下發現一些錯誤
  
    ·非托管的/托管的調試有關混合解決方案(例如本文的托管的和非托管的代碼)最為技巧的地方之一是調試問題如果你想單步調試該C++代碼或在C++代碼中設置斷點你必須啟動非托管的調試這是一個Visual StudioNET中的工程設置注意你可以非常順利地單步調試托管的和非托管的層但是在調試過程中非托管的調試確實嚴重地減慢應用程序的裝載時間和執行速度
  
    八 最後警告
  
    很明顯系統鉤子相當有力量然而使用這種力量應該是有責任性的在系統鉤子出了問題時它們不僅僅垮掉你的應用程序它們可以垮掉在你的當前系統中運行的每個應用程序但是到這種程度的可能性一般是很小的盡管如此在使用系統鉤子時你還是需要再三檢查你的代碼
  
    我發現了一項可以用來開發應用程序的有用的技術它使用系統鉤子來在微軟的虛擬PC上安裝你的喜愛的開發操作系統的一個拷貝和Visual StudioNET然後你就可以在此虛擬的環境中開發你的應用程序用這種方式當你的鉤子應用程序出現錯誤時它們將僅退出你的操作系統的虛擬實例而不是你的真正的操作系統我已經不得不重啟動我的真正的OS在這個虛擬OS由於一個鉤子錯誤崩潰時但是這並不經常
  
    注意如果你在網上訂閱了一個MSDN那麼在你整個訂閱過程中你可以自由使用虛擬PC


From:http://tw.wingwit.com/Article/program/net/201311/11904.html
  • 上一篇文章:

  • 下一篇文章:
  • 推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.