CLR 調(diào)試接口的架構(gòu)與應(yīng)用 [3] 調(diào)試事件
發(fā)表時間:2024-06-14 來源:明輝站整理相關(guān)軟件相關(guān)文章人氣:
[摘要]在上一節(jié)中簡單介紹了 CLR 調(diào)試器的框架結(jié)構(gòu),其中提到 CLR 調(diào)試環(huán)境同時支持 Native 和 Managed 兩種模式的調(diào)試事件。這一節(jié)將從整體上對調(diào)試事件做一個概括性的介紹。 首先看看 CLR 通過 ICorDebugManagedCallback 回調(diào)接口提供的 Manage...
在上一節(jié)中簡單介紹了 CLR 調(diào)試器的框架結(jié)構(gòu),其中提到 CLR 調(diào)試環(huán)境同時支持 Native 和 Managed 兩種模式的調(diào)試事件。這一節(jié)將從整體上對調(diào)試事件做一個概括性的介紹。
首先看看 CLR 通過 ICorDebugManagedCallback 回調(diào)接口提供的 Managed 調(diào)試事件。這部分的調(diào)試事件可以大致分為被動調(diào)試事件和主動調(diào)試事件:前者由 CLR 在調(diào)試程序時自動引發(fā)被動調(diào)試事件,如創(chuàng)建一個新的線程;后者由調(diào)試器通過 CLR 的其他調(diào)試接口,控制 CLR 調(diào)試環(huán)境完成某種調(diào)試任務(wù),并在適當?shù)臅r候引發(fā)主動調(diào)試事件,如斷點和表達式計算。
就被動調(diào)試事件來說,基本上對應(yīng)于 CLR 載入運行程序的若干個步驟
首先是動態(tài)環(huán)境的建立,分為進程、AppDomain和線程三級,并分別有對應(yīng)的建立和退出調(diào)試事件:
以下為引用:
interface ICorDebugManagedCallback : IUnknown
{
//...
HRESULT CreateProcess([in] ICorDebugProcess *pProcess);
HRESULT ExitProcess([in] ICorDebugProcess *pProcess);
HRESULT CreateAppDomain([in] ICorDebugProcess *pProcess,
[in] ICorDebugAppDomain *pAppDomain);
HRESULT ExitAppDomain([in] ICorDebugProcess *pProcess,
[in] ICorDebugAppDomain *pAppDomain);
HRESULT CreateThread([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugThread *thread);
HRESULT ExitThread([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugThread *thread);
HRESULT NameChange([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugThread *pThread);
//...
};
在 CLR 的實現(xiàn)上,實際上是存在有物理上的 Native Thread 和邏輯上的 Managed Thread 兩個概念的。進程和 Native Thread 對應(yīng)著操作系統(tǒng)提供的相關(guān)概念,而 AppDomain 和 Managed Thread 則對應(yīng)著 CLR 內(nèi)部的相關(guān)抽象。上面的線程相關(guān)調(diào)試事件,實際上是 Native Thread 第一次以 Managed Thread 身份執(zhí)行 Managed Code 的時候被引發(fā)的。更完整的控制需要借助后面要提及的 Native Thread 的調(diào)試事件。
此外 AppDomain 和 Managed Thread 在創(chuàng)建并開始運行后,都會根據(jù)情況改名,并調(diào)用 NameChange 調(diào)試事件,讓調(diào)試器有機會更新界面顯示上的相關(guān)信息。
其次是靜態(tài) Metadata 的載入和解析工作,也分為Assembly, Module和Class三級,并分別有對應(yīng)的建立和退出調(diào)試事件:
以下為引用:
interface ICorDebugManagedCallback : IUnknown
{
//...
HRESULT LoadAssembly([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugAssembly *pAssembly);
HRESULT UnloadAssembly([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugAssembly *pAssembly);
HRESULT LoadModule([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugModule *pModule);
HRESULT UnloadModule([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugModule *pModule);
HRESULT LoadClass([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugClass *c);
HRESULT UnloadClass([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugClass *c);
//...
};
在 CLR 中,Assembly 很大程度上是一個邏輯上的聚合體,真正落實到實現(xiàn)上的更多的是其 Module。一個 Assembly 在載入時,可以只是保護相關(guān) Manifest 和 Metadata,真正的代碼和數(shù)據(jù)完全可以存放在不同地點的多個 Module 中。因此,在 Managed 調(diào)試事件中,明確分離了 Assembly 和 Module 的生命周期。
然后就是對 IL 代碼中特殊指令和功能的支持用調(diào)試事件:
以下為引用:
interface ICorDebugManagedCallback : IUnknown
{
//...
HRESULT Break([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugThread *thread);
HRESULT Exception([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugThread *pThread,
[in] BOOL unhandled);
HRESULT DebuggerError([in] ICorDebugProcess *pProcess,
[in] HRESULT errorHR,
[in] DWORD errorCode);
HRESULT LogMessage([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugThread *pThread,
[in] LONG lLevel,
[in] WCHAR *pLogSwitchName,
[in] WCHAR *pMessage);
HRESULT LogSwitch([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugThread *pThread,
[in] LONG lLevel,
[in] ULONG ulReason,
[in] WCHAR *pLogSwitchName,
[in] WCHAR *pParentName);
HRESULT ControlCTrap([in] ICorDebugProcess *pProcess);
HRESULT UpdateModuleSymbols([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugModule *pModule,
[in] IStream *pSymbolStream);
//...
};
Break 事件在執(zhí)行 IL 指令 Break 時被引發(fā),可被用于實現(xiàn)特殊的斷點等功能;
Exception 事件在代碼拋出異常時,以及異常未被處理時被引發(fā),類似于 Win32 Debug API 中的異常事件。后面介紹調(diào)試器中對異常的處理方法時再詳細介紹;
DebuggerError 事件則是在調(diào)試系統(tǒng)處理 Win32 調(diào)試事件發(fā)生錯誤時被引發(fā);
LogMessage 和 LogSwitch 事件分別用于處理內(nèi)部類 System.Diagnostics.Log 的相關(guān)功能,類似于 Win32 API 下 OutputDebugString 函數(shù)的功能,等有機會再單獨寫篇文章介紹相關(guān)內(nèi)容;
ControlCTrap 事件響應(yīng)用戶使用 Ctrl+C 熱鍵直接中斷程序,等同于 Win32 API 下 SetConsoleCtrlHandler 函數(shù)的功能;
UpdateModuleSymbols 事件在系統(tǒng)更新某個模塊調(diào)試符號庫的時候被引發(fā),使調(diào)試器有機會同步狀態(tài)。
最后還省下幾個主動調(diào)試事件,在調(diào)試器調(diào)用 CLR 調(diào)試接口相關(guān)功能被完成或異常時引發(fā):
以下為引用:
interface ICorDebugManagedCallback : IUnknown
{
//...
HRESULT Breakpoint([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugThread *pThread,
[in] ICorDebugBreakpoint *pBreakpoint);
HRESULT BreakpointSetError([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugThread *pThread,
[in] ICorDebugBreakpoint *pBreakpoint,
[in] DWORD dwError);
HRESULT StepComplete([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugThread *pThread,
[in] ICorDebugStepper *pStepper,
[in] CorDebugStepReason reason);
HRESULT EvalComplete([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugThread *pThread,
[in] ICorDebugEval *pEval);
HRESULT EvalException([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugThread *pThread,
[in] ICorDebugEval *pEval);
HRESULT EditAndContinueRemap([in] ICorDebugAppDomain *pAppDomain,
[in] ICorDebugThread *pThread,
[in] ICorDebugFunction *pFunction,
[in] BOOL fAccurate);
//...
};
Breakpoint 和 BreakpointSetError 在斷點被觸發(fā)或設(shè)置斷點失敗時被調(diào)用,下一節(jié)介紹斷點的實現(xiàn)時再詳細討論;
StepComplete 則在調(diào)試環(huán)境因為某種原因完成了一次代碼步進(step)時被調(diào)用,以后介紹單步跟蹤等功能實現(xiàn)時再詳細討論;
EvalComplete 和 EvalException 在表達式求值完成或失敗時被調(diào)用,以后介紹調(diào)試環(huán)境當前信息獲取時再詳細討論;
EditAndContinueRemap 則用于實現(xiàn)調(diào)試時代碼編輯功能,暫不涉及。
下面是一個比較直觀的實例,顯示一個簡單的 CLR 調(diào)試環(huán)境在運行一個普通 CLR 程序除非相關(guān)調(diào)試事件的順序
以下為引用:
ManagedEventHandler.CreateProcess(3636)
ManagedEventHandler.CreateAppDomain(DefaultDomain @ 3636)
ManagedEventHandler.LoadAssembly(e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll @ DefaultDomain)
ManagedEventHandler.LoadModule(e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll @ DefaultDomain)
ManagedEventHandler.NameChange(AppDomain=cordbg)
ManagedEventHandler.CreateThread(3944 @ cordbg)
ManagedEventHandler.LoadAssembly(F:StudyDotNetDebuggercordbginDebugcordbg.exe @ cordbg)
ManagedEventHandler.LoadModule(F:StudyDotNetDebuggercordbginDebugcordbg.exe @ cordbg)
ManagedEventHandler.NameChange(AppDomain=cordbg.exe)
ManagedEventHandler.LoadAssembly(e:windowsassemblygacsystem.0.5000.0__b77a5c561934e089system.dll @ cordbg.exe)
ManagedEventHandler.LoadModule(e:windowsassemblygacsystem.0.5000.0__b77a5c561934e089system.dll @ cordbg.exe)
ManagedEventHandler.CreateThread(2964 @ cordbg.exe)
ManagedEventHandler.UnloadModule(F:StudyDotNetDebuggercordbginDebugcordbg.exe @ cordbg.exe)
ManagedEventHandler.UnloadAssembly(F:StudyDotNetDebuggercordbginDebugcordbg.exe @ cordbg.exe)
ManagedEventHandler.UnloadModule(e:windowsassemblygacsystem.0.5000.0__b77a5c561934e089system.dll @ cordbg.exe)
ManagedEventHandler.UnloadAssembly(e:windowsassemblygacsystem.0.5000.0__b77a5c561934e089system.dll @ cordbg.exe)
ManagedEventHandler.UnloadModule(e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll @ cordbg.exe)
ManagedEventHandler.UnloadAssembly(e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll @ cordbg.exe)
ManagedEventHandler.ExitAppDomain(cordbg.exe @ 3636)
ManagedEventHandler.ExitThread(3944 @ cordbg.exe)
ManagedEventHandler.ExitProcess(3636)
可以看到 CLR 首先構(gòu)造進程和 AppDomain;然后將系統(tǒng)執(zhí)行所需的 mscorlib.dll 載入;接著將要執(zhí)行的 Assembly 和缺省 Module 載入;并分析其外部應(yīng)用(system.dll),載入之;建立一個新的 Managed Thread 執(zhí)行之;最后卸載相關(guān) Module 和 Assembly,并退出環(huán)境。
在打印調(diào)試事件信息時值得注意的是很多調(diào)試接口都提供了類似的函數(shù)從 Unmanaged 環(huán)境中獲取字符串或整數(shù),如
以下為引用:
interface ICorDebugAppDomain : ICorDebugController
{
HRESULT GetName([in] ULONG32 cchName,
[out] ULONG32 *pcchName,
[out, size_is(cchName),
length_is(*pcchName)] WCHAR szName[]);
};
interface ICorDebugAssembly : IUnknown
{
HRESULT GetName([in] ULONG32 cchName,
[out] ULONG32 *pcchName,
[out, size_is(cchName),
length_is(*pcchName)] WCHAR szName[]);
};
因此在實現(xiàn)上可以將之抽象為一個 delegate,以便共享基于嘗試策略的數(shù)據(jù)獲取算法,如
以下為引用:
public class CorObject
{
protected delegate void GetStrFunc(uint cchName, out uint pcchName, IntPtr szName);
protected string GetString(GetStrFunc func, uint bufSize)
{
uint size = bufSize;
IntPtr szName = Marshal.AllocHGlobal((int)size);
func(size, out size, szName);
if(size > bufSize)
{
szName = Marshal.ReAllocHGlobal(szName, new IntPtr(size));
func(size, out size, szName);
}
string name = Marshal.PtrToStringUni(szName, (int)size-1);
Marshal.FreeHGlobal(szName);
return name;
}
protected string GetString(GetStrFunc func)
{
return GetString(func, 256);
}
}
這里使用 Marshal 對 Native 內(nèi)存的直接操作,避免編寫 unsafe 代碼。使用的時候可以很簡單地使用
以下為引用:
public class CorAssembly : CorObject
{
private ICorDebugAssembly _asm;
public CorAssembly(ICorDebugAssembly asm)
{
_asm = asm;
}
public string Name
{
get
{
return GetString(new GetStrFunc(_asm.GetName));
}
}
}
等到 CLR 2.0 支持泛型編程后,實現(xiàn)將更加方便。 :P
這一小節(jié),從整體上大致分析了 Managed 調(diào)試事件的分類和相關(guān)功能。具體的使用將在以后的文章中結(jié)合實際情況有針對性的介紹。至于 Win32 API 調(diào)試事件,介紹的資料就比較多了,這里就不在羅嗦,有興趣進一步研究的朋友可以參考我以前的一個系列文章。
Win32 調(diào)試接口設(shè)計與實現(xiàn)淺析 [2] 調(diào)試事件
下一節(jié)將介紹 CLR 調(diào)試接口中斷點如何實現(xiàn)和使用。
to be continue...