.Net平台下CLR程式載入原理分析

來源:互聯網
上載者:User
程式 Flier Lu <flier_lu@sina.com.cn>
  
注意:本系列文章在水木清華BBS(smth.org)之.Net版首發,
     轉載請保留以上資訊,發表請與作者聯絡
  
  與傳統的Win32可執行程式中的機器碼(Native Code)不同,
微軟推出的.Net架構中,可執行程式的代碼是以類似Java Byte Code的
IL (Intermediate Language)虛擬碼形式存在的。在.Net可執行程式載入後,
IL代碼由CLR (Common Language Runtime)從可執行檔中取出,
交由JIT (Just-In-Time)編譯器,根據相應的中繼資料(Metadata),
Just-In-Time 編譯成機器碼後執行。
  因此,一個CLR可執行程式的啟動過程可以分為三個步驟。
  首先,Windows的可執行程式載入器(OS Loader)載入
PE (Portable Executable)結構的可執行檔映像(PE Image),
將執行權傳遞給CLR的支援庫中的Unmanaged Code。
  其次,啟動或使用現有的CLR引擎,建立新的應用域(Application Domain),
將配件(Assembly)載入到此應用域中。
  最後,將執行權從Unmanaged Code傳遞給Managed Code,執行配件的代碼。
  下面我將詳細說明以上步驟。
  
  自從Win95發布以來,可執行程式的PE結構就沒有發生大的改動。
此次.Net平台發布,也只是利用了PE結構中現有的預留空間,
以保持PE結構的穩定,最大程度保持向後相容。
(詳情請參看筆者《MS.Net平台下CLR 擴充PE結構分析》一文)
  CLR程式在編譯後,將可執行程式入口直接以一個間接跳轉指令
指向mscoree.lib中的_CorExeMain函數(DLL將入口指向_CorDllMain函數)。
因此CLR可執行程式在被OS Loader載入後,將由_CorExeMain函數處理CLR引擎
啟動事宜。此函數將啟動或使用一個現有的CLR Host來載入IL代碼。
  常見的CLR Host有ASP.Net、IE、Shell、資料庫引擎等等,
他們的作用是啟動一個CLR執行個體,管理在此CLR執行個體中啟動並執行CLR程式。
  
  我們接著來看一看一個CLR Host是如何實際運作的。
  CLR作為一個引擎,在同一台電腦上是可以存在多個版本的,
不同版本之間可以通過配置良好共存。在
%windir%\Microsoft.NET\Framework
(%windir%表示Windows系統目錄所在位置)目錄下,
我們可以看到以版本號碼為目錄名的多個CLR版本,
如%windir%\Microsoft.NET\Framework\v1.0.3705等等,
也可以在註冊表的
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\policy\v1.0
鍵下查看詳細的版本相容性.Name是Build號,Value是相容的Build號.
而每一個CLR版本又分為Server和Workstation兩類運行庫,
我們等會講建立CLR時會詳細談到.
  CLR Host在啟動CLR之前,必須通過一個startup shim的庫進行操作,
實際上就是mscoree.dll,他提供了版本無關的操作函數,以及啟動CLR所需
的支援,如CorBindToRuntimeEx函數.
  CLR Host通過shim的支援庫,將CLR引擎載入到進程中.具體函數如下
STDAPI CorBindToRuntimeEx(LPCWSTR pwszVersion,
  LPCWSTR pwszBuildFlavor, DWORD startupFlags,
  REFCLSID rclsid, REFIID riid, LPVOID FAR *ppv);
  參數pwszVersion指定要載入的CLR版本號碼,注意必須在前面帶一個小寫"v",
如"v1.0.3705",可以通過查閱前面提到的註冊表鍵,擷取當前系統安裝的不同CLR
版本情況,或指定固定的CLR版本.也可以傳遞NULL給這個參數,系統將自動選擇最新
版本的CLR載入.
  參數pwszBuildFlavor則指定載入的CLR類型,"srv"和"wks".
前者適用於多處理器的電腦,能夠利用多CPU提高並行效能.對單CPU
系統而言,無論指定哪種類型都會載入"wks",傳遞NULL也是如此.
  參數startupFlags是一個組合參數.由多個標誌位組成.
  STARTUP_CONCURRENT_GC標誌指定是否使用並發的GC(Garbage Collection)
機制,使用並發GC能夠提高系統的使用者介面相應效率,適合視窗介面使用較多的程式.
但並發GC會因為無謂的線程上下文(Thread Context)切換損失效率.
  以下三個參數用於指定配件載入最佳化策略.我們等會詳細討論.
  STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN = 0x1 << 1,
  STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN  = 0x2 << 1,
  STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST = 0x3 << 1,
  接著的三個參數用於擷取ICorRuntimeHost介面.
  實際調用執行個體如下.
