重温CLR(二)生成、部署以及程序集
将类型生成到模块中
class Program { static void Main(string[] args) { Console.WriteLine("Hi"); } }
该应用程序定义了program类型,其中有名为Main的public static方法。Main中引用了另一个类型System.Console。System.console是Microsoft实现好的类型,用于实现这个类型的各个方法的IL代码存储在MSCorLib.dll文件中。总之,应用程序定义了一个类型,还使用了其他公司提供的类型。
为了生存这个示例应用程序,请将上述代码放到一个源代码文件中(假定为Program.cs),然后再命令行执行以下命令
csc.exe /out:program.exe /t:exe /r:MSCorLib.dll Program.cs
这个命令行指示C#编译器生成名为Program.exe的可执行文件(/out : Program.exe)。生成的文件是Win32控制台应用程序类型(/t[arget]:exe)。
C#编译器处理源文件时,发现代码引用了System.Console类型的WriteLine方法。此时,编译器要核实该类型确实存在,它确实有WriteLine方法,而且传递的实参与方法形参匹配。由于该类型在c#源代码中没有定义,所以要顺利通过编译,必须向c#编译器提供一组程序集,使它能解析对外部类型的引用。MSCorLib.dll是特殊文件,它包含所有核心类型,包括Byte,Char,String,Int32等等。事实上,由于这些类型使用得如此频繁,以至于C#编译器会自动引用MSCorLib.dll程序集。
此外,/out:program.exe /t:exe开关是C#编译器的默认设定,所以能简化命令为
csc.exe program.exe
C#编译器生成的Program.exe文件,它是标准PE(可移植执行体,Portable Executable)文件
元数据概述
Program.exe文件中到底有什么?托管PE文件由4部分构成:PE32(+)头、CLR头、元数据以及IL。PE32(+)头是windows要求的标准信息。
CLR头是一个小的信息块,是需要CLR的模块(托管模块)特有的。这个头包含模块生成时所面向CLR的major(主)和minor(次)版本号;一些标志(flag);一个MethodDef token,该token指定了模块的入口方法(前提是该模块是CUI\GUI或windows store执行体);一个可选的强名称数字签名。最后,clr头还包含模块内部的一些元数据表的大小和偏移量。可以查看CorHdr.h头文件定义的IMAGE_COR20_Header来了解CLR透的具体格式。
元数据是由几个表构成的二进制数据块。有三中表,分别是定义表(definition table)、引用表(reference table)和清单表(manifest table)。下表总结了模块元数据块中常用的定义表。
编译器编译源代码时,代码定义的任何东西都导致在上表列出的某个表中创建一个记录项。此外,编译器还会检测源代码引用的类型、字段、方法、属性和事件,并创建相应的元数据表记录项。在创建的元数据中包含一组引用表,他们记录了所引用的内容。表2-2总结了常用的引用元数据表。
可以用反编译工具查看托管pe文件中的元数据。
Program.exe包含名为Program的TypeDef。program是公共密封类,从System.Object派生。program类型还定义了两个方法:main和.ctor(构造器)。
Main是公共静态方法,用IL代码实现
将模块合并成程序集
上一节讨论的Program.exe并非只是含有元数据的PE文件,他还是程序集(assembly)。程序集是一个或多个类型定义文件及资源文件的集合。在程序及的所有文件中,有一个文件容纳了清单(manifest)。清单也是一个元数据表集合,表中主要包含作为程序集组成部分的那些文件的名称。此外,还描述了程序集的版本、语言文化、发布者、公共导出的类型以及构成程序集的所有文件。
CLR操作的是程序集。换而言之,CLR总是首先加载包含清单元数据表的文件,再根据清单来获取程序集中的其他文件的名称。下面列出了程序集的重要特点。
1 程序集定义了可重用的类型
2 程序集用一个版本号标记
3 程序集可以管理安全信息
除了包含清单元数据表的文件,程序集其他单独的文件并不具备上述特点。
类型为了顺利地进行打包、版本控制、安全保护以及使用,必须放在座位程序集一部分的模块中。程序集大多数时候只有一个文件,就像前面的Program.exe那样。然后,程序集还可以由多个文件构成:一些事含有元数据的pe文件,另一些是.gif或.jpg这样的资源文件。为便于理解,可将程序集视为一个逻辑EXE或DLL。
Microsoft为什么引入程序集的概念?这是因为使用程序集,可重用类型的逻辑表示与物理表示就可以分开。例如,程序集可能包含多个类型。可以将常用类型放到一个文件中,不常用类型放到另一个文件中。如果程序集要从internet下载并部署,那么对于含有不常用类型的文件,加入客户端不实用那些类型,该文件就永远不会下载到客户端。
使用多文件程序集的额外三个理由
1 不同的类型用不同的文件,使文件能以“增量”方式下载(就像前面在internet下载的例子描述的那样)。另外,将类型划分到不同的文件中,可以对购买和安装的应用程序进行部分或分批打包/部署。
2 可在程序集中添加资源或数据文件。例如,假定一个类型的作用是计算保险信息,需要访问精算表才能完成计算。在这种情况下,不必在自己的源代码中签入精算表。相反,可以使用有一个工具,使数据文件称为程序集的一部分。数据文件可为任意格式——只要应用程序知道如何解析。
3 程序集包含的各个类型可以用不同的编码程序语言来实现。然后 可以用工具将所有模块合并成单个程序集。
总之,程序集是进行重用、版本控制和应用安全性设置的基本单元。它允许将类型和资源文件划分到单独的文件中。这样一来,无论你自己还是用户,都可以决定打包和部署那些文件,一旦CLR加载还有清单的文件,就可确定在程序集的其他文件中。
共享程序集和强命名程序集
CLR支持两种程序集:弱命名程序集(weakly named assembly)和强命名程序集(strongly named assembly)。两种程序集结构完全相同。也就是说他们都使用前面讨论的PE文件格式头、PE32(+)头、CLR头、元数据、清单表以及IL。生成工具也相同,都是C#编译器或者AL.EXE。两者的区别在于,强命名程序集使用发布者的公钥/私钥进行了签名。这一对密钥允许对程序集进行唯一性的标识、保护和版本控制,并允许程序集部署到用户机器的任何地方,甚至可以部署到internet上。
程序集可采用两种方式部署:私有或全局。私有部署的程序集是指部署到应用程序基目录或者某个子目录的程序集。弱命名程序集只能以私有方式部署。全局部署的程序集是指部署到一些公认公认位置的程序集。CLR在查找程序集时,会检查这些位置。强命名程序集既可私有部署,也可全局部署。
全局程序集缓存
由多个应用程序访问的程序集必须放到公认的目录,而且CLR在检测到对该程序集的引用时,必须知道检查该目录。这个公认位置就是全局程序集缓存(Global Assembly Cache,GAC)。GAC的具体位置是一种实现细节,不同版本会有所变化,一般在
%systemroot%\microsoft.net\assembly
GAC目录是结构化的:其中包含许多子目录,子目录名称用算法生成。永远不要将程序集文件手动复制到GAC目录;相反,要用巩固完成这项任务。工具知道GAC内部结构,并指导如何生成正确的子目录名。
强命名程序集能放篡改
用私钥对程序集进行签名,并将公钥和签名签入程序集,CLR就可验证程序集未被修改或破坏。程序集安装到GAC时,系统对包含清单的那个文件的内容进行哈希处理,将哈希值与PE文件中签入的RSA签名进行比较。此外,系统还对程序集的其他文件的内容进行哈希处理,并将哈希值与清单文件的FileDef表中存储的哈希值进行比较。任何一个哈希值不匹配,表明程序集至少有一个文件呗篡改,程序集将无法安装到GAC。
应用程序需要绑定到程序集时,clr根据被引用程序集的属性(名称、版本、公钥等)在Gac中定位该程序集。找到被引用程序集,就返回包含他的子目录,并加装清单所在的文件。如果被引用程序集不在GAC中,CLR会查找应用程序的基目录,然后查找应用程序配置文件中标注的任何私有路径。然后,如果应用程序有MSI安装,CLR要求MSI定位程序集,如果任何位置都找不到程序集,那么绑定失败,抛出FileNotFoundException。
如果强命名程序集文件从GAC之外的位置加载,CLR会在程序集加载后比较哈希值。也就是说,每次应用程序执行并加载程序集时,都会对文件进行哈希处理,以牺牲性能为代价,保证程序集文件内容没有被篡改。
“运行时”如何解析类型引用
class Program { static void Main(string[] args) { Console.WriteLine("Hi"); } }
编译以上代码并生成程序集(假定名为Program.exe)。运行应用程序,CLR会加重并初始化自身,读取程序集CLR头,查找表示了应用程序入口方法(main)的MethodDefToken,检索MethodDefToken元数据表找到方法的IL代码再文件中的偏移量,将IL代码JIT编译成本机代码,最后执行本机代码。
对这些代码进行JIT编译,CLR会检测所有类型和成员的引用,加载他们的定义程序集(如果尚未加载)。具体地说,IL call指令应用了元数据toekn 0A000003。该token表示memberRef元数据表中的记录项3。CLR检查该memberRef记录项,发现它的字段引用了TypeRef表中的记录项(system.Console类型)。按照
TypeRef记录项,CLR被引导至一个AssemblyRef记录项
这时CLR知道了它需要的是哪个程序集。接着,CLR必须定位并加载该程序集。
高级管理控制
以下是一个示例XML配置文件
<?xml version="1.0" encoding="utf-8"?> <configuration> <appSettings> <add key="webpages:Version" value="3.0.0.0"/> <add key="webpages:Enabled" value="false"/> </appSettings> <system.web> <compilation debug="true" targetFramework="4.5"/> <httpRuntime targetFramework="4.5"/> </system.web> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35"/> <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0"/> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35"/> <bindingRedirect oldVersion="1.0.0.0-5.2.4.0" newVersion="5.2.4.0"/> </dependentAssembly> </assemblyBinding> </runtime> <system.codedom> <compilers> <compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.8.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:6 /nowarn:1659;1699;1701"/> </compilers> </system.codedom> </configuration>
这个XML文件为CLR提供了丰富的信息,具体如下所示。
probing元素
查找弱命名程序集时,检查应用程序基目录下的AuxFiles和bin\subdir子目录。对于强命名程序集,clr检查GAC或者由codeBase元素指定的url。只有在未指定codeBase元素时,CLR才会在用用程序的私有路径中检查强命名程序集。
第一个dependentAssembly,assemblyIdentity和bindingRedirect元素
查找由控制着公钥标记31bf3856ad364e35的组织发布的、语言文化为中性的SomeClassLibrary程序集的1.0.0.0版本时,改为定位同一个程序集的3.0.0.0版本。
codeBase元素
查找由控制着公钥标记31bf3856ad364e35的组织发布的、语言文化为中性的SomeClassLibrary程序集的2.0.0.0版本时,尝试在标记URL处发现他
编译方法时,CLR判断它引用了哪些类型和成员。根据这些信息,“运行时”检查进行引用的程序集的AssemblyRef表,判断程序集生成时引用了哪些程序集。然后,clr在引用程序配置文件中检查程序集/版本,进行制定的版本号重定向操作。随后,clr查找新的、重定向的程序集/版本。最后,clr在机器的machine.config文件中检查新的程序集、版本并进行制定的版本号重定向操作。
利用这些配置文件,管理员可以实际地控制CLR加载的程序集。