zoukankan      html  css  js  c++  java
  • 你的C#代码是怎么跑起来的(二)

    接上篇:你的C#代码是怎么跑起来的(一)

    通过上篇文章知道了EXE文件的结构,现在来看看双击后是怎样运行的:

    双击文件后OS Loader加载PE文件并解析,在PE Optional Header里找到基地址和RVA,通过这两个确定了程序的入口地址,这个地址指向MsCorEE.dll的_CorExeMain(),执行它。_CorExeMain()开始执行,选择加载合适版本的CLR,CLR开始运行,CLR运行时会分配一个连续的地址空间用作托管堆,并用一个指针NextObjPtr指到开始位置,下次分配内存时就从指针指的位置开始。

    CLR运行后从CLR头里找到应用程序入口标识,也就是Main()方法的MethodDefToken,通过这个标识在元数据表MethodDef里找到Main方法的偏移位置,这样就可以找到Main()的IL代码。

    CLR检查Main方法里面是否有没加载的类型,没有的话就加载进来并在托管堆上建一个类型对象,类型对象包含静态字段,方法,基类的引用。然后给类型的方法表里每个方法一个存根,存根是用于标识是否被JIT编译过。

    JIT: just-in-time Compiler,即时编译器。

    JIT编译之前CLR会对Main方法的代码进行验证,确保类型安全且元数据正确,一切没问题后先检查类型方法表里这个方法的存根,不为空的话表示已经编译过就不需要再次编译,没有的话JIT把这段IL代码编译成本地代码保存到内存中并方法表的存根做上标记,然后JIT返回编译前的位置并把原来CLR指向JIT的地址修改为指向本地代码的地址,这样函数的本地代码开始执行。程序执行到哪里就编译到哪里,没有执行到的就不会加载和编译,同样的代码再次执行的话就直接在内存里拿了,这也是为什么第一次运行C#时比较慢而后面就快的原因。这样就开始陆续执行所有的代码,程序也就跑起来了。

    在内存上,运行线程会把函数的参数和局部变量压入线程栈上,栈上的空间默认是1M,方法的参数和局部变量都会压到函数的栈帧上,方法里的对象在托管堆NextObjPtr指向的位置分配内存并把内存地址存到栈上的局部变量里。CLR会给托管堆上的每个对象包括对象类型都添加两个字段,一个对象类型指针,一个同步块索引。

    说起栈帧,大家在调试代码时应该都喜欢用CallStack吧,这可以通过看调用栈很方便来定位出问题的具体原因,这个CallStack也就是方法的栈帧的具体显示,一级一级的。

    对象类型指针从字面上就很容易知道跟类型有关。CLR刚开始运行时就分配了一个Type的对象类型,他的对象类型指针指向自己,后面创建的对象类型的对象类型指针指针就指向这个Type,而new出来的对象的对象类型指针就指向它的类型,这样所有对象都能找到自己的类型使CLR在运行时能确保类型安全。

    同步块索引的格式是前6个标志位加后面26位内容(32位系统),作用则有好几个。

    1. 调用对象的gethashcode()后标志位改变一位,后26位会存储对象的hashcode,保证对象生命周期内hashcode的唯一;

    2. lock时用到,CLR会维护一个同步块数组,每项由一个指向同步块的指针和对象指针组成,lock时同样改变标识位,然后去同步块数组找一个闲置项,后26则变成这项在数组中的索引,有人要问了,刚才hashcode不是用了这26位吗,现在变了,hashcode岂不是丢了。确实,hashcode在lock之后不能直接存到索引了,不过同步块中专门准备了一个字段用来存hashcode,所以可以转移到同步块中,这样设计是为了节省内存,因为大部分情况下是不用lock的,也就不需要增加多余的同步块。

    另外为什么是索引而不是地址呢,因为同步块数组的大小不是固定的,随着对象的增多而变大,在内存上的位置可能会发生变化,所以用索引就不用管数组在哪个位置了。

    当线程进入lock后检查同步块的m_motion,发现没有标识则进入lock区域并把标识改变,如果已经有同一个线程进去则把计数器加1,如果已经有其他线程则等待。

    3. 垃圾回收时的标识,GC触发时首先认为所有的对象都是垃圾,由局部变量,寄存器,静态变量这些根向上找,凡是包含的对象都认为还有引用,在同步块索引上修改一位标识,当所有对象都遍历过后没有标识的对象就会被清掉,然后再是整理内存、修改引用地址等。

    看个简单的例子,只用于演示,不考虑合理性:

     1 using System;
     2 
     3 namespace Test
     4 {
     5     class Program
     6     {
     7         static void Main(string[] args)
     8         {
     9             int height = 170;
    10             int weight = 60;
    11             People.Find();
    12             People developer = new Developer()(height, weight);
    13             bool isHealthyWeight = developer.IsHealthyWeight();
    14             bool isRich = developer.IsRich();
    15         }
    16     }
    17 
    18     class People
    19     {
    20         int _height;
    21         int _weight;
    22 
    23         public People(int height, int weight)
    24         {
    25             _height = height;
    26             _weight = weight;
    27         }
    28 
    29         public virtual bool IsRich();
    30 
    31         public bool IsHealthyWeight()
    32         {
    33             var healthyWeight = (Height - 80) * 0.7;
    34             return Weight <= healthyWeight * 1.1 && Weight >= healthyWeight * 0.9;
    35         }
    36 
    37         public static string Find(string id) { return ""; }
    38     }
    39 
    40     class Developer : People
    41     {
    42         public Developer(int height, int weight) : base(height, weight)
    43         { }
    44 
    45         public override bool IsRich()
    46         {
    47             return false;
    48         }
    49     }
    50     
    51 }

    *图片不清楚可以放大看

    首先判断类型是否都加载,用到了int,bool,string,这些是在mscorlib.dll程序集的system命名空间下,所以先加载mscorlib.dll程序集,再把int,bool,string加到类型对象里。另外还有我们自己定义的Developer和People,也把类型对象创建好,另外也别忘了基类object,也要加载进来。(实际上还有double啊,这里就没画了)另外继承类的类型对象里面都有个字段指向基类,所以才能往上执行到基类方法表里的方法。

    局部变量都在线程栈上,Find()方法是静态方法,直接去People类型对象的方法表里去找,找到后看是否有存根标识,没有的话做JIT编译,有的话直接运行。

    developer的实例化虽然是用People定义的,但实例还是Developer,所以developer的类型对象指针指向Developer,对象里除了类型对象指针还有实例字段,包括基类的。内存分配在托管堆上,并把地址给到线程栈上的变量中。

    虚函数也一样,在运行时已经确定是Developer,所以会调用Developer方法表里的IsRich方法,一样先JIT,再运行。

    以上就是一个简单的C#程序的运行过程和在内存上的表现,本篇主要内容来自CLR via C#这本书,小弟算是总结一下,谢谢观看。

  • 相关阅读:
    OpenGL代码学习(10)--颜色混合
    OpenGL代码学习(9)--裁剪
    OpenGL代码学习(8)--画一个圆环 花托
    OpenGL代码学习(7)--开始接触3D效果
    OpenGL代码学习(6)--尝试现有知识画3D,行不通
    OpenGL代码学习(5)--2D图形上下左右移动
    OpenGL 代码学习(4)--三角形的7种图元分别展示
    OpenGL 7种基本图元
    OpenGL 常见的固定管线着色器
    OpenGL渲染架构图介绍
  • 原文地址:https://www.cnblogs.com/brookshi/p/5278284.html
Copyright © 2011-2022 走看看