C#的CLR组成和运转介绍
clr基本
CLR(Common Language Runtime)是一个可由多种编程语言使用的“运行时”。(例如:c#,c++/cli,vb,f#,ironpython,ironruby,il...)
CLR的核心功能内存管理、程序集加载、安全性、异常处理、线程同步、泛型、尾调用指令和基本的公共语言基础结构 (CLI) 类型系统等。
托管模块是一个标准的32位microsoft windows可移值执行体pe32文件(64位系统为pe32+),他们需要clr才能执行。
托管的程序集总是利用windows的数据执行保护和地址空间布局随机化,这2个功能旨在增强整个系统的安全性。
托管模块的组成部分:pe32(或pe32+)头,clr头,元数据,il中间代码。
本地代码编译器生成的是面向特定cpu架构的代码。相反每个面向clr的编译器生成的都是il代码。
源代码文件----》编译器----》托管模块
加载CLR
.net framework sdk提供了名为clrver.exe的命令行使用程序,它能列出一台机器上安装的所有clr版本。
c#编译器可以指定一个平台(基于x86 windows,x64 windows或者ia64 windows).net4.0之前默认anycpu,4.0 exe项目默认x86
如果一个非托管应用程序调用loadlibrary来加载一个托管程序集,windows会自动加载并初始化clr,以便处理程序集中的代码。
执行clr
托管程序集同时包含元数据和il。il是与cpu无关的机器语言。il比大多数cpu机器语言都要高级。il也能使用汇编语言来写。
为了执行一个方法,首先必须把它的il转换为本地cpu指令。这是clr的jit编译器的职责。
首次执行托管exe---->jitcompiler---->查找方法、从元数据中获取il、分配内存块、编译cpu指令、修改方法对应的记录项、跳转内存块中的本地代码
第二次执行托管exe跳过jitcompiler直接跳转内存块中的本地代码
对于大多数应用程序,因jit编译造成的性能损失并不显著,而且clr的jit编译器会对本地代码进行优化,优化后的代码会获得更出色的性能。
c#调试产生的pdb文件就是帮助调试器查找局部变量并将il指令映射到源代码。
release版本就是让发布程序经过jit优化,所以线上项目最好都以release版本发布
clr提供一个在操作系统进程中执行多个托管应用程序的能力。每个托管的应用程序都在一个appdomain中执行。
IL
IL是基于栈的。
微软提供ilasm.exe的il汇编器和一个名为ildasm.exe的il反汇编器。
IL指令是无类型的(typeless)
IL最大的优势并不在于它对底层CPU的抽象,而是应用程序的健壮性和安全性。
FramWork
framework class library是一组dll程序集的名称,其中包含数千个类型定义,每个类型都公开了一些功能。
通用类型系统(common type system)描述了类型的定义和行为。一个类型可以包含另个或者多个成员。
公共语言运行规范(common language specification)详细定义了一个最小功能集。任何编译器生成的类型要向兼容于由其他符合cls面向clr的语言所生成的组件,就必须支持这个最小功能集
响应文件
响应文件是一个文本,其中包含了一组编译器命令行开关。执行csc.exe时,编译器会打开响应文件,并使用其中包含的所有开关,感觉就像是这些开关直接在命令行传递给csc.exe。
响应文件能带来很多方便,因为不必每次在编译项目时,都手动指定需要的命令行参数。
c#编译器允许同时指定多个响应文件。
程序集
程序集是一个抽象的概念,clr实际上不和模块一起工作,而是和程序集一起。
程序集是一个或多个模块/资源文件的逻辑分组。是重用、安全性以及版本控制的最小单元。在clr中相当于一个“组件”
托管模块(il+元数据)+资源文件 ----》编译器----》程序集
默认情况,编译器会把生成的托管模块转换成程序集。
对于一个可重用的、可保护的、可版本控制的组件,程序集把它的逻辑表示和物理表示区分开。
在程序集的模块中还包含与引用的程序集有关的信息(例如版本号)。这些信息使程序集可以自描述。换句话说,clr能判断出为了执行程序集中的代 码,程序集中得依赖对象是什么。不需要在注册表或active directory domain services中保存额外的信息。
程序集的所有文件中,有一个文件容纳了清单。清单也是一组元数据表的集合,表中主要包含了作为程序集的组成部分的文件的名称。
可用单独的文件对类型进行划分、可在自己的程序集中添加资源或者数据文件、程序集包含的各个类型可以用不同的编程语言来实现。
元数据
元数据是一组数据表。其中的一些数据表描述可模块中定义的内容,比如类型及成员。还有一些元数据描述了托管模块引用的内容,比如导入的类型及成员。
元数据是一些老技术的超集,这些老技术包括com的“类型库”和“接口定义语言”文件。
元数据总是嵌入和代码相同exe/dll文件中。
元数据的用途:编译时元数据消除对本地c/c++头和库文件的要求,vs使用元数据帮助你智能感知代码,类型安全验证,序列化,gc管理。
元数据是一个二进制数据块,由几个表构成。这些表分为3个类别:定义表、引用表和清单表。
定义表:moduledef,typedef,methoddef,fielddef,paramdef,propertydef,eventdef
引用表:assemblyref,mouduleref,typeref,memberref
清单表:assemblydef,filedef,manifestresourcedef,exportedtypesdef
程序集还讲语言文化作为其身份标识的一部分。没有指定具体语言文化的程序集称为语言文化中性。
部署
程序集的打包没有任何特殊需求。不需要对注册表进行任何修改,clr就可以在应用程序的目录中查找引用的程序集。
也可以使用其他机制来打包和安装程序集文件,比如使用.cab文件。还可以将程序集文件打包成一个msi。
msi文件可实现程序集的“按需安装”,在clr首次尝试加载程序集的时候才安装这个程序集。
部署到和应用程序相同的目录中得程序集称为私有部署的程序集,这是因为程序集文件不和其他任何应用程序共享(除非其他应用程序也部署到这个目录中)。
clr尝试定位一个程序集文件时,总是先在应用程序基目录中查找。如果没有找到,就会扫描几个子目录。
对于可执行应用程序(exe),配置文件必须在应用程序的基目录中,而且必须采用exe文件的全名作为文件名,再加一个.config扩展名
对于microsoft asp.net web窗体应用程序,文件必须在web应用程序的虚拟根目录中,而且总是命名为web.config
共享程序集与强命名程序集
弱命名程序集与强命名程序集都是c#编译器或者al.exe产生。他们的区别在于强命名程序集使用发布者的公钥/私钥对进行了签名,它唯一性地标识了程序集的发布者。
一个程序集可以采取两种方式来部署:私有或全局。弱命名程序集只可以私有部署。
可以使用sn.exe来生成公钥/私钥对。
全局程序集缓存
如果一个程序集要由多个应用程序访问,必须把它放到一个已知的目录中,而且clr在检测到对该程序集的一个引用时,必须知道自动检查该目录。这个已知的位置成为全局程序集缓存(GAC)。
GAC通常位于c:\windows\assembly目录下(假定windows安装到c:\windows目录)
可以使用GACUtil.exe在GAC中安装一个强命名程序集
GAC目录是结构化的:其中包含许多子目录,并用一个算法来生成这些子目录的名称。
CLR解析引用类型
CLR先加载并初始化程序集,然后读取clr头,查找标识应用程序的入口方法的methoddeftoken。然后clr会检索 methoddef元数据表,找到该方法的il代码在文件中的偏移量,把这些il代码jit编译成本地代码。编译时会对代码进行验证以确保其类型安全性。 最后执行本地代码。
clr可以在三个地方找到类型:同一个文件(早起绑定)、不同的文件但同一个程序集(当前程序集清单目录)、不同的文件不同的程序集(其他程序集清单目录)。
对于clr来说,所有程序集都是根据名称、版本、语言文化和公钥来标识的。
GAC根据名称、版本、语言文化和cpu架构来标识程序集。
类型基础
所有类型都是从system.object派生
system.object提供4个公共实例方法equals,gethashcode,tostring,gettype。
system.object提供2个受保护方法memberwiseclone,finalize.
clr要求所有对象都用new操作符来创建。
new操作符所作的事情:1.它计算类型及其所有基类型中定义的所有实例字段所需的字节数。堆上的每个对象都需要一些额外的成员--“类型对象 指针”和“同步块索引”。这些成员由clr用于管理对象。这些额外成员的字节数会计入对象大小。2.它从托管堆中分配指定类型要求的字节数,从而分配对象 的内存,分配的所有字节都设为零。3.它初始化对象的--“类型对象指针”和“同步块索引”成员。4.调用类型的实例构造器,向其传入在对new的应用中 指定的任何实参。
clr最重要的特性之一就是类型安全性。在运行时,clr总要知道一个对象是什么类型。调用gettype方法,总是知道一个对象确切的类型是什么。
clr允许将一个对象转换成为它的实际类型或者它的任何基类型。
使用c#的is和as操作符来转型。
命名空间用于对相关的类型进行逻辑性分组,开发人员使用命名空间来方便地定位一个类型。
命名空间和程序集不一定是相关的。特别是同一个命名空间中的各个类型可能是在不同的程序集中实现的。
一个线程创建时,会分配一个1mb大小的栈。这个栈的空间用于向方法传递实参,并用于方法内部定义的局部变量。栈是从高位内存地址向地位内存地址构建的。
windows进程启动后,clr加载到其中,托管堆初始化,创建一个线程。当jit将方法的il代码转化成本地cpu指令时,会注意到方法内 部引用的所有类型。在这个时候clr要确保定义了这些类型的所有程序集已加载。然后利用程序集的元数据,clr提取与这些类型有关的信息,并创建一些数据 结构来标识类型本身。
堆上的所有对象都包含两个额外的成员类型对象指针和同步块索引。定义一个类型时,可以在类型的内部定义静态数据字段。为这些静态数据字段提供支 援的字节是在类型对象自身中分配的。在每个类型对象中,最后都包含一个方法表。在方法表中,类型定义的每个方法都有一个对应的记录项。
当clr确定方法需要的所有类型对象都已创建,而且方法的代码已经编译之后,就允许线程开始执行方法的本地代码。方法的“序幕”代码执行时必须在线程栈中为局部变量分配内存。
jit编译器在类型对象的方法表中查找引用了被调用方法的记录项,对方法进行jit编译,再调用jit编译的代码。
system.object的gettype方法返回的是存储在制定对象的“类型对象指针”成员中的地址。换言之,gettype方法返回的是指向对象类型的一个指针。这样一来就可以判断出系统中任何对象的真实类型。