CLR探索系列:深入追踪托管exe加载执行过程

在上一篇“CLR探索系列之应用程序域世界的上篇中,探讨了一些关于应用程序域在托管代码执行过程中的特性和运行机制,以及一些相关的概念。

在接下来的中篇里,就从如何实现的角度,换一个角度来探讨程序集和应用程序域是如何加载,执行。以及一些有趣的问题。

首先,有一个有趣的鸡和蛋的问题。我们知道,一个应用程序集里面的代码在执行的时候,首先被load,然后经过验证,接着对IL代码JIT成为本地代码才能执行。一个应用程序集只有被先加载了才能被执行,但是加载程序集的程序集,是被什么程序集加载的呢?或者,第一个程序集,是如何被加载到CLR的世界中呢?   

首先,来查看一下Clix工具作为一个sscli提供的loader的main函数都做了些什么:
int __cdecl main(int argc, char **argv)

{

    DWORD nExitCode = 1; // error

    WCHAR* pwzCmdLine;

 

    if ( !PAL_RegisterLibrary(L"rotor_palrt")

            || !PAL_RegisterLibrary(L"sscoree") ) {

        DisplayMessageFromSystem(::GetLastError());

        return 1;

}


可以看到,在clixMain函数里面,就做了两件事情:注册Rotorpalrt模块,同时,注册sscoree模块。
在执行托管代码的库文件结构中,有三个层次:
第一层:Managed libraries
第二层:
Execute Engine(CLR)
第三层:PAL

第一层里面,主要包含的是BCL;还有一些别的托管系统的库文件。例如mscorlib.dllSystem.xml.dll或者是别的托管组件之类。
第二层里面,有我们非常熟悉的sscoree.dll,也就是rotor里面的托管程序的执行引擎。
在第三层PAL层里面,主要有两个文件:rotor_pal.dllrotor_palrt.dll;在rotor的源代码解压后,clrpalpalrt这三个文件夹是并列排列的。这也反应了这三个部分之间的关系。pal是某个特定的操作系统对PAL层的实现,而palrt是忽略操作系统的区别对PAL层的一般实现。


if ( !PAL_RegisterLibrary(L"rotor_palrt")|| !PAL_RegisterLibrary(L"sscoree") )这一行中,首先是加载了托管库文件结构里面最下面PAL层的针对编译好了的,一个特定的操作系统的实现。接着,又是调用加载了基于这个PAL_RT层上面的CLI的托管执行引擎:sscoree而对于托管代码执行需要的库文件的第三层,也就是最上面一层,BCL之类的库文件的加载,则是在创建这个托管引用程序的内存结构的几个特定类型的应用程序域中加载进去的。

这样,对于托管代码执行的时候的需要的一些库文件(按照库文件的结构,从下往上)是如何加载到内存中去,以及PAL层和CLR的加载执行顺序,我们就有了一个比较清晰的认识了。

      then
,在注册好了PAL层和CLR之后,我们再来看看作为sscli里面提供的一个loader,是如何实现load一个exe(或许是托管的)到执行的托管进程中去的。打开Clix.appLaunch函数:

//the Launch founction of Clix.Shows how launch of first Assembly.

//launch the EE of CLI

DWORD Launch(WCHAR* pFileName, WCHAR* pCmdLine)

{

       //file name

    WCHAR exeFileName[MAX_PATH + 1];

    DWORD dwAttrs;

       //define the error type

    DWORD dwError;

    DWORD nExitCode;

 

    dwAttrs = ::GetFileAttributesW(pFileName);

 

//省略若干对于文件名表示的文件的相关检查代码

 

    if (dwError != ERROR_SUCCESS) {

        // We can't find the file, or there's some other problem. Exit with an error.

        fwprintf(stderr, L"%s: ", pFileName);

        DisplayMessageFromSystem(dwError);

        return 1;   // error

    }

 

       //DWORD Exit Code.

       //这里,调用导入进来的

    nExitCode = _CorExeMain2(NULL, 0, pFileName, NULL, pCmdLine);

 

    // _CorExeMain2 never returns with success

    _ASSERTE(nExitCode != 0);

 

    DisplayMessageFromSystem(::GetLastError());

 

    return nExitCode;

}

首先,我们看这一句:_ASSERTE(nExitCode != 0);程序运行到这里的时候,就是对一个托管程序的执行已经完成了,PALEE和相关的加载的了的BCL以及相关的托管模块和应用程序域,这些东西都已经退出内存,我们对这个加载的exe文件的执行,就到此为止了。It is the time for us to show down the lightsand went home……^_^

接着,我们再来看这一句: nExitCode = _CorExeMain2(NULL, 0, pFileName, NULL, pCmdLine)这里,就开始执行外部导入函数了,也是经常看到的非常频繁的CorExeMain这个函数。不同的是,后面多了一个2。这是商业版本和开源版本的一点小小的区别了。

 

在商业版本的DotNet Framework 中,这个地方调用的函数是_CorExeMain();可以用Dependency walkerPEID,或者是Inspect,来查看任何一个本机上面生成好了的托管的Module。查看某个Module的导入的库。

下面是我用inspect来查看一个托管模块的导入函数情况:

o_zhong_1.jpg


同时,下面是我用Dependency walker来查看MSCOREE.dll的内部函数:

r_zhong_2.jpg


这里,可以看到mscoree.dll里面包含的_CorExeMain这个函数,同时,如果是一个dll的话,就间接执行_CorDllMain这个函数。

 

下面,就来看看_CorExeMain这个函数都做了些什么。打开VM虚拟机目录下面的ceemain.cpp文件查看这个函数是如何实现的,都做了些什么。这个文件中包含了大部分对ee的操作,初始化,关闭等等:

//**********************************************************

// This entry point is called from the native entry piont of the loaded

// executable image.  The command line arguments and other entry point data

// will be gathered here.  The entry point for the user image will be found

// and handled accordingly.

//**********************************************************

__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

{

 

    // This entry point is used by clix

    BOOL bRetVal = 0;

 

    //BEGIN_ENTRYPOINT_VOIDRET;

 

    // 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))

       //如果相关的检查成功,就在这里初始化EE,调用这个文件里面的CoInitializeEE方法

        result = CoInitializeEE(COINITEE_DEFAULT | COINITEE_MAIN);

 

    if (FAILED(result)) {

        VMDumpCOMErrors(result);

        SetLatchedExitCode (-1);

        goto exit;

    }

 

    // This is here to get the ZAPMONITOR working correctly

    INSTALL_UNWIND_AND_CONTINUE_HANDLER;

 

    // Load the executable

    bRetVal = ExecuteEXE(pImageNameIn);

 

    if (!bRetVal) {

        // The only reason I've seen this type of error in the wild is bad

        // metadata file format versions and inadequate error handling for

        // partially signed assemblies.  While this may happen during

        // development, our customers should not get here.  This is a back-stop

        // to catch CLR bugs. If you see this, please try to find a better way

        // to handle your error, like throwing an unhandled exception.

        EEMessageBoxCatastrophic(IDS_EE_COREXEMAIN2_FAILED_TEXT, IDS_EE_COREXEMAIN2_FAILED_TITLE);

        SetLatchedExitCode (-1);

    }

UNINSTALL_UNWIND_AND_CONTINUE_HANDLER;

 

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;

}

 

这里,就完成了对一个exe文件的加载过程。同时,在bRetVal = ExecuteEXE(pImageNameIn);这一行也调用了执行这个文件的方法。继续查看这个方法的实现:

BOOL STDMETHODCALLTYPE ExecuteEXE(HMODULE hMod)

{

    STATIC_CONTRACT_GC_TRIGGERS;

 

    _ASSERTE(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);

    }

    EX_END_NOCATCH;

 

    ETWTraceStartup::TraceEvent(ETW_TYPE_STARTUP_EXEC_EXE+1);

    TIMELINE_END(STARTUP, ("ExecuteExe"));

 

    return TRUE;

}

