CLR的执行模型

文章导读数据库

1.将源代码编译为托管模块编程

2.将托管模块合并为程序集windows

3.加载CLR数组

4.执行程序集代码安全

5.Framework类库---FCL网络

6.通用类型系统---CTS数据结构

7.公共语言规范---CLS架构

什么是CLR异步

简单的翻译过来:公共语言运行时。编程语言

这家伙跟使用那种编程语言无关,只要你的编译器是面向CLR的就行,他跟随.net framework 一块儿安装。

固然了,只有托管模块的运行才须要CLR,非托管代码生成的模块,那就另当别论了!

目前,微软已经编写出了多种面向CLR的语言编译器,好比:微软C/C++,C#,VB,F#,以及IL。

好了,知道了什么是CLR,接下来就开始探索程序背后的秘密吧!

出于我的缘由,本系列文章中的代码语言,以及开发工具均为C#,VisualStudio 2010

将源代码编译为托管模块

咱们用本身擅长的语言完成代码的编写以后,编译器首先要把源代码编译成一个托管模块。

不管使用哪一种编译器,最终的结果都是一个托管模块

那么什么是托管模块呢?

托管模块:简单来讲就是一个标准的windows PE文件,32位程序就是PE32,64位程序就是PE32+。

托管模块包含的内容:

  PE32或PE32+头,这个头标识了文件是GUI,CUI仍是DLL,以及和本地CPU代码有关的信息;

  CLR头,这个头里面包含了使一个模块成为一个托管模块所需的信息,他包含CLR版本,Main方法的MethodDef元数据标记等信息;

  元数据,元数据就是一组数据表的集合,主要包含两种表:描述模块中定义的类型和成员,描述模块引用的类型和成员;

  IL代码(注意:IL代码存在于托管模块中),编译器将IL代码编译成本地的CPU指令;

注意:本地代码编译器生成的是面向特定CPU架构的代码;面向CLR的编译器生成的都是IL代码。

一般状况下,编译器生成的元数据老是嵌入到和IL代码相同的EXE/DLL文件中,这样就保证了这两种数据老是保持同步。

说到这里,咱们顺便再说一下[元数据]的做用:

  首先,有了元数据,编译器能够直接从托管模块中读取元数据,消除了对本地C/C++头和库文件的需求;

  而后,也是咱们最经常使用的一个功能:VisualStudio的智能感知,经过解析元数据,能够指出一个类型提供了哪些的字段,属性,方法,若是是方法的话还能指出方法所需的参数类型;

  还有,CLR的代码验证也是经过元数据来确保你的代码只能执行类型安全的操做;

  再者,有了元数据,类型中的字段也能够被序列化到内存块儿中,经过网络发送到另一台机器,反序列化以后重建类型对象;

  最后,元数据还容许垃圾回收器跟踪对象的生命周期,以肯定什么时候进行垃圾回收;

题外话:在全部的面向CLR的编译器中,只有C/C++编译器最特殊,由于他既能编译托管代码,也能编译非托管代码;

将托管模块合并为程序集

前面说了那么多托管模块,其实,CLR并不直接和托管模块关联,CLR直接面向的是程序集。

程序集是一个抽象的感念,简单来讲,他就是对模块和资源文件进行的逻辑分组;同时,程序集也是程序重用,安全和版本控制的最小单元;在CLR的世界里,程序集就是一个组件。

编译器先是把源代码编译生成托管模块,而后再把托管模块合并为一个程序集文件,这个程序集文件其实也是一个标准的PE文件(没错,咱们前面说过,托管模块也是一个PE文件,这是正确的!!!),与托管模块相似,程序集文件也包含一组数据块儿,称做[清单],这个清单就是另一组简单的元数据表,这些表记录了组成这个程序集的文件信息(托管模块)以及这些文件中定义的公共类型。而后,这些清单文件还记录了与这个程序集相关联的资源或数据文件信息。

