《.NET之美》之程序集
一、什么是程序集(Assembly)?
经由编译器编译得到的,供CLR进一步编译执行的那个中间产物,在WINDOWS系统中,它一般表现为·dll或者是·exe的格式,但是要注意,它们跟普通意义上的WIN32可执行程序是完全不同的东西,程序集必须依靠CLR才能顺利执行。 ----百度百科之程序集
程序集可分为两种类型:
(1)、可执行程序,后缀为.exe(GUI,图形用户接口;或CUI,命令行用户接口)
(2)、类库,后缀为.dll
其结构如下图:
在其构成中,只有PE头、CLR头、清单是必须的。其他均为可选的。
二、程序集结构解析
1、清单(Manifest)
我们要如何去查看一个程序集的清单呢?
此时我们就要借助微软自带的强大的工具ILDASM,此程序如果你要装Visual Studio就会自动帮你装上去,路径在:
C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\ildasm.exe.
这里为了演示方便,我新建了一个程序集AssemblyLib作为演示。我们点击此工具的文件按钮,选择我们那个程序集文件,界面就如下图所示:
我们点击视图-元信息-显示! 滚动到Assembly就可看到AssemblyDef元数据表
里面包含了程序集的名称、公钥、版本、程序集特性等信息。我们接着往下翻,就可以看到FileDef表、ExportedTypeDef表、ManifestResourceDef表。FileDef表描述了构成程序集的模块信息、ExportedTypeDef表描述了外部模块中存在的类型信息、ManifestResourceDef表包含的是嵌入到程序集的资源信息。
除了主要的这四张表以外,清单中还包含了AssemblyRef表,此表定义了该程序集所引用的其他程序集信息。
从上面可以看出,清单描述了程序集的几乎一切信息,回答了这样几个问题:“程序集是什么(名称、版本、特性等)”“由什么构成(模块、资源)”和“外部依赖是什么(引用了其他哪些程序集)”。
2、元数据
所谓元数据就是描述数据的数据,这样看来,清单也是属于元数据的一种。
元数据与清单类似,也包含了几张表:
ModuleDef表,包含了当前模块的名称和后缀等信息。
TypeDef表,包含了每个类型的信息,这些信息包括类型名称、类型的基类、标记等信息(public、private等)。
MethodDef表,包含了每个方法的信息,这些信息包括方法名称、签名、标记等信息(public、static、virtual等)。
类似地,还有FieldDef、ParamDef、PropertyDef、EventDef几张表。
类型元数据中除了包含模块中定义的类型以外,还包含外部类型的引用,这些信息包含在另外一组表中:TypeRef、MemberRef。
我们只要知道,类型元数据,定义了程序集中所有类型的信息。
3、程序集资源
程序集中还可以包含资源(Resource),资源可以是字符串,也可以是任何格式的文件,比如图片、Excel文档等。
现在假设我们需要在项目中得到一张图片的资源,我们通常有三种做法:
<1>、将图片保存在程序根目录的文件夹下,然后通过路径获得。
第一种方法我们都很熟悉,这里就不作介绍了。
<2>、将图片作为资源嵌入程序集内。
第二种方法我们平常是比较少遇到的,我们看一下如何去做。
程序集的资源(Resource)是一段具有名称的字节数组。可以将资源想象成一个Dictionay<string,byte[]>,即一个以string为键,以byte[]
为值的字典。因为字节数组是二进制形式,所以资源可以是任何文件。
将资源嵌入程序集内也有两种方法:
①、将文件直接嵌入程序集
这种方法只要将文件添加到项目中,然后查看文件的"属性",将"生成操作"的值设为嵌入的资源。这里需要注意两点,一是资源加到程序集以后,资源的名称并不等于文件名,VS会自动在文件名前面加上程序集的默认命名空间、文件所在的文件夹名。二是
资源的名称是大小写敏感的。
那我们在程序中如何获取资源呢?
可以在调用Assembly类型的实例方法GetManifestResourceNames()中获得程序集的所有资源名称,接下来调用GetManifestResourceStream()方法获得资源的字节流 。
Assembly asm = Assembly.GetExecutingAssembly(); // 获得当前执行的程序集
string[] nameArray = asm.GetManifestResourceNames(); // 获得资源名称
foreach (string name in nameArray) {
Console.WriteLine(name);
using (Stream s = asm.GetManifestResourceStream(name)) {// 获得字节流
}
②、将.resources资源文件嵌入程序集
在第一种方法,资源在程序集中是零散的,我们为了集中管理资源,可以使用.resx文件将资源嵌入到程序集当中。.resx文件是一个XML格式的文本文件,记录了程序集中包含的资源名称和路径,它是程序开发时的设计工具,通过可视化的方式来对程序集中的资源进行分类和管理。注意.resx只是一个XML文本文件,类似一个资源清单,本身并不是程序集资源。在生
成程序集时,.resx会被自动转换为.resources文件,.resources文件包含了实际的资源文件(例如图片或者音频),并嵌入到程序集当中,但习惯上仍将.resx称作资源文件。
<3>、将资源作为独立程序集
在第二种方法中我们将资源直接嵌入到程序集中,会迅速增大程序集的体积,因此,我们可以将资源单独放在一个单独的程序集中,然后再由主程序集引用它。这样做的好处就是如果主程序没有
用到资源,那么就不用去加载这个程序集。我们可以先引用资源程序集的.dll(假设为res.dll),然后用如下的代码去访问资源程序集的资源:
Assembly asm = Assembly.Load("res");
ResourceManager r = new ResourceManager("Resource", asm);
...
在多语言的应用程序中,通常会将各个不同地区的语言文本作为资源,单独放到各自独立的程序集中,使得应用程序可以根据计算机的本地语言来显示相应资源中的文本。
此时我们只要在项目中新建一个资源文件,Resource.en.resx,在这个资源文件中添加字符串资源,名称为"address",值为"China,GuangDong,Shenzhen"。
重新生成项目,在bin\debug文件夹下,会看多多了一个子文件夹en,其中包含了ConsoleApp.resources.dll文件,该程序集包含了英文版本的资源。类似地,可以创建包含了德
语、日语等其他国家语言的程序集,这种程序集有一个形象的名字,叫做卫星程序集(Satellite Assembly)
Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en");
ResourceManager r = new ResourceManager("ConsoleApp.Resource",
Assembly.GetExecutingAssembly());
string address = r.GetString("address");
Console.WriteLine("address:" + address);
其中的"Thread.CurrentThread.CurrentUICulture=new System.Globalization.CultureInfo("en");"语句,将当前的UI区域性改为了en,即英语地区国家。之后,运行ConsoleApp.exe,会看到显示的输出为:
address:China, Guang Dong, Shenzhen
三、强名称程序集
1、强名称的定义
我们在新建程序集时,对程序集进行命名,那此时怎么命名比较好呢?
比如在上面我们建的程序集Assemblylib,别人也可以建这个名称的程序集,我们如何去比较好的划分呢。首先,我们先对同一个程序集不同版本进行划分,我们给程序集加上版本号以及区域性。 如下图:
这样之后,我们可以很好地分清自己的程序集之间不会发生冲突,但还是无法区分别人与自己的程序集。
为了解决这个问题,我们可以继续加入公司名、公司的URL以及GUID,这样,这个程序集的规则就有了唯一标识,这个时候就会衍生了另一个问题别人拿到你这个程序集后就可以看到你这个程序集的信息,从而进行仿冒。出于这些考虑,微软选择了使用公钥/私钥非对称加密(RSA)的方式,并结合使用了散列函数(SHA1)来保证:程序集的唯一性、防仿冒性、防篡改性。
2、为程序集赋予强名称
接下来我们看下如何实现防伪冒性:
我们需要用到一个工具SN.exe ,这个工具在你装VS的时候就会自动帮你装好,这个文件笔者的目录为:C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools
我们可以使用vs自带的开发人员命令提示符,此工具笔者在:C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio 2017\Visual Studio Tools下 打开命令提示符输入sn.exe,我们可以看到如下图的帮助页面:
再输入 sn -k D:\Assemblysnk.snk 回车后就会将秘钥写入D盘根目录下
我们打开D盘根目录即可看到这个秘钥文件。
上面的文件中包含公钥及私钥,接下来我们将公钥提取出来并另存为另一个文件。
从上面的结果可以看出,公钥的字节数很长,有128字节,操作起来很不方便。因此,对公钥进行了散列运算,获得了一个它的8字节的哈希值,也就是公钥标记。由于公钥标记是公钥的摘要,或者“指纹”,它们是对等的,此时我们只要关注公钥标记即可。 我们再将公钥加入程序集规则即可。 单纯加公钥别人可以仿冒,并无多大意义,公私钥对就显出了效果。
我们使用VS,将公私钥对加入签名中。 这样就做到了程序集的防伪冒。(加密部分笔者这里就不一一赘述,有兴趣的请自行了解)。
总结
在这一部分的阅读学习中,笔者先解释了什么是程序集,分析了程序集的结构,还介绍了在程序集中嵌入资源的几种方法,接下来讲了强名称程序集如何一步步去做到唯一性与防伪。在我们日常的开发过程中,程序集是我们接触最多的文件,平时我们只懂得如何去生成与引用,并不知其具体的原理,这样进一步的了解,对我们以后的开发有着奠定基石的作用!