4

.Net CLR R2R编译的原理简析 - 江湖评谈

 2 years ago
source link: https://www.cnblogs.com/tangyanzhi1111/p/16511852.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

.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。欢迎大家一起讨论。


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK