• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            牽著老婆滿街逛

            嚴(yán)以律己,寬以待人. 三思而后行.
            GMail/GTalk: yanglinbo#google.com;
            MSN/Email: tx7do#yahoo.com.cn;
            QQ: 3 0 3 3 9 6 9 2 0 .

            使用 Windows XP 的兩種強(qiáng)大的工具在您的代碼中檢測并堵塞 GDI 泄漏

            轉(zhuǎn)載自:http://www.microsoft.com/china/MSDN/library/Windev/WindowsXP/DetPluGDILeakCodTooWinXP.mspx?mfr=true



            發(fā)布日期: 8/2/2004 | 更新日期: 8/2/2004

            Christophe Nasarre

            本文假設(shè)您熟悉 Win32 和 C#

            下載本文的代碼: GDILeaks.exe (13,279KB)

            摘要

            在以前的一篇文章中,作者設(shè)計(jì)了一種簡單的方法來檢測圖形設(shè)備接口 (GDI) 對象,這些對象并未由 Windows 9x 平臺(tái)上基于 Win32 的應(yīng)用程序正確地進(jìn)行發(fā)布。因?yàn)橛行└掳姹镜?Windows 需要一種不太相同的 GDI 泄漏方法,作者已經(jīng)更新了針對那些操作系統(tǒng)的方法。他構(gòu)建并說明了兩種工具,這兩種工具旨在檢測并消除在 Windows XP、Windows 2000 和 Windows NT 上運(yùn)行的應(yīng)用程序中的 GDI 泄漏。

            在 Windows® 95、Windows 98 和 Windows Me 中,圖形設(shè)備接口 (GDI) 句柄是一個(gè) 16 位的值,任何應(yīng)用程序都可以使用它來調(diào)用 GDI API 的函數(shù)。在 2001 年 3 月一期的 MSDN® Magazine 中,我講述了如何利用這些平臺(tái)的 16 位特性來構(gòu)建 GDIUsage,這是一種所有應(yīng)用程序都可使用的列出、比較并顯示 GDI 對象的工具(參見“Resource Leaks:Detecting, Locating, and Repairing Your Leaky GDI Code”)。本文將說明如何編寫用于 Windows XP 的同種類型的工具。我這里將要使用的方法同樣很好地適用于 Windows 2000 和 Windows NT® 4.0,但出于本文的目的,我將使用 Windows XP 來表示所有這三種平臺(tái)。

            fig01

            圖 1 Windows 2000 中 GDI 的使用

            本文說明了 Windows 9x 和 Windows XP 平臺(tái)的不同,提出了在工具的實(shí)現(xiàn)過程中產(chǎn)生問題的解決方案,您可以在圖 1 中看到該工具。我將解釋如何利用代碼插入機(jī)制來確定某個(gè)進(jìn)程的 GDI 資源消耗情況,以及如何修補(bǔ)進(jìn)程或 DLL,使其在創(chuàng)建 GDI 對象時(shí)得到通知。接下來,我將說明如何編寫 Win32® 調(diào)試器來驅(qū)動(dòng)某一進(jìn)程,如何讓該進(jìn)程和調(diào)試器彼此之間進(jìn)行通信,以及如何實(shí)現(xiàn)調(diào)用堆棧管理器來提供有關(guān) GDI 對象資源分配的額外信息。

            Windows 9x 與 Windows XP

            對于 Windows XP,一系列 GDI 對象均與各個(gè)進(jìn)程相關(guān),大部分由 win32k.sys 按內(nèi)核模式進(jìn)行托管,設(shè)備驅(qū)動(dòng)程序負(fù)責(zé) USER 和 GDI 實(shí)現(xiàn)。Win32 應(yīng)用程序通過由 user32.dll 和 gdi32.dll 提供的 API 調(diào)用這些系統(tǒng)服務(wù)。因?yàn)?Windows 基于每個(gè)進(jìn)程保留了 GDI 對象的記錄,所以只有創(chuàng)建了 GDI 對象的應(yīng)用程序能夠使用該對象對應(yīng)的 GDI 函數(shù)。

            Windows 9x 版本的 GDIUsage 使用 GetObjectType API 函數(shù),該函數(shù)提供給定句柄值的 GDI 對象的類型,以檢查某個(gè)隨機(jī)值是否是個(gè)有效的 GDI 句柄。然而,與 Windows 9x 不同,Windows XP GDI 對象句柄完全是個(gè) 32 位的值。其可能的范圍是從 0 到 0xFFFFFFFF,為了列出所有真實(shí)的 GDI 對象,各個(gè)可能的句柄值都需提供給 GetObjectType。這就造成了實(shí)際的性能問題。下列代碼需要幾分鐘來執(zhí)行,遺憾的是,很多通過運(yùn)行測試應(yīng)用程序檢測到的 GDI 對象都不是真的(其中有 500 多個(gè)!):

            DWORD dwObjectType;
            DWORD hGdi;
            for (hGdi = 0; hGdi < 0xFFFFFFFF; hGdi++)
            {
            dwObjectType = ::GetObjectType((HANDLE)hGdi);
            if (dwObjectType != 0)
            {
            TRACE("0x%08x -> %u\n", hGdi, dwObjectType);
            }
            }
            

            這意味著還需要有使用 GetObject 的額外測試代碼(可能會(huì)很長)來獲得一個(gè)可靠的列表。循環(huán)周期使得該方法不可用。本文提供了兩種其他的解決方案。第一種方案使用 GDI 管理的句柄表,而第二種方案通過掛鉤來自 GDI API 的函數(shù)進(jìn)行工作。值得一提的是,第一種解決方案在未來的 OS 版本中既得不到支持也不能保證具有相同的行為。找到另一種可獲得真正 GDI 對象列表的方法是要解決的首要問題。

            對于 Windows 9x 的 GDIUsage,有可能利用對應(yīng)于每個(gè) GDI 對象類型(如位圖的 BitBlt 或畫筆的 FillRect)的函數(shù)來顯示 GDI 對象。但是由于當(dāng)利用由另一個(gè)進(jìn)程創(chuàng)建的 GDI 對象來調(diào)用這些 API 函數(shù)時(shí)會(huì)失敗,我所開發(fā)的工具的主要功能(即能夠“看到”正在泄漏的資源)消失了。另一方面,顯示的代碼在創(chuàng)建 GDI 對象的應(yīng)用程序中運(yùn)行正常。解決方案是顯而易見的 — 顯示引擎必須運(yùn)行于其他進(jìn)程的上下文中。本文的稍后部分將講述一種基于將 Windows 掛鉤作為進(jìn)程間通信機(jī)制的實(shí)現(xiàn)。

            最后,用 Win32 調(diào)試 API 將這些方法結(jié)合起來,因此,就獲得了可以運(yùn)行于 Windows XP 和其他 32 位 Windows 平臺(tái)上的 GDIUsage 版本的實(shí)現(xiàn)。

            GDI 如何管理句柄

            在我 2002 年 8 月 的文章中,WinDBG 用來說明進(jìn)程環(huán)境塊 (PEB) 結(jié)構(gòu)。在那篇文章中,GdiSharedHandleTable 字段應(yīng)引起您的注意,該字段如圖 2 轉(zhuǎn)載所示。事實(shí)上,這是一個(gè)指向表的指針,其中 GDI 存儲(chǔ)了它的句柄,甚至那些由其他進(jìn)程創(chuàng)建的句柄。在他撰寫的 Windows Graphics ProgrammingWin32 GDI and DirectDraw (Prentice Hall,2002 年)一書中,F(xiàn)eng Huan 提供了另一種訪問該表的方法,但他也描述了該表中每個(gè) 0x4000 項(xiàng)的結(jié)構(gòu),如下所示:

            typedef struct
            {
            DWORD pKernelInfo;
            // 2000/XP layout but these fields are inverted in Windows NT
            WORD  ProcessID;
            WORD  _nCount;
            WORD  nUpper;
            WORD  nType;
            DWORD pUserInfo;
            } GDITableEntry;
            

            每一項(xiàng)都存儲(chǔ)了 GDI 句柄的詳細(xì)信息,句柄的值很容易計(jì)算。它的低 16 位是在表中的索引,其高 16 位保存在 nUpper 字段中。顧名思義,ProcessID 字段包含創(chuàng)建對象的進(jìn)程 ID。有了這些信息,簡單的循環(huán)就可以允許您列出某個(gè)特定進(jìn)程正在使用的對象,而這也正是 GDIndicator 所做的事情,如圖 3 所示。

            如果您有興趣了解獲得運(yùn)行進(jìn)程列表的不同方法,您可以閱讀我在 2002 年 6 月一期上發(fā)表的文章。每個(gè)進(jìn)程都有一個(gè) ID,用來從共享的表中收集該進(jìn)程使用的 GDI 對象,并利用 ProcessID 字段進(jìn)行比較。得到的計(jì)數(shù)值顯示在每個(gè)對象類型列下的 GDIndicator 中。

            與其他列不同,第三列顯示兩個(gè)值。第一個(gè)值是調(diào)用 GetGuiResources 的結(jié)果(這應(yīng)該返回該進(jìn)程使用的 GUI 對象句柄的計(jì)數(shù)值),第二個(gè)加括號(hào)的值是在解析 GDI 句柄共享表的過程中得到的和。您可以在圖 3 中看到,這兩個(gè)值通常是不同的,而 GetGuiResources 總是返回較大的計(jì)數(shù)值。沒有文獻(xiàn)說明這種不同的原因,與常用對象或未發(fā)布對象也沒有什么明顯的關(guān)系。有可能是在您背后分配給 GDI 沒有存儲(chǔ)在共享表中的對象,因此是您沒有涉及的對象。

            這種隱藏分配的一個(gè)例子發(fā)生在圖標(biāo)操作的過程中。當(dāng)您創(chuàng)建或加載某個(gè)圖標(biāo)時(shí),Windows 需要多個(gè)位圖來實(shí)現(xiàn)透明效果。通常一個(gè)用于掩碼,一個(gè)用于可視圖形。與位圖不同,圖標(biāo)由 USER 系統(tǒng)組件來處理,而不是由 GDI 來處理。這可能就是當(dāng)調(diào)用 GetGuiResources 來了解 GDI 的使用情況時(shí) GetGuiResources 背后的代碼好像沒有跟蹤這些分配的原因。

            通過 API 掛鉤來跟蹤對象分配

            您已經(jīng)看到,要了解特定的過程使用哪些 GDI 對象并不容易。怎樣才能知道對象是否已由應(yīng)用程序代碼或背后的 GDI 自身加以分配呢?如果創(chuàng)建 GDI 對象時(shí) Windows 能夠通知您,那么就很容易存儲(chǔ)它的句柄值并構(gòu)建由應(yīng)用程序分配的對象列表。遺憾的是,Win32 API 并沒有為開發(fā)人員提供這種通知機(jī)制。

            如果您想知道何時(shí)創(chuàng)建了新對象,必須了解圖 4 中列出的函數(shù)調(diào)用。

            幸運(yùn)的是,在作者的文章“Learn System-Level Win32 Coding Techniques by Writing an API Spy Program”中(發(fā)表于 1994 年 12 月一期的 MSJ),Matt Pietrek 說明了如何編寫 Win32 領(lǐng)域的 API 偵探引擎。給定一個(gè)特定模塊(進(jìn)程或 DLL),該引擎可以用您自己的函數(shù)地址替換被調(diào)用函數(shù)(由 DLL 導(dǎo)出)的地址。一旦執(zhí)行了這種替換,每次被偵探的模塊調(diào)用一個(gè)掛鉤函數(shù)時(shí),將在其所在位置執(zhí)行您自己的句柄。

            該 API 掛鉤原則已經(jīng)過多年的改進(jìn)(參見 1998 年 2 月 1999 年 6 月 MSJ 的 John Robbins Bugslayer 專欄。)如果您需要了解不同 Windows 平臺(tái)的可能實(shí)現(xiàn),應(yīng)該閱讀 Jeffrey Richter 的 Programming Applications For Microsoft Windows Fourth Edition“(Microsoft® 出版社,1999 年)一書的第 22 章,以及 John Robbins 的 Debugging Applications(Microsoft 出版社,2000 年)一書。這里我使用了 John Robbins 的方法。

            fig05

            圖 5 調(diào)用 GetDC 的內(nèi)存布局

            John 的 HookImportedFunctionsByName helper 函數(shù)接受修補(bǔ)函數(shù)列表、導(dǎo)出它們的系統(tǒng) DLL 的加載地址、調(diào)用被修補(bǔ)函數(shù)的模塊,以及要重新定向到的存根列表。關(guān)于退出,它填充了包含所有被修補(bǔ)函數(shù)地址的列表。例如,如果 App.exe 正從 USSER32.DLL 調(diào)用 GetDC,則您將得到如圖 5 所示的內(nèi)存布局。如果我用下面的輸入?yún)?shù)調(diào)用 HookImportedFunctionsByName,它將產(chǎn)生如圖 6 所示的不同布局。

            系統(tǒng)修補(bǔ)函數(shù)列表 (GetDC)

            導(dǎo)出函數(shù)的 DLL 的地址 (USER32.dll)

            模塊調(diào)用(App.exe 直接調(diào)用 GetDC)

            修補(bǔ)函數(shù)的列表(來自于 Hook.dll 的 GetDC)

            在該特例中,包含所有已修補(bǔ)函數(shù)地址的列表應(yīng)是 initial@。

            fig06

            圖 6 對 GetDC 修補(bǔ)調(diào)用的內(nèi)存布局

            除了圖 4 中列出的每個(gè)函數(shù)調(diào)用外,用同一機(jī)制來掛鉤自由的 GDI 對象的函數(shù)(如圖 7 所示)。 有了這兩種類型的通知,您就有可能跟蹤運(yùn)行的活動(dòng) GDI 對象。

            CGDIReflect 類負(fù)責(zé)提供靜態(tài)的存根方法,這些方法將替代系統(tǒng)函數(shù)被調(diào)用。該類派生于 CAPIReflect,其主要目標(biāo)是利用宏將給定模塊的函數(shù)調(diào)用重新定向到靜態(tài)類成員中。這種替換是通過對 DoReflect 的調(diào)用來完成的,DoReflect 接受調(diào)用方模塊句柄作為參數(shù)。派生類的作用就是將您有興趣接收有關(guān)信息的每個(gè)系統(tǒng)函數(shù)映射到適當(dāng)?shù)拇娓瘮?shù),本文后面的部分將對此進(jìn)行討論。

            遵循消息映射機(jī)制,定義了一組宏來幫助您自動(dòng)定義并聲明存根函數(shù)。在 /P 編譯器選項(xiàng)的幫助下,可獲得每個(gè)包含所有擴(kuò)展宏代碼的源文件的 .i file。您需要觀察結(jié)果文件的大小,該結(jié)果文件可能很大,但這種方法準(zhǔn)確地顯示了哪些代碼被執(zhí)行,您將在本文后面的圖和源代碼中看到。

            從 USER 和 GDI 重定向函數(shù)需要三步,稍后我將對這三步進(jìn)行概述。使用同一示例,我將講述如何修補(bǔ) user.dll 中的 GetDC。

            第一步是利用 DECLARE_REFLECT_APIxxx 宏聲明 CGDIReflect 中的靜態(tài)變量,其中 xxx 表示該函數(shù)的參數(shù)數(shù)量(GetDC 有 1個(gè),它接受 HWND 作為參數(shù))。該聲明用 BEGIN_REFLECT_LIST 和 END_REFLECT_LIST 框起來,前者定義了一個(gè)隱藏的、提供跟蹤服務(wù)的 TraceReflectCall helper 方法,后者沒起什么作用:

            BEGIN_REFLECT_LIST()
            DECLARE_REFLECT_API1(GetDC, HDC, HWND, hWnd)
            END_REFLECT_LIST()
            

            但是,DECLARE_REFLECT_API 宏需要提供系統(tǒng)函數(shù)名、其返回類型及其參數(shù)列表(類型和名稱)。提供這些信息允許將宏擴(kuò)展到 CGDIReflect(實(shí)際上是個(gè)存根)的靜態(tài)方法中,該方法共享同一原型并執(zhí)行下列步驟。首先,通過別名調(diào)用初始的系統(tǒng)函數(shù)(后來由DEFINE_API_REFLECT 實(shí)例化)。之后,由前一調(diào)用分配的句柄及其類型被存儲(chǔ)到一個(gè) CHandleInfo 結(jié)構(gòu)中(參見圖 8),這是供將來使用的一些額外數(shù)據(jù)(參見下一部分有關(guān) DoStackAddressDump 的討論)。最后,CGDIReflect 的靜態(tài)映射成員被更新,從而用前面講到的結(jié)構(gòu)來與新分配的句柄以及想俘獲的創(chuàng)建對象相關(guān)聯(lián)。

            第二步通過下列宏實(shí)現(xiàn)每個(gè)靜態(tài)成員,作為掛鉤系統(tǒng)函數(shù)地址的別名:

            DEFINE_API_REFLECT(CGDIReflect, GetDC);
            

            在我的示例中,宏可擴(kuò)展到:

            CGDIReflect::GetDCProc CGDIReflect::__GetDC = 0;
            

            在第三步和最后一步,需要實(shí)例化所有這些成員,然后在執(zhí)行時(shí)使用。在這兩種情況下都要調(diào)用 FillStubs 方法。首先,將 APIR STATE INIT 作為參數(shù),由 Init調(diào)用 FillStubs,并且沒有模塊句柄要修補(bǔ)。這在利用 GetProcAddress 計(jì)算系統(tǒng)函數(shù)的地址時(shí)發(fā)生,然后地址存儲(chǔ)在別名成員(對于 __GetDC 為 GetDC)中。

            接著,在新的 DLL 需要修補(bǔ)時(shí)調(diào)用 FillStubs。用 APIR STATE ENABLE 作為參數(shù),DoReflect調(diào)用 FillStubs。它將由模塊進(jìn)行的每個(gè)系統(tǒng)調(diào)用 (GetDC) 重定向到相應(yīng)的靜態(tài)存根方法(本示例中為 _GetDC),而模塊的加載地址作為參數(shù)被傳遞。該方法遵循與 MFC 消息映射相同的模式:
            BEGIN_IMPLEMENT_API_REFLECT()
            •••
            IMPLEMENT_API_REFLECT(hModule, "USER32.DLL", GetDC);
            •••
            END_IMPLEMENT_API_REFLECT()
            

            為了在執(zhí)行過程中幫助調(diào)試宏,一些 AfxTrace 調(diào)用分散在擴(kuò)展代碼中。根據(jù)圖 9 中所示的值,SetTraceLevel 允許您選擇跟蹤哪個(gè)操作。

            現(xiàn)在有了 CGDIReflect 類,它允許您將由特定模塊發(fā)出的任一調(diào)用重定向到存根方法,該方法的唯一作用是將新建的 GDI 對象存儲(chǔ)到映射中。但是該實(shí)現(xiàn)有一個(gè)缺陷 — 它不是線程安全的。如果您需要檢查的應(yīng)用程序是多線程的,幾個(gè)線程都在調(diào)用 GDI 函數(shù),產(chǎn)生的行為可能不確定,因?yàn)榭缇€程訪問句柄映射是不同步的。

            利用堆棧跟蹤監(jiān)視分配

            每次對重要函數(shù)的調(diào)用 都終止于一個(gè)存根,該存根完成兩項(xiàng)操作。第一項(xiàng)是將分配的句柄及其類型包裝到 GDIReflect.h 中聲明的一個(gè) CHandleInfo 對象中。第二項(xiàng)操作更有趣。在 CHandleInfo 對象中,當(dāng)前調(diào)用堆棧的每個(gè)函數(shù)地址都存儲(chǔ)在 m_pStack 中 — 分配的 DWORDs 數(shù)組 — 而 m_pStack 中保存的地址數(shù)保存在 m Depth 中。因此,除了以圖形方式表示 GDI 對象外,還可能顯示導(dǎo)致分配特定 GDI 對象的函數(shù)調(diào)用堆棧,如圖 10 所示。

            fig10

            圖 10 導(dǎo)致分配對象的單元

            當(dāng)需要瀏覽堆棧時(shí),imagehlp.dll 和 dbghelp.dll 是您最好的朋友。為了便于您的使用,John Robbins 已經(jīng)將該引擎包裝到了 CSymbolEngine 類中,該類的發(fā)展歷程在 1998 年 4 月 1999 年 2 月 MSJBugslayer 專欄中有所介紹。在 John Robbins 的 Debugging Applications 一書中詳細(xì)地介紹了使用了 DBGHELP 的最后一個(gè)版本,該書我在前面提到過。

            CSymbolEngine 類是一個(gè)在由 DBGHELP 導(dǎo)出的許多函數(shù)頂部的低級層。StackManager.cpp(位于本文頂部鏈接處代碼下載中的 /Common 目錄內(nèi))中實(shí)現(xiàn)的 CstackManager 提供了只有三個(gè)有趣方法的更高級功能。第一個(gè)是 DoStackAddressDump,它利用 CSymbolEngine 分配并填充一組當(dāng)前的調(diào)用堆棧。該方法由每個(gè)存根調(diào)用,并存儲(chǔ)導(dǎo)致對象分配的每個(gè)函數(shù)地址。

            十六進(jìn)制地址對計(jì)算機(jī)有好處,但對人沒什么好處。為了將由 DoStackAddressDump 返回的地址數(shù)組轉(zhuǎn)換為可讀的格式,如圖 10 所示,必需調(diào)用 DumpStackAllocation。該方法接受堆棧轉(zhuǎn)儲(chǔ)及其深度,然后在一個(gè) CString 中返回轉(zhuǎn)換后的堆棧。該方法的調(diào)用方能夠選擇他希望在每個(gè)地址間使用的行分隔符,要么是選擇 \r\n 在編輯框中顯示 CString,要么是選擇 \n,利用 Trace 或 OutputDebugString簡單地將其進(jìn)行記錄。該方法背后并沒有什么魔法,對于給定數(shù)組中的每個(gè)地址,它都調(diào)用 ConvertStackAddressIntoFunctionName。

            魔法在其他地方存在。當(dāng)堆棧由 DoStackAddressDump 轉(zhuǎn)儲(chǔ),而地址存儲(chǔ)于返回的數(shù)組中時(shí),該方法還可以利用 CSymbolEngine 中定義的 SymGetModuleInfo、SymGetSymFromAddr 和 SymGetLineFromAddr(有關(guān)實(shí)現(xiàn)細(xì)節(jié),請參見代碼下載中 StackManager.cpp 中的 ConvertAddress)找到對應(yīng)于地址的符號(hào)。為什么現(xiàn)在進(jìn)行轉(zhuǎn)換?答案很簡單:在這個(gè)特定的時(shí)刻,您確信相應(yīng)的 DLL 被加載,但稍后調(diào)用 DumpStackAllocation 時(shí)情況可能會(huì)不一樣。

            如果頻繁創(chuàng)建 GDI 對象,就會(huì)產(chǎn)生許多堆棧轉(zhuǎn)儲(chǔ),并保存在 m_HandleMap 中。但保存在該映射中的 CHandleInfo 對象保留的是地址數(shù)組,而不是轉(zhuǎn)換后的字符串?dāng)?shù)組。技巧是利用一個(gè)映射成員(如 m_AddressToName)來跟蹤轉(zhuǎn)換。這就避免了在堆棧轉(zhuǎn)儲(chǔ)中存儲(chǔ)長字符串來代替每個(gè)地址的 DWORD 類型,因此減少了對內(nèi)存的消耗。另一個(gè)好處是,堆棧轉(zhuǎn)儲(chǔ)運(yùn)行速度會(huì)更快,因?yàn)槔?m_AddressToName 來作為緩沖區(qū),從而避免了對符號(hào)引擎進(jìn)行查詢。

            即使您知道如何掛鉤一系列 GDI 函數(shù),您仍需要了解在哪些模塊中調(diào)用這些 GDI。我們說,在共享 DLL 中使用 MFC 的應(yīng)用程序正在創(chuàng)建一個(gè) Cpen 對象,從而操作與 Windows 畫筆相關(guān)的 API。真正調(diào)用 CreatePen(它返回畫筆的句柄)是在 MFC DLL 中(而不是在調(diào)用應(yīng)用程序代碼中)完成的。如果只掛鉤由可執(zhí)行文件調(diào)用的 API 函數(shù),就會(huì)丟失來自由應(yīng)用程序使用的所有 DLL 的調(diào)用。

            通過調(diào)試來確定需要修補(bǔ)的 DLL

            在 Windows XP 中,獲得在給定時(shí)間由某個(gè)進(jìn)程加載的所有 DLL 的列表非常簡單,這要感謝 PSAPI 函數(shù) EnumProcessModules,正如 Matt Pietrek 在“Under the Hood”一文中所述,該文發(fā)表在 1996 年 8 月發(fā)行的 MSJ 中。但是,對于動(dòng)態(tài)加載的庫,這個(gè)問題需要一點(diǎn)竅門。除了掛鉤系統(tǒng)函數(shù),還必須掛鉤主程序發(fā)出(ANSI 與 UNICODE 版本)的 LoadLibrary 調(diào)用,從而檢測何時(shí)加載了一個(gè)新 DLL,并遞歸地對其進(jìn)行相同的掛鉤處理。

            需要回答最后的兩個(gè)問題。第一,如何知道在偵探的進(jìn)程中哪些 DLL 需要修補(bǔ)?第二,如何確保這些代碼在另一個(gè)進(jìn)程中執(zhí)行?如果有這樣的解決方案,那么還有可能用它來顯示任何 GDI 對象句柄的圖形化表示。在2002 年 8 月發(fā)表的有關(guān)調(diào)試的文章中,一個(gè)調(diào)試過的進(jìn)程通過使用 Win32 調(diào)試 API 來動(dòng)態(tài)或靜態(tài)地檢測加載的 DLL。這種方法的主要缺點(diǎn)是需要啟動(dòng)和調(diào)試應(yīng)用程序。與 Windows 9x 版本的 GDIUsage 不同,被檢測的 GDI 對象必須是由調(diào)試過的應(yīng)用程序分配的對象,這可以在圖 1 中看到。

            Win32 調(diào)試 API 允許您編寫代碼來啟動(dòng)某個(gè)應(yīng)用程序(調(diào)試對象),并且當(dāng)發(fā)生事件時(shí)(如加載一個(gè)新的 DLL)獲得通知。這正是您所需要的。要輕松地編寫調(diào)試器,您只需重載在調(diào)試事件發(fā)生時(shí)要調(diào)用的虛方法。CGDIDebugger 類派生于 CapplicationDebugger,在我的 2002 年 8 月的文章中介紹過 CapplicationDebugger。圖 11 顯示了 CGDIDebugger 所重載方法的名稱,并解釋了每個(gè)方法的作用。稍后我將討論調(diào)試器和調(diào)試對象之間的通信機(jī)制。

            fig12

            圖 12 搜索字符串

            除了這些方法,已經(jīng)重載了 OnOutputDebugStringDebugEvent,從而將調(diào)試對象(前綴為 >)留下的蹤跡重定向到專用的列表框中。還有可能將一個(gè)選擇復(fù)制到剪貼板,或者搜索一個(gè)字符串,如圖 12 所示。當(dāng)利用 TRACE 或 OutputDebugString 添加跟蹤時(shí),它就會(huì)出現(xiàn)在該列表框中。這是一種調(diào)試代碼的有效機(jī)制,并可以標(biāo)出調(diào)試對象(前綴為 >)和調(diào)試器的輸出結(jié)果之間的差異。

            在另一個(gè)進(jìn)程中插入運(yùn)行代碼

            現(xiàn)在還有一個(gè)遺留問題需要解決:一定有一種方法可以使一些代碼在另一個(gè)應(yīng)用程序的上下文中運(yùn)行。幸運(yùn)的是,Jeffrey Richter 很早以前就在他的文章“Load Your 32-bit DLL into Another Process's Address Space Using INJLIB”(MSJ,1994 年 5 月)中解決了這個(gè)問題。

            由于通常我們只對使用 GDI API 的應(yīng)用程序感興趣,因此我們可以假定這樣的應(yīng)用程序至少使用一個(gè)窗口來顯示它的圖形。(否則,它為何需要 GDI?)因此,在不同的解決方案中,基于 Windows 掛鉤的方案好像是最佳的選擇。當(dāng)調(diào)用下面的掛鉤時(shí),任何進(jìn)程中的任何線程執(zhí)行 GetMessage 時(shí) Windows 將會(huì)調(diào)用 GetMessageHookProc 回調(diào)函數(shù):

            SetWindowsHookEx(WH_GETMESSAGE, GetMessageHookProc, hInstance, 0)
            

            由于這是一個(gè)系統(tǒng)范圍的掛鉤(最后的參數(shù)為 0),回調(diào)函數(shù)的代碼必須位于某個(gè) DLL 中,該 DLL 被映射到其線程調(diào)用 GetMessage 的各個(gè)進(jìn)程的地址空間。

            如果掛鉤進(jìn)程和驅(qū)動(dòng)應(yīng)用程序適合,用預(yù)先定義的消息在它們之間建立通信信道就非常簡單。這是一種允許調(diào)試器為一些插入代碼(運(yùn)行在被偵探應(yīng)用程序上下文中)發(fā)送請求的不錯(cuò)方法,確切地說,這就是 GDI 對象顯示對話框所需的!當(dāng)掛鉤進(jìn)程攔截了第一條消息時(shí),它首先重定向已經(jīng)加載的 DLL 調(diào)用。然后,它啟動(dòng)一條新的線程,專門處理來自調(diào)試器的請求。這就在調(diào)試器和調(diào)試對象之間創(chuàng)建一條通信信道(有關(guān)詳細(xì)信息,請參見 GDITrace.cpp 中的 StartInfiltratedThread)。

            由調(diào)試器和調(diào)試對象從掛鉤進(jìn)程和滲透線程函數(shù)中調(diào)用的函數(shù)都已經(jīng)收集到了 GDITrace.dll 中,GDITrace.dll 的行為由 CGDITraceApp 類來實(shí)現(xiàn)。該 DLL 與調(diào)試器應(yīng)用程序 GDIUsage 靜態(tài)鏈接,但是它動(dòng)態(tài)加載到觸發(fā) Windows 掛鉤的進(jìn)程中。由調(diào)試器調(diào)用的函數(shù)在 _GdiTrace.h 中聲明,并集合在 GdiTrace.cpp 中,從而幫助您理解調(diào)試器使用的是哪一部分,調(diào)試對象使用的是哪一部分。但為什么要在同一個(gè) DLL 中混合不同的代碼?需要在這兩種代碼之間共享一些變量,在同一個(gè) DLL 的實(shí)例之間共享變量的值很簡單,如圖 13 所示。

            這些代碼用讀/寫/共享屬性 (rws) 定義了名稱為 .shared 的 PE 區(qū)域,這些屬性包含 5 個(gè)前綴為 s_ 的需要共享的變量。根據(jù)這些聲明,Windows 將這些變量保存到一個(gè)加載 DLL 的進(jìn)程共享的內(nèi)存塊中。因此,這些變量在各個(gè)進(jìn)程中的值都相同,特別是調(diào)試器和調(diào)試對象。我們看一下當(dāng)調(diào)試器啟動(dòng)一個(gè)調(diào)試對象時(shí)會(huì)發(fā)生什么情況,以及如何使用這些變量。

            當(dāng)啟動(dòng)調(diào)試對象時(shí),調(diào)試器線程接收到一個(gè) CREATE_PROCESS_DEBUG_EVENT,它由 OnCreateProcessDebugEvent 處理,OnCreateProcessDebugEvent 反過來又調(diào)用 StartTraceGDI。該函數(shù)執(zhí)行 SetSharedVariables,用調(diào)試器線程的 ID 設(shè)置 s_dwCallingThreadID 的值。如果當(dāng)前進(jìn)程的 ID 與保存在 s_dwProcessID 中的 ID 相同,掛鉤進(jìn)程就會(huì)知道它是在調(diào)試對象的上下文中運(yùn)行,并根據(jù)已經(jīng)加載的 DLL 開始修補(bǔ) GDI 調(diào)用。接著,由掛鉤進(jìn)程啟動(dòng)的專用線程在 dwInfiltratedThreadID 中保存了它的 ID。最后,當(dāng)該掛鉤進(jìn)程成功運(yùn)行時(shí),s_bDebuggeeIsStarted 被設(shè)置為 TRUE,然后由調(diào)試器用它來決定滲透線程是否已經(jīng)準(zhǔn)備好響應(yīng)請求。

            如果需要在調(diào)試器和調(diào)試對象之間傳遞或檢索 GDI 對象句柄列表,就需要一個(gè)正好比一個(gè) DWORD 或一個(gè) BOOL 大的共享緩沖區(qū)。除了這 5 個(gè)變量,還要使用一個(gè)名為 GDITrace SharedBuffer 的內(nèi)存映射文件,對應(yīng)的內(nèi)存由 CGDITraceApp 的成員 m_lpvMem 指定。它在 DLL 啟動(dòng)期間被初始化(有關(guān)詳細(xì)的實(shí)現(xiàn),請參見 GDITrace.cpp 中的 CGDITraceApp::InitInstance)。只有當(dāng)這兩個(gè)進(jìn)程加載 DLL 時(shí)該緩沖區(qū)才需要?jiǎng)?chuàng)建和初始化:作為調(diào)試器的 GDIUsage 及其當(dāng)前的調(diào)試對象。

            s_dwProcessID 共享的變量用來識(shí)別兩個(gè)進(jìn)程間的區(qū)別。如果沒有啟動(dòng)的調(diào)試對象,它的值總是 0;否則,它就包含調(diào)試對象進(jìn)程的 ID。當(dāng) DLL 加載到進(jìn)程中時(shí),它的 InitInstance 檢查 s_dwProcessID 是否等于 0(應(yīng)當(dāng)是 GDIUsage)或者等于 GetCurrentProcessId(應(yīng)當(dāng)是調(diào)試對象),從而創(chuàng)建內(nèi)存映射文件。

            從調(diào)試器到調(diào)試對象的通信

            調(diào)試器使用 s_dwInfiltratedThreadID 共享變量來發(fā)送一個(gè)請求(利用 PostThreadMessage 通過一個(gè)簡單的 Windows 消息),該請求將由調(diào)試對象中的滲透線程來處理。當(dāng)調(diào)試對象通知調(diào)試器這樣的一個(gè)請求已經(jīng)完成時(shí),需要另一個(gè) s_dwCallingThreadID 共享變量。例如,當(dāng)用戶單擊“Take Snapshot!”按鈕時(shí),GDIUsage 需要從調(diào)試對象收集已分配的 GDI 對象。

            GDIUsage 發(fā)送一條 TM_GET_LIST 消息給調(diào)試對象中的滲透線程,調(diào)試對象的值保存在 s_dwInfiltratedThreadID 中。它將執(zhí)行連同參數(shù) UM_SNAPSHOT_READY 一起發(fā)送給 OnGetList 函數(shù),該參數(shù)將被用作回調(diào)消息被 GDIUsage 主對話框接收。為什么不簡單地使用同樣的 TM_GET_LIST?答案與共享代碼有關(guān)。“Take Snapshot!”與“Compare”按鈕都需要獲得相同的已分配 GDI 對象列表(雖然使用該列表的方法不同),以更新 GDIUsage 用戶界面的相應(yīng)部分。

            為了總結(jié)一下在前面段落中我已經(jīng)論述的內(nèi)容,TM_GET_LIST 線程消息觸發(fā)對調(diào)試對象端 GDI 對象的處理。另外,根據(jù)用戶定義的兩條消息,有兩種方法可以更新 GDIUsage 主對話框:UM_SNAPSHOT_READY 與 UM_COMPARE_READY。

            滲透線程喚醒并要求 OnGetList 來處理請求。該 CGDITraceApp 方法通過修補(bǔ)的存根枚舉出調(diào)試對象中檢測到的 GDI 分配,并將每個(gè)對象的說明(句柄值和類型)按照下面的 GDI_LIST 格式復(fù)制到由 m_lpvMem z指向的內(nèi)存映射文件共享的緩沖區(qū)中:

            typedef struct
            {
            DWORD    dwType;
            HGDIOBJ  hObject;
            } GDI_ITEM;
            typedef struct
            {
            DWORD    dwCount; // count of meaningful GDI_ITEM slots in Items
            GDI_ITEM Items[];
            } GDI_LIST;
            

            為了通知調(diào)試器列表已經(jīng)準(zhǔn)備好,一條相同的 TM_GET_LIST 消息被發(fā)送回由 s_dwCallingThreadID 識(shí)別的線程。該消息由負(fù)責(zé)調(diào)試事件的線程在 GDIUsage 上下文中接收并發(fā)送給 CGDIDebugger::OnThreadMessage。該方法通過用一個(gè)指向共享內(nèi)存的指針向主對話框發(fā)送正確的用戶消息(本示例為 UM_SNAPSHOT_READY)來通知 UI 線程,共享內(nèi)存由內(nèi)存映射文件定義,而調(diào)試對象將 GDI 對象保存在該文件中。通過使用 CGdiResources 的 CreateFromList 方法,這個(gè)自動(dòng)封送處理的序列化列表用來實(shí)例化 CGdiResources。該類用來包裝一個(gè) GDI 對象列表,同時(shí)還提供諸如枚舉與圖形顯示等服務(wù)。

            死鎖與計(jì)時(shí)問題

            在深入了解遠(yuǎn)程 GDI 對象的圖形顯示之前,您應(yīng)當(dāng)了解一下可能發(fā)生的死鎖問題。以前探討的收集 GDI 對象的技術(shù)都是異步的,因?yàn)樗蕾囌{(diào)試器和調(diào)試對象之間交換的 Windows 消息。如果需要進(jìn)行強(qiáng)同步通信,可以使用 Win32 事件。例如,滲透線程等待一個(gè)有特定名稱的事件,當(dāng)一條消息發(fā)送到它的隊(duì)列中或者事件獲得信號(hào)通知時(shí),它就調(diào)用 MsgWaitForMultipleObjectsEx 來喚醒(有關(guān)詳細(xì)的源代碼信息,請參見 GDITrace.cpp 中的 CGDITraceApp::InfiltratedThreadProc)。在該實(shí)現(xiàn)中,用事件來要求線程結(jié)束它的生存期,因此不是真正的同步。

            另一種需要真正同步行為的情況是,當(dāng)調(diào)試對象加載一個(gè) DLL 時(shí)修補(bǔ) GDI 調(diào)用。為了截獲 GDI 調(diào)用,調(diào)試器必須盡快通知滲透線程。否則,調(diào)試對象可能在安裝截獲存根之前就開始分配 GDI 對象。這里是一個(gè)不錯(cuò)的實(shí)現(xiàn)方案:

            1.

            調(diào)試器線程獲得通知,通過 WaitForDebugEvent 返回的LOAD_DLL_DEBUG_EVENT 已在調(diào)試對象中加載了一個(gè) DLL。

            2.

            OnLoadDLLDebugEvent 重寫方法接收 DLL 對應(yīng)的 hModule,將它保存在一個(gè)新的共享變量中,并通過為事件發(fā)送信號(hào)來請求調(diào)試對象為該特定的 DLL 修補(bǔ) GDI 調(diào)用。

            3.

            如果調(diào)試對象加載另一個(gè) DLL,為了避免重新進(jìn)入,OnLoadDLLDebugEvent 等待另一個(gè)在調(diào)試對象完成其修補(bǔ)工作后的信號(hào)通知事件。

            4.

            MsgWaitForMultipleObjectsEx 喚醒調(diào)試對象-滲透線程,因?yàn)樗却囊粋€(gè)事件已經(jīng)由信號(hào)通知。

            5.

            CGDITraceApp::OnNewDLL 方法為在由共享變量定義的地址處加載的 DLL 重定向 GDI 調(diào)用,該共享變量由調(diào)試器用 DLL hModule 填充。

            6.

            調(diào)試器等待的事件由滲透線程發(fā)信號(hào)通知。

            7.

            滲透線程調(diào)用 MsgWaitForMultipleObjectsEx 等待完成另一個(gè)請求。

            8.

            調(diào)試器線程繼續(xù)進(jìn)行,因?yàn)樗却氖录呀?jīng)由信號(hào)通知。

            注意,最后兩步的順序號(hào)相同,因?yàn)樗鼈兊拇a在由 Windows 調(diào)度的兩個(gè)不同線程中運(yùn)行。不要指望一個(gè)會(huì)在另一個(gè)之前執(zhí)行。

            雖然這種方案看起來很完美,但它會(huì)在調(diào)試器線程和插入調(diào)試對象的線程之間引起死鎖。第 3 步和第 4 步之間有一個(gè)隱患。Win32 調(diào)試 API 假定調(diào)試器正在利用 WaitForDebugEventsumes 等待一個(gè)調(diào)試對象事件。當(dāng)該函數(shù)返回時(shí),一直到 ContinueDebugEvent 被調(diào)用,調(diào)試對象中的所有線程(甚至那些沒有生成調(diào)試事件的線程)都被掛起。因此,停留在第 3 步和第 4 步的調(diào)試器線程將永遠(yuǎn)不會(huì)由調(diào)試對象執(zhí)行,因?yàn)樵谕酵ㄐ沤Y(jié)束后 ContinueDebugEvent 才應(yīng)執(zhí)行。要切記,從由調(diào)試器接收的調(diào)試事件來同步調(diào)試對象中的行為是不可能的。

            在 GDIUsage 的情況下,基于消息的機(jī)制提供的通知速度好像已足夠迅速。真正的問題出在其他地方。由于調(diào)試對象檢索到第一條消息后就啟動(dòng)滲透線程,所以所有靜態(tài)鏈接的 DLL 都已經(jīng)初始化。如果其中的一些已經(jīng)分配了 GDI 對象,這樣的消耗將永遠(yuǎn)不會(huì)被 GDIUsage 檢測到。在這種情況下,需要另一種方法來執(zhí)行修補(bǔ)代碼。GDIUsage 幫助您檢測和找到的 GDI 對象創(chuàng)建不是在應(yīng)用程序的生存期內(nèi)發(fā)布的,而是在其啟動(dòng)時(shí)發(fā)布的。

            顯示 GDI 對象

            圖 1 中所示的 GDIUsage 用戶界面允許用戶獲得在任何給定時(shí)間點(diǎn)某進(jìn)程使用的 GDI 對象的快照,并且稍后可以與同一進(jìn)程的當(dāng)前狀態(tài)進(jìn)行對比。每組對象都保存在 CGdiResources 對象中。可以在 TM_GET_LIST 注釋中看到,對象列表一旦通過內(nèi)存映射文件在調(diào)試對象和調(diào)試器之間被序列化和封送處理,利用 CreateFromList 就可以初始化一個(gè) CGdiResources 對象。

            在 Windows 9x 版本的 GDIUsage 中,負(fù)責(zé)顯示 GDI 對象的 CgdiResourcesDlg 類接受指向 CGdiResources 對象的指針作為參數(shù)。遺憾的是,在 Windows XP 版本的 GDIUsage 中,由于用來以圖形方式顯示 GDI 對象(在調(diào)試對象內(nèi)部創(chuàng)建)的 GDI 函數(shù)總是出故障,因此在調(diào)試器的上下文中,CGdiResourcesDlg 就變得毫無價(jià)值。

            解決方案是移去 GDITrace.dll 內(nèi)部的 CGdiResourceDlg 和 CgdiResources,同先前討論的一樣,GDITrace.dll 通過 Windows 掛鉤被加載到調(diào)試對象中。在檢索完已分配對象的列表后,就該顯示這個(gè)列表了。使用的通信機(jī)制和先前討論的一樣,但在顯示列表并為滲透的線程發(fā)送消息之前,該列表必須在調(diào)試器的上下文中保存到內(nèi)存映射文件。調(diào)試器將 CGdiResources 列表(或者當(dāng)前的快照,或者對比的結(jié)果,取決于用戶單擊的按鈕)序列化到內(nèi)存映射文件,并將一條 TM_SHOW_LIST 消息發(fā)送給調(diào)試對象中滲透的線程,其中利用 GDIUsage 主對話框窗口的句柄作為參數(shù)。在這樣的處理中,由 CGDIDebugger::ShowList 完成序列化,而由 ShowRemoteList 完成消息發(fā)送。

            與 TM_GET_LIST 一樣,由滲透的線程處理 TM_SHOW_LIST 消息,接著發(fā)送到 CGDITraceApp::OnShowList。這種方法根據(jù)內(nèi)存映射文件中保存的序列化與封送數(shù)據(jù)初始化 CgdiResources。現(xiàn)在,有可能讓 CGdiResourcesDlg 來顯示對應(yīng)的 GDI 對象了。

            與 TM_GET_LIST 消息不同,為了將該顯示命令發(fā)送給調(diào)試對象,調(diào)試器不需要任何返回代碼或信息。在任何情況下,用戶都希望 GDIUsage 保持禁用,直到他關(guān)閉了該 CGdiResourcesDlg 對話框。這就是將 GDIUsage 主對話框的句柄作為父窗口傳遞給 CGdiResourcesDlg 對象的原因。

            除了有兩點(diǎn)改進(jìn)之外,該類的實(shí)現(xiàn)從 Windows 9x 版本以后就沒有更改過。第一點(diǎn)改進(jìn)是利用構(gòu)建 GDI 句柄的方法來檢測它是否引用一個(gè)常用對象以及是否在列表框的對應(yīng)行中添加一個(gè) *。這種功能在 GDIUsage 中是不可見的,因?yàn)閯?chuàng)建常用對象的 API 沒有被修補(bǔ)過,但您可能在編寫自己的代碼時(shí)想實(shí)現(xiàn)它。

            第二點(diǎn)改進(jìn)是,為了呈現(xiàn)對應(yīng)的堆棧跟蹤,用戶單擊或者雙擊 GDI 對象列表時(shí)需要進(jìn)行檢測,如圖 10 所示。由于不應(yīng)當(dāng)將 CGdiResourcesDlg 代碼鏈接到堆棧跟蹤代碼,因此定義了一個(gè)通用的回調(diào)接口 INotificationCallBack。CGDITraceApp 類派生于該界面,它實(shí)現(xiàn)了 OnDoubleClick 并使用 CGdiResourcesDlg::SetNotificationCallBack 對其自身進(jìn)行注冊。

            當(dāng)用戶雙擊某個(gè) GDI 對象時(shí),CGdiResourcesDlg 要檢查是否已經(jīng)注冊了一個(gè)回調(diào)。如果是,它就調(diào)用該回調(diào)的 OnDoubleClick 方法,并以被雙擊的 GDI 對象對應(yīng)的 CGdiObj 說明作為參數(shù)。然后,CGDITraceApp 從傳遞的 CGdiObj 提取調(diào)用堆棧,并用它實(shí)例化 CcallStackDlg,從而為用戶顯示堆棧跟蹤(有關(guān)實(shí)現(xiàn)的詳細(xì)信息,請參見 GDITrace.cpp 中的 CGDITraceApp::OnDoubleClick)。

            這種功能也添加到了圖 3 所示的 GDIndicator 工具中。與 GDIUsage 不同,插入機(jī)制是基于 CreateRemoteThread 的,并且在我 8 月份的文章中已經(jīng)進(jìn)行過論述。一個(gè)要注意的有趣事實(shí)是,代碼的序列化和顯示與 GDIUsage 完全相同;只是更改了遠(yuǎn)程處理機(jī)制。由于沒有被記錄的堆棧跟蹤,因此沒有注冊雙擊的處理程序。

            還有最后兩個(gè)缺陷必須進(jìn)行處理。由于顯示 GDI 對象的對話框陷入了另一個(gè)進(jìn)程上下文,因此會(huì)發(fā)生一些怪異的事情。首先,在 Windows 2000 和 Windows XP 中,從另一個(gè)進(jìn)程中設(shè)置前臺(tái)的窗口絕非易事。您必須在擁有前臺(tái)窗口的進(jìn)程中調(diào)用 AllowSetForegroundWindow,從而讓另一個(gè)進(jìn)程設(shè)置它的一個(gè)窗口,并作為一個(gè)新的前臺(tái)窗口。在用戶取消之前,對話框代碼會(huì)在另一個(gè)進(jìn)程當(dāng)前線程的上下文中無限循環(huán)地運(yùn)行。因此,在關(guān)閉該對話框之前,GDIndicator 一直掛起。為了避免這種討厭的行為,在對話框的生存期間,要隱藏它的主窗口。

            第二個(gè)負(fù)面影響更難解決。對話框控制的線程中創(chuàng)建的窗口將不再接收它們的所有消息,因?yàn)樵搶υ捒蜻M(jìn)程對它們進(jìn)行了篩選。例如,如果您使用這里概述的工具來找出由 Notepad 分配的 GDI 對象,它的重畫功能將是不錯(cuò)的選擇,而且可以輕松地導(dǎo)航到它的菜單中,但在關(guān)閉對話框之前,選擇的命令不會(huì)觸發(fā)事件。這是一種復(fù)雜的使用功能,但在搜索難以琢磨的 GDI 漏洞時(shí)確實(shí)頗有價(jià)值。

            小結(jié)

            Windows 9x 和 Windows XP 之間,GDI 已有所不同。雖然程序中的許多代碼都重新使用了其 Windows 9x 實(shí)現(xiàn)中的代碼,例如對 GDI 對象組的管理以及它們的圖形顯示,但獲得由某個(gè)進(jìn)程分配的 GDI 對象列表的內(nèi)在機(jī)制卻是全新的。Windows XP 版本是基于 Win32 調(diào)試 API、DLL 插入、Windows 掛鉤和 API 補(bǔ)丁的,并且與對應(yīng)的 Windows 9x 相比,提供的功能更多。

            除了導(dǎo)致每個(gè) GDI 分配的調(diào)用列表外,還增加了最后的處理。當(dāng)遠(yuǎn)程進(jìn)程終止時(shí),插入的 DLL 的 ExitInstance 方法被調(diào)用。CGDITraceApp 最后利用該通知枚舉 GDI 對象并轉(zhuǎn)儲(chǔ)仍然有效的對象。這與在調(diào)試版本中使用 DEBUG_NEW 作為內(nèi)存分配器來檢測內(nèi)存泄漏時(shí)從 MFC 應(yīng)用程序中獲得的最后轉(zhuǎn)儲(chǔ)是一樣的。

            假如您知道創(chuàng)建所感興趣的資源需要調(diào)用的 API 函數(shù),那么這些機(jī)制可以用來找出其他類型的漏洞。例如,利用現(xiàn)有的工具,如來自 Platform SDK 的 oh.exe 與 dh.exe 或者來自 http://www.sysinternals.com 的 ProcessExplorer,可以發(fā)現(xiàn)內(nèi)核對象泄漏(例如文件和注冊表項(xiàng))。雖然它們可以提供任何進(jìn)程使用的內(nèi)核對象列表(還提供與 ProcessExplorer 的對比),但您可以利用本文所述技術(shù)提供的調(diào)用堆棧和最終泄漏轉(zhuǎn)儲(chǔ)來更加輕松地跟蹤系統(tǒng)漏洞。

            相關(guān)文章,請參閱:
            Windows XP Escape from DLL Hell with Custom Debugging and Instrumentation Tools and Utilities
            Windows XP: Escape from DLL Hell with Custom Debugging and Instrumentation Tools and Utilities, Part 2
            有關(guān)背景信息,請參閱:
            Windows Graphics Programming: Win32 GDI and DirectDraw by Feng Huan (Prentice Hall PTR, 2000)
            Debugging Applications by John Robbins (Microsoft Press, 2002)
            Programming Applications for Microsoft Windows by Jeffrey Richter (Microsoft Press, 1999)

            Christophe Nasarre 是法國 Business Objects 的一位開發(fā)經(jīng)理。他已經(jīng)為 3.0 版本以后的 Windows 編寫了幾個(gè)底層工具。您可以通過 cnasarre@montataire.net 與他聯(lián)系。

            轉(zhuǎn)到原英文頁面


            posted on 2011-03-10 19:00 楊粼波 閱讀(2410) 評論(-1)  編輯 收藏 引用


            只有注冊用戶登錄后才能發(fā)表評論。
            網(wǎng)站導(dǎo)航: 博客園   IT新聞   BlogJava   博問   Chat2DB   管理


            精品久久人妻av中文字幕| 精品综合久久久久久97超人| 曰曰摸天天摸人人看久久久| 青青草国产成人久久91网| 久久精品无码一区二区三区免费| 94久久国产乱子伦精品免费| 精品久久国产一区二区三区香蕉| 热久久视久久精品18| 久久精品国产亚洲av影院| 97久久精品人人澡人人爽 | 久久综合九色综合网站| 久久香蕉国产线看观看精品yw| 久久精品一区二区国产| 久久综合偷偷噜噜噜色| 69久久夜色精品国产69| 一级做a爰片久久毛片看看| 久久精品一区二区三区不卡| 久久国产免费直播| 国产日韩久久免费影院| 久久久久亚洲av无码专区导航| 久久亚洲国产精品123区| 久久99国产综合精品| 久久人妻AV中文字幕| 热久久国产欧美一区二区精品| 狠狠狠色丁香婷婷综合久久五月| 久久国产欧美日韩精品免费| 一级做a爰片久久毛片16| 国产精品对白刺激久久久| 久久人妻AV中文字幕| 亚洲人成无码网站久久99热国产| 韩国三级中文字幕hd久久精品 | 久久ZYZ资源站无码中文动漫| 亚洲伊人久久综合中文成人网| 国产精品综合久久第一页| www久久久天天com| 国产成人久久精品区一区二区| 97久久久久人妻精品专区| 大伊人青草狠狠久久| 久久精品嫩草影院| 88久久精品无码一区二区毛片| 色综合合久久天天综合绕视看|