天天看點

一起談.NET技術,Visual Studio插件GDIWatch實作淺析

  GDIWatch 是Virgo Software 開發的一個for Visual Studio的插件,支援2005/2008/2010,它的功能主要是在一個類似watch的視窗上顯示被調試程式的GDI對象的目前狀态,比如HBRUSH的顔色,大小,圖檔等等,并且它還能在調試過程中高亮顯示有變化的項目,友善程式員跟蹤調試畫圖函數。

  (小聲說一下,crack在文中提供了) 

  這是官方的截圖:

一起談.NET技術,Visual Studio插件GDIWatch實作淺析

  順便再貼一個 GDIWatch 在 VS2010上使用的效果圖:

一起談.NET技術,Visual Studio插件GDIWatch實作淺析

  感覺還不賴,使用起來也挺友善的,就是拽個變量到它上面就可以了。

  GDIWatch 不是免費軟體,作者給了15天的試用期,如果需要繼續使用就要到官網 www.gdiwatch.com 聯系作者擷取注冊碼。

  P.S. 話說前天我在公司正好想上他的網站看看價錢如何,結果發現他的首頁不知出現神馬問題沒法顯示了,囧啊。

  P.P.S. 印象中貌似是要100多美刀的樣子。

  好了, 言歸正傳,我當初之是以找到這個軟體是因為前陣子一直在寫畫圖的代碼,本來是想說在網上找個VC6的插件的(沒辦法,公司還是在用),先是在 CodeProject 上找到一篇某位國人很久以前發表的文章,可是他居然不是開源的(這不坑爹嗎),而且遠沒有 GDIWatch 那麼友善好用(不給力啊),最奇怪的是CodeProject 居然讓他把文章給發表上去了(我勒個去),真是無奈。

  不過該作者倒是簡單提到了一下他實作的方法:

  The steps to do watch Image is :

  (1)get the selection text by ISelectionText interface

  (2)get the value of selection text by IDebugger interface

  (3)Read the memeory or bitmap data from the debugged process memory space

  (4)show it

  最後隻找到這個支援VS2005+的 GDIWatch,于是開始尋思這玩意怎麼實作,我想如果不是很複雜的話說不定可以在閑暇時間做一個for VC6的版本出來的說。

  我首先思考的是要實作這樣的插件最重要是要解決哪些問題:

  1、最最重要的是,必須能夠跨程序“通路”被調試程序的GDI objects,這是當然的;

  2、必須能跟VS協調運作,響應調試動作并及時更新GUI,要像VS自己的watch那麼好用;

  3、必須有界面能顯示GDI objects,這......必須的;

  當然要完善這個插件的話,還需要盡量滿足下列條件:

  1、避免使用undocumented trick,保證相容性;

  2、如GDIWatch那樣支援拖放變量名到GUI上;

  3、高亮有變化的内容,友善跟蹤;

  在定下上面這些條件後,下一步就是逐個解決問題了。

  首先,要擷取GDI對象的屬性,基本是要走這條路:

<a href="http://msdn.microsoft.com/en-us/library/dd144905(VS.85).aspx">DWORD GetObjectType(__in  HGDIOBJ h);</a>

<a href="http://msdn.microsoft.com/en-us/library/dd144869(VS.85).aspx">HGDIOBJ GetCurrentObject(__in  HDC hdc,__in  UINT uObjectType);</a>

<a href="http://msdn.microsoft.com/en-us/library/dd144904(v=VS.85).aspx">int GetObject(__in   HGDIOBJ hgdiobj, __in   int cbBuffer, __out  LPVOID lpvObject);</a>

  然而,GDI對象是基于程序的,GDIWatch作為一個插件,也就是VS的一個DLL,它如果要拿被調試程序的GDI對象句柄來直接用必然是不行的,

  GDI objects 也不在 DuiplicateHandle 這個API支援的 object handle 的範疇之内。

  當然了,GDI對象畢竟也是資料,在使用者模式不能做到的,在核心模式肯定有奇淫巧計可以做到,比如說通路GDI對象表:

<a href="http://topic.csdn.net/t/20031009/14/2337150.html">http://topic.csdn.net/t/20031009/14/2337150.html</a>

<a href="http://hi.baidu.com/qzccan/blog/item/154b542375171440ac34de08.html">http://hi.baidu.com/qzccan/blog/item/154b542375171440ac34de08.html</a>

  說起來有一款軟體很可能就是這麼實作的,叫做 GDIView,它可以檢視指定程序目前打開的所有GDI objects并顯示其屬性:

