当写好了C#的源代码后,C#编译器会把源代码编译成一个托管模块而非最终的机器语言。这个涉及到CLR的相关功能,因为CLR就是为了将不同的语言编写(例如C#、VB)出来的源代码可以更好地在不同平台使用,而不是针对某一种架构CPU进行编译。编译器编译出托管模块后,最终通过JIT编译器动态实时编译托管模块中的IL(中间代码)代码为CPU指令。
回过头来说托管模块,托管模块主要包含这些数据:元数据、IL中间代码、CLR头、PE32+头(32位系统为PE32)
元数据:一种数据表,其中包含着该模块内一些数据。有三种元数据表,分别是定义元数据表、引用元数据表、清单元数据表。定义元数据表的内容为该模块源代码中定义的类、字段、方法等信息,引用元数据表的内容为该模块引用了哪些类、成员等。清单(Manifest)则是程序集的一个必备要求,托管模块都有元数据,但不一定都有清单数据表。当一个托管模块有了清单之后,它就可以看作是一个程序集。清单当中记录着该程序集中的其他文件的名称,如下图用ILDasm查看的一个程序集的清单内容:
因为我在文件头using了很多其他的程序集,所以清单中就会显示出来我这个程序集引用了那些其他的程序集。除了这些,清单还记录着一些类似于版本号以及安全信息的一些内容在末尾。
IL中间代码:编译器生成的中间代码,之后CLR中的JIT编译器会将其翻译成最终的CPU指令
CLR头:包含使这个模块成为托管模块的信息,例如一些标志、托管模块入口方法(Main)的元数据token、以及一些其他不重要数据的位置/大小。CLR头在托管模块中并不是很重要,数据大小占比也是非常小。
PE32+头:标准的Windows PE文件头
托管模块的核心内容主要是前面两个,即元数据以及IL中间代码。这四种数据组成了托管模块,但CLR并不通过托管模块工作,CLR通过程序集进行工作。程序集是一个抽象的概念,它可以看作是一个或多个托管模块的逻辑性分组。程序集除了托管模块,还包括程序集中需要用到的资源文件,例如jpg等
程序集一般不是exe就是dll,这俩的区别就是,exe是有自己的程序入口的(Main),dll则是功能性比较多,没有自己的入口。例如在Visual Studio中,一个项目就可以看作是一个程序集。之前提到过托管模块的清单,这里细说一下多个或者单个模块是如何合并为一个程序集的。首先,程序集是必须有清单元数据表的,因为清单记录着程序集的版本、语言文化、发布者等内容,还包括着构成程序集的文件的描述文件。
举个例子,比如我现在有两个源代码文件在一个项目里,分别是RUT.cs和FUT.cs,分别定义不同的类型与方法,这两个源代码就可以看作是两个托管模块,后缀是.netmodule(因为编译器会将源代码编译成托管模块)现在要打包把他们合并成一个程序集。RUT里的东西不常用,而FUT里的东西常用,所以编译合并的时候我们就可以把清单元数据放到FUT.netmodule中,这样这两个托管模块就有了清单,可以看作是一个程序集。清单中记录着程序集中所有文件的信息,包括不常用的RUT.newmodule。
还有一些其他的形式,比如将清单元数据放到一个空的托管模块中
或者程序集中只有一个托管模块也是可以的。
CLR最终将源代码编程生成程序集,然后对程序集进行执行。所以流程大概就是源代码-托管模块-程序集。
程序集有很多好处,比如一个公司要对产品开发很多新的功能模块,就可以这些功能模块各自放到单独的一个程序集中(dll)。这些功能有些常用有些不常用,甚至有些功能甚至可能根本用不到,那么公司就可以只让用户下载用户能用到的那些功能模块,这样就可以减少下载量,同时程序的模块化也得到了保证。