这里,终于找到了我们需要找的东西,调用了应用程序域里面的执行Main函数的方法,接着打开Assembly.Cpp文件里面的这个方法,查看这个方法是如何实现在一个应用程序域里面执行一个新加载的ModuleMain函数的:

INT32 Assembly::ExecuteMainMethod(PTRARRAYREF *stringArgs)

{

 

       ………………..

 

    BEGIN_ENTRYPOINT_THROWS;

 

    Thread *pThread = GetThread();

    MethodDesc *pMeth;

    {

        // This thread looks like it wandered in -- but actually we rely on it to keep the process alive.

        pThread->SetBackground(FALSE);

   

        GCX_COOP();

 

        pMeth = GetEntryPoint();

        if (pMeth) {

            RunMainPre();

            hr = ClassLoader::RunMain(pMeth, 1, &iRetVal, stringArgs);

        }

    }

      

       //省略执行结束的销毁相关内容的执行逻辑    

 

    return iRetVal;

}

到这里,找到了最后执行一个load了的模块的Main方法的地方,是在ClassLoader里面的RunMain方法中。而上面的ExecuteMainMethod方法,只是为Module的执行提供了一个从应用程序域的角度来控制的环境,为已经加载了的一个模块的执行分配一个线程,同时,处理这个模块执行好了之后相关的操作。

