|
本文假設(shè)你熟悉 Win32,DLL
定制調(diào)試診斷工具和實(shí)用程序――擺脫DLL"地獄"(DLL Hell)的困擾(一)
定制調(diào)試診斷工具和實(shí)用程序――擺脫DLL"地獄"(DLL Hell)的困擾(二)
定制調(diào)試診斷工具和實(shí)用程序――擺脫DLL"地獄"(DLL Hell)的困擾(三)
定制調(diào)試診斷工具和實(shí)用程序――擺脫DLL"地獄"(DLL Hell)的困擾(四)
定制調(diào)試診斷工具和實(shí)用程序――擺脫DLL"地獄"(DLL Hell)的困擾(五)
摘要
本文前面討論了用幾種不同的方法來(lái)獲取進(jìn)程及其相關(guān) DLLs 的信息,例如通過(guò) PSAPI、NTDLL 以及TOOLHELP32 庫(kù)提供的 APIs,在這一部分,作者給出了幾種獲得系統(tǒng)級(jí)信息的非常規(guī)方法,你可以輕松將它們集成到自己的工具包中。本文范例包含三個(gè)實(shí)用工具:
LoadLibrarySpy,監(jiān)視并掃描應(yīng)用程序加載了哪些 DLLs;
WindowDump,獲取任何窗口的的內(nèi)容以及窗口的詳細(xì)描述信息;
FileUsage,重定向控制臺(tái)程序,揭示哪個(gè)進(jìn)程正在使用打開(kāi)的文件;
本文前面的部分討論了如何用有著良好文檔描述的 API 函數(shù)來(lái)獲取運(yùn)行進(jìn)程列表以及它們加載的 DLLs 信息。接下來(lái)我將用不同的方法,或者說(shuō)是非正式的方法來(lái)獲取系統(tǒng)級(jí)信息,首先,我將深入分析 Win32 調(diào)試 API 以及 Windows 加載器(Windows Loader)提供的痕跡來(lái)揭示給定進(jìn)程是如何加載 DLL 的。我將借助我的 CApplicationDebugger 可重用類,用幾種不同的方法來(lái)分析 DLL 重定位的原因。
接著,我將生成兩個(gè)工具。LoadLibrarySpy 掃描 DLL 重定位。WindowDump 竊取任何窗口的內(nèi)容和詳細(xì)描述信息。最后,在討論進(jìn)程環(huán)境塊(PEB)內(nèi)部結(jié)構(gòu)之前,我會(huì)向你展示如何操縱控制臺(tái)程序產(chǎn)生的輸出以便摸索尋找一些未公開(kāi)的信息。
回到 DLL Hell
前面我們已經(jīng)看到獲取所有靜態(tài)或動(dòng)態(tài)加載的 DLLs 列表是很容易的事情。但是對(duì)動(dòng)態(tài)加載的DLL而言,情況比想象的稍微復(fù)雜一些。例如,DllSpy 和 ProcessSpy 兩個(gè)工具依據(jù)某個(gè)時(shí)間點(diǎn)獲得的快照。因此,有可能出現(xiàn)來(lái)不及掃描某個(gè)被快速加載和卸載的DLL。Win32 調(diào)試 API 提供了對(duì)這個(gè)問(wèn)題的解決辦法:在調(diào)試程序時(shí), 這些 API 可以對(duì)被調(diào)試程序加載和卸載的任何DLL了如指掌。
要實(shí)現(xiàn)我的意圖,并不需要一個(gè)功能完整,名副其實(shí)的調(diào)試器,但我必須偵測(cè)到新 DLL 何時(shí)被加載到進(jìn)程地址空間。因此,我將討論 Win32 調(diào)試 API 的基本知識(shí)以及它們?cè)?Windows NT、Windows 2000 和 Windows XP 操作系統(tǒng)中有用的擴(kuò)展。
為了調(diào)試一個(gè)程序,你首先必須使用用下面這些特殊的標(biāo)志之一調(diào)用 CreateProcess 來(lái)啟動(dòng)擬調(diào)試的程序。DEBUG_PROCESS 表示請(qǐng)求來(lái)自被調(diào)試程序以及被調(diào)試程序啟動(dòng)的每一個(gè)進(jìn)程的事件。DEBUG_ONLY_THIS_PROCESS 表示只請(qǐng)求來(lái)自被調(diào)試程序的事件(而不是來(lái)自其子進(jìn)程的事件)。
使用 DEBUG_ONLY_THIS_PROCESS 標(biāo)志時(shí),調(diào)試器將接收不到來(lái)自被調(diào)試程序啟動(dòng)的進(jìn)程事件。性能監(jiān)視器(perfmon.exe)就是一個(gè)很好的例子,此標(biāo)志對(duì)這個(gè)程序沒(méi)有作用。性能監(jiān)視器是一個(gè)簡(jiǎn)單的打包程序,其作用 只不過(guò)是啟動(dòng)另外一個(gè)程序――微軟管理控制臺(tái)(MMC),并傳遞任何所需的參數(shù)使它顯示性能計(jì)數(shù)器。
在被調(diào)試程序的生命期內(nèi),Windows 通知調(diào)試器 Figure 1 所列出的事件。這些事件由 DEBUG_EVENT 結(jié)構(gòu)描述,如 Figure 2 所示。
Figure 1 Events Received by the Debugger
Event Value
|
Description
|
CREATE_PROCESS_DEBUG_EVENT
|
This is the first event received by the debugger, even before LOAD_DLL_ DEBUG_EVENT for statically linked DLLs.
|
EXIT_PROCESS_DEBUG_EVENT
|
This is the last event received by the debugger. It means the debuggee has reached the end of its life.
|
EXCEPTION_DEBUG_EVENT
|
An exception occurs. Its description is in u.Exception. It is received before any catch when the dw- FirstChance flag is set. If there is no catch, a second event is received before the debuggee is terminated.
|
CREATE_THREAD_DEBUG_EVENT
|
A new thread is created. Its description is in u.CreateThread.
|
EXIT_THREAD_DEBUG_EVENT
|
The description of an exiting thread is set in the u.ExitThread member.
|
LOAD_DLL_DEBUG_EVENT
|
When a DLL is mapped in the debuggee address space, either statically linked or dynamically loaded, this event is received by the debugger.
|
UNLOAD_DLL_DEBUG_EVENT
|
Unlike the previous event, this occurs only when a DLL is dynamically unloaded. This means it cannot be used to detect when each statically loaded DLL is unloaded at the end of the process life.
|
OUTPUT_DEBUG_STRING_EVENT
|
Each time the debuggee calls OutputDebugString, the debugger receives this event with the string in u.DebugString.lpDebug.StringData, but in the debuggee address space.
|
RIP_EVENT
|
According to the documentation, this event is received when a RIP-de-bugging event (system debugging error) occurs, but I have never seen this in practice.
|
|
01.Figure 2 DEBUG_EVENT
02.
03.typedef struct _DEBUG_EVENT {
04.DWORD dwDebugEventCode;
05.DWORD dwProcessId;
06.DWORD dwThreadId;
07.union
08.{
09.EXCEPTION_DEBUG_INFO Exception;
10.CREATE_THREAD_DEBUG_INFO CreateThread;
11.CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
12.EXIT_THREAD_DEBUG_INFO ExitThread;
13.EXIT_PROCESS_DEBUG_INFO ExitProcess;
14.LOAD_DLL_DEBUG_INFO LoadDll;
15.UNLOAD_DLL_DEBUG_INFO UnloadDll;
16.OUTPUT_DEBUG_STRING_INFO DebugString;
17.RIP_INFO RipInfo;
18.} u;
19.} DEBUG_EVENT, *LPDEBUG_EVENT;
為了接收這些事件,調(diào)試器必須調(diào)用 WaitForDebugEvent。該函數(shù)阻塞調(diào)試器的運(yùn)行,直到被調(diào)試程序發(fā)生 Figure 1 所列的事件之一,或者超時(shí)參數(shù)中給定的秒數(shù)為止。當(dāng)調(diào)試器處理某個(gè)事件時(shí),它調(diào)用 ContinueDebugEvent 讓被調(diào)試程序繼續(xù)其生命之旅。注意:在調(diào)試器中,當(dāng) WaitForDebugEvent 解除阻塞時(shí),所有被調(diào)試者線程被凍結(jié),在調(diào)用 ContinueDebugEvent 期間被解凍。參見(jiàn) Figure 3:
Figure 3 調(diào)試事件流
CApplicationDebugger
調(diào)用 CreateProcess 的線程必須是進(jìn)入調(diào)試循環(huán)的線程。既然調(diào)試器阻塞于 WaitForDebugEvent,因此最好讓這部分代碼運(yùn)行在一個(gè)與主UI線程不同的專門線程中。本文將其行為包裝在 CApplicationDebugger 類中,其聲明參見(jiàn)本文附帶源代碼中的 ApplicationDebugger.h 文件,這個(gè)類的一部分靈感還來(lái)自 Matt Pietrek 的 LoadProf32(參見(jiàn) MSJJul95.exe)。
CApplicationDebugger 是一個(gè)虛擬類,因?yàn)槟愕脧乃缮?shí)現(xiàn)自己的重寫版本,以便特定的調(diào)試事件發(fā)生時(shí)進(jìn)行相應(yīng)的調(diào)用。這個(gè)類被用于生成 LoadLibrarySpy(參見(jiàn) Figure 4),這是一個(gè)調(diào)試程序和監(jiān)控 DLL 加載和卸載的工具,不論是靜態(tài)加載還是動(dòng)態(tài)加載,也不論是不是有加載地址沖突,它都能監(jiān)控。
Figure 4 LoadLibrarySpy
調(diào)用 CreateProcess 是在 CApplicationDebugger::LoadTheProcess 中進(jìn)行的,為簡(jiǎn)單起見(jiàn),參數(shù)使用 DEBUG_ONLY_THIS_PROCESS。如果需要,你可以將 CApplicationDebugger 擴(kuò)展成能處理來(lái)自多個(gè)被調(diào)試進(jìn)程的事件,對(duì)于 MMC 管理單元(snap-ins)很有用。
CLoadLibrarySpyDlg 類負(fù)責(zé)對(duì)話框自身的處理,同時(shí)也是暗中監(jiān)視 CApplicationDebugger 派生類的線程宿主。CModuleListCtrl 類負(fù)責(zé)顯示附屬到每個(gè)DLL的詳細(xì)信息 CModuleInfo*;針對(duì)每個(gè) DLL,這個(gè)類存儲(chǔ)的詳細(xì)信息見(jiàn) Figure 5。
Figure 5 DLL Details
Type
|
Member
|
Description
|
CString
|
m_szName
|
Module name
|
DWORD
|
m_LoadAddress
|
hModule
|
DWORD
|
m_PreferedLoadAddress
|
Supposed loading address (at link time)
|
CString
|
m_szReason
|
Gets real info
|
BOOL
|
m_bDynamic
|
TRUE if loaded through LoadLibrary
|
BOOL
|
m_bAfterStartup
|
TRUE if loaded after the process starts
|
DWORD
|
m_nLoaded
|
Number of times it has been loaded
|
DWORD
|
m_nRemoved
|
Number of times it has been unloaded
|
CString
|
m_szFullPath
|
Full path name of the DLL
|
DWORD
|
m_Position
|
Loading position, starting from 1
|
當(dāng)某個(gè) DLL 被加載,對(duì)話框便調(diào)用 AddModule 方法;反之卸載DLL時(shí),則執(zhí)行 RemoveModule 方法。這兩個(gè)方法都以 UpdateModule 方法告終,從而更新與該 DLL 對(duì)應(yīng)的 CModuleObject 對(duì)象的 m_nLoaded 或 m_nRemoved。如果不存在這樣的對(duì)象,則會(huì)創(chuàng)建一個(gè)新的對(duì)象,并將它添加到列表框中。
不要為 m_nLoaded 或 m_nRemoved 而困惑。如果你針對(duì)某一行的相同 DLL 多次調(diào)用 LoadLibrary,調(diào)試器只會(huì)收到 LOAD_DLL_DEBUG_EVENT 一次,并且 m_nLoaded 被賦值為 1。如果調(diào)試器接收到某個(gè) DLL 的 UNLOAD_DLL_DEBUG_EVENT,你便可以確定該 DLL 不再被該進(jìn)程使用。因此,對(duì)于靜態(tài) DLLs 而言,你決不會(huì)收到此事件,即使可能在進(jìn)程被啟動(dòng)后,它們被動(dòng)態(tài)加載并用 LoadLibrary/FreeLibrary 卸載。
處理被調(diào)試程序的事件
一旦被調(diào)試程序的進(jìn)程啟動(dòng)后,調(diào)試器便等待某些事件的發(fā)生。這就是為什么它應(yīng)該在一個(gè)與主 UI 線程不同的單獨(dú)線程中的原因,當(dāng)主窗口是一個(gè)模式對(duì)話框時(shí)尤其如此!
為了在 CLoadLibrarySpyDlg 中有效地使用 CApplicationDebugger,GoThreadProc 線程過(guò)程首先聲明一個(gè) CApplicationDebugger 對(duì)象,指定要執(zhí)行的命令行并說(shuō)明是否截獲來(lái)自被調(diào)試程序的 OutputDebugString 或 TRACE 輸出。接著,DebugProcess 阻塞,直到被調(diào)試程序終止(接收 EXIT_PROCESS_DEBUG_EVENT 或第二次的未處理異常),或者重寫的方法之一未返回 DBG_CONTINUE。
線程與對(duì)話框之間的溝通機(jī)制很簡(jiǎn)單:當(dāng)某個(gè)被調(diào)試事件發(fā)生時(shí),調(diào)試器線程將 Figure 6 中所列的消息發(fā)送到對(duì)話框。其中第一個(gè)消息是在加載了所有靜態(tài)鏈接的 DLLs 時(shí)發(fā)送;也就是說(shuō),當(dāng) Windows 觸發(fā)第一個(gè)(偽)斷點(diǎn)時(shí),便發(fā)信號(hào)給調(diào)試器,然后調(diào)試器調(diào)用可重寫的 OnProcessRunning 將消息發(fā)送給對(duì)話框。第二個(gè)消息是當(dāng)被調(diào)試程序卸載某個(gè) DLL 時(shí),由可重寫的 OnUnloadDLLDebugEvent 調(diào)試事件處理例程發(fā)送
Figure 6 Debugger Thread Messages
ID
|
wParam
|
lParam
|
Description
|
UM_INITPROCESS
|
0
|
0
|
The statically linked DLLs have all been loaded. The icon changes from static to dynamic after this event.
|
UM_FREELIBRARY
|
0
|
CModuleInfo*
|
The DLL has been unloaded. The corresponding line is updated in CModuleListCtrl.
|
UM_LOADLIBRARY
|
0
|
CModuleInfo*
|
The DLL has been loaded. The corresponding line is added or updated in CModuleListCtrl.
|
第三個(gè)消息需要所解釋幾句,為了創(chuàng)建 CModuleInfo,需要 DLL 的全路徑名。而在本文第一部分中,我們沒(méi)有提供任何方法直接從其 hModule 或加載地址獲取 DLL 文件名。即便是當(dāng)調(diào)試器接收到此事件時(shí)(因?yàn)樗赡転g覽到了它的 PE 頭),DLL已經(jīng)被映射到被調(diào)試程序的地址空間,這時(shí),Windows 還沒(méi)有初始化 PSAPI 所需的數(shù)據(jù)結(jié)構(gòu)。
事實(shí)上,LoadDll.lpImageName 域是一個(gè) LOAD_DLL_DEBUG_INFO 結(jié)構(gòu)成員,LOAD_DLL_DEBUG_INFO 來(lái)自 DEBUG_EVENT 結(jié)構(gòu)中的聯(lián)合 u(參見(jiàn) Figure 2),LoadDll.lpImageName 總是指向被調(diào)試程序地址空間中一塊具備讀/寫/執(zhí)行權(quán)限的奇怪的內(nèi)存區(qū)域,LOAD_DLL_DEBUG_INFO 結(jié)構(gòu)定義如下:
1.typedef struct _LOAD_DLL_DEBUG_INFO {
2.HANDLE hFile;
3.LPVOID lpBaseOfDll;
4.DWORD dwDebugInfoFileOffset;
5.DWORD nDebugInfoSize;
6.LPVOID lpImageName;
7.WORD fUnicode;
8.} LOAD_DLL_DEBUG_INFO, *LPLOAD_DLL_DEBUG_INFO;
被加載的DLL的路徑名就包含在此內(nèi)存塊中。MSDN 在線幫助文檔是這樣描述 IpImageName 的:
“...與 hFile 關(guān)聯(lián)的文件名指針。該成員可能為 NULL,也可能包含被調(diào)試進(jìn)程地址空間中的串指針地址。這個(gè)地址可能為 NULL 或者指向?qū)嶋H的文件名。
如果 fUnicode 是一個(gè)非零值,則名字串是 Unicode,否則是 ANSI 串。該成員是可選項(xiàng)。調(diào)試器必須考慮處理 lpImageName 為 NULL 或 *lpImageName(在被調(diào)試進(jìn)程的地址空間中)為 NULL 的情況。很顯然,系統(tǒng)決不會(huì)為某個(gè)創(chuàng)建進(jìn)程事件提供映像名,同時(shí)它也不可能為第一個(gè) DLL 事件傳遞映像名。系統(tǒng)也決不會(huì)在源于 DebugActiveProcess 函數(shù)調(diào)用的調(diào)試事件中提供這個(gè)信息?!?
OnLoadDLLDebugEvent 可重寫方法將上述解釋翻譯為在 99% 的情況下可工作的純 C++ 代碼。其余 1% 不工作的情況是指加載 ntdll.dll:這種情況既是文檔中所說(shuō)的第一個(gè) DLL 事件。即使延遲到下一個(gè)被調(diào)試程序事件發(fā)生時(shí)(參見(jiàn) CLoadLibraryDebugger 的 OnDebugEvent)才獲取路徑名。在文檔的描述中,可以調(diào)用 SearchPath 從模塊名獲得全路徑名,“system32”對(duì)于 ntdll.dll 并不感到驚訝。這個(gè) API 函數(shù)使用與 LoadLibrary 同樣的算法在文件系統(tǒng)中查找某個(gè) DLL。從理論上講,因?yàn)樗怯烧{(diào)試器調(diào)用的,有可能返回的文件并不是被調(diào)試程序加載的那個(gè)文件――例如,在調(diào)試器文件夾中存在另外一個(gè)版本的 ntdll.dll。在實(shí)際應(yīng)用中,ntdll.dll 得不到打補(bǔ)丁的機(jī)會(huì),并且被拷貝到了某個(gè)與 system32 不同的目錄。
防止泄漏
文檔中關(guān)于 Win32 調(diào)試 API 的另一方面的描述是必須釋放不同的 XXX_DEBUG_EVENT 結(jié)構(gòu)返回的句柄。Matt Pietrek 在其 November 1995 MSJ“Under the Hood”專欄文章中指出:在 XXX_DEBUG_EVENT 結(jié)構(gòu)中返回到調(diào)試器的句柄應(yīng)該被關(guān)閉。事實(shí)上,幾乎每個(gè)句柄都必須用 CloseHandle 關(guān)閉。只有一個(gè)例外,就是存儲(chǔ)在 CREATE_THREAD_DEBUG_EVENT 中的線程句柄,它應(yīng)該在進(jìn)程終止時(shí)由系統(tǒng)來(lái)關(guān)閉。其它的句柄如果不關(guān)閉,便會(huì)造成增長(zhǎng)速度非??斓南到y(tǒng)資源泄漏,有關(guān)的句柄如 Figure 7 所示。這類垃圾的收集由 CApplicationDebugger::HandleDebugEvent 自動(dòng)處理。
Figure 7 XXX_DEBUG_EVENT Handles
Event
|
Handles to Close
|
LOAD_DLL_DEBUG_EVENT
|
u.LoadDll.hFile
|
CREATE_PROCESS_DEBUG_EVENT
|
u.CreateProcessInfo.hFile
u.CreateProcessInfo.hProcess
u.CreateProcessInfo.hThread
|
不論你使用哪種清除方法,每次你調(diào)試某個(gè)進(jìn)程時(shí),系統(tǒng)不可避免地要泄漏兩個(gè)句柄:信號(hào)機(jī)(semaphore )和端口(port),兩者都沒(méi)有命名。為了讓你確信 CApplicationDebugger 不負(fù)責(zé)處理這種泄漏,請(qǐng)?jiān)试S我指出:用 sysinternals 的 ProcessExplorer 或 Windows Resource Kit 中的 DH.EXE 可以觀察到 Visual Studio 6.0 和 Visual Studio .NET 中同樣的泄漏行為。
現(xiàn)在你已經(jīng)看到了如何用 Win32 調(diào)試 API 來(lái)獲取某個(gè)進(jìn)程執(zhí)行期間在其地址空間中加載和卸載的 DLLs 確切列表。Windows 本身提供了另外一個(gè)途徑來(lái)獲取有關(guān) DLLs 的其它詳細(xì)信息。
參考資料
The Win32 Debugging Application Programming Interface;
Bugslayer: Windows 2000 and LDR Messages, A COM Symbol Engine, Finding Bloated Functions, and More DEB Sample: Debug Event Browser;
Spawn Console Processes with Redirected Standard Handles;
GetWindowModuleFileName & GetModuleFileName Work Only with the Calling Process
在后續(xù)文章中,我將介紹 Windows Loader,它知道一切。
(待續(xù))
|