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应用程序来作为小白鼠。

咱就不采用在调用的关键的mscorwksMscoeEE的方法上面下断点来跟踪,然后解释每个断点调用堆栈和环境的解释方法了,直接把main threadcall stack给打出来一行一行的去挖掘好了。

 

打开Windbg,附加到进程,加载好相关的symbol2.0SOS,切换到第0Thread然后输出其调用堆栈:

 

0:000> k

ChildEBP RetAddr 

0012f4a0 7c92e9ab ntdll!KiFastSystemCallRet

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 79011b5f mscorwks!_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文件的格式,或者其版本,可能是loadmscorwks的版本不正确造成的。也可能是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

      

       在前面的上一篇文章中已经说过,MethodDescmethodtableEEClassMethodDescChunk这些结构的关系和区别。MethodDescCLR中对应的托管方法的非托管的结构。

       下面的三个方法,主要是实现了定位和寻找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在执行完毕之后的调用堆栈。里面有很多和ThreadJITGC相关的功能的初始化和额外线程的启动,譬如,主线程创建FinalizerThreadGCThread,由于没有下断点跟踪,所以这里都没有展现出来。

此文的主要目的,在于提供一种阅读和分析Rotor的方法,让对Rotorsscli的分析,不仅仅限制与对静态代码的分析,我们还可以结合DotNet应用程序的运行,动态的分析代码的执行和实现。

同时,研究不同类型应用程序调用堆栈,里面还有非常多有意思的东西可以发掘。

 

3/25/2008 10:38:33 AM

 

posted on 2008-03-25 10:50 lbq1221119 阅读(...) 评论(...) 编辑 收藏

导航

公告

统计