我们就接着追踪最后RunMain最后都干了些啥,最后一段代码,也是vm虚拟机目录下面的clsload.cpp这个文件里面的方法,(从这里,我们也看到了Rotor中非常好的层次设计和架构设计,每一层的事情和相关的处理逻辑,都控制相关的层面上面,绝不在上面一层做下面的一层的事情):

/* static */

HRESULT ClassLoader::RunMain(MethodDesc *pFD ,

                             short numSkipArgs,

                             INT32 *piRetVal,

                             PTRARRAYREF *stringArgs /*=NULL*/)

{

    STATIC_CONTRACT_THROWS;

    _ASSERTE(piRetVal);

 

    DWORD       cCommandArgs = 0;  // count of args on command line

    DWORD       arg = 0;

    LPWSTR      *wzArgs = NULL; // command line args

    HRESULT     hr = S_OK;

 

    *piRetVal = -1;

 

    // The exit code for the process is communicated in one of two ways.  If the

    // entrypoint returns an 'int' we take that.  Otherwise we take a latched

    // process exit code.  This can be modified by the app via setting

// Environment's ExitCode property.

//设置返回code的类型

    if (stringArgs == NULL)

        SetLatchedExitCode(0);

 

       //pFD这个指针是指向的每个在内存里面的实例的instance data的方法列表,也就是一个叫做ObjHeader的指针。我们在深入研究System.Object在内存里面布局的时候,会看到这个东西。对于每个在内存中的instance,实例的相关数据在内存中开始的第一个地址的前一个位置,保存的是一个指向这个MethodTable方法列表的指针。这个方法列表,是保存在EE的私有内存地址中的,用来方便对执行的时候的实例和对象的控制。而这个指针的前一个指针,则表示的是一个叫做SyncBlock table的东西,也是用于EE对对象的控制的。而一个实例的数据,是保存在GC堆里面的。

       //下面一句的用处,就是如果这个指向这个实例的方法的指针是空的时候,(一个对象的方法可以为空,但是指向这个对象的实例的method table的指针不能为空),就会提示错误。

    if (!pFD) {

        _ASSERTE(!"Must have a function to call!");

        return E_FAIL;

    }

 

    CorEntryPointType EntryType = EntryManagedMain;

    ValidateMainMethod(pFD, &EntryType);

 

    if ((EntryType == EntryManagedMain) &&

        (stringArgs == NULL)) {

        // If you look at the DIFF on this code then you will see a major change which is that we

        // no longer accept all the different types of data arguments to main.  We now only accept

        // an array of strings.

 

        wzArgs = CorCommandLine::GetArgvW(&cCommandArgs);

        // In the WindowsCE case where the app has additional args the count will come back zero.

        if (cCommandArgs > 0) {

            if (!wzArgs)

                return E_INVALIDARG;

        }

    }

 

    ETWTraceStartup::TraceEvent(ETW_TYPE_STARTUP_MAIN);

    TIMELINE_START(STARTUP, ("RunMain"));

 

    EX_TRY_NOCATCH

    {

        MethodDescCallSite  threadStart(pFD);

       

        PTRARRAYREF StrArgArray = NULL;

        GCPROTECT_BEGIN(StrArgArray);

 

        // Build the parameter array and invoke the method.

              //分为两种情况来处理:有参数和没有参数

        if (EntryType == EntryManagedMain) {

            if (stringArgs == NULL) {

                // Allocate a COM Array object with enough slots for cCommandArgs - 1

                StrArgArray = (PTRARRAYREF) AllocateObjectArray((cCommandArgs - numSkipArgs), g_pStringClass);

 

                // Create Stringrefs for each of the args

                for( arg = numSkipArgs; arg < cCommandArgs; arg++) {

                    STRINGREF sref = COMString::NewString(wzArgs[arg]);

                    StrArgArray->SetAt(arg-numSkipArgs, (OBJECTREF) sref);

                }

            }

            else

                StrArgArray = *stringArgs;

        }

 

#ifdef STRESS_THREAD

        OBJECTHANDLE argHandle = (StrArgArray != NULL) ? CreateGlobalStrongHandle (StrArgArray) : NULL;

        Stress_Thread_Param Param = {pFD, argHandle, numSkipArgs, EntryType, 0};

        Stress_Thread_Start (&Param);

#endif

 

        ARG_SLOT stackVar = ObjToArgSlot(StrArgArray);

 

        if (pFD->IsVoid())

        {

            // Set the return value to 0 instead of returning random junk

            *piRetVal = 0;

            threadStart.Call(&stackVar);

        }

        else

        {

            *piRetVal = (INT32)threadStart.Call_RetArgSlot(&stackVar);

            if (stringArgs == NULL)

            {

                SetLatchedExitCode(*piRetVal);

            }

        }

        GCPROTECT_END();

 

        fflush(stdout);

        fflush(stderr);

    }

    EX_END_NOCATCH

 

    ETWTraceStartup::TraceEvent(ETW_TYPE_STARTUP_MAIN+1);

    TIMELINE_END(STARTUP, ("RunMain"));

 

    return hr;

}