CComPtr<ICorRuntimeHost> spHost;
CHECK(CorBindToRuntimeEx(NULL, L"wks",
  STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN | STARTUP_CONCURRENT_GC,
  CLSID_CorRuntimeHost, IID_ICorRuntimeHost, (void **)&spHost));
  這行代碼載入最高版本CLR的wks類型運行庫,為單應用域進行最佳化並使用並發GC機制.
  前面提到了配件載入最佳化策略,要理解這個概念,我們必須先瞭解應用域的概念.
傳統Win程式中,資源的分配管理單位是進程,作業系統以進程邊界將應用程式執行個體隔離開
,
單個進程的崩潰不會對其他進程產生直接影響,進程也不能直接使用其他進程的資源.
進程很好,但使用進程的代價太大,為此Win32引入了線程的概念.同一進程中的線程能夠
共用資源,線程管理和切換的代價也遠遠小於進程.但因為在同一進程中,線程的崩潰會直

影響到其他線程的運行,也無法約束線程間資料的直接存取等等.
  為此,CLR中引入了Application Domain應用域的概念.應用域是介於進程和線程
之間的一種邏輯上的概念.他既有線程輕巧,管理切換快捷的優點,也有進程在穩定性方面
的優點,單個應用域的崩潰不會直接影響到同一進程中的其他應用域,應用域也無法直接
訪問同一進程中的其他應用域的資源,這方面和進程完全相同.
  而CLR的管理就是完全面嚮應用域一級.CLR不能卸載(Unload)某個類型或配件,
必須以應用域為單位啟動/停止代碼,擷取/釋放資源.
  CLR在執行一個配件時,會建立一個應用域,將此配件放入新的應用域.如果多個應用域
同時使用到一個配件,就要涉及到前面提到的配件載入最佳化策略了.最簡單的方法是使用
STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN標誌,每個應用域擁有一份獨立的
配件的鏡像,這樣速度最快,管理最方便,但佔用記憶體較多.相對的是所有應用域共用一份
配件的鏡像,(使用STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN標誌)
這樣節約記憶體,但在此配件中存在靜態變數等資料時,因為要保證每個應用域有獨立的數
據,
所以會一定程度上影響效率.折中的方案是使用
(使用STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST標誌)
此時,只有那些有Strong Name的配件才會被多個應用域共用.
  這裡又涉及到一個概念Strong Name.他是一個配件的身份證明,他由配件的
名字/版本/culture以及數位簽章等組成.在配件發布時用以區別不同版本.
也在安全/版本控制等方面起到重要作用,以後有機會會專門講解.暫且跳過.
  擷取了ICorRuntimeHost介面的指標後,我們可以以此指標取得當前/預設應用域,
並可枚舉CLR引擎執行個體中所有的應用域.
  CComPtr<IUnknown> spUnk;
  CComPtr<_AppDomain> spAppDomain;
  
  CHECK(spHost->GetDefaultDomain(&spUnk));
  spAppDomain = spUnk; spUnk = NULL;
  wcout << L"Default AppDomain is " << (wchar_t
*)spAppDomain->GetFriendlyName() << endl;
  
  CHECK(spHost->CurrentDomain(&spUnk));
  spAppDomain = spUnk; spUnk = NULL;
  wcout << L"Current AppDomain is " << (wchar_t
*)spAppDomain->GetFriendlyName() << endl;
  
  HDOMAINENUM hEnum;
  CHECK(spHost->EnumDomains(&hEnum));
  spUnk = NULL;
  while(spHost->NextDomain(hEnum, &spUnk) != S_FALSE)
  {
    spAppDomain = spUnk; spUnk = NULL;
    wcout << (wchar_t *)spAppDomain->GetFriendlyName() << endl;
  }
  CHECK(spHost->CloseEnum(hEnum));
  當前應用域是指當前線程運行時所在應用域.注意線程屬於進程,但不屬於某個應用域,
  
一個線程可以跨應用網域作業.可以通過線程類的Thread.GetDomain擷取線程當前所在
應用域.
  預設應用域是CLR引擎載入後自動建立的應用域,其生命期貫串CLR引擎的使用期,
一般在此應用域中執行CLR Host的Managed Code端管理代碼,而不執行使用者代碼.
  接下來,是載入使用者代碼所在配件的時候了.方法有兩種,一是接著使用完全的
