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位來決定。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章