WinDbg+Rotor解析WinForm调用堆栈及实现
前段寫過一篇文章“CLR探索系列:深入追蹤托管exe加載執行過程”,在那篇文章中,主要是側重靜態代碼的分析,追蹤源代碼的流程一步一步看是如何實現的。
這次,寫一篇文章,結合Windbg,從一個托管應用程序執行的調用堆棧開始,追蹤其調用堆棧中的線索,以及這些托管應用程序執行中調用的功能實現,來展示托管代碼的加載和執行的流程和實現。
首先還是找一個小白鼠:
<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
?
?????public partial class Form1 : Form
??? {
??????? public Form1()
??????? {
??????????? InitializeComponent();
??????? }
??????? private void button1_Click(object sender, EventArgs e)
??????? {
??????????? System.Object o = new object();
??????????? lock (o)
??????????? {
??????????????? System.GC.Collect();
??????????? }
???????? }
}
?
這里找的是一個WinForm應用程序來作為小白鼠。
咱就不采用在調用的關鍵的mscorwks和MscoeEE的方法上面下斷點來跟蹤,然后解釋每個斷點調用堆棧和環境的解釋方法了,直接把main thread的call stack給打出來一行一行的去挖掘好了。
?
打開Windbg,附加到進程,加載好相關的symbol和2.0的SOS,切換到第0個Thread然后輸出其調用堆棧:
?
0:000> k
ChildEBP RetAddr?
0012f4a0 7c92e9ab ntdll!KiFastSystemCallRet
<?xml:namespace prefix = st1 ns = "urn:schemas-microsoft-com:office:smarttags" />0012f3c4 7b08432d USER32!NtUserWaitMessage+0xc
0012f434 7b08416b System_Windows_Forms_ni+0xb432d
0012f464 7b0c69fe System_Windows_Forms_ni+0xb416b
0012f490 79e88ee4 System_Windows_Forms_ni+0xf69fe
0012f510 79e88e31 mscorwks!CallDescrWorkerWithHandler+0xa3
0012f650 79e88d19 mscorwks!MethodDesc::CallDescr+0x19c
0012f668 79e88cf6 mscorwks!MethodDesc::CallTargetWorker+0x20
0012f67c 79f084b0 mscorwks!MethodDescCallSite::Call+0x18
0012f7e0 79f082a9 mscorwks!ClassLoader::RunMain+0x220
0012fa48 79f0817e mscorwks!Assembly::ExecuteMainMethod+0xa6
0012ff18 79f07dc7 mscorwks!SystemDomain::ExecuteMainMethod+0x398
0012ff68 79f05f61 mscorwks!ExecuteEXE+0x59
0012ffb0 79011b5fmscorwks!_CorExeMain+0x11b
0012ffc0 7c816fd7 mscoree!_CorExeMain+0x2c
0012fff0 00000000 KERNEL32!BaseProcessStart+0x23
?
BaseProcessStart表示的是運行的Winform啟動的進程。在前面的上一篇文章里面已經分析過,并且用相關的工具查看過,一個托管模塊開始運行的時候只想的是_CorExeMain方法。這是mscoree里面的一個方法。而mscoree只是選擇加載CLR版本的一個Loader。
之后,就跳轉到了選擇了特定版本了的mscorwks里面的_CorExeMain中:
0012ffb0 79011b5f mscorwks!_CorExeMain+0x11b
在sscli中,這個程序的名字就換成了_CorExeMain2來顯示對商業版本的區別。打開CorExeMain的定義:
?
__int32 STDMETHODCALLTYPE _CorExeMain2(? // Executable exit code.
??? PBYTE? ?pUnmappedPE,????????????? ???// -> memory mapped code
??? DWORD?? cUnmappedPE,?????????? ????// Size of memory mapped code
??? __in LPWSTR? pImageNameIn,????????? // -> Executable Name
??? __in LPWSTR? pLoadersFileName,? ????// -> Loaders Name
__in LPWSTR? pCmdLine)??????????? // -> Command Line
?
?????? 在Load這個應用程序的image的時候,這個entry point是從native entry point中被call的。在_CorExeMain2中,我們可以看到如下部分屬性和方法:
__int32 STDMETHODCALLTYPE _CorExeMain2( …)?????????
{
??? // This entry point is used by clix
??? BOOL bRetVal = 0;
?
??? // Before we initialize the EE, make sure we've snooped for all EE-specific
??? // command line arguments that might guide our startup.
??? HRESULT result = CorCommandLine::SetArgvW(pCmdLine);
?
??? if (!CacheCommandLine(pCmdLine, CorCommandLine::GetArgvW(NULL))) {
??????? LOG((LF_STARTUP, LL_INFO10, "Program exiting - CacheCommandLine failed\n"));
??????? bRetVal = -1;
??????? goto exit;
??? }
?
??? if (SUCCEEDED(result))
??????? result = CoInitializeEE(COINITEE_DEFAULT | COINITEE_MAIN);
??? if (FAILED(result)) {
??????? VMDumpCOMErrors(result);
??????? SetLatchedExitCode (-1);
??????? goto exit;
??? }
?
??? // Load the executable
??? bRetVal = ExecuteEXE(pImageNameIn);
?
if (!bRetVal) {
?????? //這里,如果出現錯誤的話,可能的原因不正確的metadata文件的格式,或者其版本,可能是load的mscorwks的版本不正確造成的。也可能是signed assemblies和對應的錯誤處理程序不匹配造成的。總之,運行正確的話,是不會走到這里的。這個地方也可以作為CLR在開發的時候調試下斷點的一個地方。
??????? EEMessageBoxCatastrophic(IDS_EE_COREXEMAIN2_FAILED_TEXT, IDS_EE_COREXEMAIN2_FAILED_TITLE);
??????? SetLatchedExitCode (-1);
??? }
?
//當程序走到這個地方的時候,it is the time to shut off the lights and went home了。這些都是程序退出的時候的執行的動作。
exit:
??? STRESS_LOG1(LF_STARTUP, LL_ALWAYS, "Program exiting: return code = %d", GetLatchedExitCode());
??? STRESS_LOG0(LF_STARTUP, LL_INFO10, "EEShutDown invoked from _CorExeMain2");
??? EEPolicy::HandleExitProcess();???
??? //END_ENTRYPOINT_VOIDRET;
??? return bRetVal;
}
?
OK,從上面的程序里面,我們大概看到了一個托管模塊的生命周期。所以,最關鍵的一句就在這里了:
bRetVal = ExecuteEXE(pImageNameIn);?
這一行,也就是對應這上面堆棧調用的倒數第四行了。
??????
接下來讓我們看看這個ExecuteEXE方法都做了些什么吧:
BOOL STDMETHODCALLTYPE ExecuteEXE(HMODULE hMod)
{
??? if (!hMod)
??????? return FALSE;
?
??? ETWTraceStartup::TraceEvent(ETW_TYPE_STARTUP_EXEC_EXE);
??? TIMELINE_START(STARTUP, ("ExecuteExe"));
?
??? EX_TRY_NOCATCH
??? {
??????? // Executables are part of the system domain
??????? SystemDomain::ExecuteMainMethod(hMod);
??? }
??? return TRUE;
}?
?????? 運行到這里,就可以看到,SystemDomain已經啟動,同時開始執行Main方法。繼續查看上面的調用堆棧:
?
0012f7e0 79f082a9 mscorwks!ClassLoader::RunMain+0x220
0012fa48 79f0817e mscorwks!Assembly::ExecuteMainMethod+0xa6
0012ff18 79f07dc7 mscorwks!SystemDomain::ExecuteMainMethod+0x398
??????
可以看到,繼SystemDomain之后,又將Assembly load到了Domain中,最后是用ClassLoader來執行Main程序。這里就不一一展示其實現了。
?????? 在執行了RunMain方法的時候,這時下面的三個堆棧:
?
0012f510 79e88e31 mscorwks!CallDescrWorkerWithHandler+0xa3
0012f650 79e88d19 mscorwks!MethodDesc::CallDescr+0x19c
0012f668 79e88cf6 mscorwks!MethodDesc::CallTargetWorker+0x20
0012f67c 79f084b0 mscorwks!MethodDescCallSite::Call+0x18
??????
?????? 在前面的上一篇文章中已經說過,MethodDesc,methodtable,EEClass,MethodDescChunk這些結構的關系和區別。MethodDesc是CLR中對應的托管方法的非托管的結構。
?????? 下面的三個方法,主要是實現了定位和尋找Call Target,負責托管代碼的編譯等工作。
CallDescrWorkerWithHandler是調用的一個外部C語言編寫的函數,Call這個方法的目的,是為了把MethodTable與操作系統平臺相關的Exception Handle程序聯系起來。
?
最后的堆棧最上面的幾行:
?
0012f3c4 7b08432d USER32!NtUserWaitMessage+0xc
0012f434 7b08416b System_Windows_Forms_ni+0xb432d
0012f464 7b0c69fe System_Windows_Forms_ni+0xb416b
0012f490 79e88ee4 System_Windows_Forms_ni+0xf69fe
?
這個地方首先調用了System.Windows.Forms.NI中來初始化Winform的顯示。這里的ni后綴表面調用的是nGen函數。最后停在USER32!NtUserWaitMessage上面,等待用戶的操作。
?
這里的分析,只是展示了一個WinForm在執行完畢之后的調用堆棧。里面有很多和Thread,JIT和GC相關的功能的初始化和額外線程的啟動,譬如,主線程創建FinalizerThread和GCThread,由于沒有下斷點跟蹤,所以這里都沒有展現出來。
此文的主要目的,在于提供一種閱讀和分析Rotor的方法,讓對Rotor(sscli)的分析,不僅僅限制與對靜態代碼的分析,我們還可以結合DotNet應用程序的運行,動態的分析代碼的執行和實現。
同時,研究不同類型應用程序調用堆棧,里面還有非常多有意思的東西可以發掘。
?
3/25/2008 10:38:33 AM
?
總結
以上是生活随笔為你收集整理的WinDbg+Rotor解析WinForm调用堆栈及实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: HR怎么从面试中了解程序员的真实水平?需
- 下一篇: 帧中继故障排除