CLR探索系列:深入追蹤托管exe加載執(zhí)行過(guò)程
在上一篇“CLR探索系列之應(yīng)用程序域世界”的上篇中,探討了一些關(guān)于應(yīng)用程序域在托管代碼執(zhí)行過(guò)程中的特性和運(yùn)行機(jī)制,以及一些相關(guān)的概念。
在接下來(lái)的中篇里,就從如何實(shí)現(xiàn)的角度,換一個(gè)角度來(lái)探討程序集和應(yīng)用程序域是如何加載,執(zhí)行。以及一些有趣的問(wèn)題。
首先,有一個(gè)有趣的“雞和蛋”的問(wèn)題。我們知道,一個(gè)應(yīng)用程序集里面的代碼在執(zhí)行的時(shí)候,首先被load,然后經(jīng)過(guò)驗(yàn)證,接著對(duì)IL代碼JIT成為本地代碼才能執(zhí)行。一個(gè)應(yīng)用程序集只有被先加載了才能被執(zhí)行,但是加載程序集的程序集,是被什么程序集加載的呢?或者,第一個(gè)程序集,是如何被加載到CLR的世界中呢?
首先,來(lái)查看一下Clix工具作為一個(gè)sscli提供的loader的main函數(shù)都做了些什么:
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;
}
可以看到,在clix的Main函數(shù)里面,就做了兩件事情:注冊(cè)Rotor的palrt模塊,同時(shí),注冊(cè)sscoree模塊。
在執(zhí)行托管代碼的庫(kù)文件結(jié)構(gòu)中,有三個(gè)層次:
第一層:Managed libraries
第二層:Execute Engine(CLR)
第三層:PAL
第一層里面,主要包含的是BCL;還有一些別的托管系統(tǒng)的庫(kù)文件。例如mscorlib.dll,System.xml.dll或者是別的托管組件之類(lèi)。
第二層里面,有我們非常熟悉的sscoree.dll,也就是rotor里面的托管程序的執(zhí)行引擎。
在第三層PAL層里面,主要有兩個(gè)文件:rotor_pal.dll,rotor_palrt.dll;在rotor的源代碼解壓后,clr,pal,palrt這三個(gè)文件夾是并列排列的。這也反應(yīng)了這三個(gè)部分之間的關(guān)系。pal是某個(gè)特定的操作系統(tǒng)對(duì)PAL層的實(shí)現(xiàn),而palrt是忽略操作系統(tǒng)的區(qū)別對(duì)PAL層的一般實(shí)現(xiàn)。
在 if ( !PAL_RegisterLibrary(L"rotor_palrt")|| !PAL_RegisterLibrary(L"sscoree") )這一行中,首先是加載了托管庫(kù)文件結(jié)構(gòu)里面最下面PAL層的針對(duì)編譯好了的,一個(gè)特定的操作系統(tǒng)的實(shí)現(xiàn)。接著,又是調(diào)用加載了基于這個(gè)PAL_RT層上面的CLI的托管執(zhí)行引擎:sscoree。而對(duì)于托管代碼執(zhí)行需要的庫(kù)文件的第三層,也就是最上面一層,BCL之類(lèi)的庫(kù)文件的加載,則是在創(chuàng)建這個(gè)托管引用程序的內(nèi)存結(jié)構(gòu)的幾個(gè)特定類(lèi)型的應(yīng)用程序域中加載進(jìn)去的。
這樣,對(duì)于托管代碼執(zhí)行的時(shí)候的需要的一些庫(kù)文件(按照庫(kù)文件的結(jié)構(gòu),從下往上)是如何加載到內(nèi)存中去,以及PAL層和CLR的加載執(zhí)行順序,我們就有了一個(gè)比較清晰的認(rèn)識(shí)了。
then,在注冊(cè)好了PAL層和CLR之后,我們?cè)賮?lái)看看作為sscli里面提供的一個(gè)loader,是如何實(shí)現(xiàn)load一個(gè)exe(或許是托管的)到執(zhí)行的托管進(jìn)程中去的。打開(kāi)Clix.app的Launch函數(shù):
//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);
//省略若干對(duì)于文件名表示的文件的相關(guān)檢查代碼
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.
//這里,調(diào)用導(dǎo)入進(jìn)來(lái)的
nExitCode = _CorExeMain2(NULL, 0, pFileName, NULL, pCmdLine);
// _CorExeMain2 never returns with success
_ASSERTE(nExitCode != 0);
DisplayMessageFromSystem(::GetLastError());
return nExitCode;
}
首先,我們看這一句:_ASSERTE(nExitCode != 0);程序運(yùn)行到這里的時(shí)候,就是對(duì)一個(gè)托管程序的執(zhí)行已經(jīng)完成了,PAL,EE和相關(guān)的加載的了的BCL以及相關(guān)的托管模塊和應(yīng)用程序域,這些東西都已經(jīng)退出內(nèi)存,我們對(duì)這個(gè)加載的exe文件的執(zhí)行,就到此為止了。It is the time for us to show down the lights,and went home……^_^
接著,我們?cè)賮?lái)看這一句: nExitCode = _CorExeMain2(NULL, 0, pFileName, NULL, pCmdLine);這里,就開(kāi)始執(zhí)行外部導(dǎo)入函數(shù)了,也是經(jīng)常看到的非常頻繁的CorExeMain這個(gè)函數(shù)。不同的是,后面多了一個(gè)2。這是商業(yè)版本和開(kāi)源版本的一點(diǎn)小小的區(qū)別了。
在商業(yè)版本的DotNet Framework 中,這個(gè)地方調(diào)用的函數(shù)是_CorExeMain();可以用Dependency walker,PEID,或者是Inspect,來(lái)查看任何一個(gè)本機(jī)上面生成好了的托管的Module。查看某個(gè)Module的導(dǎo)入的庫(kù)。
下面是我用inspect來(lái)查看一個(gè)托管模塊的導(dǎo)入函數(shù)情況:

