共享程序集
3.1 两种程序集、两种部署方式
1、.NET 框架支持两种程序集:强命名程序集(strongly named assembly)和非强命名程序集。
强命名程序集有一个发布者的公钥/私钥对签名,其中的公钥/私钥对唯一地标识了程序集的发布者。利用公钥/私钥对我们可以对程序集进行唯一的标识、安全策略和版本策略。
2、一个程序集有两种部署方式:私有方式和全局方式。
非强命名程序集只能进行私有部署。
全局部署方式将程序集部署在一些CLR确知的地方,当CLR搜索程序集时,它会知道到这些地方去查找。强命名程序集既可以进行私有部署,也可以进行全局部署。
3.2 强命名程序集
1、由于简单地用文件名导致了目前所谓的DLL Hell,强命名程序集就是CLR用来唯一地标识一个程序集的机制。
一个强命名程序集包含四个唯一标识程序集的特征:文件名(无扩展名)、版本号、语言文化标识及一个公有密钥标记(它是公有密钥产生的一个值)。
利用 System.Reflection.AssemblyName 类,可以创建一个程序集,并获取一个程序集名称的各个部分。
2、创建一个强命名程序集首先得有一个密钥,这个密钥可以用强命名实用工具(SN.exe,Strong Name Utility)产生SN.exe的所有命令行开关都是区分大小写的。
例子:
SN –k MyCompany.keys
创建一个名为MyCompany.keys的文件,文件中包含一对以二进制格式存储的公有密钥和私有密钥。
查看公有密钥,现以-p命令开关创建一个只包含公有密钥的文件:
SN –p MyCompany.keys MyCompany.PublicKey
然后以 –tp 命令开关查看:
SN –tp MyCompany.PublicKey
公钥非常大,为了方便开发人员(及终端用户),公有密钥标记(public key)应运而生。公有密钥标记是一个64位的公有密钥散列值。SN.exe 的 –tp 命令行开关在输出的末尾显示了和完整公有密钥相对应的公有密钥标记。
3、有了公钥/私钥对,只需把 System.Reflection.AssemblyKeyFileAttribute 特性的一个实例应用到源代码中就可以创建强命名程序集了:
[assembly:AssemblyKeyFile(“MyCompany.keys”)]
编译器在源代码中遇到该特性时,将打开其中制定的文件 MyCompany.keys ,用私有密钥对程序集进行签名,并将公有密钥嵌入到清单中。只有包含清单的那个PE文件被签名,其他文件时不能被显式签名的。
4、签名过程:
程序集的FileDef清单元数据表(参见2.3的2)包含组成该程序集的所有文件的一个列表,每个文件名称被加入到该清单中时,文件内容被转换成一个散列值,该散列值将和文件名一起存入FileDef表中。(默认散列算法时SHA-1,可以用 AL.exe 的 /algid 命令或应用于程序集上的System.Reflection.AssemblyAlgorithmIdAttribute 定制特性来改变默认的算法);
生成包含清单的PE文件后,该PE文件的整个内容被转换为一个散列值(使用的算法总是SHA-1,不能改变),该散列值经由发布者的私有密钥签名,生成的RSA数字签名被存储在PE文件的一个保留区域(不包括在散列值的计算中)。最后,PE文件的CLR表头被更新以反映数字签名在文件中的嵌入位置(?在PE文件中的偏移值?)。
另外,发布者的公有密钥也被嵌入到PE文件的AssemblyDef清单元数据表中。
5、如果程序集应用到其他程序集的类型和成员,编译器就会在生成的托管模块中嵌入一个AssemblyRef元数据表。该表的每个条目标识的被引用的程序集也有其公有密钥信息,因为公有密钥过大,为了节省存储空间,这个元数据表存储的只是被引用程序集的公有密钥标记而不是完整的公有密钥。
3.3 全局程序集缓存
1、如果一个程序集被多个应用程序访问,那么该程序必须被放在一个CLR已确知的目录下,且CLR在探测到对该程序集的应用时,它必须能自动到该目录下寻找这个程序集。这个已确知的目录叫做全局程序集缓存(Global Assembly Cache,即GAC),它通常位于下面目录中:
C:\Windows\Assembly\GAC
GAC是一个结构化的目录,它包含许多子目录。我们可以调用 GACUtil.exe(安装强命名程序集最常见的工具)并指定 /i命令开关来将一个强命名程序集安装到GAC中,也可以指定 /u 命令开关将程序集从 GAC中卸载。(/i /u命令开关适合于开发人员在测试环境中使用,在世界生产环节推荐使用 /ir /ur 开关)
安装.NET框架时,安装程序会同时安装一个【资源管理器】shell扩展程序(shFusion.dll),它能清楚友好地显示GAC的内容。用它打开 c:\Windows\Assembly 目录下,就可以看到安装到GAC中的程序集。
3.3.1 GAC的内部结构
GAC的目的就是为在强命名程序集和子目录间维持一个关系。同一个公司的同一程序集的不同版本在这里存放在不同的目录下。
3.4 引用强命名程序集
1、第二章里提到用 CSC.exe 的 /reference 命令行开关来指定期望引用的程序集文件名。如果文件名包含了完整路径,则 CSC.exe 加载指定路径下的文件。如果指定的是不带路径的文件,它将在以下目录中查找引用的程序集:
a、当前工作路径;
b、编译器目前使用的CLR所在的目录。MSCorLib.dll总是包含在该目录中(System.Object就定义在这个程序集中)。该目录类似于:
C:\Windows\Micorsoft.NET\Framework\v1.0.3427 ;
c、任何用CSC.exe的 /lib 命令行开关指定的目录;
d、任何LIB环境变量中指定的目录。
安装.NET框架时,微软的程序集文件会被分别拷贝到 CLR所在目录及 GAC目录中。在CLR所在目录中拷贝是使我们能够方便的生成自己的程序集;GAC中的拷贝则是用于运行时加载这些程序集。
CSC.exe 不在GAC 中查找所引用的程序集的原因是因为需要指定的路径比较麻烦。
2、响应文件(response file):是一个包含一组编译器命令行开关的文本文件。执行CSC.exe 命令时,编译器会打开响应文件,并象使用通用命令行传递的开关一样使用响应文件中指定的命令行开关。
例子:
一个响应文件MyProject.rsp 包含下列文本:
/out:MyProject.exe
/target:winexe
为了使CSC.exe利用这些设置,可以象这样调用:
csc.exe @MyProject.rsp CodeFile1.cs CodeFile2.cs
C#编译器支持多个响应文件。
运行CSC.exe时它会自动在当前目录中搜索一个本地的CSC.rsp文件;另外编译器还会在CSC.exe所在的目录中搜索一个全局的CSC.rsp文件。
本地响应文件和全局响应文件中的设置有冲突时,本地响应文件中的设置将覆盖全局响应文件的设置;类似的,显示传递给CSC.exe的设置也将覆盖本地响应文件中的设置。
CSC.rsp包含类似以下的命令行:
/r:Systme.dll
/r:Accessibility.dll
/r:System.Data.dll
等等;
需要的话我们也可以向CSC.rsp文件中添加自己的命令行开关。
3.5 强命名程序集的防篡改特性
1、用私有密钥为程序集签名可以确保该程序集的生产者为对应公有密钥的持有者:
a、当程序集被安装到GAC目录内时,系统将会对包含清单的文件内容进行散列转换,并用得到的散列值来和嵌入在PE文件中的RSA数字签名进行比较。如果相同证明程序集文件的内容没有被篡改,并且可以知道我们拥有着和发布者的私有密钥相对应的公有密钥;
b、系统会对程序集中其他文件的内容进行散列转换,然后将得到的散列值和清单文件中FileDef表内存储的散列值进行比较。如果发现有任何不匹配的情况,则证明至少有一个程序集的文件被篡改了。
2、当应用程序需要绑定一个程序集时,CLR将使用所引用的程序集的一些属性(名称、版本、语言文化及公有密钥)来在GAC中定位程序集。
如果找到被引用的程序集,它所在的子目录被返回,保存清单的那个文件将被加载。这样确保了运行时加载的程序集和编译时生成的程序集总时来自同一个发布者。
如果被引用的程序集不在GAC中:
b、CLR将在应用程序的基目录中查找,如果还没找到则
c、CLR将到应用程序的配置文件中标识的私有路径中查找
d、如果应用程序时使用MSI来安装的,CLR会要求MSI来定位程序集。
如果找不到程序集绑定失败系统抛出一个 System.IO.FileFoundException 异常。
当强命名程序集文件是从一个非GAC的地方加载时CLR会在程序集被加载的时候比较散列值(若是GAC中加载,则在程序安装时比较散列值?)。如果检测到散列值不匹配抛出 System.IO.FileLoadException 异常。
3.6 延迟签名
1、在开发和测试程序集时,允许访问安全的私有密钥可能会导致私钥泄密,因而,.NET框架支持一种称作延迟签名(delayed signing)的技术,有时也叫局部签名(partial signing)。
延迟签名允许我们只使用公司的公有密钥就可以生成程序集:
a、程序集引用我们的程序集,使用公有密钥允许它们将正确的公有密钥嵌入到AssemblyRef元数据条目中;
b、公有密钥生成的程序集还允许被放在GAC的内部结构中;
c、但用公有密钥生成的程序集因为没有经过散列转换也没有在文件中嵌入数字签名,将失去篡改保护功能。
2、实现延迟签名:
a、将公司的公有密钥存放在一个文件中,然后将该文件名传递给程序集生成工具(3.2中的2有详细讲述);
b、将下面的两个特性加到源代码中告诉生成工具对程序集进行延迟签名:
[assembly:AssemblyKeyFile(“MyCompanyPublicKey.keys”)]
[assembly:AssemblyDelySign(true)]
如果使用AL.exe工具,还可以在命令行中指定 /keyf[ile]和 /delay[sign]开关来实现;
c、将程序集安装到GAC,必须通过给 SN.exe 工具指定 -Vr 命令行开关阻止系统对程序集进行完整性验证。这样还可以使CLR在运行时加载程序集的时候,跳过对其内任何文件的散列值的检查:
SN.exe -Vr MyAssembly.dll;
d、完成开发和测试后,打包和部署程序集时需要用SN.exe 的 -r 命令行开关使用私有密钥对程序集进行签名,用SN.exe的 -Vu 或 -Vx 命令行开关恢复对程序集的验证过程:
SN -Vu MyAssembly.dll
3、一些组织将它们的密钥对保存在一个硬件设备中,一些加密服务商(CSP)为抽象这些密钥的位置提供了某些“容器”。
如果我们的公钥/私钥保存在一个CSP容器里,则应该使用 System.Reflection.AssemblyKeyNameAttribute特性或者 AL.exe 的 /keyn[ame] 命令行开关;而当使用SN.exe将私有密钥添加到延迟签名的程序集商时,则用 -Rc命令行开关代替 -R 。
3.7 强命名程序集的私有部署
不一定都得装到GAC中,实际上,如果只有少数程序共享到你的程序集时,你的强命名程序集也可以进行私有部署。当其他程序集引用该程序集时得为其安装一个XML配置文件(2.7中1例子有说明)。
3.8 并存执行
CLR能够将名称相同但路径不同的多个文件加载到同一个地址空间,这在.NET框架中称为并存(side-by-side)执行,它是解决Windows中“DLL hell”问题的一项关键技术。DLL的并存执行能力允许我们创建的新版本程序集不必维持向后兼容。
(都是因为强命名的程序集用文件名(无扩展名)、版本号、语言文化标识及一个公有密钥标记标识,可以将同一名称的程序集同本公司其他版本程序集、其他公司的程序集区分开来。)
3.9 CLR如何解析类型引用
例子:
public class App {
static public void Mian(System.String[] args) {
System.Console.WriteLine(“Hi”)’
}
}
该代码被编译生成一个程序集App.exe。当我们运行该应用程序时,CLR将加载并初始化它。然后CLR会读取该程序集的CLR表头来寻找标识应用程序入口方法(Main)的MethodDefToken。根据MethodDef元数据表,CLR会定位到文件中该方法的IL代码所处的偏移,然后将其以JIT的方法编译为本地代码,同时完成代码的类型安全验证过程,最后执行编译后的本地代码。
当CLR以JIT的方式编译该段代码时,它会检测到所有应用到的类型和成员,并加载定义他们的程序集。
上面的例子里,IL代码中有一个引用为System.Console.WriteLine。IL 有一call指令标引用的元数据标记标识了MemberRef元数据中的一个条目。a、CLR会查找MemberRef,并发现其中的一个字段指向了TypeRef 表中的一个条目(即System.Console 类型)。 b、从TypeRef条目中,CLR会被导向到一个AssemblyRef条目上:“MSCorLib,Version=1.0.3300.0,Culture=”neutral”,PublicKeyToken=b77a5c561934e089”。到这里CLR将知道自己需要哪个程序集。
3.10 高级管理控制(配置)
2.7中提到如何配置CLR搜索和绑定程序集的方式,仅仅讨论了定位元素privatePath属性,本节介绍其他一些XML配置文件元素。
1、几个配置元素:(书中的例子太长,有兴趣的朋友翻翻书吧)
probing:指示CLR在应用程序在其指定的目录中查找弱命名程序集;
dependentAssembly、assemblyIdentity 和 bindingRedirect:指示CLR在定位原来指定的程序集时重新转定位另一个指定的程序集,可以实行版本号重定向;
codeBase:指示CLR指示在指定的目录或URL地址查找程序集,它也可以被用于弱程序集(这种情况下,CLR忽略程序集版本号及XML codeBase元素中指定的版本号);
publisherPolicy:指示CLR是否忽略部署的发布者策略文件。
2、CLR确定程序集引用的程序集:
a、查找AssemblyRef表知道要引用的程序集;
b、在应用程序的配置文件中查找程序集,同时会应用其中指定的版本号重定向策略;如果publisherPlocy元素的apply属性被设置为yes或者忽略该元素CLR将会检查GAC并应用任何程序集发布者认为必要的版本号重定向策略(发布者策略);
c、接着CLR会到机器内的Machine.config文件中查找程序集,并应用任何其中指定的版本号重定向策略;
d、最后CLR知道它应该加载哪个版本的程序集,并首先试图从GAC中加载,如果程序集不在GAC中,并没有指定codeBase元素,CLR会按照第2章所说的规则来定位程序集。
利用这些配置文件,管理员可以完全控制CLR应该加载哪些程序集。如果希望机器内所有程序集采用最新的程序集,则应该修改机器中的Machine.config文件。
2.7的5提到.NET中提供了方便的GUI配置工具。
3.10.1 发布者策略控制
当发布者修复了原版本程序集中的bug时,希望用户用新版本代替新版本,如果让每个用户自己通过修改他的应用程序集或机器上的XML配置文件是非常不方便的。发布者策略控制就可以使得在安装新的程序集时,能够创建可以安装到用户机器上的“策略信息”。
发布者策略程序集必须被安装到GAC中。
3.11 修复错误的应用程序
1、当控制台或者Windows窗体应用程序正在一个用户帐号下运行时,CLR会保持一个应用程序实体加载的程序集的记录,加载信息被累积在内存中,并在程序结束时被写入以下磁盘路径中: C:\Documents and Settings\UserName\LocalSettings\Application Data\ApplicationHistory
目录中每个文件标识着一个特殊的应用程序,文件名类似:App.exe.c4bc1771.ini。其中的16进制数是一个标识文件路径的散列值,它用来区分不同子目录下的同名文件。
2、当一个应用程序运行时,CLR会维持一个应用程序所加载的程序集的集合“快照”。当应用程序结束时,该信息将和应用程序相关联的.ini文件内容比较:
a、如果应用程序但前加载的程序集集合和先前加载的程序集集合相同,也就是.ini文件中的信息和内存中的信息相同,内存里的信息将被丢弃;
b、如果内存中的信息不同于.ini文件中的信息,CLR会将内存中的信息追加到.ini文件中。
c、默认情况下,.ini文件能够存储5个快照。
3、由于某种原因,如果我们运行的应用程序出了问题,因为CLR保持了一个应用程序使用的程序集的历史记录,我们就可以为引用程序集创建一个XML配置文件,其中的元素告诉CLR使用最近一次运行正常时加载的程序集。
用.NET框架配置工具我们可以很容易做到这一点,运行工具――『应用程序』――『修复应用程序』,就可以进行配置。
对应用程序XML配置文件所做的改变会被注释元素标识,注释元素包括:“.NET Appliction Restore BeginBlock”和“.NET Appliction Restore EndBlock”
摘自:http://blog.csdn.net/jilm168/archive/2007/09/20/1792401.aspx