码迷,mamicode.com
首页 > Web开发 > 详细

深入剖析.NET运行机制

时间:2015-11-26 20:53:11      阅读:210      评论:0      收藏:0      [点我收藏+]

标签:

深入剖析.NET运行机制

比较认同一个专业的说法,称对象之间的调用为 “消息传递”,正如其描述“通过发送和接受消息”。为什么说其专业?因为对于单机开发,调用者(用户)和被调用者(服务)处于同一台计算机内存之中,CPU执行指令仅仅是根据地址取出内存数据进行处理。但实质上仍然是“消息传递”,而分布式应用通过socket实现不同协议间的远程服务调用,更加直观。

.NET的程序集相关知识

有一个知识点十分重要,这被一些新手所忽视,导致他们有时很难理解一些技术书籍的内存原理分析。就是程序从编译到运行的过程。借此文,我来解释一下,如有错误,望大家指出,一定要指出。

当我们依赖google或者CTRL+C、CTRL+V编写完成代码之后,点击发布或生成,本机安装的编译器会对代码文件进行分析编译(C/C++针对特定CPU编译过程发生在宿主服务器上)。针对与.NET,机器上安装的CLR虚拟机,会让其内置的对应语言的编译器将代码编译为版本相符的MSIL语言,并且保存文件格式为dll,这个dll称之为托管程序集,也是一个PE文件。

.PE文件一般包含以下几个部分:

PE头

Data Directory(数据目录)

.text segment

.data segment

.rdata segment

.idata segment

.edata segment

....其他部分不再列出。

“那么CLR的托管程序集是怎么样的?我听说会有一个CLR头?还有元数据以及IL代码?(CLR via C#)” 当然,这是准确无误的,《CLR via C#》圣经读物,怎会轻易出错?

在windows XP之前的操作系统,这些section都被保存在 PE 文件中的.text segment中,并且.text 块中还保存了“非托管的存根函数”

什么意思呢?你可以理解为是一段在.net编译时(非.net编译可能是其他的存根代码或者没有存根代码)加入的非托管代码。而PE头中的某一块域(其实是 AddressOfEntryPoint 域)指向这段代码,当windows加载器加载PE文件时(托管程序集),便会执行这段代码,这段代码会调用_CoreExeMain_CoreDllMain的代码(前者针对exe文件,后者针对dll文件),由于这两个函数都是导入函数(依赖别的程序集),则windows加载包含此函数的mscoree.dll,再去找到 data directory (数据目录,这个目录也是在.net编译时生成的)。data directory包含了CLR头的位置和大小,在根据CLR头的位置在.text块中找到CLR头,CLR头包含了CLR的版本信息、程序集签名等很多信息,根据这些信息,非托管存根代码调用对应的CLR,并启动CLR,注意:如果CLR已经启动,则CLR直接加载程序集到内存。

但是XP以及之后的操作的系统,对windows加载器进行了升级,使其能够自动识别.net程序集,无需通过“非托管存根函数”来调用“_CoreExeMain”或“_CoreDllMain”,而是直接根据CLR头启动CLR。

那么,至此,.NET 的PE文件的编译和加载启动过程已经清晰了。接下来总结一下:

XP 之前的.net程序集PE文件:

PE文件各部分名称用处说明
PE Header 存储了PE文件格式(exe还是dll),创建信息,entry point(入口点) data directory 位置信息等  
data directory(数据目录) 包含了CLR头的位置和大小 注意,这个是.net编译时添加的块
.text 包含了CLR头,元数据,IL代码以及 非托管存根函数 注意,这个非托管存根函数也是.net编译时加入的
.idata 导入函数,即此PE运行依赖的其他程序集的函数 _CoreExeMain和_CoreDllMain函数便是从mscoree.dll导入的函数  
.edata 导出函数,即此PE公开的可对外服务的函数  
.rdata 常量以及只读数据  
..... 其他块,不赘述  

XP以及之后操作系统.NET的PE文件:

PE文件各部分名称用处说明
PE   Header 存储了PE文件格式(exe还是dll),创建信息等。如果程序集包含非托管代码, PE头还存储非托管代码的一些信息(entry point)  
CLR Header 记录了CLR版本,Main方法(如果有),还记录了元数据、IL代码 的位置和大小 注意:不在生成 data directory部分
.text 元数据,IL代码 注意:
1.不在生成专门用于调用_CoreExeMain和_CoreDllMain函数 的非托管存根函数
2.CLR头不在.text中,单独被放置为一个部分。
3.元数据,IL代码还是在.text 段中的,只是CLR via c#书中 为了更好说明,将其提取出来了。
.idata 导入函数,即此PE运行依赖的其他程序集的函数 _CoreExeMain和_CoreDllMain函数便是从mscoree.dll导入的函数  
.edata 导出函数,即此PE公开的可对外服务的函数  
.rdata 常量以及只读数据  
..... 其他块,不赘述  

CLR头是用来启动CLR,那么元数据和IL代码是用来干什么的呢?这里不再详述,比较麻烦,建议阅读《CLR via C#》。我这里只提供书中的一幅图:

技术分享

从这幅图中可以看出,元数据其中之一用途就是当IL代码再被JIT(即时编译)时,如果IL引用到了一个类型则会根据元数据去正确的创建类型对象。 当然,还有十分重要的一个作用就是:为开发人员提供反射机制。

应用程序域 应用程序域的作用,我想大家都应该知晓,主要是为了隔离代码影响。比如IIS中启动多个web应用,每个应用的错误不会影响到其他站点。这就是应用程序域的典型案例。 当CLR被启动后,首先会创建一个系统级别应用程序域。该程序域的创建由CLR主导,对开发者完全透明,系统应用程序域的主要功能:

  • 1)创建其他两个应用程序域(共享应用程序域和默认应用程序域)。
  • 2)将mscorlib.dll加载到共享应用程序域中(在下面将进一步讨论)。
  • 3)记录进程中所有其他的应用程序域,包括提供加载/卸载应用程序域等功能。
  • 4)记录字符串池中的字符串常量,因此允许任意字符串在每个进程中都存在一个副本。
  • 5)初始化特定类型的异常,例如内存耗尽异常,栈溢出异常以及执行引擎异常等。

