程序集是 .NET Framework 应用程序的构造块;程序集构成了部署、版本控制、重复使用、激活范围控制和安全权限的基本单元。最终由CLR管理这些程序集中代码的执行。这意味着必须在目标机器上安装好 .NET Framework 。
公共语言运行时(Common Language Runtime,CLR)是一个可由多种编程语言使用的“运行时”,CLR的核心功能(比如内存管理、程序集加载、安全性、异常处理和线程同步)可由面向CLR的所有语言使用。
高级语言通常只公开了CLR的所有功能的一个子集。然而,IL汇编器语言允许开发人员访问CLR的所有功能。CLR允许在不同编程语言之间方便切换,同时又保持紧密集成,所以当你使用的高级语言隐藏了你需要的CLR功能,那么可以使用“混合语言编程”或使用IL汇编语言实现。
1. 程序集执行以下功能:
a) 包含公共语言运行时执行的代码。如果可迁移可执行 (PE) 文件没有相关联的程序集清单,则将不执行该文件中的 Microsoft 中间语言 (MSIL) 代码。
b) 程序集形成安全边界。程序集就是在其中请求和授予权限的单元。
c) 程序集形成类型边界。每一类型的标识均包括该类型所驻留的程序集的名称。在一个程序集范围内加载的 MyType 类型不同于在其他程序集范围内加载的 MyType 类型。
d) 程序集形成引用范围边界。程序集的清单包含用于解析类型和满足资源请求的程序集元数据。它指定在该程序集之外公开的类型和资源。该清单还枚举它所依赖的其他程序集。
e) 程序集形成版本边界。程序集是公共语言运行时中最小的可版本化单元,同一程序集中的所有类型和资源均会被版本化为一个单元。程序集的清单描述您为任何依赖项程序集所指定的版本依赖性。
f) 程序集形成部署单元。当一个应用程序启动时,只有该应用程序最初调用的程序集必须存在。其他程序集(例如本地化资源和包含实用工具类的程序集)可以按需检索。这就使应用程序在第一次下载时保持精简。
g) 程序集提供允许同时运行多个版本的软件组件(称作并行执行)的基本结构。
h) 程序集可以是静态的或动态的。静态程序集可以包括 .NET Framework 类型(接口和类),以及该程序集的资源(位图、JPEG 文件、资源文件等)。静态程序集存储在磁盘上的可迁移可执行 (PE) 文件中。您还可以使用 .NET Framework 来创建动态程序集,动态程序集直接从内存运行并且在执行前不存储到磁盘上。您可以在执行动态程序集后将它们保存在磁盘上。
2. 程序集的优点
程序集旨在简化应用程序部署并解决在基于组件的应用程序中可能出现的版本控制问题。
最终用户和开发人员比较熟悉当今基于组件的系统所产生的版本控制和部署问题。一些最终用户曾经历过在计算机上安装新应用程序失败的事情,发现已有应用程序突然停止工作。许多开发人员花费了大量的时间来使所有必需的注册表项保持一致,以便激活 COM 类。
通过在 .NET Framework 中使用程序集,许多开发问题得以解决。因为程序集是不依赖于注册表项的自述组件,所以程序集使无相互影响的应用程序安装成为可能。程序集还使应用程序的卸载和复制得以简化。
3. 强名称的程序集
强名称是由程序集的标识加上公钥和数字签名组成的。其中,程序集的标识包括简单文本名称、版本号和区域性信息(如果提供的话)。
强名称是使用相应的私钥,通过程序集文件(包含程序集清单的文件,并因而也包含构成该程序集的所有文件的名称和散列)生成的。
1) 强名称程序集显示名称的语法:
<程序集名称>, <版本号>, <区域性>, <公钥标记>
( 如果没有区域性值,请使用 Culture=neutral )
2) 程序集版本号:
<主版本>.<次版本>.<内部版本号>.<修订号>
eg:2.5.719.2
a) 前两个编号:构成了公众对一个版本的理解。如上例为程序集的2.5版本
b) 第三个编号:是程序集的build号。如果公司每天都要生成程序集,那么每天都应该递增这个build号
c) 最后一个编号:指出当前build的修订次数。如因为重大bug当天生成了两次程序集,那么revision号就应该递增。
3) 强名称提供如下好处:
a) 强名称依赖于唯一的密钥对来确保名称的唯一性。任何人都不会生成与您生成的相同的程序集名称,因为用一个私钥生成的程序集的名称与用其他私钥生成的程序集的名称不相同。
b) 强名称保护程序集的版本沿袭。强名称可以确保没有人能够生成您的程序集的后续版本。用户可以确信,他们所加载的程序集的版本出自创建该版本(应用程序是用该版本生成的)的同一个发行者。
c) 强名称提供可靠的完整性检查。通过 .NET Framework 安全检查后,即可确信程序集的内容在生成后未被更改过。注意:强名称中或强名称本身并不暗含信任级别,例如由数字签名和支持证书提供的信任。
d) 在.NET中,只有强名称签名的程序集才能放到全局程序集缓存中。
在引用具有强名称的程序集时,您应该能够从中受益,例如版本控制和命名保护。如果此具有强名称的程序集以后引用了具有简单名称的程序集(后者没有这些好处),则您将失去使用具有强名称的程序集所带来的好处,并依旧会产生 DLL 冲突。因此,具有强名称的程序集“应当只”引用其他具有强名称的程序集。
更多关于强名称签名请参见 《(2)强名称程序集与数字证书》
4. 延迟为程序集签名(部分签名)
一个单位可以具有开发人员在日常使用中无法访问的严密保护的密钥对。公钥通常是可用的,但对私钥的访问权仅限于少数个人。开发强名称程序集时,每个引用具有强名称的目标程序集的程序集中都包含了用于为目标程序集指定强名称的公钥的标记。这要求公钥在开发过程中可用。
您可以在生成时使用延迟签名(部分签名),在可迁移可执行 (PE) 文件中为强名称签名保留空间,将实际签名延迟至后面某些阶段(通常就在传送程序集之前)。
下面的步骤说明了延时对程序集签名的过程:
使用 Windows 软件开发包 (SDK) 提供的 强名称工具 (Sn.exe) 从将执行最终签名的单位获取密钥对的公钥部分。此密钥通常是 .snk 文件的形式。
使用 System.Reflection 中的两种自定义特性来批注程序集的源代码:
1) AssemblyKeyFileAttribute,它将包含公钥的文件的名称作为参数传递给其构造函数。
2) AssemblyDelaySignAttribute,它通过将 true 作为参数传递给其构造函数,表明正在使用延迟签名。例如:
a) [assembly:AssemblyKeyFileAttribute("myKey.snk")]
b) [assembly:AssemblyDelaySignAttribute(true)]
3) 编译器将公钥插入程序集清单,并在 PE 文件中为完整的强名称签名保留空间。真正的公钥必须在生成程序集时存储,以便引用此程序集的其他程序集可获取密钥以存储在它们自已的程序集引用中。
4) 由于程序集没有有效的强名称签名,所以必须关闭该签名的验证。通过将“强名称”工具与–Vr 选项一起使用来执行此操作。
下面的示例关闭名为 myAssembly.dll 的程序集的验证。
sn –Vr myAssembly.dll
-Vr选项会将程序集的身份添加到以下注册表:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\StrongName\Verification
为了确保这些密钥的安全性,密钥值绝对不能持久存储在一个磁盘文件中。“加密服务提供程序”(Cryptographic Service Provder,CSP)提供了对这些密钥的位置进行抽象的容器。以Microsoft使用的CSP为例,一旦访问它提供的容器,就会自动从一个硬件设备中获取私钥。
5) 以后,通常是在即将交付前,重新开启强名称签名验证。通过将“强名称”工具与–R 选项来实际进行强名称签名 (这步不能省,否则会产生安全漏洞) 。
下面的示例使用 sgKey.snk 密钥对为名为 myAssembly.dll 的程序集签署强名称。
sn -R myAssembly.dll sgKey.snk
5. 附属程序集
标记了具体的语言文化的程序集称为附属程序集。附属程序集通常只包含语言文化特有的资源,不包含任何代码,为附属程序集指定的语言文化应准确反映程序集中包含的资源的语言文化。
部署一个附属程序集时,应该把它存到一个专门的子目录中,子目录的名称应该与语言文化的文本相匹配。
在运行时,可以用System.Resources.ResourceManager类来访问一个附属程序集的资源。
通常使用AL.exe工具来生成附属程序集。之所以不用编译器,是因为附属程序集中不应包含任何代码。
一般不应生成引用了附属程序集的一个程序集。换言之,程序集的AssemblyRef记录项只应应用语言文化中性的程序集。想访问包含在一个附属程序集中的类型或成员,应使用“反射技术”。
6. 全局程序集缓存
安装有公共语言运行时的每台计算机都具有称为全局程序集缓存的计算机范围内的代码缓存。全局程序集缓存中存储了专门指定给由计算机中若干应用程序共享的程序集。
1) 一个程序集可以采用两种方式来部署:私有和全局。
a) “私有部署的程序集”是指部署到应用程序基目录或者一个子目录中的程序集。弱命名程序集只能以私有方式部署。
b) “全局部署的程序集”是指部署到一些已知位置,即全局程序集缓存(Global Assembly Cache,GAC);CLR在查找程序集时,会检查这些位置。强名称程序集既可以私有部署,也可以全局部署。
应当仅在需要时才将程序集安装到全局程序集缓存中以进行共享。一般原则是:程序集依赖项保持专用,并在应用程序目录中定位程序集,除非明确要求共享程序集。另外,不必为了使 COM 互操作或非托管代码可以访问程序集而将程序集安装到全局程序集缓存。
因为将程序集安装到GAC中,会破坏我们的一些基本目标,即:简单地安装、备份、还原、移动和卸载应用程序。所以,建议程序员尽量避免全局部署,尽量使用私有部署。
2) 要将程序集安装到全局程序集缓存中的原因有以下几点:
a) 共享位置。
可将应用程序公共使用的程序集放在全局程序集缓存中。
b) 文件安全性。
管理员通常使用访问控制列表 (ACL) 来保护 systemroot 目录,以控制写入和执行访问。因为全局程序集缓存安装在 systemroot 目录中,它继承了该目录的 ACL。建议只允许具有“管理员”权限的用户从全局程序集缓存中删除文件。
c) 并行版本控制。
可在全局程序集缓存中维护程序集的多个副本(名称相同但版本信息不同)。
d) 其他搜索位置。
在探测或使用配置文件中的基本代码信息之前,公共语言运行时会先检查全局程序集缓存中符合程序集请求的程序集。
3) 可采用三种方法将程序集安装到全局程序集缓存中:
a) 使用全局程序集缓存工具 (Gacutil.exe)。
您可以使用 Gacutil.exe 将强名称程序集添加到全局程序集缓存,并查看全局程序集缓存的内容。
在命令提示符处,键入下列命令将强名称程序集安装到全局程序集缓存中:
gacutil –I <assembly name>
注意:Gacutil.exe 只用于开发,不应用于将产品程序集安装到全局程序集缓存中。
b) 使用 Microsoft Windows Installer 。
这是将程序集添加到全局程序集缓存的最常用方法,建议采用。此安装程序可提供全局程序集缓存中程序集的引用计数,还具有其他优点。
c) 使用 Mscorcfg.msc(.NET Framework 配置工具) (.NET 4.0 已删)
Microsoft 管理控制台 (MMC) 单元,可以查看全局程序集缓存并将新的程序集添加到该缓存。
在全局程序集缓存中部署的程序集必须具有强名称。将一个程序集添加到全局程序集缓存时,必须对构成该程序集的所有文件执行完整性检查。缓存执行这些完整性检查以确保程序集未被篡改。
4) 使自己的程序集出现在“.NET”选项卡的列表中
在vs中引用GAC程序集时,通过在 “.NET” 选项卡列表中选择;对于自己的程序集目录若需要他出现在“.NET”选项卡列表中,可在注册表中配置:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\AssemblyFolders
(或HKEY_LOCAL_MACHINE)
7. 程序集启动并运行
托管模块是一个标准的32位Microsoft Windows可移植执行体(PE32)文件(Portable Executable),或者是一个标准的64位Windows可移植执行体(PE32+)文件,它们都需要CLR才能执行。
1) 一个托管PE文件由4个部分构成:PE32(+)头、CLR头、元数据以及IL。
元数据是一个二进制数据块,由几个表构成。分为三个类别:定义表、引用表和清单表。
通过工具:IL反汇编器可查看元素据。“视图|元数据|显示”(或Ctrl+M)
a) 定义表:ModuleDef、TypeDef、MethodDef、FieldDef、ParamDef、PropertyDef、EventDef。
b) 引用表:AssemblyRef、ModuleRef、TypeRef、MemberRef。
c) 清单也是一组元素据表的集合(AssemblyDef、FileDef、ManifestResourceDef、ExportedTypesDef),表中主要包含了作为程序集的组成部分的那些文件的名称,此外,它们还描述了程序集的版本、语言文化、发布者、公开导出的类型以及构成程序集的所有文件。
CLR总是首先加载包含“清单”元素据表的文件,再根据这个“清单”来获取程序集中的其他文件的名称。
2) CLR托管代码相对于非托管代码的一个优势:
Windows进程需要使用大量操作系统资源,所以进程数量太多,会损害性能并制约可用的资源。
CLR提供了在一个操作系统进程中执行多个托管应用程序的能力。每个托管的应用程序都在一个AppDomain中执行。默认情况下,每个托管的EXE文件都在它自己的独立空间中运行,这个地址空间只有一个AppDomain。然而,CLR的宿主进程(比如IIS或者Microsoft SQL Server)可决定在单个操作系统进程中运行多个AppDomain。
3) /platform选项:Any CPU、x86、x64、intel Itanium
-------/platform开关选项对生成的模块的影响以及在运行时的影响
/platform开关 |
生成托管托管模块 |
X86 Windows |
X64 Windows |
IA64 Windows |
anycpu(默认) |
PE32/不明确指定 |
作为32位应用程序运行 |
作为64位应用程序运行 |
作为64位应用程序运行 |
x86 |
PE32/x86 |
作为32位应用程序运行 |
作为WoW64位应用程序运行 |
作为WoW64位应用程序运行 |
x64 |
PE32+/x64 |
不运行 |
作为64位应用程序运行 |
不运行 |
Itanium (Intel安腾处理器) |
PE32+/Itanium |
不运行 |
不运行 |
作为64位应用程序运行 |
Windows的【64位版本】提供一种名为WoW64(Windows on Windowss64)的技术,允许运行32位Windows应用程序。该技术甚至允许使用x86本机代码的32位应用程序在Itanium机器上运行。这是因为WoW64技术能模拟x86指令集,虽然这样做会显著影响性能。
Windows检查好EXE文件头,决定是创建32位、64位还是WoW64进程之后,会在进程的地址空间中加载MSCorEE.dll的x86,x64或IA64版本。然后,进程的主线程调用MSCorEE.dll中定义的一个方法。这个方法初始化CLR,加载EXE程序集,然后调用其入口方法(Main)。
更多C#编译器选项请参加: 《csc.exe(C# 编译器)》
4) 方法调用过程:
为了执行一个方法,首先必须把它的IL转换成本地CPU指令,由CLR中的JITCompiler函数负责。由于IL是“即时“(just in time)编译的,所以通常CLR的这个组件称为JITter或JIT编译器。
JIT会在定义程序集的MethodDef元数据表中查找被调用的方法的IL。接着,JIT验证IL代码,并将IL代码编译成本地CPU指令并【存储到动态内存】中,再次调用的方法以本地代码的形式全速运行。
一旦应用程序终止,编译好的代码也会被丢弃。所以将来再次运行应用程序,或者同时启动应用程序的两个实例(实例是使用不同的操作系统进程—eg:x86和x64),JIT编译器必须再次将IL编译为本地指令。
《反射机制》系列:
参考书籍: CLR via C#(第3版)
参考资源: