对.Net Framework的认识(1)
.Net Framework主要有两部分组成,CLR+类库。
3C: CTS(Common Type System), CLS(Common Language Specification), CLR(Common Language Runtime)。
CTS是个类型标准,任何以.NET平台作为目标的语言必须建立它的数据类型与CTS的类型间的映射,从而使符合CTS的各种语言之间可以无缝互操作,CTS还提供了语言间的继承,例如C#类继承VB.Net类。
CLS制定了一种以.NET平台为目标的语言所必须支持的最小特征,以及该语言与其他.NET语言之间实现互操作性所需要的完备特征。也就是说两种不同语言之间要想能够使用对方的成员及类型,则必须满足CLS,且只有在CLS的范围内的部分能够被互相使用。[assembly:CLSComplaint(true)]用于告诉编译器检查CLS相容的特性,只检查public和protected类型及成员。
CLR负责安全地载入和运行用户程序代码,包括对不用对象的垃圾回收和安全检查。在CLR监控之下运行的代码,称为托管代码(managed code)。
CLS是CTS的子集,CLR是CTS的具体实现。
.Net平台结构图:
CLR结构图:
结构图中的最底下两层也叫做CLI(Common Language Infrastructure)。
符合CTS的语言经过自己的编译器编译后会生成托管模块,托管模块合并成程序集后在CLR上运行。
有些语言一部分符合CTS(也就是托管代码),一部分不符合(也就是非托管代码),那么,符合的部分就会被编译成托管模块,不符合的则直接编译成机器码(native code)。
托管模块是个PE32(32位机器)或PE32+(64位机器)文件(Portable Executable,可移植可执行)包括
1、PE32/PE32+头:标准Windows PE头,如果是PE32头,则可以在Windows 32位或64位机器上运行,如果是PE32+头,则只能在Windows 64位机器上运行。
2、CLR头:描述了CLR版本(包括Major和Minor),PE文件入口(是元数据中方法定义表或者文件定义表的入口引用,如果是可执行文件必须有入口点,如果是非可执行文件,若是纯IL,可以没有入口点,如果嵌入了机器码,则必须把DLLMain作为入口点函数),资源(包括托管资源和非托管资源,IL汇编器在每个托管可执行文件中只能嵌入一个资源文件),文件是否受到强名称签名保护,文件里是否嵌入了机器码,是在32位机器上还是64位机器上运行,保存非托管代码的虚函数的表的大小和地址,元数据引用。
CLR头结构图:
3、元数据
4、IL(Intermediate Language)中间码,在需要时被CLR动态读取并由JIT编译成机器码执行,它是基于堆栈的。JIT编译器编译IL成机器码只在该IL第一次被执行时做,之后直接读取编译后的机器码,编译后的机器码是放在内存中的,如果应用程序结束则会被释放。
.Net应用程序结构分成应用程序域,程序集,模块(托管模块),类型,成员这几个层次,CLR加载管理应用程序域,包括把程序集加载到相应的应用程序域里并控制程序集中的内存布局。
强名称签名(strong name siganature)是用来认证程序集(Assembly),作为程序集的唯一性标识,并且防止被他人篡改冒充你发布的程序集。强名称签名包括没有扩展名的程序集名,版本号,语言化标志,公钥,以及用发布者的私钥进行签名。
元数据是由各种表和各种堆流(Heap Stream)构成。
元数据中的表分为三类,定义表,引用表和清单表。定义表描述了定义的内容,包括ModuleDef、TypeDef、MethodDef、FieldDef、ParamDef、PropertyDef、EventDef等;引用表描述了引用的内容,包括AssemblyRef、ModuleRef、TypeRef、MemberRef等;清单表描述了构成Assembly的文件,有Assembly中的文件实现的公共导出类型,与Assembly相关联的资源和数据文件,包括AssemblyDef、FileDef、ManifestResourceDef、ExportedTypesDef等。CLR会先加载清单表,然后根据清单表里的内容加载其他文件。
元数据中的表有很多有父子关系,为了优化这些关系并提供定位效率,父数据和子数据都会排序,父数据一般指向子数据的第一个,而子数据的最后一个是下一个父数据的第一个子数据的上一个,如图所示:
元数据中的堆流,分为6种,如图所示:
其中,
#Strings:用来存储元数据项的名字,如类名、方法名等 //Heap Stream
#Blob: 用来存储一些内部的对象实例,如默认值什么的 //Heap Stream
#US: 用户定义的字符串常量 //Heap Stream
#GUID: 包含各种全局统一标志符 //Heap Stream
#~: “优化的”、“压缩的”元数据,里面的元数据表以优化方式存储(我们在父子关系中刚刚提到过的) //Table Stream
#-: 非优化的元数据(和#~不能共存) //Table Stream
其中(#~或#-)、#GUID、#Strings是必不可少的。
操作元数据可以通过三种方法:
1、.Net的反射类库
2、微软提供的Metadata Unmanaged API
3、二进制层面的逆向工程分析
元数据的用途:
1、方便CLR定位执行IL代码。
2、方便各语言之间的交互操作。
3、验证代码,确保执行安全操作。
4、正反序列化。
5、GC跟踪对象生存期以及对象类型。
程序集:一个或多个托管模块/资源文件的逻辑分组,是最小的重用,安全性及版本控制单元。根据编译工具设置可以生成单文件程序集也可以生成多文件程序集。一个程序集可以有多个命名空间,一个命名空间也可以有多个程序集,因此在引用某个类型时,除了要确定命名空间外,还需要确定程序集。
当在进程中要执行托管代码时,需要加载CLR,一个Windows进程中只能加载一个版本的CLR,一旦CLR被加载到进程中,就无法被卸载,要想卸载只能终止进程。AppDomain是一个应用执行的独立环境,它存在于CLR中,一个进程里可以有多个AppDomain,该进程中的线程并不局限于任何一个AppDomain,但在特定的时间里,一个线程仅在一个AppDomain里运行。AppDomain是相互隔离的(包括对象,方法,数据等),每个AppDomain有自己的安全策略和配置策略。一个AppDomain发生异常或者崩溃不会影响其他AppDomain,它是一种“逻辑进程”。每个AppDomain都有自己的局部存储区(由.Net框架分配)。同一个程序集(Assembly)可以被多个AppDomain加载,一般情况下在每个AppDomain里它都有自己的一个实例,当这类程序集被加载到某个AppDomain后,要想卸载该程序集,必须卸载该AppDomain。不过有些特殊的程序集会被所有的AppDomain共享(比如MSCorLib.dll),这类特殊的程序集被加载在CLR中,只有当进程终止时才会被卸载。CLR初始化时会创建一个AppDomain,这个AppDomain就是Default AppDomain,它只有在进程终止时才会被卸载。
CTS的一些规定:
1、一个类型可以包含0或多个成员
2、类型可视化及类型成员访问规则
3、定义继承、虚方法和对象生成期的管理规则
4、所有类型必须继承自System.Object
使用[assembly:CLSComplaint(true)]标志程序集(必须放在namespace外),则编译器会检查程序集的CLS兼容性。对于不符合CLS语法的方法,在另一个语言里是无法看到它的。
COM:Component Object Model,由dll和exe组成。
与COM的互操作:
1、托管代码可以调用DLL中的非托管函数
2、托管代码可以使用现成的COM组件
3、非托管代码可以使用托管类型
CUI:Console User Interface,控制台程序
GUI:Graphics User Interface,窗口程序
创建强命名程序集步骤:
1、生成公钥/私钥对,使用SN命令,该命令大小写敏感
如:SN -k MyKey.keys
MyKey.keys是用来保存生成的公钥/私钥对的文件
2、将原有程序集变成强命名程序集
如:csc /keyfile:MyKey.keys app.cs
这里的app.cs必须是包含清单表的文件,不能对不包含清单表的文件签名。编译器会打开保存公钥/私钥对的文件用私钥签名并把公钥嵌入清单表中。其中,用私钥对文件签名是指程序集的FileDef清单中列出的文件的内容被私钥进行哈希处理并与文件名一起存入FileDef中。
GAC(Global Assembly Cache):用于缓存许多应用程序需要用到的dll,避免把这些dll都拷贝一份到应用程序的执行目录下,比如System.Data。可以通过GACUtil /i sample.dll把自己的程序集安装到GAC里,也可以通过GACUtil /u sample.dll把自己的程序集移出GAC,安装到GAC的程序集必须是强签名的。拷贝GAC目录中的dll,可以通过cmd先到C:\WINDOWS\assembly下,然后根据需要的dll到达它所在的目录,然后用copy命令拷贝到其他文件夹下。由于GAC需要运行命令来安装及卸载并且获取它里面的dll比较麻烦,所以它不属于简单复制式部署。另外GAC安装还需要一定级别的系统权限才行。
is和as关键字:is用于匹配类型是否正确,as用于转换类型,都不会抛出异常,as转换失败就赋null值。
在C#中string和String的区别在于string是C#映射String的,而String是.Net Framework自带的类。
值类型是在stack中分配内存,引用则是在heap中分配。struct和enum都是值类型。装箱就是把值类型变成引用类型,拆箱就是把引用类型变成值类型(当然前提是要能够变成),建议值类型最好别装箱操作,因为影响效率。另外值类型不受GC(垃圾回收机制)影响,因为它是分配在stack里的。
装箱的过程是在Heap分配内存,把字段值复制到新分配的内存中,返回对象地址。
拆箱的过程就是根据引用获取到对象内部值类型对应的地址,无需再分配内存和复制。
每一个实例对象都包含一个引用指向实例对象对应的类型(包含静态字段和方法表)。
基元类型和FCL(Framework Class Library)类型
基元类型就是特定语言自己定义的一些关键字,映射相应的FCL类型。
FCL类型是受CLR支持的.Net Framework标准类型。
不同的语言有些基元类型名字会一样,但会对应不同的FCL类型。
下图是C#的基元类型及对应的FCL类型及是否满足CLS规则