Assembly学习心得
说明:
最近开始准备把学到的.NET知识重新整理一遍,眼过千遍不如手过一遍,所以我准备记下我的学习心得,已备参考。J
各位都是大虾了,如果有哪些错误或者不完整的地方,还请不吝指出。
多谢了。
本文分为两部分:
第一部分是一些必须了解的概念;
第二部分是一个完整的例子来逐一说明这些概念;
第一部分 基本概念
托管模块(Managed Module)
托管模块是一个需要CLR才能执行的标准Windows可移植可执行(portable executable,简称PE)文件。
元数据(Metadata)
简单的讲,元数据就是一个数据表的集合,在这些表中,其中一些用于描述托管模块中所定义的内容(比如所定义的类型和它们的成员),另外还有一些用于描述托管模块中所引用的内容(比如被引用的类型和它们的成员)。
URL: ms-help://MS.MSDNQTR.2004APR.1033/cpguide/html/cpconmetadataoverview.htm
程序集清单(Assembly Manifest)
程序集清单是另外一些元数据表的集合。这些表描述了组成程序集的文件,程序集所有文件中实现的公有导出类型,以及一些程序集相关的资源文件或数据文件。
ms-help://MS.MSDNQTR.2004APR.1033/cpguide/html/cpconAssemblyManifest.htm
1.程序集(Assembly)的概念:
首先:程序集是一个或多个托管模块,以及一些资源文件的逻辑组合。因为它是一个逻辑上的组合,所以程序集的逻辑表示和物理表示可以相互分离。如何将代码和资源划分到不同的文件中完全取决于我们。例如,我们可以将一些很少使用的类型或资源放在一个单独的Assembly Module中,然后根据需要(比如第一次用到的时候),从web上下载它们。如果没有用到,它们将不会被下载。这样既节省磁盘空间,也减少了安装时间。程序集允许我们将文件的部署分解开来,同时又将所有的文件看作一个单独的集合。
其次:因为CLR是直接和程序集打交道的,所以程序集也是组件复用,以及实施安全策略和版本策略的最小单元(安全策略,版本信息等都只能是加在程序集上)。
注意:程序集是一个逻辑组合,它可以包含很多个文件。大多数程序集(比如使用Visual Studio.NET创建的那些)一般都是单文件程序集,也就是只有一个.exe或者.dll文件(目前.NET的程序集只有这两种格式)。在这种情况下,程序集清单(manifest)直接嵌入到单文件程序集中。但是,你也可以用“程序集生成工具”(Al.exe)来创建多文件程序集。也可以只创建一个只包含清单的程序集。
2.强命名程序集(Strong Name Assembly)的概念
因为不同的公司可能会开发出有相同名字的程序集来,如果这些程序集都被复制到同一个相同的目录下,最后一个安装的程序集将会代替前面的程序集。这就是著名的Windows “DLL Hell”出现的原因。
很明显,简单的用文件名来区分程序集是不够的,CLR需要支持某种机制来唯一的标识一个程序集。这就是所谓的强命名程序集。
一个强命名程序集包含四个唯一标志程序集的特性:文件名(没有扩展名),版本号,语言文化信息(如果有的话),公有秘钥。
这些信息存储在程序集的清单(manifest)中。清单包含了程序集的元数据,并嵌入在程序集的某个文件中。
下面的字符串标识了四个不同的程序集文件:
“MyType, Version=1.0.1.0, Culture=neutral, PublicKeyToken=bf5779af662fc055”
“MyType, Version=1.0.1.0, Culture=en-us, PublicKeyToken=bf5779af662fc055”
“MyType, Version=1.0.2.0, Culture=neturl, PublicKeyToken=bf5779af662fc055”
“MyType, Version=1.0.2.0, Culture=neutral, PublicKeyToken=dbe4120289f9fd8a”
如果一个公司想唯一的标识它的程序集,那么它必须首先获取一个公钥/私钥对,然后将共有秘钥和程序集相关联。不存在两个两个公司有同样的公钥/私钥对的情况,正是这种区分使得我们可以创建有着相同名称,版本和语言文化信息的程序集,而不引起任何冲突。
与强命名程序集对应的就是所谓的弱命名程序集。(其实就是普通的没有被强命名的程序集)。两种程序集在结构上是相同的。都使用相同的PE文件格式,PE表头,CLR表头,元数据,以及清单(manifest)。二者之间真正的区别在于:强命名程序集有一个发布者的公钥/私钥对签名,其中的公钥/私钥对唯一的标识了程序集的发布者。利用公钥/私钥对,我们可以对程序集进行唯一性识别、实施安全策略和版本控制策略,这种唯一标识程序集的能力使得应用程序在试图绑定一个强命名程序集时,CLR能够实施某些“已确知安全”的策略(比如只信任某个公司的程序集)。
3. 如何创建强命名程序集(Strong Name Assembly)
创建一个强命名程序集首先需要获得一个用强命名实用工具(Strong Name Utility,即SN.exe,.NET SDK自带)产生的密钥。
下面简要介绍一下SN.exe的一些用法。
要产生一个公钥/私钥对:
a) SN –k MyCompany.Keys
该命名告诉SN.exe创建一个名为MyCompany.keys的文件。MyCompany.keys文件将包含以对以二进制格式存储的公有密钥和私有密钥。
b)查看公有密钥:
首先生成一个只包含公有密钥的文件:
SN –p MyCompany.keys MyCompany.PublicKey
然后用-tp参数查看:SN –tp MyCompany.PublicKeys
Public key is
0024000004800000940000000602000000240000525341310004000001000100bb7214723ffc13
901343df4b9c464ebf7ef4312b0ae4d31db04a99673e8163768cc0a2a7062e731dbeb83b869f05
09bf8009e90db5c8728e840e782d2cf928dae35c2578ec55f0d11665a30b37f8636c08789976d8
ee9fe9a5c4a0435f0821738e51d6bdd6e6711a5acb620018658cce93df37d7e85f9a0104a58450
53995ce8
Public key token is 2dc940d5439468c2
创建好了公钥/私钥对,创建强命名程序集就很容易了。只需要把System.Reflection.AssemblyKeyFileAttribute特性加入到源代码中就可以了: [assembly:AssemblyKeyFile("MyCompany.keys")]
说明:公钥/私钥对文件的扩展名可以是任意的(也可以没有),因为编译的时候都是以元数据的格式读取的。
4. 程序集的部署方式
一个程序集有两种部署方式:
a) 私有方式
和应用程序部署在同一目录下的程序集称作私有部署程序集。弱命名程序集只能进行私有部署。
b)全局方式
全局部署方式将程序集部署在一些CLR已确知的地方,当CLR搜索程序集时,它会知道到这些地方去找。强命名程序集既可以进行私有部署,也可以进行全局部署。
程序集种类
|
是否可以进行私有部署
|
是否可以进行全局部署
|
普通程序集
|
是
|
否
|
强命名程序集
|
是
|
是
|
5.如何部署强命名程序集(Strong Name Assembly)和GAC
a)GAC的概念
如果一个Assembly要被多个应用程序访问,那么他就必须放在一个CLR已确知的目录下,并且CLR在探测到有对该Assembly的引用时,它必须能自动到该目录下寻找这个程序集。这个已确知的目录称作GAC(Global Assembly Cache),就是全局程序集缓存。它一般位于下面的目录下:
GAC的作用就是提供给CLR一个已知的确定的目录去寻找引用的程序集。
b) GAC的内部结构
GAC是一个特殊的结构化的目录,用Windows Explorer浏览你会以为它只是一个包含很多程序集的普通目录。其实不是这样的,在命令行下查看,你会发现它实际上包含很多子目录,子目录的名字和程序集的名称是相同的,但它们都不是实际的程序集,实际的程序集位于程序集名对应的目录下。比如进入GCFWK子目录,我们会发现其中又有很多的子目录。
机器内每一个安装到GAC的GCFWK.dll在GCFWK中都会有一个子目录。这里只有一个目录表明只有一个版本的GCFWK程序集被安装。实际的程序集保存在每一个对应的版本目录下。目录的名称以下划线的形式分割为“(Version)_(Culture)_(PublicKeyToken)”。
GCFWK的语言文化信息为netture,就表示为1.0.0.0__bf5779af662fc055”。
表示得意义是:
“GCFWK, Version=1.0.0.0, Culture=neutral, PublicKeyToken=bf5779af662fc055”
如果语言文化信息为”ja”,就表示为”1.0.0.0_ja_bf5779af662fc055”
表示得意义是:
“GCFWK, Version=1.0.0.0, Culture=ja, PublicKeyToken=bf5779af662fc055”
GAC包含很多子目录,这些子目录是用一种算法来产生的,我们最好不要手动将程序集拷贝到GAC中,相反,我们应使用工具来完成这样的工作。因为这些工具知道GAC的内部结构J
在开发和测试中,最常用的工具就是GACUtil.exe。
在GAC中注册程序集跟COM注册差不多,但相对更容易:
1.把程序集添加到GAC中:
GACUtil /i sample.dll
(参数/i是安装的意思)
2.把程序集移出GAC
GACUtil /u sample.dll
(参数/u就移除的意思)
注意:不能将一个弱命名程序集安装到GAC中。
如果你试图把弱命名程序集加入到GAC中,会收到错误信息:” Failure adding assembly to the cache: Attempt to install an assembly without a strong name”
d) 强命名程序集的私有部署
把程序集安装到GAC有几个好处。首先,GAC使得很多程序可以共享程序集,这从整体上减少了使用的物理内存;其次,我们很容易将一个新版的程序集部署到GAC中,并通过一种发布者策略(差不多就是一种重定向方法,比如将原来引用版本为1.0.0.0程序集的程序,通过更改它的配置文件,转而让程序去引用版本为2.0.0.0的程序集)来使用新版本;最后,GAC还提供了对不同版本程序集的并存(side-by-side)管理方式。但是,GAC的安全策略通常只允许管理员更改,同时,向GAC中安装程序集也破坏了.NET框架的简单拷贝部署的许诺。
除了向GAC或者以私有部署方式部署强命名程序集之外,我们还可以将强命名程序集部署在仅为一小部分程序知道的某个任意目录下。配置每一个应用程序的XML配置文件,让它们指向一个公有目录,这样,在运行时,CLR将知道到哪里去找这个强命名程序集。但这样又有可能会引发”DLL Hell”的问题,因为没有哪个程序可以控制这个程序集何时被卸载。这在.NET中也是不被鼓励的。
6.并行执行(Side - By - Side)
这里是一个强命名程序集的例子:
首先有一个App.exe程序集,它绑定这一个版本为2.0.0.0的Calculus.dll程序集和一个版本为3.0.0.0的AdvMath.dll程序集。而AdvMath.dll程序集同时又绑定着一个版本为1.0.0.0的Calculus.dll的程序集。如下图:
An application that requires different versions of the Calculus.dll assembly
CLR能够将名程相同但路径不同的多个文件加载到同一个地址空间,这在.NET中称为并存执行(Side-By-Side)执行,它是解决Windows中”DLL Hole”问题的关键技术。
如.NET Framework 1.0 和.NET Framework 1.1都可以同是在一台机器上运行,这用到的就是并存执行(Side-By-Side)执行。
更多参考:
ms-help://MS.MSDNQTR.2004APR.1033/cpguide/html/cpconSide-by-SideExecutionTop.htm
http://www.microsoft.com/china/msdn/archives/library/dnnetdep/html/sidexsidenet.asp
7. CLR如何解析类型引用
CLR在解析一个被引用的类型时,它可以在以下三个地方的其中之一找到该类型:
· 同一个文件
对同一个文件中类型的访问在编译时就已经确定下来了,CRL直接从该文件中加载被引用的类型。完成加载后,程序将继续运行。
·不同的文件,相同的程序集
CLR首先确保被引用的文件在当前程序集清单中的FileDef表内。CLR然后会在加载程序集清单文件的目录中查找被引用的文件。该文件被加载的同时,CLR会检查它的散列值以确保文件的完整性,之后便会找到相应的类型成员。完成加载后,程序将继续运行。
·不同的文件,不同的程序集
当被引用的程序集在一个不同的程序集文件中时,CLR会首先加载包含被引用程序集的清单所在的文件。如果该文件没有包含所需要的类型,CLR会根据此清单文件加载适当的文件。这样也会找到相应类型的成员。完成加载后,程序将继续运行。
如果在解析类型引用的过程中出现任何错误,比如文件找不到,文件不能被加载,散列值不匹配等等,系统将会抛出相应的异常。
下图演示了类型的绑定过程:
程序集种类
|
是否可以引用弱命名程序集
|
是否可以引用强命名程序集
|
普通程序集
|
是
|
是
|
强命名程序集
|
否
|
是
|
第二部分 实例
下面是一个完整的例子来逐一说明上面所提到的概念,以加深理解。
整个实例包含7个文件(在主目录下):
主目录为 …/Assembly ----源程序目录
…/Assembly/Bin ---- 编译结果输出的目录,也就是应用程序主目录。
文件名
|
类型
|
说明
|
App.cs
|
Code源文件
|
主程序,包含程序入口,属于namespace1
|
ClassA.cs
|
Code源文件
|
类型A,包含一个静态方法,属于namespace1
|
ClassB.cs
|
Code源文件
|
类型B,包含一个静态方法,属于namespace2
|
AssemblyInfo.cs
|
Code源文件
|
包含程序集签名信息,版本信息等
|
App.Key
|
公钥/私钥对文件
|
用来给程序集签名,生成强命名程序集
|
App.PublicKey
|
只包含共有密钥
|
只储存共有密钥,用SN.exe来查看
|
App.exe.config
|
Xml格式配置文件
|
App.exe的应用程序配置文件
|
源代码
App.cs
namespace namespaceA {
public class App {
static void
System.Console.WriteLine(ClassA.ShowMe());
System.Console.WriteLine(namespaceB.ClassB.ShowMe());
}
}
}
ClassA.cs
namespace namespaceA
{
public class ClassA {
public static string ShowMe() {
return "This is ClassA";
}
}
}
ClassB.cs
namespace namespaceB
{
public class ClassB {
public static string ShowMe() {
return "This is ClassB";
}
}
}
AssemblyInfo.cs
////////////////////////////////////////////////////////////////////////////////
// Module: AssemblyInfo.cs
////////////////////////////////////////////////////////////////////////////////
using System.Reflection;
////////////////////////////////////////////////////////////////////////////////
// Set CompanyName, LegalCopyright, and LegalTrademarks
[assembly: AssemblyCompany("App Company")]
[assembly: AssemblyCopyright("Copyright (C) 2004 @ App Company")]
[assembly: AssemblyTrademark("App is a test only program")]
////////////////////////////////////////////////////////////////////////////////
// Set ProductName and ProductVersion
[assembly: AssemblyProduct("App Product")]
[assembly: AssemblyInformationalVersion("1.0.0.0")]
////////////////////////////////////////////////////////////////////////////////
// Set FileVersion and AssemblyVersion
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyTitle("App type assembly")]
[assembly: AssemblyDescription("App Aassembly is a test only assembly")]
////////////////////////////////////////////////////////////////////////////////
// Set Culture
[assembly: AssemblyCulture("")]
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile("App.key")]
[assembly: AssemblyKeyName("")]
App.key和App.PublicKey是二进制格式存储的,不能直接查看。后面例子中会用到。
1.把源代码编译为托管模块(Managed Module)
csc /out:bin/classA.module /t:module classA.cs
参数: /out: 输出路径
/t: 输出格式。可以有四种,分别是:
library ---- DLL程序集
exe ---- 控制台可执行程序(也是程序集的一种)
winexe ---- Windows可执行程序(同样也是程序集的一种)
module ---- 托管模块(程序集的一部分)
说明:
托管模块的的扩展名可以是任意的(也可以没有),因为编译的时候都是以元数据的格式读取的。
2.把源代码编译为程序集(Assembly)
l把ClassB编译为一个单文件程序集
csc /out:bin/classB.dll /t:library classB.cs
l把App.cs,ClassA.module和ClassB.dll编译为一个多文件程序集
csc /out:bin/App.exe /t:exe app.cs /addmodule:bin/classA.module /r:bin/classB.dll
参数:
/addmodule: 把托管模块添加到程序集中
/r: 添加引用
说明:
上面生成的程序集因为没有经过公钥/私有签名,所以生成的是非强命名类型的程序集。
生成的程序集App.exe的清单中只包含对classA.module托管模块的说明,并不包含classA.module的元数据,所以App.exe和classA.moudle必须在同一目录中。App.exe在运行时,如果用到对classA.module中类型的引用,则会去classA.moudel文件进行查找,如果classA.moude文件不存在,则会引发System.IO.FileNotFoundException。如果App.exe不会用到class.module中的类型,则classA.module存不存在都不会对App.exe的执行产生任何影响(这就是上面提到的Assembly的好处之一,Assembly只是一个逻辑上的组合)。
App.exe还用到了对ClassB.dll的引用,因为classB.dll不是一个强命名类型,所以它只能进行私有部署,可以和App.exe放在一起,也可以放在主目录下的其他子目录下。(后面通过应用程序更改配置文件,可以重定向指向classB.dll的引用)。
3.更改应用程序配置文件(App.exe.config),重定向对classB.dll的引用。
现在App.exe,classA.moudle和classB.dll都在Bin目录下,app.exe在运行时会找到所有它需要的类型,所以运行正常。
如果把在Bin目录下新建一个目录,比如sub,并把classB.dll移动到sub目录下,再运行App.exe就会出错。同样会引发System.IO.FileNotFoundException错误,因为App.exe运行时需要的classB类型找不到。这时候就需要更改添加(如果没有)或更改应用程序配置文件,应用程序配置文件是一个xml格式的配置文件,和web.config文件的作用差不多,是配置应用程序运行时的行为的。
注意:配置文件的名字必须是应用程序名字再加一个.config,且必须在同一目录下
详细信息参考:
ms-help://MS.MSDNQTR.2004APR.1033/cpguide/html/cpconnetapplicationconfigurationscenarios.htm
App.exe.config文件的内容:
xml version="1.0" encoding="utf-8" ?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="sub"/>
<
runtime>
当App.exe运行时,它就会在主目录下的sub目录中找到classB.dll,然后继续执行。
说明:
当CLR需要定位一个程序集时,它将扫描应用程序的几个子目录,下面是才CLR扫描一个程序集时的顺序:
…/Assembly/Bin/classB.DLL.
…/Assembly/Bin/classB/classB.DLL.
…/Assembly/Bin/sub/classB.DLL.
…/Assembly/Bin/sub/classB/classB.DLL.
…/Assembly/Bin/classB.EXE.
…/Assembly/Bin/classB/classB.EXE.
…/Assembly/Bin/sub/classB.EXE.
…/Assembly/Bin/sub/classB/classB.EXE.
注意:
如果App.exe引用的是强命名程序集,CLR会首先在GAC中查找,然后才按照上面的顺序查找。
4.创建和查看公钥/私钥对文件
创建公钥/私钥对文件可以用.NET SDK自带的工具(SN.exe)来创建。
首先,创建一个公钥/私钥对文件
SN -k App.key
然后,用这个文件创建只包含共有密钥的文件:
SN -p App.key App.publickey
然后用-tp参数查看
SN –tp App.publickey
有了公钥/私钥对,创建强命名程序集就很容易了。只需要把System.Reflection.AssemblyKeyFileAttribute特性加到源代码中就可以了。
[assembly: AssemblyKeyFile("App.key")]
一般都加到AssemblyInfo.cs文件中。
现在重新build classB.cs,得到的将是一个强命名的程序集:
csc /out:bin/classB.dll /t:library classB.cs AssemblyInfo.cs
用ILDasm.exe查看,你会发现,在Assembly中的Public Key会有一大串值,这个就是程序集的公有密钥,它保证了整序集的唯一性。
6. 把强命名的程序集classB.dll加入到GAC中。
使用工具GACUtil.exe
把classB.dll加入到GAC中:
GACUtil /I classB.dll
删除掉classB.dll,然后重新Build App.exe:
csc /out:bin/app.exe /t:exe app.cs /addmodule:bin/classA.module /r:classB.dll
App.exe运行正确,表明classB.dll已经成功加入到GAC中,成为一个共享程序集了。
程序集相互引用的规则:
程序集种类
|
是否可以引用弱命名程序集
|
是否可以引用强命名程序集
|
普通程序集
|
是
|
是
|
强命名程序集
|
否
|
是
|
把classB.dll移出GAC:
GACUtil /u classB
注意:
移出的时候不能指定扩展名(因为在GAC中每一程序集对于对应的都是一个目录,而不是实际的程序集)。
如果classB.dll不是一个强命名的程序集,而你想把app.exe build为一个强命名的,就会出错:
error CS1577: Assembly generation failed -- Referenced assembly 'ClassB' does??????? not have a strong name
你可以试一试。J
7.并行执行(Side-By-Side)的例子
http://www.microsoft.com/china/msdn/archives/library/dnnetdep/html/sidexsidenet.asp
参考资料:
l Applied Microsoft .NET Framework Programming ---- Jeffrey Richter
l MSND Library