我们注意到,系统应用程序域会自动创建共享应用程序域默认应用程序域,这对于开发人员也是透明的,也就是说一个.net程序一旦启动,就会有三个应用程序域。 系统应用程序域的作用已经说明了,而共享应用程序域主要是用于加载一些与 默认应用程序域无关 的代码,即“非用户代码”,比如mscoree.dll,一些system命名空间的类型等,由其名称可知,其内部加载的成程序集可被所有应用程序域访问。 默认应用程序域就是我们代码(用户代码)执行的地方,开发者还可以通过系统应用程序域创建多个应用程序域,但往往很少有这种需求。

内存工作方式

上面说完了程序集的编译以及加载原理,下面来主要说一下.net程序内存的工作方式。

现在假设我们的程序已经编译OK,并且被保存再磁盘上的一处位置。我们准备双击运行它,首先这必须是一个exe文件,否则,无法直接运行。随后,这个exe文件被加载到内存的代码区,(这里也说明一下,程序集尽量实现单一职责,否则加载了一个很大的程序集却只用小部分功能,简直是浪费内存!)当exe文件被加载到内存代码区后,windows加载器开始加载CLR,而后,CLR启动后根据exe文件的CLR头信息中的Main方法入口,从而进入用户代码。从Main方法开始,IL代码就被不断取出交给JIT即时编译成机器指令,从而达到功能实现。

OK,有人会问,所有的代码都会一次性交给IL进行即时编译吗?No!面向对象只是我们进行业务设计时更易建模领域模型,对于计算机,它永远只知道,你让它做什么,他就去做什么。绝不会多做任何一点儿。所以,一切都是面向函数!函数即指令!一个dll,加载到内存,没有被调用,没有main方法,它则永不会被JIT!当然我们只是举例,没有被调用,也根本不可能被加载到内存! 而Main方法就是所有程序的入口函数,Main方法其内部的所有代码都会被JIT(Main方法所在线程即主线程)。 如下文件:

    class Program{
        static void main(string[] args){
            Console.WeiteLine("Test");
            int a=1;
            int b=2;
            int c=add(a,b);
            Console.WriteLine(c);
        }   
        static int add(int a,int b){
            return a+b;
        }
    }

当CLR调用Main方法时,首先将Main方法的IL指令交给JIT翻译为机器指令,此时,发现Main方法属于Program类型,且是静态方法,便会在内存堆上分配空间,根据元数据中的TypeDef创建类型内部结构,即Type对象。注意:非program对象,因为并没有new指令。然后将Main方法的IL指令翻译为CPU指令,紧接着进入Main方法内部的其他IL代码,从而正式开始应用程序主线程的运行。内存分配图如下:

技术分享

    1.CLR调用Main方法;
    2.JIT到代码区查找元数据和IL代码
    3.JIT根据找到的信息编译出CPU指令,static构造函数和main函数
    4.编译后的Main函数开始执行,进入应用程序主线程
    5.调用add方法;(不应该指向 JIT?待思考)
    6.JIT将add方法的IL代码翻译为CPU指令并执行。

注意:JIT有非常强大的缓存功能,也就是说相同的代码在同一应用程序域不会被JIT第二次,而是使用之前JIT出的CPU指令

总结

本文主要针对.NET的运行原理进行了分析,但运行在虚拟机之上的语言的编一个运行过程大致相同,可能由于一些语言的性质不同,会做一些编译优化,比如F#函数式语言,scala多范式语言等。特别强调:本文为作者个人多年工作经验和书籍阅读带来的个人理解,并通过博文进行分享,有一个目的是希望有更加清晰明白的人能指出其中的不足或错误,以免误人子弟,同时学习进步。

有什么疑问,请留言,我们一起讨论,谢谢!

参考:

《CLR via c#》

《.NET高级调试》

 

深入剖析.NET运行机制

标签:

原文地址:http://www.cnblogs.com/phoozyan/p/4998727.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!