在上一節中簡單介紹了 clr 調試器的框架結構,其中提到 clr 調試環境同時支持 native 和 managed 兩種模式的調試事件。這一節將從整體上對調試事件做一個概括性的介紹。
首先看看 clr 通過 icordebugmanagedcallback 回調接口提供的 managed 調試事件。這部分的調試事件可以大致分為被動調試事件和主動調試事件:前者由 clr 在調試程序時自動引發被動調試事件,如創建一個新的線程;后者由調試器通過 clr 的其他調試接口,控制 clr 調試環境完成某種調試任務,并在適當的時候引發主動調試事件,如斷點和表達式計算。
就被動調試事件來說,基本上對應于 clr 載入運行程序的若干個步驟
首先是動態環境的建立,分為進程、appdomain和線程三級,并分別有對應的建立和退出調試事件:
以下為引用:
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 的實現上,實際上是存在有物理上的 native thread 和邏輯上的 managed thread 兩個概念的。進程和 native thread 對應著操作系統提供的相關概念,而 appdomain 和 managed thread 則對應著 clr 內部的相關抽象。上面的線程相關調試事件,實際上是 native thread 第一次以 managed thread 身份執行 managed code 的時候被引發的。更完整的控制需要借助后面要提及的 native thread 的調試事件。
此外 appdomain 和 managed thread 在創建并開始運行后,都會根據情況改名,并調用 namechange 調試事件,讓調試器有機會更新界面顯示上的相關信息。
其次是靜態 metadata 的載入和解析工作,也分為assembly, module和class三級,并分別有對應的建立和退出調試事件:
以下為引用:
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 很大程度上是一個邏輯上的聚合體,真正落實到實現上的更多的是其 module。一個 assembly 在載入時,可以只是保護相關 manifest 和 metadata,真正的代碼和數據完全可以存放在不同地點的多個 module 中。因此,在 managed 調試事件中,明確分離了 assembly 和 module 的生命周期。
然后就是對 il 代碼中特殊指令和功能的支持用調試事件:
以下為引用:
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 事件在執行 il 指令 break 時被引發,可被用于實現特殊的斷點等功能;
exception 事件在代碼拋出異常時,以及異常未被處理時被引發,類似于 win32 debug api 中的異常事件。后面介紹調試器中對異常的處理方法時再詳細介紹;
debuggererror 事件則是在調試系統處理 win32 調試事件發生錯誤時被引發;
logmessage 和 logswitch 事件分別用于處理內部類 system.diagnostics.log 的相關功能,類似于 win32 api 下 outputdebugstring 函數的功能,等有機會再單獨寫篇文章介紹相關內容;
controlctrap 事件響應用戶使用 ctrl+c 熱鍵直接中斷程序,等同于 win32 api 下 setconsolectrlhandler 函數的功能;
updatemodulesymbols 事件在系統更新某個模塊調試符號庫的時候被引發,使調試器有機會同步狀態。
最后還省下幾個主動調試事件,在調試器調用 clr 調試接口相關功能被完成或異常時引發:
以下為引用:
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 在斷點被觸發或設置斷點失敗時被調用,下一節介紹斷點的實現時再詳細討論;
stepcomplete 則在調試環境因為某種原因完成了一次代碼步進(step)時被調用,以后介紹單步跟蹤等功能實現時再詳細討論;
evalcomplete 和 evalexception 在表達式求值完成或失敗時被調用,以后介紹調試環境當前信息獲取時再詳細討論;
editandcontinueremap 則用于實現調試時代碼編輯功能,暫不涉及。
下面是一個比較直觀的實例,顯示一個簡單的 clr 調試環境在運行一個普通 clr 程序除非相關調試事件的順序
以下為引用:
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 首先構造進程和 appdomain;然后將系統執行所需的 mscorlib.dll 載入;接著將要執行的 assembly 和缺省 module 載入;并分析其外部應用(system.dll),載入之;建立一個新的 managed thread 執行之;最后卸載相關 module 和 assembly,并退出環境。
在打印調試事件信息時值得注意的是很多調試接口都提供了類似的函數從 unmanaged 環境中獲取字符串或整數,如
以下為引用:
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[]);
};
因此在實現上可以將之抽象為一個 delegate,以便共享基于嘗試策略的數據獲取算法,如
以下為引用:
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 內存的直接操作,避免編寫 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 支持泛型編程后,實現將更加方便。 :p
這一小節,從整體上大致分析了 managed 調試事件的分類和相關功能。具體的使用將在以后的文章中結合實際情況有針對性的介紹。至于 win32 api 調試事件,介紹的資料就比較多了,這里就不在羅嗦,有興趣進一步研究的朋友可以參考我以前的一個系列文章。
win32 調試接口設計與實現淺析 [2] 調試事件
下一節將介紹 clr 調試接口中斷點如何實現和使用。
to be continue...
新聞熱點
疑難解答