.Net CLR R2R编译的原理简析

前言
躺平了好一段时间了,都懒得动了。本文均为个人理解所述,如有疏漏,请指正。

楔子
金庸武侠天龙八部里面,少林寺至高无上的镇寺之宝,武林人士梦寐以求的内功秘笈易筋经被阿朱偷了,但是少林寺也没有大张旗鼓的派出高手去寻找,为啥?
这种少林寺至高无上的内功秘笈,一般的江湖人士根本看不懂。除非内功深厚的高手。
来看看.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。欢迎大家一起讨论。

    原文作者:江湖评谈
    原文地址: https://www.cnblogs.com/tangyanzhi1111/p/16511852.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