一起談.NET技術,Visual Studio插件GDIWatch實作淺析

  不過這些都屬于tricks,不是标準的做法,而且我也不熟悉具體實作方法,是以隻能放棄。

  其實,畢竟目标程序是在被調試的狀态下,這還是給了插件解決這個問題的環境,或者說至少有一些條件可以被利用。

  調試器是可以有辦法讀寫被調試程序的記憶體的,可以在被調試程序的運作空間插入一段代碼讓它執行,隻要上面提到的 GetObjectType 等API是在被調試程序的領域執行的,那麼句柄就是有效的,自然能得到所需的結果。

  要讀寫記憶體,必然是這條路:

<a href="http://msdn.microsoft.com/en-us/library/ms684320(VS.85).aspx">HANDLE WINAPI OpenProcess(__in  DWORD dwDesiredAccess,  __in  BOOL bInheritHandle,  __in  DWORD dwProcessId);</a>

<a href="http://msdn.microsoft.com/en-us/library/ms680553(VS.85).aspx">BOOL WINAPI ReadProcessMemory(__in   HANDLE hProcess,  __in   LPCVOID lpBaseAddress,  __out  LPVOID lpBuffer,  __in   SIZE_T nSize,  __out  SIZE_T *lpNumberOfBytesRead);</a>

<a href="http://msdn.microsoft.com/en-us/library/aa366890(VS.85).aspx">LPVOID WINAPI VirtualAllocEx(__in HANDLE hProcess,  __in_opt LPVOID lpAddress,  __in SIZE_T dwSize,  __in DWORD flAllocationType,  __in DWORD flProtect);</a>

<a href="http://msdn.microsoft.com/en-us/library/ms681674(v=VS.85).aspx">BOOL WINAPI WriteProcessMemory(__in   HANDLE hProcess,  __in   LPVOID lpBaseAddress,  __in   LPCVOID lpBuffer,  __in   SIZE_T nSize,  __out  SIZE_T *lpNumberOfBytesWritten);</a>

  接下來的事情大概是這樣:

  設計一段代碼,主要做的事情是接受指定的GDI句柄,然後通過 GetObjectType/GetCurrentObject/GetObject 等API去擷取 GDI object 的相關資訊,然後将結果儲存在某個buffer。

  假設這段代碼是一個C函數,那麼代碼大緻是:

  可以看出是利用編譯器生成代碼的習慣,通過一個額外的空函數 AfterThreadFunc 得到 ThreadFunc 的可能大小(即 nCodeSize = AfterThreadFunc - ThreadFunc)。

  此外也可以嘗試基于X86彙編指令自行組裝 GetGDIObjectInfo 的二進制代碼,不過不是很容易閱讀和維護代碼。

  不過這裡還有一個需要注意的地方,CodeProject 的那篇文章提到了,就是同一個API的位址在不同程序中可能會被映射到不同的位址上,是以要拷貝的代碼中肯定是不能直接那樣調用的,LoadLibrary 和 GetProcAddress 就是很好的一個能得到正确的位址的方法。前面的 GetGDIObjectInfo 函數還使用了 new operator,也要對應修改為API函數如 VirtualAlloc 等。

  在終于把這個GetGDIObjectInfo函數的代碼拷貝到目标程序後,下一步最為重要,就是要設法讓被調試程序執行該函數。

  既然插件已經是調試器的小弟,那麼當然可以利用debug API來實作,而不必用到 CreateRemoteThread 這樣感覺稍微猥瑣的方法。

  VS 應該是通過 WaitForDebugEvent 等一系列API來進行調試的,是以可以攔截它,比如在先調用 SuspendThread 把目前程序中所有非插件子產品所線上程給暫停掉,然後它的函數頭部加個 jmp,讓它先跳轉到自己的一個函數,在這個函數裡,要先進行一些邏輯判斷,在适合的時機利用 GetThreadContext/SetThreadContext 來操作被調試程序,比如修改eip,然後 ContinueDebugEvent 讓被調試程序執行 GetGDIObjectInfo 函數,在取得GDI對象的資訊buffer後,拷貝到插件自己的記憶體空間上,調用 ResumeThread 恢複所有之前被暫停的線程,最後不要忘了還要跳轉回 WaitForDebugEvent 的函數裡。

  關于運用debug API的,最近的 Writing Windows Debugger 系列文章貌似不錯,我有時間要看看。

  做完上面這些事情後,可以給插件的視窗post 一個消息,讓它讀取 GetGDIObjectInfo 傳回的結果并更新GUI。

  至于BITMAP這個比較特殊的對象,可以用 CreateDIBSection 這個API。

  最後就是那個類似watch視窗的屬性清單控件,我沒找到現成的,不過倒是有一個還不錯的封裝類 CPropTree,隻是還需要在它的基礎上加不少代碼進行增強。

  P.S. 終于把這幾天的想法記錄下來,感覺真是說起來容易做起來難啊,這個小小的插件要真正實作起來還是相當麻煩的,有大量的工作要做,難怪人家要賣 100 多美刀的說......

繼續閱讀