代码改变世界

C#编译基础知识(二)

2014-07-01 13:26  HaiZL  阅读(191)  评论(0)    收藏  举报

程序集(Assembly)与我们平时code的过程联系的是如此的紧密,但是更多情况下它们对我们又好像是透明的,就算完全不知道它是什么也可以照常编码,但有时候又会被莫名其妙的报错搞得十分的郁闷,在本文章里面将对程序集与托管模块做一个大致的介绍,理解其中的一些内部细节有助于深入理解CLR运行机理

1 程序集的基本认识


直观上看来,程序集好像就是一个dll,譬如说:一个类库的输出就是一个dll,项目添加引用时选择的也是dll,编译之后类文件都跑到dll中去了。这么理解也没什么错,毕竟借助实体对象可以更容易的理解抽象概念。这个程序集恰好就是一个抽象的概念。

首先,程序集是一个或多个模块/资源文件的逻辑性分组。其次,程序集是重用、安全性及版本控制的最小单元。取决于你对于编译器或工具的选择,既可以生成单文件程序集,也可以生成多文件程序集。在CLR的世界中,程序集是相当于一个“组件”。

CLR Via C# (第三版) 

一般情况下,单文件程序集就是一个dll或exe(以下的exe不特殊说明,都是指通过/t:exe生成的控制台应用程序)。要生成为exe,那么类中必须包含静态Main方法,如果包含了多个静态Main方法,则需要/main:开关指定需要作为入口方法所在的类型。一个包含了静态Main方法的类在选择生成为dll或exe时,唯一的区别在于CLR头和元数据中对入口位置的一个标记(如下图)。正因为exe也是一个程序集,所以生成的这个exe文件也一样可以被其他类引用并访问其中的类型。

  CLR头

元数据

 多文件程序集一般是指程序集包含了多个模块,或者内嵌了资源,最后的程序集就不光是一个dll,可能是一系列文件。注意,这个多文件程序集下面的例子演示了一个包含了多个模块程序集的生成:

        Eat.cs

public class Eat{
 public static string Doit(){
  return "eating...";
 }
}

       Travel.cs

public class Travel{
 public static string Doit(){
  return "traveling...";
 }
}

  使用/target:module开关编译生成两个模块文件,eat.netmodule、travel.netmodule。

  

  再使用/addmodule:将这两个模块文件合并到应用程序集,这个操作在vs中是无法进行的,只能通过命令行编译。

    csc /t:library /out:D:\Activity.dll /addmodule:D:\travel.netmodule /addmodule:D:\eat.netmodule

  这个操作也可以使用AL.exe(程序集链接器),查看AL命令的列表,你会发现好多开关的作用与csc里面的相同,它们的作用却是一致,只不过AL是面向程序集的,也就是与哪一个编程语言无关的,使用vb生成的module也可以用AL来链接。

  编译之后就会生产Activity.dll,如果用ildasm反编译查看,你会发现里面并没有包含任何类的IL信息,只有一个mainfest。但是在元数据中却有对所对应的模块的引用。

    

   元数据

     

  如果后续有类c1需要引用这个程序集,那么在c1生成时,这两个模块文件必须与activity.dll保持在同一目录,但是最后运行时则只需要c1引用的类对应的模块保持在同一目录即可。也就是说,在引用该程序集的类生成时,程序集的所有文件都需要在场,在运行时,则只需要实际引用了的模块在场就行。大家有兴趣可以试一下,这里就不举例了。

  前面提到,程序集还可以嵌入资源或文件,如.resx文件,在编译之后都会嵌入到dll中,这里使用的是/resource:开关,如果只是希望把指定文件链接为外部程序集资源,则只能使用/linkresource:开关。使用链接的方式时,这些托管模块、文件、资源,从逻辑上讲属于同一个程序集,换句话讲,就是说这个程序集是多文件的,深究这个多文件程序集的概念没什么意义,知道它存在这么个特性,了解如何使用就好了。关于在运行时嵌入\链接到程序集的资源、文件如何获取,可以参考msdn上关于Assembly类的方法描述。

2 托管模块


事实上在编译时,总是先把类信息编译到托管模块,然后再合并到程序集,除了csc.exe外,还可以使用AL.exe合并模块到程序集,csc不使用/:addmodule开关生成的程序集就只包含一个模块,模块中包含了如下的内容:

  PE32或PE32+头:这是一个标准的Windows PE文件头,这个文件头会标识文件可以在32位还是64位下运行,以及文件类型GUI、CUI 或DLL,和一个指出文件生成时间的时间标记。通过ildasm查看到这一标头信息内容可以看到这些内容。下面以编译生成时指定是32位还是64位,可以看到最终生成PE32头的差异。

  CLR头:其中包含了需要的CLR版本,一些标志,托管模块入口方法(Main方法)的MethodDef元数据标记,以及模块的元数据、资源、强名称、一些flag以及不太重要的的数据项的位置/大小。这个同样可以通过ildasm的标头信息内容查看到,前面在提到exe与dll程序集差异的时候给出了示例。

  元数据:主要有两种类型的表,一种类型的表描述源代码中定义的类型和成员,另一种类型的表描述源代码中引用的类型和成员。用ILdasm反汇编一个托管模块,选择视图-元数据-显示就可以看到内容,其中会包含像Global functions、Global fields、Global MemberRefs、TypeDef、TypeRef、AssemblyRef、AssemblyRef等描述,根据这些信息,程序集能够自我描述,在任何时刻CLR总是能知道类的任何信息,许多重要的过程都依赖于它,如序列化,反射,对象初始化,vs的智能提示也是依靠它而来。总的来说,元数据里面总是能包含关于这个模块的所有信息。

  IL(中间语言)代码:编译器编译源代码生产的代码,在运行时JIT将IL编译成本地指令。IL与我们的源代码一一对应,我们把经优化后的IL与源代码比较,还是很容易读懂的。

    .entrypoint 指令告诉CLR把这个函数标记为整个应用程序的入口点(Entry Point )

    .maxstack 8 指定在方法执行过程中需要的计算堆栈的最大值

    ldstr 从堆栈中加载一个字符

    call 调用方法

    ret 从当前方法返回

    

模块合并到程序集后,程序集里会有一个清单manifest来记录它所包含的模块信息,资源、引用及定义的程序集信息等,这个manifest的内容可以用ildasm工具查看。清单的信息主要由CLR运行时使用,做一些验证相关的操作。