Native Code或者說Unmanaged Code通過BCL的COM封裝介面操作;二是將操作
移交給Managed Code部分的CLR Host代碼執行.後者實現簡單,速度較快.
筆者以後將單獨以一篇文章介紹CLR Host的Managed Code部分代碼的設計編寫.
這裡將簡要介紹第一種實現.
  以Unmanaged Code完整實現CLR Host雖然麻煩,但功能更加強大.但因為操作中
要不斷在Unmanaged/Managed Code之間切換,效率受到一定影響.(切換的調用
是通過IDispatch介面實現,本身效率就很低,加上CCW(COM Callable Wrapper)
的封裝,低於直接使用Managed Code的效率.
  以Unmanaged Code調用配件,必須知道配件的部分資訊,如配件的名字,
要調用的類的名字,要調用的函數等等.可以指定參數的方式來使用,也可以通過
PE映像中CLR頭的IL入口EntryPointToken以及Metadata的資訊來擷取
(詳情請參看筆者《MS.Net平台下CLR 擴充PE結構分析》一文Metadata篇)
這裡為了樣本簡單,採用參數傳遞方式.
  if(argc < 4)
  {
    cerr << "Usage: " << argv[0] << " <Assembly Name> <Class Name> <Main
Function Name> <Parameters>" << endl;
  }
  else
  {
    _bstr_t bstrAssemblyName(argv[1]),
            bstrClassName(argv[2]),
            bstrMainFuncName(argv[3]);
    ...
  }
  例子中以命令列方式傳遞配件/類/函數名資訊.
  spUnk = NULL;
  CHECK(spHost->GetDefaultDomain(&spUnk));
  spAppDomain = spUnk; spUnk = NULL;
  首先擷取預設應用域,在此應用域中建立指定配件中指定類.這裡為例子簡潔
直接在預設應用域中載入配件,實際開發中應避免這種方式,而採用建立新應用域
的方式來載入配件.關於建立應用域以及建立時的配置,設計問題較多,以後再專門
寫文章詳述,這裡略去.
  _ObjectHandlePtr spObj = spAppDomain->CreateInstance(bstrAssemblyName,
bstrClassName);
  CComPtr<IDispatch> spDisp = spObj->Unwrap().pdispVal;
  建立配件中類執行個體後,取得一個_ObjectHandlePtr類型值,
通過Unwrap()調用擷取IDispatch介面,然後就可以通過此介面,以傳統的COM
方式控制CLR中的類.
    int ArgCount = argc-4;
    DISPID dispid;
    LPOLESTR rgszName = bstrMainFuncName;
    VARIANTARG *pArgs = new VARIANTARG[ArgCount];
    for(int i=0; i<ArgCount; i++)
    {
      VariantInit(&pArgs[i]);
      pArgs[i].vt = VT_BSTR;
      pArgs[i].bstrVal = _bstr_t(argv[4+i]);
    }
    DISPPARAMS dispparamsNoArgs = {pArgs, NULL, ArgCount, 0};
  
    CHECK(spDisp->GetIDsOfNames(IID_NULL, &rgszName, 1,
LOCALE_SYSTEM_DEFAULT, &dispid));
    CHECK(spDisp->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT,
DISPATCH_METHOD,
      &dispparamsNoArgs, NULL, NULL, NULL));
    delete[] pArgs;
  以上例子代碼,將命令列傳入參數放入參數數組,以IDispatch->Invoke調用指定名字
的方法.其後台操作均由CCW進行傳遞.如果要直接運行一個Assembly,可以使用
IAppDomain.ExecuteAssembly更加便捷.如
  CHECK(spAppDomain->ExecuteAssembly(bstrAssemblyName, NULL));
  至此,一個簡單但完整的CLR Host程式就完成了,他可以以完全的Unmanaged Code
啟動CLR引擎,載入指定Assembly,以指定參數運行指定的類的方法.
  下面是完整的樣本程式,VC7編譯通過,VC6修改一下應該也沒有問題.
  
hello.cs
  
using System;
  
namespace Hello
{
    /// <summary>
    /// Summary description for Class1.
    /// </summary>
    public class Hello
    {
        public void SayHello(string Name)
        {
                Console.WriteLine("Hello "+Name);
        }
    }
}
  
ClrHost.cpp
  
// CLRHost.cpp : Defines the entry point for the console application.
//
  
#include "stdafx.h"
  
#include <mscoree.h>
  
#import <mscorlib.tlb> rename("ReportEvent", "ReportEvent_")
using namespace mscorlib;
  
#include <assert.h>
  
#include <string>
#include <memory>
#include <iostream>
using namespace std;
  
typedef HRESULT (__stdcall * GetInfoFunc)(LPWSTR pbuffer, DWORD cchBuffer,
DWORD* dwlength);
  
