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

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

 using System;

 namespace Test
{
class Program
{
static void Main(string[] args)
{
int height = ;
int weight = ;
People.Find();
People developer = new Developer()(height, weight);
bool isHealthyWeight = developer.IsHealthyWeight();
bool isRich = developer.IsRich();
}
} class People
{
int _height;
int _weight; public People(int height, int weight)
{
_height = height;
_weight = weight;
} public virtual bool IsRich(); public bool IsHealthyWeight()
{
var healthyWeight = (Height - ) * 0.7;
return Weight <= healthyWeight * 1.1 && Weight >= healthyWeight * 0.9;
} public static string Find(string id) { return ""; }
} class Developer : People
{
public Developer(int height, int weight) : base(height, weight)
{ } public override bool IsRich()
{
return false;
}
} }

*图片不清楚可以放大看

首先判断类型是否都加载,用到了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#这本书,小弟算是总结一下,谢谢观看。

你的C#代码是怎么跑起来的(二)的更多相关文章

  1. 转【面向代码】学习 Deep Learning(二)Deep Belief Nets(DBNs)

    [面向代码]学习 Deep Learning(二)Deep Belief Nets(DBNs) http://blog.csdn.net/dark_scope/article/details/9447 ...

  2. 一只代码小白git托管路上的二三事

    [经验]一只代码小白git托管路上的二三事 写在前面的话 寒假的时候,娄老师给我们布置了代码托管的作业,并要求把托管地址发给学委.因假期的时候没有带电脑回家,所以只是在手机上草草注册了,也稀里糊涂就将 ...

  3. Java代码加密与反编译(二):用加密算法DES修改classLoader实现对.class文件加密

    Java代码加密与反编译(二):用加密算法DES修改classLoader实现对.class文件加密 二.利用加密算法DES实现java代码加密 传统的C/C++自动带有保护机制,但java不同,只要 ...

  4. CLR via C#(01)-.NET平台下代码是怎么跑起来的

    1. 源代码编译为托管模块 程序在.NET框架下运行,首先要将源代码编译为托管模块.CLR是一个可以被多种语言所使用的运行时,它的很多特性可以用于所有面向它的开发语言.微软开发了多种语言的编译器,编译 ...

  5. 你的C#代码是怎么跑起来的(一)

    写了那么多C#代码,大家有没有想过自己写的代码编译后的可执行文件内部是什么样子,是怎样在系统上运行的? 编译成exe,然后双击exe文件运行,这中间到底发生了些什么呢,这篇先来剖析下exe内部的样子: ...

  6. 你编写的Java代码是咋跑起来的?

    如果你是一名 Java 开发人员,你肯定指定 Java 代码有很多种不同的运行方式.比如说可以在开发工具(IDEA.Eclipse等)中运行,可以双击执行 jar 文件运行,也可以在命令行中运行,甚至 ...

  7. PyODPS DataFrame 的代码在哪里跑

    在使用 PyODPS DataFrame 编写数据应用时,尽管编写的是同一个脚本文件,但其中的代码会在不同位置执行,这可能导致一些无法预期的问题,本文介绍当出现相关问题时,如何确定代码在何处执行,以及 ...

  8. VSCode代码修改后跑起来没反应,打开本地文件,代码没变化

    两种解决办法: 首先:修改VSCode默认配置文件,点击左下角设置标志图 -> 设置,出来了设置相关的东西,搜索 files.autoSave 第一种:把"files.autoSave ...

  9. JavaScript代码段整理笔记系列(二)

    上篇介绍了15个常用代码段,本篇将把剩余的15个补齐,希望对大家有所帮助!!! 16.检测Shift.Alt.Ctrl键: event.shiftKey; //检测Shift event.altKey ...

随机推荐

  1. Linux下Nagios的安装与配置[转]

    一.Nagios简介 Nagios是一款开源的电脑系统和网络监视工具,能有效监控Windows.Linux和Unix的主机状态,交换机路由器等网络设置,打印机等.在系统或服务状态异常时发出邮件或短信报 ...

  2. linux网站服务Apache+php+mysql的安装

    1.挂载光盘 自己习惯将光盘挂载在/media/cdrom目录,在做本地yum源的时候此目录为默认目录之一 [root@localhost /]# mount /dev/cdrom /media/cd ...

  3. 一:Shell基础

    1.shell概述  shell是一个命令行解释器,它为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用shell来启动,挂起,停止甚至是编写一些程序:  shell还是 ...

  4. FineReport报表系统实例方案之医院院长查询分析系统

    医院院长查询系统 目前,大中型医院的信息处理正从传统手工方式飞速向电脑信息化建设方案转变,一个大中型医院担负着繁重的医疗和科研任务,以及繁杂的事务性工作,院长必须时刻与各科室保持密切的连续,以便随时了 ...

  5. CoCreateInstance调用COM接口伪流程

    在编写组件程序时,经常会使用CoCreateInstance直接取COM组件的接口,非常方便,那CoCreateInstance到底干了些什么事呢?1.CoCreateInstance取COM组件的接 ...

  6. Android+Sqlite 实现古诗阅读应用(二)

    传送门:Android+Sqlite 实现古诗阅读应用(一) Hi,又回来了,最近接到很多热情洋溢的小伙伴们的来信,吼开心哈,我会继续努力的=-=! 上回的东西我们做到了有个textview能随机选择 ...

  7. GTAC 2015将于11月10号和11号召开

    今年的GTAC注册已经结束,将会在11月10号和11号在Google马萨诸塞州剑桥办公室召开.大家可以关注https://developers.google.com/google-test-autom ...

  8. 贪吃蛇(C++实现,VC6.0编译,使用了EasyX图形库)

    程序效果: 代码: //main.cpp 1 #include <iostream> #include<fstream> #include <graphics.h> ...

  9. 在Ubuntu14.04下安装vsftp服务器

    猜想在Ubuntu下搭建ftp服务器来实现windows和ubuntu下文件互传是一件很简单的事儿,但是在网上找了好几篇文章都不行,故自己在这里总结一下方法. 首先安装vsftp服务器 sudo ap ...

  10. fancybox的配置项

    Fancybox的API和配置选项说明 属性名 默认值 简要说明 padding 10 浏览框内边距,和css中的padding一个意思 margin 20 浏览框外边距,和css中的margin一个 ...