.Net CLR R2R編譯的原理簡析

2022-07-23 18:00:28

前言
躺平了好一段時間了,都懶得動了。本文均為個人理解所述,如有疏漏,請指正。

楔子
金庸武俠天龍八部裡面,少林寺至高無上的鎮寺之寶,武林人士夢寐以求的內功祕笈易筋經被阿朱偷了,但是少林寺也沒有大張旗鼓的派出高手去尋找,為啥?
這種少林寺至高無上的內功祕笈,一般的江湖人士根本看不懂。除非內功深厚的高手。
來看看.Net裡面看不懂的內功祕笈R2R原理。


概念:
R2R編譯實質上就是把方法執行的結果儲存在二進位制的動態連結庫裡面,在呼叫這個方法的時候,直接從動態連結庫裡面獲取到方法的結果。而不需要經過RyuJit繁瑣的編譯,提升程式的效能。是一種AOT的預編譯形式。


編譯
dotnet publish -c Release -r win-x64 -p:PublishReadyToRun=true


整體過程:
當CLI命令裡面標記了PublishReadyToRun,Rosyln重新編譯生成的動態連結裡面會生成Native Header,裡面儲存了當前模組的方法的執行結果。此後在CLR載入它的時候,CLR會查詢動態連結庫裡的Native Header是否存在,如果存在,則在呼叫方的時候,直接獲取到此方法的結果。
由於過程過於複雜此處只是提綱:

CLI(PublishReadyToRun:true)->Rosyln(Native Header) -> CLR (Get NH) 

預編譯儲存結構

typedef struct _IMAGE_RUNTIME_FUNCTION_ENTRY {
    DWORD BeginAddress;
    DWORD EndAddress;
    union {
        DWORD UnwindInfoAddress;
        DWORD UnwindData;
    } DUMMYUNIONNAME;
} _IMAGE_RUNTIME_FUNCTION_ENTRY, *_PIMAGE_RUNTIME_FUNCTION_ENTRY;


構成方式:
動態連結庫裡面會分配一段記憶體空間,稱之為Nativie Header。裡面儲存了包括如下內容:
1.編譯器識別符號(CompilerIdentifier)
2.匯入方法段(ImportSections)
3.執行時方法(RuntimeFunctions)
4.方法入口點(MethodDefEntryPoints)
5.異常資訊(ExceptionInfo)
6.偵錯資訊(DebugInfo)
7.延遲方法載入呼叫快(DelayLoadMethodCallThunks)
等等總共高達18項資訊,由於這些東西過於複雜此處只列出其中的前面幾個。構成了Native Header。


載入R2R
CLR在進行一個模組載入的時候,它會初始化R2R,如果判斷此模組有Native Header,那麼把裡面的18項資訊加入到記憶體當中。程式碼如下(過於複雜,省略了大部分)

PTR_ReadyToRunInfo ReadyToRunInfo::Initialize(Module * pModule, AllocMemTracker *pamTracker)
{
    // 此處省略一百萬行程式碼
    return new (pMemory) ReadyToRunInfo(pModule, pModule->GetLoaderAllocator(), pLayout, pHeader, nativeImage, pamTracker);
}

