前言

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

楔子

金庸武侠天龙八部里面,少林寺至高无上的镇寺之宝,武林人士梦寐以求的内功秘笈易筋经被阿朱偷了,但是少林寺也没有大张旗鼓的派出高手去寻找,为啥?

这种少林寺至高无上的内功秘笈,一般的江湖人士根本看不懂。除非内功深厚的高手。

来看看.Net里面看不懂的内功秘笈R2R原理。

概念:

R2R编译实质上就是把方法运行的结果存储在二进制的动态链接库里面,在调用这个方法的时候,直接从动态链接库里面获取到方法的结果。而不需要经过RyuJit繁琐的编译,提升程序的性能。是一种AOT的预编译形式。

编译

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

整体过程:

当CLI命令里面标记了PublishReadyToRun,Rosyln重新编译生成的动态链接里面会生成Native Header,里面保存了当前模块的方法的运行结果。此后在CLR加载它的时候,CLR会查找动态链接库里的Native Header是否存在,如果存在,则在调用方的时候,直接获取到此方法的结果。

由于过程过于复杂此处只是提纲:

  1. CLI(PublishReadyToRuntrue)->RosylnNative Header -> CLR (Get NH)

预编译存储结构

  1. typedef struct _IMAGE_RUNTIME_FUNCTION_ENTRY {
  2. DWORD BeginAddress;
  3. DWORD EndAddress;
  4. union {
  5. DWORD UnwindInfoAddress;
  6. DWORD UnwindData;
  7. } DUMMYUNIONNAME;
  8. } _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项信息加入到内存当中。代码如下(过于复杂,省略了大部分)

  1. PTR_ReadyToRunInfo ReadyToRunInfo::Initialize(Module * pModule, AllocMemTracker *pamTracker)
  2. {
  3. // 此处省略一百万行代码
  4. return new (pMemory) ReadyToRunInfo(pModule, pModule->GetLoaderAllocator(), pLayout, pHeader, nativeImage, pamTracker);
  5. }
  6. ReadyToRunInfo::ReadyToRunInfo(Module * pModule, LoaderAllocator* pLoaderAllocator, PEImageLayout * pLayout, READYTORUN_HEADER * pHeader, NativeImage *pNativeImage, AllocMemTracker *pamTracker)
  7. : m_pModule(pModule),
  8. m_pHeader(pHeader),
  9. m_pNativeImage(pNativeImage),
  10. m_readyToRunCodeDisabled(FALSE),
  11. m_Crst(CrstReadyToRunEntryPointToMethodDescMap),
  12. m_pPersistentInlineTrackingMap(NULL)
  13. {
  14. // pHeader就是动态链接库里面的native header,它包含了Signature,MajorVersion,CoreHeader等。
  15. STANDARD_VM_CONTRACT;
  16. if (pNativeImage != NULL)
  17. {
  18. // 此处省略
  19. }
  20. else
  21. {
  22. m_pCompositeInfo = this;
  23. m_component = ReadyToRunCoreInfo(pLayout, &pHeader->CoreHeader);
  24. m_pComposite = &m_component;
  25. m_isComponentAssembly = false;
  26. }
  27. //获取运行时R2R方法的内存虚拟地址和所占的长度,后面用获取到的索引得到R2R方法的入口地址
  28. IMAGE_DATA_DIRECTORY * pRuntimeFunctionsDir = m_pComposite->FindSection(ReadyToRunSectionType::RuntimeFunctions);
  29. if (pRuntimeFunctionsDir != NULL)
  30. {
  31. m_pRuntimeFunctions = (T_RUNTIME_FUNCTION *)m_pComposite->GetLayout()->GetDirectoryData(pRuntimeFunctionsDir);
  32. m_nRuntimeFunctions = pRuntimeFunctionsDir->Size / sizeof(T_RUNTIME_FUNCTION);
  33. }
  34. else
  35. {
  36. m_nRuntimeFunctions = 0;
  37. }

调用过程:

当你在C#代码里面调用方法的时候,CLR检测当前方法所在的模块是否包含R2R信息,如果包含则获取到R2R信息,通过R2R信息,获取到Native Header里面的RuntimeFunctions和MethodDefEntryPoints。然后通过这两项计算出这个方法在RuntimeFunctions内存块里面的索引,通过这个索引计算出方法在RuntimeFunctions内存块的偏移值,通过偏移值获取属性BeginAddress,也就是方法在二进制动态链接库里面存储的结果。过程比较复杂,下面贴出部分代码。

  1. PCODE MethodDesc::GetPrecompiledR2RCode(PrepareCodeConfig* pConfig)
  2. {
  3. STANDARD_VM_CONTRACT;
  4. PCODE pCode = NULL;
  5. #ifdef FEATURE_READYTORUN
  6. Module * pModule = GetModule(); //获取被调用的方法所在模块
  7. if (pModule->IsReadyToRun()) //检测此模块思否包含R2R信息
  8. {
  9. //如果包含,则获取到R2R信息,然后获取被调用方法的入口点
  10. pCode = pModule->GetReadyToRunInfo()->GetEntryPoint(this, pConfig, TRUE /* fFixups */);
  11. }
  12. }
  13. //获取被调用方法入口点
  14. PCODE ReadyToRunInfo::GetEntryPoint(MethodDesc * pMD, PrepareCodeConfig* pConfig, BOOL fFixups)
  15. {
  16. mdToken token = pMD->GetMemberDef();
  17. int rid = RidFromToken(token);//获取被调用方法的MethodDef索引
  18. if (rid == 0)
  19. goto done;
  20. uint offset;
  21. if (pMD->HasClassOrMethodInstantiation())
  22. {
  23. //此处省略一万字
  24. }
  25. else
  26. {
  27. // 这个m_methodDefEntryPoints就是Native Header里面的方法入口点项。通过函数入口点项获取到被调用方法所在运行时方法(RuntimeFunctions)的索引
  28. if (!m_methodDefEntryPoints.TryGetAt(rid - 1, &offset))
  29. goto done;
  30. }
  31. uint id;
  32. offset = m_nativeReader.DecodeUnsigned(offset, &id);
  33. if (id & 1)
  34. {
  35. if (id & 2)
  36. {
  37. uint val;
  38. m_nativeReader.DecodeUnsigned(offset, &val);
  39. offset -= val;
  40. }
  41. if (fFixups)
  42. {
  43. BOOL mayUsePrecompiledNDirectMethods = TRUE;
  44. mayUsePrecompiledNDirectMethods = !pConfig->IsForMulticoreJit();
  45. if (!m_pModule->FixupDelayList(dac_cast<TADDR>(GetImage()->GetBase()) + offset, mayUsePrecompiledNDirectMethods))
  46. {
  47. pConfig->SetReadyToRunRejectedPrecompiledCode();
  48. goto done;
  49. }
  50. }
  51. id >>= 2;
  52. }
  53. else
  54. {
  55. id >>= 1;
  56. }
  57. _ASSERTE(id < m_nRuntimeFunctions);
  58. //上面经过了一系列的计算,把这个真正的索引id作为m_pRuntimeFunctions也就是native header项RuntimeFunctions的内存块的索引,然后获取到属性BeginAddress,也就是被调用方法的入口点。
  59. pEntryPoint = dac_cast<TADDR>(GetImage()->GetBase()) + m_pRuntimeFunctions[id].BeginAddress;
  60. 这个地方是更新了下被调用方法的入口点
  61. m_pCompositeInfo->SetMethodDescForEntryPointInNativeImage(pEntryPoint, pMD);
  62. return pEntryPoint;
  63. }

以上参考如下:

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

.Net CLR R2R编译的原理简析的更多相关文章

  1. PHP的错误报错级别设置原理简析

    原理简析 摘录php.ini文件的默认配置(php5.4): ; Common Values: ; E_ALL (Show all errors, warnings and notices inclu ...

  2. Java Annotation 及几个常用开源项目注解原理简析

    PDF 版: Java Annotation.pdf, PPT 版:Java Annotation.pptx, Keynote 版:Java Annotation.key 一.Annotation 示 ...

  3. Java Android 注解(Annotation) 及几个常用开源项目注解原理简析

    不少开源库(ButterKnife.Retrofit.ActiveAndroid等等)都用到了注解的方式来简化代码提高开发效率. 本文简单介绍下 Annotation 示例.概念及作用.分类.自定义. ...

  4. [转载] Thrift原理简析(JAVA)

    转载自http://shift-alt-ctrl.iteye.com/blog/1987416 Apache Thrift是一个跨语言的服务框架,本质上为RPC,同时具有序列化.发序列化机制:当我们开 ...

  5. Spring系列.@EnableRedisHttpSession原理简析

    在集群系统中,经常会需要将Session进行共享.不然会出现这样一个问题:用户在系统A上登陆以后,假如后续的一些操作被负载均衡到系统B上面,系统B发现本机上没有这个用户的Session,会强制让用户重 ...

  6. SIFT特征原理简析(HELU版)

    SIFT(Scale-Invariant Feature Transform)是一种具有尺度不变性和光照不变性的特征描述子,也同时是一套特征提取的理论,首次由D. G. Lowe于2004年以< ...

  7. 基于IdentityServer4的OIDC实现单点登录(SSO)原理简析

    写着前面 IdentityServer4的学习断断续续,兜兜转转,走了不少弯路,也花了不少时间.可能是因为没有阅读源码,也没有特别系统的学习资料,相关文章很多园子里的大佬都有涉及,有系列文章,比如: ...

  8. 动态代理 原理简析(java. 动态编译,动态代理)

    动态代理: 1.动态编译 JavaCompiler.CompilationTask 动态编译想理解自己查API文档 2.反射被代理类 主要使用Method.invoke(Object o,Object ...

  9. 【超精简JS模版库/前端模板库】原理简析 和 XSS防范

    使用jsp.php.asp或者后来的struts等等的朋友,不一定知道什么是模版,但一定很清楚这样的开发方式: <div class="m-carousel"> < ...

随机推荐

  1. Linux操作系统,为什么需要内核空间和用户空间?

    点击上方"开源Linux",选择"设为星标" 回复"学习"获取独家整理的学习资料! 本文以 32 位系统为例介绍内核空间(kernel sp ...

  2. 面试突击44:volatile 有什么用?

    volatile 是 Java 并发编程的重要组成部分,也是常见的面试题之一,它的主要作用有两个:保证内存的可见性和禁止指令重排序.下面我们具体来看这两个功能. 内存可见性 说到内存可见性问题就不得不 ...

  3. HttpContext.TraceIdentifier那严谨的设计

    前言 Asp.Net Core中有一个不受人重视的属性HttpContext.TraceIdentifier,它在链路追踪中非常有用,下面是官方的定义: 在项目中一般会将该字段输出到每一条日志中,也可 ...

  4. ElasticSearch7.3学习(二十六)----搜索(Search)参数总结、结果跳跃(bouncing results)问题解析

    1.preference 首先引入一个bouncing results问题,两个document排序,field值相同:不同的shard上,可能排序不同:每次请求轮询打到不同的replica shar ...

  5. Android——RelativeLayout

    代码:<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android= ...

  6. npm run serve修改为npm run dev

    找到package.json文件,打开文件找到  "serve": "vue-cli-service serve"  这一行,把前面的 serve 修改 dev ...

  7. FFT 小记

    写在前面 \(Q:\) 为什么会心血来潮去学 FFT \(A:\) 当本蒟蒻还在努力消化凸包时:.所以本蒟蒻也来看一下 等等 摸头警告 .思维已经废了 About FFT FFT( \(Fast\ F ...

  8. Dubbo的基本使用

    Dubbo分为提供者和消费方  并且两者都要注册到ZK上 提供者 注解    @Service   这是dubbo包下的 消费组 注解    @Reference 远程注入 第一步导入依赖 <! ...

  9. Colab教程(超级详细版)及Colab Pro/Colab Pro+使用评测

    在下半年选修了机器学习的关键课程Machine learning and deep learning,但由于Macbook Pro显卡不支持cuda,因此无法使用GPU来训练网络.教授推荐使用Goog ...

  10. Swoole一键操作基于阿里云的RDS数据库迁移+OSS文件搬迁

    传统的数据库搬迁思路是把数据库表的结构及数据都查询出来,然后通过循环进行数据结构重组拼接.然后导出!数据量少的话,这样当然是没毛病.当数据量太大的时候,服务器的内存开销就吃不住了,很容易炸掉,导致服务 ...