WinDbg+Rotor解析WinForm调用堆栈及实现
前段写过一篇文章“CLR探索系列:深入追踪托管exe加载执行过程”,在那篇文章中,主要是侧重静态代码的分析,追踪源代码的流程一步一步看是如何实现的。
这次,写一篇文章,结合Windbg,从一个托管应用程序执行的调用堆栈开始,追踪其调用堆栈中的线索,以及这些托管应用程序执行中调用的功能实现,来展示托管代码的加载和执行的流程和实现。
首先还是找一个小白鼠:
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
0012fa48
0012ff18
0012ff68
0012ffb0 79011b
0012ffc0
0012fff0 00000000 KERNEL32!BaseProcessStart+0x23
BaseProcessStart表示的是运行的Winform启动的进程。在前面的上一篇文章里面已经分析过,并且用相关的工具查看过,一个托管模块开始运行的时候只想的是_CorExeMain方法。这是mscoree里面的一个方法。而mscoree只是选择加载CLR版本的一个Loader。
之后,就跳转到了选择了特定版本了的mscorwks里面的_CorExeMain中:
0012ffb0 79011b
在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方法。继续查看上面的调用堆栈:
0012fa48
0012ff18
可以看到,继SystemDomain之后,又将Assembly load到了Domain中,最后是用ClassLoader来执行Main程序。这里就不一一展示其实现了。
在执行了RunMain方法的时候,这时下面的三个堆栈:
在前面的上一篇文章中已经说过,MethodDesc,methodtable,EEClass,MethodDescChunk这些结构的关系和区别。MethodDesc是CLR中对应的托管方法的非托管的结构。
下面的三个方法,主要是实现了定位和寻找Call Target,负责托管代码的编译等工作。
CallDescrWorkerWithHandler是调用的一个外部C语言编写的函数,Call这个方法的目的,是为了把MethodTable与操作系统平台相关的Exception Handle程序联系起来。
最后的堆栈最上面的几行:
这个地方首先调用了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
posted on 2008-03-25 10:50 lbq1221119 阅读(3733) 评论(2) 编辑 收藏 举报