ReadyToRunInfo::ReadyToRunInfo(Module * pModule, LoaderAllocator* pLoaderAllocator, PEImageLayout * pLayout, READYTORUN_HEADER * pHeader, NativeImage *pNativeImage, AllocMemTracker *pamTracker)
    : m_pModule(pModule),
    m_pHeader(pHeader),
    m_pNativeImage(pNativeImage),
    m_readyToRunCodeDisabled(FALSE),
    m_Crst(CrstReadyToRunEntryPointToMethodDescMap),
    m_pPersistentInlineTrackingMap(NULL)
{
    // pHeader就是動態連結庫裡面的native header,它包含了Signature,MajorVersion,CoreHeader等。
    STANDARD_VM_CONTRACT;

    if (pNativeImage != NULL)
    {
        // 此處省略
    }
    else
    {
        m_pCompositeInfo = this;
        m_component = ReadyToRunCoreInfo(pLayout, &pHeader->CoreHeader);
        m_pComposite = &m_component;
        m_isComponentAssembly = false;
    }

    //獲取執行時R2R方法的記憶體虛擬地址和所佔的長度,後面用獲取到的索引得到R2R方法的入口地址
    IMAGE_DATA_DIRECTORY * pRuntimeFunctionsDir = m_pComposite->FindSection(ReadyToRunSectionType::RuntimeFunctions);
    if (pRuntimeFunctionsDir != NULL)
    {
        m_pRuntimeFunctions = (T_RUNTIME_FUNCTION *)m_pComposite->GetLayout()->GetDirectoryData(pRuntimeFunctionsDir);
        m_nRuntimeFunctions = pRuntimeFunctionsDir->Size / sizeof(T_RUNTIME_FUNCTION);
    }
    else
    {
        m_nRuntimeFunctions = 0;
    }
    

呼叫過程:
當你在C#程式碼裡面呼叫方法的時候,CLR檢測當前方法所在的模組是否包含R2R資訊,如果包含則獲取到R2R資訊,通過R2R資訊,獲取到Native Header裡面的RuntimeFunctions和MethodDefEntryPoints。然後通過這兩項計算出這個方法在RuntimeFunctions記憶體塊裡面的索引,通過這個索引計算出方法在RuntimeFunctions記憶體塊的偏移值,通過偏移值獲取屬性BeginAddress,也就是方法在二進位制動態連結庫裡面儲存的結果。過程比較複雜,下面貼出部分程式碼。

PCODE MethodDesc::GetPrecompiledR2RCode(PrepareCodeConfig* pConfig)
{
    STANDARD_VM_CONTRACT;

    PCODE pCode = NULL;
#ifdef FEATURE_READYTORUN
    Module * pModule = GetModule(); //獲取被呼叫的方法所在模組
    if (pModule->IsReadyToRun()) //檢測此模組思否包含R2R資訊
    {
	    //如果包含,則獲取到R2R資訊,然後獲取被呼叫方法的入口點
        pCode = pModule->GetReadyToRunInfo()->GetEntryPoint(this, pConfig, TRUE /* fFixups */);
    }
}

//獲取被呼叫方法入口點
PCODE ReadyToRunInfo::GetEntryPoint(MethodDesc * pMD, PrepareCodeConfig* pConfig, BOOL fFixups)
{
    mdToken token = pMD->GetMemberDef(); 
    int rid = RidFromToken(token);//獲取被呼叫方法的MethodDef索引
    if (rid == 0)
        goto done;

    uint offset;
    if (pMD->HasClassOrMethodInstantiation())
    {
	   //此處省略一萬字
    }
    else
    {
	    // 這個m_methodDefEntryPoints就是Native Header裡面的方法入口點項。通過函數入口點項獲取到被呼叫方法所在執行時方法(RuntimeFunctions)的索引
        if (!m_methodDefEntryPoints.TryGetAt(rid - 1, &offset))
            goto done;
    }

    uint id;
    offset = m_nativeReader.DecodeUnsigned(offset, &id);

    if (id & 1)
    {
        if (id & 2)
        {
            uint val;
            m_nativeReader.DecodeUnsigned(offset, &val);
            offset -= val;
        }

        if (fFixups)
        {
            BOOL mayUsePrecompiledNDirectMethods = TRUE;
            mayUsePrecompiledNDirectMethods = !pConfig->IsForMulticoreJit();

            if (!m_pModule->FixupDelayList(dac_cast<TADDR>(GetImage()->GetBase()) + offset, mayUsePrecompiledNDirectMethods))
            {
                pConfig->SetReadyToRunRejectedPrecompiledCode();
                goto done;
            }
        }

        id >>= 2;
    }
    else
    {
        id >>= 1;
    }

    _ASSERTE(id < m_nRuntimeFunctions);
	//上面經過了一系列的計算,把這個真正的索引id作為m_pRuntimeFunctions也就是native header項RuntimeFunctions的記憶體塊的索引,然後獲取到屬性BeginAddress,也就是被呼叫方法的入口點。
    pEntryPoint = dac_cast<TADDR>(GetImage()->GetBase()) + m_pRuntimeFunctions[id].BeginAddress;
	這個地方是更新了下被呼叫方法的入口點
    m_pCompositeInfo->SetMethodDescForEntryPointInNativeImage(pEntryPoint, pMD);
    return pEntryPoint;
}

以上參考如下:
1.https://github.com/dotnet/runtime/blob/main/src/coreclr/gc/gchandletable.cpp
2.https://github.com/dotnet/runtime/blob/main/src/coreclr/gc/gc.cpp
3.https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/readytoruninfo.cpp
4.https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/prestub.cpp
5.https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/nativeformatreader.h

結尾:
一直認為技術是可以無限制的免費分享和隨意攫取,如果你喜歡可以隨意轉載修改。微信公眾號:jianghupt QQ群:676817308。歡迎大家一起討論。