《CLR via C#》读书笔记 之 共享程序集和强名称程序集
第三章 共享程序集和强名称程序集
2013-02-27
摘要: 本章讲述了为解决版本问题而建立的基础结构。
3.1 两种程序集,两种部署
3.2 为程序集分配强名称
3.3 全局程序集缓存
3.4 在生成的程序集中引用一个强名称程序集
3.5 强名称程序集如何防范篡改
3.6 延迟签名
3.8 “运行时”如何解析引用类
3.9 高级管理控制(配置)
3.1 两种程序集,两种部署
弱命名程序集和强命名程序集在结构上完全一致:相同的PE文件格式、PE32(+)头、CLR头、元数据、清单表以及IL。
区别在于:强命名程序集使用发布者的公钥/私钥进行了签名,它唯一标识了程序集的发布者。这对秘钥允许对程序集进行唯一性的标识、保护和版本控制,并允许程序集部署到用户机器的任何地方,甚至可以部署到Internet上。
表1 弱命名和强命名程序集的部署方式
程序集种类 | 私有部署 | 全局部署 |
强命名 | 可以 | 可以 |
弱命名 | 可以 | 不可以 |
3.2 为程序集分配强名称
以前,当多个应用程序访问的程序集放在同一个目录中,当有版本修改,或同名程序集时,便无法控制,通常最后一个安装的程序集就会覆盖原先的程序集,着就是Windows的“DLL hell”现象的根源。
显然,只根据文件名来区分程序集时不够的。CLR必须提供对程序集程序集进行唯一标识的机制,这正是“强名称程序集”的来历。如果一个程序集有一个唯一的标记,那么这个程序集就可以叫做强命名程序集。在.NET框架中是通过公钥/私钥加密来产生这个唯一标记的。
一个强命名程序集具有4个重要attributes:文件名(不计扩展名)、版本号、语言文化(culture)以及一个公钥标记(是由公钥派生的哈希值,即公钥标记)。如下图,有3个不同的程序集:
3.3 全局程序集缓存
GAC作用: 是可以存放一些有很多程序都要用到的公共Assembly,例如System.Data、System.Windows.Forms等等。这样,很多程序就可以从GAC里面取得Assembly,而不需要再把所有要用到的Assembly都拷贝到应用程序的执行目录下面。
GAC(Globle Assembly Cache)位于以下目录:
- .NET 3.5和以前版本: C:\Windows\Assembly
- .NET 4: C:\Windows\Microsoft.NET\Assembly
.NET 4.0 has a new GAC, why?
GAC目录是结构化的:其中包括许多子目录,从而保证不同的程序集在不同的目录下,永远不要将程序集手动复制到GAC目录。在开发测试期间,安装一个强名称程序集,常用工具是GACUtil.exe
GACUtil /i <assembly_path> //将某个程序集安装到全局缓存中 GACUtil /u <assembly_path> //.net 将某个程序集从全局缓存中卸载 gacutil /u <完全限定的程序集名称> //.net 4.0 将某个程序集从全局缓存中卸载 gacutil /u "Program,Version=0.0.0.0, Culture=neutral, PublicKeyToken=51C39234D5F82021" sn -T <assembly_path> //查看.NET程序集的PublicKeyToken(公钥标记)
注意:将程序集文件全局部署到GAC中,是对程序集进行注册的一种手段。虽然实际上Windows注册表没有受到任何影响,但程序集安装到GAC中,会破坏我们的一个基本目标,即:简单的安装、备份、还原、移动、卸载应用程序。所以,建议尽量避免全局部署,尽量使用私有部署。
3.4 在生成的程序集中引用一个强名称程序集
在生成(即编译)时,使用CSC.exe的/reference编译器开关来指定想要引用的程序集文件名。
如果文件名是一个完整路径 ,CSC.exe会加载指定的文件,并根据它的数据来生成程序集;
如果指定一个不含路径的文件名,CSC.exe会试在以下目录查找程序集:
- 工作目录。
- 包含CSC.exe本身的目录(%SystemRoot%\Microsoft.NET\Framework\v4.0.####),目录中还包含CLR的各种DLL文件。
- 使用/lib编译器开关指定的任何目录。
- 使用LIB环境变量指定的任何目录。
在运行时,不会在CSC.exe本身的目录来加载,而是从GAC加载(具体看3.8节)。
安装.NET Framework时,实际会安装Microsoft的程序集的两套拷贝。一套安装到编译器/CLR目录,另一套安装到一个GAC子目录。
CSC.exe编译器之所以不再GAC中查找引用的程序集,是因为你必须知道程序集的路径,但GAC的结构有没有正式公开。
注意:在一台机器上安装.NET Framework时,回想程序集的x86,x64或IA64版本安装到编译器/CLR目录中。在生成程序集时,可引用已安装的任何版本的文件,因为所有的版本都包含完全一致的元数据。
3.5 强名称程序集如何防范篡改
用一个私钥对程序集签名,可保证程序集是由对应公钥的持有者生成的。程序集安装到GAC时,系统对包含清单的那个文件的内容进行哈希处理,并将哈希值与PE文件中嵌入的RSA数字签名进行比较(在公钥解除了对它的签名后)。如果两个值完全一致,表明文件的内容未被篡改,可保证你拿到的公钥和发布者的私钥是对应的。除此之外,系统还会对程序集的其他文件的内容进行哈希处理,并将哈希值与清单文件的FileDef表中存储的哈希值进行比较。任何一个哈希值不匹配,表明程序集至少有一个文件被篡改,程序集将无法安装到GAC。
将一个强命名程序集安装到了GAC是,系统会执行一次检查。这个检查只在安装时执行一次。相反,从非GAC的一个目录加载强命名程序集是,每次加载都会进行检查。
3.6 延迟签名
当你准备好对自己的强名称程序集进行打包时,必须使用受到严密保护的私钥对它签名。然而,在开发和测试程序集时,访问这些受到严密保护的私钥可能有些费事。有鉴于此,.NET Framework提供了对延迟签名(delay signing)的支持。延迟签名只用公司的公钥来生成一个程序集。由于使用了公钥引用你的程序集的哪些程序集会在它们的AssemblyRef元数据表的记录项中嵌入正确的公钥值。另外,它还使程序集能正确存储到GAC中。
当然,不用公司的私钥对文件进行签名,变无法实现篡改保护。这是由于无法对程序集的文件进行哈希处理,无法在文件中嵌入一个数字签名。
延迟签名步骤:
//1)生成公钥/私钥对 sn -k Program.snk //2)提取出公钥并存放在public.snk中。
sn -p Program.snk public.snk //3)对程序集进行延迟签名 csc /keyfile:public.snk /delaysign /t:library Program.cs //4)让CLR信任程序集的内容,不执行签名验证 (这个命令会在注册表中添加相应项 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\StrongName\Verification,所以对同一程序集只需执行一次。至此 Program.dll就可以安装到GAC中) sn -Vr Program.dll //5)在最终部署前,一定要用私钥进行签名。否则,其他人可以用公钥做一个相同签名的程序集,替换掉这个程序集。 sn -R Program.dll Program.snk //6)打开验证,在4)中的注册表项会被相应移除 sn -Vu Program.dll
上述代码中:sn -Vr C:\Temp\Bugs\2856482\fix\32\TestDllDiffBit.dll,不执行签名验证。执行该命令,会在注册表中添加相应项(其中,‘TestDllDiffBit’是程序集名称,‘D36A8B911D28EESC’是公钥标记),如下图所示:
3.8 “运行时”如何解析引用类
源代码
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 System.Console.WriteLine("Hi"); 6 } 7 }
对以上代码进行编译,生成程序集“Program.exe”。
查看Main()方法的IL代码(运行ILDasm.exe,选择“视图”|“显示字节”,然后双击树形视图中的Main方法)
1 .method private hidebysig static void Main(string[] args) cil managed// SIG: 00 01 01 1D 0E 2 { 3 .entrypoint 4 // 方法在 RVA 0x2050 处开始 5 // 代码大小 13 (0xd) 6 .maxstack 8 7 IL_0000: /* 00 | */ nop 8 IL_0001: /* 72 | (70)000001 */ ldstr "Hi" 9 IL_0006: /* 28 | (0A)000011 */ call void [mscorlib]System.Console::WriteLine(string) 10 IL_000b: /* 00 | */ nop 11 IL_000c: /* 2A | */ ret 12 } // end of method Program::Main
CLR执行Main方法过程:
(1)运行这个应用程序时,CLR会加载并初始化程序集“Program.exe”
(2)CLR读取程序集的CLR头,查找表示了入口方法(Main)的MethodDefToken。
(3)CLR检索MethodDef元数据表,找到该方法(Main)的IL代码在文件中的偏移量,把这些IL代码JIT编译成本地代码。
在第(3)步进行JIT编译时,CLR会检查对类型和成员的所有引用,并加载它们的程序集(若尚未加载)。从上述代码可以看出代码有一个堆System.Console.WriteLine()的引用,我们来看一下CLR在如何通过元数据来定位引用成员所在所在程序集:
(1)IL call指令引用了元数据token(0A)000011。这个token对应于MemberRef元数据表(表0A)的记录项11.
(2)CLR检查这个MemberRef记录项,发现它的一个字段引用了一个TypeRef表中的记录项(System.Console类型)。
(3)CLR根据TypeRef记录项,被引导至一个AssemblyRef记录项。
(4) 执行本地代码
解析一个引用类型时,CLR可能在以下三个地方找到类型:
(1) 同一个文件
(2) 不同的文件,但同一个程序集
(3) 不同的文件,不同的程序集
下图可以看到CLR如何通过元数据来定位一个类型的定义程序集。
图1 基于引用了一个方法或类型的IL代码,CLR如何利用元数据来定位一个类型的定义程序集
3.9 高级管理控制(配置)
配置文件示例:
这个示例中,probing元素, 查找弱命名程序集时,检查应用程序基目录的AuxFiles和bin\subdir子目录;查找强命名程序集时,CLR先检查GAC或者有codebase元素指定的URL。只有在未指定codeBase元素的前提下,才会在查找probing指定的目录。
编译一个方法时,CLR确定它引用了哪些类型和成员。根据这些信息,“运行时”检查发出引用的那个程序集的AssemblyRef表,判断该程序集在生成时引用了哪些程序集。然后,CLR在应用程序配置文件中检查程序集版本,并进行设定的任何版本号的重定向操作。
配置文件允许使用和元数据所记录的不完全匹配的一个程序集版本。这种灵活性非常有用。
发布者策略控制
发布者策略就是由发布者设置与程序集同名的配置文件重定向程序集。下面是一个用于JeffTypes.dll程序集的示例文件(JeffTypes.config)
<configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="JeffTypes" publicKeyToken="32abrka3ke0adkad" culture="neutral"/> <bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0"/> <coddBase version="2.0.0.0" herf="http://www.Wintellect.com/JeffTypes.dll"/> </dependentAssembly> </assemblyBinding> </runtime> </configuration>
当然,发布者只能为自己创建的程序集设置策略。除此之外,上面展示的是发布策略配置文件中唯一能使用的元素;例如,probing和publisherPolicy元素是不能使用的。
下面要创建包含这个发布者策略配置文件的一个程序集。命令如下:
AL.exe /out:Policy.1.0.JeffTypes.dll /version:1.0.0.0 /keyfile:MyCompany.snk /linkresource:JeffTypes.config
- /out 开关指示AL.exe创建一个新的PE文件,本例就是Policy.1.0.JeffTypes.dll,其中,除了一个清单外什么都没有。名称第一部分(Policy)告诉CLR该程序集包含发布者策略信息,第二和第三部分(1.0)告诉CLR这个程序集适用于major和minor为1.0的任何版本的JeffTypes程序集,第四部分(JeffTypes)指的是与发布者策略对应的程序集的名称,第五部分(dll)指的是要生成发布者策略程序集文件的扩展名。
- /version 是着发布者策略程序集的版本。与JeffTypes程序集版本无关。
- /keyfile 指示Al.exe使用发布者的“公钥/私钥对”对发布者策略程序集进行签名。这个密钥必须匹配于所有版本的JeffTypes程序集的密钥对。
- /linkresource 这个开关告诉AL.exe将xml配置文件作为一个资源链接(而非嵌入)到程序集。最后的程序集由两个文件(Policy.1.0.JeffTypes.dll和JeffType.config)构成。
一旦生成发布者策略程序集,就可随同新的JeffTypes.dll程序集打包并部署到用户机器上。发布者程序集必须安装到GAC中。
注意:如果发布者推出一个发布者策略程序集时,新的程序集引入的bug很多,管理员可配置应用程序配置文件指示CLR忽略发布者策略程序集,配置如下:
<publisherPolicy apply="no"/>