注意了:托管模块是由编译器编译而来的,程序集是由托管模块文件以及资源文件等[合并]而来的;

加载CLR

每一个程序集都被生成为一个可执行应用程序,或是DLL文件(包含一组被可执行应用程序使用的类型文件),CLR的任务就是管理这些程序集中的代码执行,因此,在执行这些文件以前,咱们必须确保.net framework已经在咱们的机器上正确安装。

在继续探索以前,咱们先了解一下32位系统和64位系统。若是程序集中只包含类型安全的托管代码,那么,这些代码文件将可以在任何安装了.net framework的操做系统中运行。

可是,有时候,咱们写的代码只但愿在特定的windows版本中运行,为了实现这个目标,C#编译器提供了一个/platform命令行开关(在属性,生成,目标平台中),经过这个开关,咱们就能指定咱们的程序只能在特定的平台(CPU架构)中运行;

windows在执行一个可执行文件时,会先查看文件的头信息,以肯定程序是要32位仍是64位的地址空间,同时windows还会检查头文件中的CPU架构信息,以确保计算机的CPU架构与程序的CPU类型匹配。

在64位的windows操做系统中,还提供了一种称做“WoW64”(windows on windows64)的技术,这种技术经过模拟x86指令集,使那些包含了x86本地代码的32位应用程序运行在Itanium机器上,可是这样作的的性能损失也是比较大的!

在检查完程序集的头信息以后,windows会建立用于该程序的进程,windows加载MSCorEE.dll程序集到进程的地址空间中,而后进程的主线程会调用定义在MSCorEE.dll中的方法,该方法初始化(若是CLR为初始化)CLR,加载exe程序集,接着调用其入口函数:Main,此时,托管应用程序开始执行!

总结来讲,一个应用程序的执行过程能够分为如下几个步骤:

  1.windows检测EXE文件头,是32?64?WoW64?;

  2.建立对应版本的windows进程;

  3.在(2)中建立的进程里面加载对应版本的MSCoreEE.dll;

  4.由(2)建立的进程主线程调用MSCoreEE.dll中定义的方法,暂时称为MethodA(注意:真实dll中并不存在此方法名);

  5.MethodA初始化CLR,加载EXE程序集;

  6.调用Main()方法;

注意:使用x86开关编译的dll没法在非64位windows系统的64位进程中运行,可是能够在64位windows系统中的64位进程中做为WoW64应用程序运行;

执行程序集代码

为了执行一个方法,必须把该方法的IL代码转换成本地CPU指令,而这一功能是由CLRJIT编译器(记着这个NB的[编译器])提供的;

在执行Main方法以前,CLR会检测Main方法中的全部引用类型,并为这些类型分配一个内部数据结构,该数据结构用于管理对全部引用类型的访问;

举例来讲,若是Main方法中调用了Console类的WriteLine(string message)方法,那么WriteLine方法是这样执行的:

  1.CLR分配[内部数据结构],暂且称其为ObjectA吧,Console类型中的每一个方法在ObjectA中都有一个对应的记录项,每一个记录项都指向一个[CLR内部未文档化的函数:  JITCompiler],这个函数就是前面说的NB的[编译器]

  2.WriteLine方法第一次调用时,JITCompiler函数会被调用,JITCompiler函数在程序集的元数据中查找WriteLine方法的IL代码;

  3.JITCompiler函数验证IL代码,将IL代码编译为本地CPU指令,而且将这些CPU指令保存到一个动态分配的内存块中;

  4.JITCompiler函数返回ObjectA,找到WriteLine方法对应的记录项,修改其最初对JITCompiler函数的指向,让它如今指向(3)中的内存块的地址;

  5.JITCompiler函数跳转到内存块中的代码,执行这些代码;

  6.代码执行完成,JITCompiler函数从新返回到Main方法中,继续执行下面的代码;

在同一个应用程序进程中,一个方法只有在首次执行的时候才会调用JITCompiler函数,JITCompiler函数将本地CPU代码保存到内存块中,之后对该方法的重复调用都会使用同一份本地CPU代码,可是,一旦应用程序终止,编译好的CPU代码也会被丢弃!