在这个方法中,还设计到了一系列对COM口的交互,其中每一行,都需要对托管应用程序在内存中的结构有个清晰的了解,对这个方法做深入的分析,以及执行的流程,就是下一篇博文的事情了。^_^

 

 

Ps:分析追踪了大概67个文件,从CLI,到CLR再到应用程序域,然后到ClassLoad里面的方法,终于在一定层次上面搞清楚了一个托管应用程序的加载过程,以及这个过程中CLIEEAppDomain的加载执行过程和顺序.终于从程序的加载,刨到了应用程序域的世界。这个和我的初衷,写一篇从代码分析应用程序域似乎有些不符合

为此,就改一个题目,改成系列中的中篇吧

另外,我以前是写了一半保存在blog上面的,后来中途写的时候,没保存好,丢失了,又重新写了一遍不过灵机一动,在baidu里面搜索我的文章的题目,在快照里面找到了我以前的一个版本,^_^,以后都在word里面先写好了再整过来。

 

文章里面有纰漏的地方,欢迎大家指正!:)

后记:

补充说明下,一个托管对象在内存里面的格式:

托管对象的结构如下:

                 m_SyncBlockValue

对象指针->  m_pMethodTable

                 Data

在每个托管对象的开始是该对象类型的方法表。在方法表之前是m_SyncBlockValue

m_SyncBlockValue的高6位用来标记m_SyncBlockValue的用途。SyncBlockValue的低26位用来存储哈希码,SyncBlock索引或SpinBlock

26位值的含义由高6位来决定。

posted on 2007-12-21 17:24  lbq1221119  阅读(4013)  评论(6编辑  收藏  举报

导航