• CLR是如何工作的


    MetData和引擎初始化

    ====================

    托管Assembly本身只包含CLR可识别的MetaData(元资料), 不包含机器指令. 托管Assembly都与mscoree.dll绑定. mscoree.dll在system32目录下, 全称是Microsoft Core Execution Engine. 它的功能是选择合适的CLR Execution Engine来加载.

    2009-11-11 18-18-06

    多个版本的CLR可以共存. CLR的目录在C:\Windows\Microsoft.NET\Framework. 当前系统中最新版本的CLR对应的mscoree.dll文件被拷贝到system32目录下.

    当mscoree.dll加载后, 它根据托管代码的metadata和app.config, 选择恰当版本的引擎加载. 同时mscoree还负责判断应该用何种GC Flavor. GC Flavor包括Workstation GC和Server GC. 在CLR1中, Workstation GC对应到mscorwks.dll, 而Server GC对应到mscorsvr.dll文件. 在CLR2中虽然保留了mscorsvr.dll文件, 但是mscorwks.dll已经包含了两种GC Flavor的实现, 只需要加载mscorwks就可以了.

    CLR加载后, 先初始化CLR需要的各种功能, 比如必要的全局变量, 引擎需要的模块(ClassLoader, assembly Loader, JitEngine, Copntext等), 启动Finalizer thread和GC thread, 创建System AppDomain和Shared AppDomain, 创建RCDebugger Thread, 加载CLR基础类(比如mscorlib.dll, system.dll)

    当CLR引擎初始化完成后, CLR会找到当前exe的元数据, 然后找到Main函数, 编译Main函数, 执行Main函数.

    JIT动态编译

    =================

    1. 全托管代码

    假设C#函数foo1要调用foo2. 当CLR编译foo1的时候, 无论foo2是否已经编译成机器代码, call指令都是吧执行指向到跟foo2相关的一个内存地址(stub). 当执行这个call指令的时候, 如果foo2没有被CLR编译, stub中的代码就会把执行定向到CLR JitEngine, 这样对foo2的调用便导致了CLR JitEngine的启动来编译foo2函数. Jit Engine编译完成之后, CLR把编译好的机器代码拷贝到进程中由cLR管理的某一块内存(loader heap)上, 然后Jit Engine把编译好的foo2函数入口地址填回到stub中.

    通过这样的技术, 第二次对foo2调用的时候, foo2的stub指向的已经是编译好的地址了, 于是不需要再次编译. 当然第一次编译完成之后, JitEngine同时需要负责执行刚刚编译好的函数.

    2. 托管代码调用非托管代码

    在CLR的执行过程中, 如果使用到的都是托管代码, 编译和执行就按照上面的逻辑进行. 但是不可避免的, 托管代码需要调用非托管代码. 这里分两种情况.

    第一种是调用系统API和DLLImport. 比如CLR中使用FileStream打开一个文件, 最终还是要调用到CreateFileW. 通过DLLImport调用自定义的非托管函数, 以及COM Interop也属于这种情况.

    第二种是调用CLR Runtime的功能, 比如内存分配, 异常派发.

    两种情况都使用stub技术. 对于第一种情况, 不关事PInvoke还是COM Interop发生的时候, 托管代码调用的都是由CLR创建的stub. 在这个stub中CLR会做一些必要的工作, 然后把控制权交给对应的非托管代码. 必要的工作包括把必要的函数参数拷贝到非托管的内存上, marshal必要的类型, 锁住需要跟非托管代码交互的托管内存区域, 防止GC移动这块内存. 如果是COM Interop, 还包括对非托管接口指针进行必要的QueryInterface等等. 当非托管调用结束后, 执行权返回stub, 再次进行必要的工作后, 回到托管代码.

    第二种情况中, 对CLR功能的调用往往是隐式发生的.

    一类是编译器直接生成对CLR stub的调用. 比如new/throw关键词. 动态编译引擎对这些关键词的处理是生成函数调用到特殊的stub上, stub再把执行定位到CLR引擎中的关键函数. 就分配内存来讲, 比如new一个StringBuilder object, 动态编译生成的指令吧执行权定向到特殊的stub, 该stub包含了指令来调用CLRzhong的内存分配函数, 同时传入类型信息.

    另一类是通过吧托管代码标示为internal call来编译. Internal call表示该托管函数其实是某些unmanaged函数的映像, 编译引擎在编译internal call的时候, 会直接把标记的internalcall属性的CLR方法, 直接跟unmanaged的函数实现对应起来. 该对应关系是在CLR的实现中通过C++的一张静态表定义的.

    GC内存管理

    =================

    CLR引擎初始化的时候会向操作系统申请连续内存作为managed heap. 所有的managed object都分配在managed heap中. 对于任何一种托管类型, 由于类型信息保存在metadata中, 所以CLR清楚如何生成正确的内存格式.

    当托管类型分配请求定向到CLR中后, CLR首先检查managed heap是否足够. 如果足够, CLR直接使用鲜有内存, 根据类型信息填入必要的格式资料, 然后把地址传递给托管代码使用. 如果托管堆不够, CLR执行GC试图请扫除一部分内存. 如果GC无法清扫出内存, CLR 向OS请求更多的内存作为managed heap.如果OS拒绝内存请求, OutOfMemory就发生了. CLR内存分配的特点是:

    1. 大多数情况下比非托管代码内存分配速度快. CLR保持内部指针指向当前托管堆中的free point. 只要内存足够, CLR直接把当前指针所在地址作为内存分配出去, 然后用指针加/减分配出去的内存的长度. 对于非托管代码的内存分配,不管是Heap Manager, 还是Virtual Memory allocation, 都需要做相应计算才能找到合适的内存进行分配.

    2. 由于托管对象受到CLR的管理, GC发生的时候CLR可以对托管object 进行随意移动, 然后休整保存object的stub信息, 保证托管代码不会受此影响. 移动object 可以防止内存碎片的产生, 提高内存使用效率. 对于非托管代码来说, 由于程序可以直接使用指针, 所以无法进行内存碎片整理.

    3. GC可以在任何时候触发, 但是GC不能在任何时候发生. 比如某一个线程正在做regex的匹配, 访问到大量的托管object, 很多object的地址保存到CPU寄存器上进行优化. 如果GC发生,导致object地址变化, 恢复运行后CPU寄存器上的指针可能就会无效. 所以GC必须在所有线程的执行状态都不会受到GC 影响的时候发生. 当线程的执行状态不受影响时, 该线程的PreEmptive GC属性石1, 否则是0.  这个开关受到CLR的控制, 很多stub中的代码会操作这个开关. 比如托管代码调用了MessageBox.Show, 该方法最后会调用到MessageBox API. 在stub调用API从托管代码变化到非托管代码前, stub会通过CLR内部方法把PreEmptive设定为1, 表示GC可以发生了. 大致的情况是, 当线程idle的时候(线程idle的时候肯定是在等某一个系统API, 比如sleep护着WaitForSingleObject), PreEmptive为1. 当线程在托管代码中干活的时候, PreEmptive为0. 当GC触发的时候, GC必须等到所有的线程都进入了PreEmptive模式后, 才可以发生.

    Exception Handling 异常处理

    ==========================

    异常处理在CLR中也非常有特色. 比如, NullReferenceException和Access Violation其实是密切相关的. 当编译的托管代码执行的时候, 对于NULL object的访问, 首先出发的是Access Violation. 但是聪明的CLR已经设定好了对应的FS:[0]寄存器来截获可能的异常. CLR截获异常后, 首先检查异常的类型, 对于Access Violation, CLR先检查当前的代码是否是托管代码, 对应的类型信息是什么. 发现是NULLobject访问后, CLR再把这个Access Violaiton异常标记为已处理, 然后生成对应的NullReferenceException抛出来. 当NullReferenceException被CLR设定的FS:[0]截获后, CLR发现异常是CLR Exception, 于是找对应的catch语句执行.

    CLR异常发生之后可以打印出callstack, 原因在于CLR可以通过原数据采集所有的类型信息, 同时CLR在thread中通过多种机制记录运行状态. 保存在Stack中的Frame就是其中的一种重要的数据结构. Frame是CLR保存在stack中的小块数据结构. 当therad的执行状态发生改变的时候, 比如在托管代码和非托管代码中切换, 异常产生, remoting调用等等的时候, CLR会恰当的插入Frame来标示状态的改变. thread中所有的frame是通过指针链接在一起的, 所以CLR可以方便的获取一个thread的各种状态情况.

    总结:

    1. 运行托管assembly的时候, 先会加载mscoree.dll.

    2. 系统中最新的mscoree.dll被加载, 然后mscoree.dll根据托管assembly的metadata决定该加载那个版本的CLR. 同时加载GC Flavor.

    3. CLR执行初始化

    4. CLR找到当前exe的metadata, 找到, 编译, 执行main函数.

    5. 过程中后可能遇到另外的函数, 第一次运行的时候都要先编译, 然后用stub技术让调用者拿到编译后的函数入口, 完成调用.

    6. 运行过程中, 如果请求内存会用到GC的一些特性.

    7. 出了异常, 会用到CLR的一些特性.

    资料来源《Windows用户态程序高效排错》

  • 相关阅读:
    file_put_contents实现内容追加
    Laravel5.5 实现session配置
    easywechat在laravel框架中的应用-代码参考
    Laravel框架模型层表单验证
    工厂方法模式及php实现
    简单工厂模式及php实现
    Actionscript,AS3,MXML,Flex,Flex Builder,Flash Builder,Flash,AIR,Flash Player之关系
    php之依赖注入和控制反转
    php pdo操作数据库
    odbc连接数据库
  • 原文地址:https://www.cnblogs.com/awpatp/p/1601219.html
Copyright © 2020-2023  润新知