到这里,咱们应该会明白,托管代码(好比咱们用的C#)在真正执行以前是经历了两次编译的:

  1.由对应的语言编译器(C#编译器)编译为包含IL代码的托管模块;

  2.由JIT编译器(JITCompiler函数)将程序集中的IL代码编译为本地CPU指令;

通过了这两个步骤,托管代码就能够畅快的执行了!

但是,有人就要问了,非托管代码(C/C++)都是针对一种具体的CPU平台编译的,一旦调用,就能直接执行;托管代码却要编译两次,那托管程序是否是性能就差呢?是的,经历两次编译以后,确实会产生一些额外的性能开销。这点是咱们全部使用托管代码的人必须认可的!

可是,CLR的JIT编译器会针对本地代码进行优化,尽量将这些性能损失降到最低,并且,在某些状况下,通过优化的代码性能还要好过非托管代码(本人为验证,水平不够!!!)。咱们也可使用.net framework提供的NGen.exe本地代码生成器预生成一份本地代码,来提升程序的性能,;

前面讲到JIT编译器会将IL再编译为本地CPU代码,其实,在进行此次编译以前,CLR会执行一个名为[验证]的过程,这个过程会检查高级IL代码,确保代码所作的一切操做都是安全可靠的。

在windows中,每个进程都有它本身的地址空间,这样每一个应用程序就会占用一些地址空间,若是同时运行多个程序的话,势必会形成性能浪费,可是,CLR提供了一种很NB的能力:让多个托管应用程序在同一个系统进程中执行。在这种状况下,每一个托管应用程序都在系统进程中的一个独立AppDomain中运行,一个系统进程中能够同时存在多个AppDomain。

题外:默认状况下C#编译器只容许生成安全的(safe)代码,然而,同时也容许开发人员编写不安全(unsafe)代码;C#编译器要求全部包含不安全代码的方法都用unsafe关键字标记,并且,C#编译器还要求使用/unsafe编译器开关编译源代码;当JIT编译器试图编译一个unsafe方法时,会首先验证包含该方法的程序集是否被授予了System.Security.Permissions.SecurityPermission权限,并且System.Security.Permissions.SecurityPermissionFlag的SkipVerification标记是否已被设置,若是已被设置,JIT编译器就会编译不安全代码,而且容许它执行,不然,会抛出System.InvalidProgramException或System.Security.VerificationException异常;不过,默认状况下,从本地或是“网络共享”加载的程序集都会被授予彻底信任,经过Internet执行的程序集则不会授予执行不安全代码的权限;

咱们能够经过Peverify.exe这个工具来验证咱们想要验证的程序集的安全性,这个工具随VisualStudio一块儿安装具体使用方法能够参考MSDN

Framework类库

.net framework中包含了framework类库(Framework Class Library),FCL就是一组程序集的统称,其中定义了大量的类型,每一个类型都公开了一些功能,具备相关功能的一组类型能够放在同一命名空间下;开发人员能够利用这些类型建立各类应用程序:

  1.Web Service:利用Asp.net XML Web Service技术和Windows Communication Foundation(WCF)技术能够轻松的处理经过Internet发送的消息;

  2.Web Form:利用Asp.net能够开发基于HTML的应用程序;

  3.Windows Form:利用Windows窗体编程技术或是WPF编程技术,经过使用操做系统提供的功能,开发GUI应用程序;

  4.富Internet应用程序:利用Sliverlight技术;

  5.Windows控制台应用程序:编译器,应用程序和工具通常都是做为控制台实现;

  6.Windows服务;

  7.数据库存储过程;

  8.组件库;

部分常规FCL命名空间:

命名空间 内容说明
System 包含每一个类型都要用到的全部基本类型
System.Data 包含用于数据库通讯以及处理数据的类型
System.IO 包含用于执行流I/O以及浏览目录/文件的类型
System.Net 包含进行低级网络通讯,并与一些经常使用Internet协议协做的类型
System.Runtime.InteropServices 包含容许托管代码访问非托管操做系统平台功能的类型(好比com组件,win32函数,定制dll中的函数)
System.Security 包含用于保护数据和资源的类型
System.Text 包含处理各类编码方式的文本的类型
System.Threading 包含用于异步操做和同步资源访问的类型
System.Xml 包含用于处理XML架构和数据的类型

更多FCL类型的介绍,能够参考Microsoft SDK配套文档;

通用类型系统

通用类型系统:Common Type System(CTS);

framework中定义了大量的类型,如何定义一个类型,必须有一个规范,准则,这个工做就是CTS来作的。CTS描述了类型的定义和行为。

一个类型能够包含零个或者多个成员,具体的成员都有哪些呢?

  字段(Field):用来描述对象的状态,能够经过类型和名字区分;

  方法(Method):主要是针对对象执行一个操做,一般会改变对象的状态,方法包含:名称,签名(参数的类型和个数),若干修饰符,返回值(若是有的话);

  属性(Property):对调用者像是字段,对类型实现者像是方法,一般能够利用属性建立只读或是只写字段;

  事件(Event):实现了一种在对象和其余对象之间的通知机制;

除此以外,CTS还定义了类型的可访问性和类型成员的访问规则;

对于类型:

  有public,assembly(C#中使用internal修饰符);

对于类型成员:

  public:无限制访问;

  family or assembly:可由任何程序集的派生类型访问,也能够由同一程序集的任何类型访问,在C#中用protected internal修饰符标识;

  assembly:由同一程序集内的类型访问,一般用internal修饰符标识;

  family and assembly:成员可由派生类访问,但这些派生类必须在同一程序集内定义,C#没有实现这种机制;

  family:可由派生类访问,C#使用protected关键字标识;

  private;

除此以外,CTS还为类型继承,虚方法,对象生存周期等定义了相应的规则。

不管使用哪一种语言,类型的行为都是彻底一致的,由于最终都是由CLR的CTS来定义这些行为;

注:全部类型都必须从预约义的System.Object类型继承,这个类型定义了一组基本的行为:

  比较两个实例的想等性;

  获取实例的哈希码;

  查询一个实例的真正类型;

  执行实例的浅(按位)拷贝;

  获取实例对象的当前状态的一个字符串表示;

公共语言规范

COM容许使用不一样的语言建立的对象之间互相通讯,但是语言的种类不少,各自的语法,以及各自支持的类型都不相同,如何实现这种[通讯]呢?

俗话说:无规矩不成方圆,因此,为了实现这个宏伟的目标,就须要制定一套规矩,CLS(Common Language Specification)就是干这个的。CLR可以实现这种语言间的集成,是由于CLR使用了标准的类型集,元数据以及公共执行环境;那么CLS到底是如何规范这种集成呢?其实,CLS定义了一个全部语言都支持的最小功能子集,任何编译器生成的类型想要兼容于由其它“符合CLS,面向CLR的语言”所生成的组件,就必须支持这个最小功能集。

须要注意的是,若是要定义一个供其它语言使用的类型,该类型得是public或是protected,并且不能在该类型中使用位于CLS外部的任何功能功能;若是你定义了一个这样的类型,那么最好在类型上面添加一个验证:[assembly:CLSCompliant(true)],将该attribute应用于程序集便可;

至此,咱们稍微总结一下:在CLR中,一个类型的成员要么是一个字段(数据),要么是一个方法(行为),因此,每种编程语言都必须都必须能访问字段和方法。但是,每种编程语言都会公开各类概念,像是:枚举,数组,索引器,委托,事件,构造器,析构器,操做符重载,转换操做符等等。编译器在源代码中遇到上述任何一种构造,必须将其转化为字段或方法,使CLR和其余任何编程语言都可以访问这些构造。

相关文章
相关标签/搜索