#define CHECK(v) \
  if(FAILED(v)) \
    cerr << "COM function call failed - " << GetLastError() << " at " <<
__FILE__ << ", " << __LINE__ << endl;
  
wstring GetInfo(GetInfoFunc Func)
{
  wchar_t szBuf[MAX_PATH];
  DWORD dwLength;
  if(SUCCEEDED((Func)(szBuf, MAX_PATH, &dwLength)))
    return wstring(szBuf, dwLength);
  else
    return NULL;
}
  
int _tmain(int argc, _TCHAR* argv[])
{
  CComPtr<ICorRuntimeHost> spHost;
  
  CHECK(CorBindToRuntimeEx(NULL, L"wks",
    STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN | STARTUP_CONCURRENT_GC,
    CLSID_CorRuntimeHost, IID_ICorRuntimeHost, (void **)&spHost));
  
  wcout << L"Load CLR " << GetInfo(GetCORVersion)
        << L" from " << GetInfo(GetCORSystemDirectory)
        << endl;
  
  CHECK(spHost->Start());
  
  CComPtr<IUnknown> spUnk;
  CComPtr<_AppDomain> spAppDomain;
  
#ifdef _DEBUG
  CHECK(spHost->GetDefaultDomain(&spUnk));
  spAppDomain = spUnk; spUnk = NULL;
  wcout << L"Default AppDomain is " << (wchar_t
*)spAppDomain->GetFriendlyName() << endl;
  
  CHECK(spHost->CurrentDomain(&spUnk));
  spAppDomain = spUnk; spUnk = NULL;
  wcout << L"Current AppDomain is " << (wchar_t
*)spAppDomain->GetFriendlyName() << endl;
  
  HDOMAINENUM hEnum;
  CHECK(spHost->EnumDomains(&hEnum));
  spUnk = NULL;
  while(spHost->NextDomain(hEnum, &spUnk) != S_FALSE)
  {
    spAppDomain = spUnk; spUnk = NULL;
    wcout << (wchar_t *)spAppDomain->GetFriendlyName() << endl;
  }
  CHECK(spHost->CloseEnum(hEnum));
#endif // _DEBUG
  
  if((argc < 2) || (argc == 3))
  {
    cerr << "Usage: " << argv[0] << " <Assembly Name> <Class Name> <Main
Function Name> <Parameters>" << endl;
  }
  else
  {
    spUnk = NULL;
    CHECK(spHost->GetDefaultDomain(&spUnk));
    spAppDomain = spUnk; spUnk = NULL;
  
    _bstr_t bstrAssemblyName(argv[1]);
    if(argc == 2)
    {
      CHECK(spAppDomain->ExecuteAssembly(bstrAssemblyName, NULL));
    }
    else
    {
      _bstr_t bstrClassName(argv[2]),
              bstrMainFuncName(argv[3]);
  
      _ObjectHandlePtr spObj =
spAppDomain->CreateInstance(bstrAssemblyName, bstrClassName);
      CComPtr<IDispatch> spDisp = spObj->Unwrap().pdispVal;
  
      DISPID dispid;
      LPOLESTR rgszName = bstrMainFuncName;
      DISPPARAMS dispparamsArgs = {NULL, NULL, 0, 0};
  
      int ArgCount = argc-4;
      if(ArgCount > 0)
      {
        dispparamsArgs.cArgs = ArgCount;
        dispparamsArgs.rgvarg = new VARIANTARG[ArgCount];
        VARIANTARG *pArgs = dispparamsArgs.rgvarg;
        for(int i=0; i<ArgCount; i++)
        {
          VariantInit(&pArgs[i]);
          pArgs[i].vt = VT_BSTR;
          pArgs[i].bstrVal = _bstr_t(argv[4+i]);
        }
      }
  
      CHECK(spDisp->GetIDsOfNames(IID_NULL, &rgszName, 1,
LOCALE_SYSTEM_DEFAULT, &dispid));
      CHECK(spDisp->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT,
DISPATCH_METHOD,
        &dispparamsArgs, NULL, NULL, NULL));
      delete[] dispparamsArgs.rgvarg;
    }
  }
  
  CHECK(spHost->Stop());
  
    return 0;
}
  

相關文章

E-Commerce Solutions

Leverage the same tools powering the Alibaba Ecosystem

Learn more >

Apsara Conference 2019

The Rise of Data Intelligence, September 25th - 27th, Hangzhou, China

Learn more >

Alibaba Cloud Free Trial

Learn and experience the power of Alibaba Cloud with a free trial worth $300-1200 USD

Learn more >

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。