同時(shí),下面是我用Dependency walker來(lái)查看MSCOREE.dll的內(nèi)部函數(shù):

這里,可以看到mscoree.dll里面包含的_CorExeMain這個(gè)函數(shù),同時(shí),如果是一個(gè)dll的話,就間接執(zhí)行_CorDllMain這個(gè)函數(shù)。
下面,就來(lái)看看_CorExeMain這個(gè)函數(shù)都做了些什么。打開(kāi)VM虛擬機(jī)目錄下面的ceemain.cpp文件查看這個(gè)函數(shù)是如何實(shí)現(xiàn)的,都做了些什么。這個(gè)文件中包含了大部分對(duì)ee的操作,初始化,關(guān)閉等等:
//**********************************************************
// 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.
//處理和文件名一起傳遞進(jìn)來(lái)的命令參數(shù)。首先確定是不是一個(gè)托管的模塊,并且對(duì)其進(jìn)行一系列的檢查。如果不是就直接退出托管環(huán)境的加載。
HRESULT result = CorCommandLine::SetArgvW(pCmdLine);
//把命令行緩存起來(lái)。
if (!CacheCommandLine(pCmdLine, CorCommandLine::GetArgvW(NULL))) {
LOG((LF_STARTUP, LL_INFO10, "Program exiting - CacheCommandLine failed\n"));
bRetVal = -1;
goto exit;
}
if (SUCCEEDED(result))
//如果相關(guān)的檢查成功,就在這里初始化EE,調(diào)用這個(gè)文件里面的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;
}
這里,就完成了對(duì)一個(gè)exe文件的加載過(guò)程。同時(shí),在bRetVal = ExecuteEXE(pImageNameIn);這一行也調(diào)用了執(zhí)行這個(gè)文件的方法。繼續(xù)查看這個(gè)方法的實(shí)現(xiàn):
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;
}
這里,終于找到了我們需要找的東西,調(diào)用了應(yīng)用程序域里面的執(zhí)行Main函數(shù)的方法,接著打開(kāi)Assembly.Cpp文件里面的這個(gè)方法,查看這個(gè)方法是如何實(shí)現(xiàn)在一個(gè)應(yīng)用程序域里面執(zhí)行一個(gè)新加載的Module的Main函數(shù)的:
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);
}
}
//省略執(zhí)行結(jié)束的銷(xiāo)毀相關(guān)內(nèi)容的執(zhí)行邏輯
return iRetVal;
}
到這里,找到了最后執(zhí)行一個(gè)load了的模塊的Main方法的地方,是在ClassLoader里面的RunMain方法中。而上面的ExecuteMainMethod方法,只是為Module的執(zhí)行提供了一個(gè)從應(yīng)用程序域的角度來(lái)控制的環(huán)境,為已經(jīng)加載了的一個(gè)模塊的執(zhí)行分配一個(gè)線程,同時(shí),處理這個(gè)模塊執(zhí)行好了之后相關(guān)的操作。
我們就接著追蹤最后RunMain最后都干了些啥,最后一段代碼,也是vm虛擬機(jī)目錄下面的clsload.cpp這個(gè)文件里面的方法,(從這里,我們也看到了Rotor中非常好的層次設(shè)計(jì)和架構(gòu)設(shè)計(jì),每一層的事情和相關(guān)的處理邏輯,都控制相關(guān)的層面上面,絕不在上面一層做下面的一層的事情):
/* 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.
//設(shè)置返回code的類(lèi)型
if (stringArgs == NULL)
SetLatchedExitCode(0);
//pFD這個(gè)指針是指向的每個(gè)在內(nèi)存里面的實(shí)例的instance data的方法列表,也就是一個(gè)叫做ObjHeader的指針。我們?cè)谏钊胙芯?/span>System.
//下面一句的用處,就是如果這個(gè)指向這個(gè)實(shí)例的方法的指針是空的時(shí)候,(一個(gè)對(duì)象的方法可以為空,但是指向這個(gè)對(duì)象的實(shí)例的method table的指針不能為空),就會(huì)提示錯(cuò)誤。
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.
//分為兩種情況來(lái)處理:有參數(shù)和沒(méi)有參數(shù)
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;
}
在這個(gè)方法中,還設(shè)計(jì)到了一系列對(duì)COM口的交互,其中每一行,都需要對(duì)托管應(yīng)用程序在內(nèi)存中的結(jié)構(gòu)有個(gè)清晰的了解,對(duì)這個(gè)方法做深入的分析,以及執(zhí)行的流程,就是下一篇博文的事情了。^_^
Ps:分析追蹤了大概6,7個(gè)文件,從CLI,到CLR再到應(yīng)用程序域,然后到ClassLoad里面的方法,終于在一定層次上面搞清楚了一個(gè)托管應(yīng)用程序的加載過(guò)程,以及這個(gè)過(guò)程中CLI,EE,AppDomain的加載執(zhí)行過(guò)程和順序.終于從程序的加載,刨到了應(yīng)用程序域的世界。這個(gè)和我的初衷,寫(xiě)一篇從代碼分析應(yīng)用程序域似乎有些不符合…
為此,就改一個(gè)題目,改成系列中的中篇吧…
另外,我以前是寫(xiě)了一半保存在blog上面的,后來(lái)中途寫(xiě)的時(shí)候,沒(méi)保存好,丟失了,又重新寫(xiě)了一遍…不過(guò)靈機(jī)一動(dòng),在baidu里面搜索我的文章的題目,在快照里面找到了我以前的一個(gè)版本,^_^,以后都在word里面先寫(xiě)好了再整過(guò)來(lái)。
文章里面有紕漏的地方,歡迎大家指正!:)
后記:
補(bǔ)充說(shuō)明下,一個(gè)托管對(duì)象在內(nèi)存里面的格式:
托管對(duì)象的結(jié)構(gòu)如下:
m_SyncBlockValue
對(duì)象指針-> m_pMethodTable
Data
在每個(gè)托管對(duì)象的開(kāi)始是該對(duì)象類(lèi)型的方法表。在方法表之前是m_SyncBlockValue。
m_SyncBlockValue的高6位用來(lái)標(biāo)記m_SyncBlockValue的用途。SyncBlockValue的低26位用來(lái)存儲(chǔ)哈希碼,SyncBlock索引或SpinBlock。
低26位值的含義由高6位來(lái)決定。
posted on 2007-12-21 17:24 lbq1221119 閱讀(4043) 評(píng)論(6) 收藏 舉報(bào)
浙公網(wǎng)安備 33010602011771號(hào)