程序集基本概念
文/玄魂
前言
程序集是.NET应用程序的基本单位,包含了程序的资源、类型元数据和MSIL代码。根据程序集生成方式的不同,可分为静态程序集和动态程序集。程序集又可分为单文件程序集和多文件程序集,多文件程序集将程序集中的文件按类型组织到多个文件中。每个程序集,无论是静态的还是动态的,均包含描述该程序集中各元素彼此如何关联的数据集合,程序集清单就包含这些程序集元数据。程序集还可以进行版本控制和强名称等安全设置。
反射提供了封装程序集、模块和类型的对象(Type 类型)。可以使用反射动态创建类型的实例,将类型绑定到现有对象,从现有对象获取类型并调用其方法或访问其字段和属性。如果代码中使用了属性,可以利用反射对它们进行访问。
2.1 程序集
CLR运行程序总是首先加载程序集,然后根据程序集中的清单去加载并初始化其他内容。程序集由一个或者若干个模块组成,每个模块就是一个PE文件。开发人员可以通过命令行调用编译器,合并多个模块到程序集。程序集共享和强名称是代码访问安全里两个重要的概念,通过程序集可以实施代码级别的安全策略。
2.1.1 模块的操作
程序集由模块组成,每个程序集可以包含至少一个模块。每一个模块就是一个标准的PE文件(见第1章)。接下来,我们学习使用编译器来进行模块的生成、设置等基本操作。
首先创建一个hello.cs文件,如代码清单2-1所示。
代码清单2-1 hello.cs代码
using System; public class Hello { public Hello() { } public static int void Main(string[] args) { Console.WriteLine("hello world!"); Console.Read(); } }
文件创建完毕之后,从命令行启动C#的编译器,输入如下命令:
csc /out:hello.exe /t:exe /r:MSCorLib.dll hello.cs
如图2-1所示,编译器首先打印的是版本信息。
图2-1 命令行调用C#编译器
然后启动生成的hello.exe,见图2-2。可以看到程序输出的“hello world”。
图2-2 运行hello.exe
上面的代码虽然很简单,但仍然有必要做细致的分析。首先,创建一个名为hello的类型、一个hello()方法、一个Main(string[]args)静态方法。然后添加类型引用,引用MSCorLib.dll中的Console.WriteLine(string)、Console.Read()方法。当编译器把上面的C#代码编译成MSIL代码时,遇到Console.WriteLine(string)和Console.Read()的时候,会在指定的程序集中(MSCorLib.dll)中寻找Console类型并判断相应方法的调用是否正确。
说明 对于C#编译器,默认会自动引用MSCorLib.dll,并设置/out:hello.exe /t:exe命令,所以该生成exe文件的命令可直接为:csc hello.cs。
这个hello.exe文件究竟是什么呢?首先肯定它是一个托管PE文件,是一个模块,同时它还是一个程序集。第1章已经介绍过PE文件了,这里不再重复。通过csc的/t:exe、/t:winexe或者/t:library命令行开关得到文件,然后通过ILDasm查看都会发现程序集清单(也就是该模块同时是一个程序集),那么可不可以生成不包含程序集清单的模块呢?答案是csc的/t:module命令行开关,通过该命令将生成一个.netmodule文件。新建一个hh.cs文件,如代码清单2-2所示。
代码清单2-2 hh.cs源码
public class Hh { public string Hello() { return "hello"; } }
对hh.cs使用如下命令:
csc /t:module hh.cs
该命令生成的hello.netmodule文件是一个标准的DLL PE文件,但是它不能被CLR加载,使用的时候必须将它嵌入程序集中。后文将介绍如何把模块添加到程序集中。
2.1.2 程序集概念
简单地说,程序集是.NET应用程序的基本单位,是CLR运行托管程序的最基本单位。它通常的表现形式是PE文件,区分PE文件是不是程序集或者说模块和程序集的根本区别是程序集清单,一个PE文件如果包含了程序集清单那么它就是程序集。下面简要说明程序集的基本功能。
注意 每个程序集只能包含一个入口点,控制台程序的Main方法,dll文件的DllMain方法或者Windows程序的WinMain方法。
一个非程序集的PE文件中的IL代码是不能被执行的。
(1) 形成程序运行的安全边界
程序集就是在其中请求和授予权限的单元。关于安全边界的问题会在2.1.3节详细论述。
(2) 形成程序的引用范围边界
程序集的清单包含用于解析类型和满足资源请求的程序集元数据。它指定在该程序集之外公开的类型和资源。该清单还枚举它所依赖的其他程序集。
(3) 形成程序的版本边界
程序集是公共语言运行库中最小的可版本化单元,同一程序集中的所有类型和资源均会被版本化为一个单元。
(4) 形成程序的部署单元
当一个应用程序启动时,只有该应用程序最初调用的程序集必须存在,其他程序集(例如本地化资源和包含实用工具类的程序集)可以按需检索。这就使应用程序在第一次下载时保持精简。
(5) 支持并行执行
同一台计算机上可以同时有运行库的多个版本,并且可以有使用其中某个运行库版本的应用程序和组件的多个版本。并行执行使您能够更多地控制应用程序绑定到的组件版本和应用程序使用的运行库版本。
程序集从创建方式上可分为动态程序集和静态程序集。动态程序集是程序执行时创建的程序集,创建后可以保存在磁盘上。静态程序集包括类型、资源等,多被包含在托管PE文件中。
程序集又可分为单文件程序集和多文件程序集。一个程序集通常由程序集清单、类型元数据、MSIL代码和资源组成。对于程序集来说,程序集清单是必需项。单文件程序集把上述所有内容包含在一个PE文件中(图2-3),而对于多文件程序集则可能分割在不同的文件中(图2-4),比如编译代码的模块 (.netmodule)、资源(例如 .bmp 或 .jpg 文件)或应用程序所需的其他文件。如果您希望组合以不同语言编写的模块并优化应用程序的下载过程,可创建一个多文件程序集,优化下载过程的方法是将很少使用的类型放入只在需要时才下载的模块中。
图2-3 单文件程序集
图2-4 多文件程序集
注意 构成多文件程序集的那些文件实际上并非由文件系统来链接,而是通过程序集清单进行链接,公共语言运行库将这些文件作为一个单元来管理。
本节介绍的最后一个概念,也是程序集的标志性概念,即程序集清单。每一个程序集,无论是静态的还是动态的,均包含描述该程序集中各元素彼此如何关联的数据集合。程序集清单就包含这些程序集元数据。程序集清单包含指定该程序集的版本要求和安全标识所需的所有元数据,以及定义该程序集的范围和解析对资源和类的引用所需的全部元数据。程序集清单可以存储在具有 Microsoft 中间语言 (MSIL) 代码的 PE 文件(.exe 或 .dll)中,也可存储在只包含程序集清单信息的独立 PE 文件中。
程序集清单的内容如表2-1所示。
表2-1 程序集清单内容
信息 |
说明 |
程序集名称 |
指定程序集名称的文本字符串 |
版本号 |
主版本号和次版本号,以及修订号和内部版本号。公共语言运行库使用这些编号来强制实施版本策略 |
区域性 |
有关该程序集支持的区域性或语言的信息。此信息只应用于将一个程序集指定为包含特定区域性或特定语言信息的附属程序集(具有区域性信息的程序集被自动假定为附属程序集) |
强名称信息 |
如果已经为程序集提供了一个强名称,则为来自发行者的公钥 |
程序集中所有文件的列表 |
在程序集中包含的每一文件的散列及文件名。请注意,构成程序集的所有文件所在的目录必须是包含该程序集清单的文件所在的目录 |
类型引用信息 |
运行库用来将类型引用映射到包含其声明和实现的文件的信息。该信息用于从程序集导出的类型 |
有关被引用程序集的信息 |
该程序集静态引用的其他程序集的列表。如果依赖的程序集具有强名称,则每一引用均包括该依赖程序集的名称、程序集元数据(版本、区域性、操作系统等)和公钥 |
通过ILDasm的MANIFEST选项可以查看程序集清单,如图2-5所示。查看Hello.exe的程序集清单,可以看到外部引用,程序集的版本号、名称,散列等信息。
图2-5 hello.exe的程序集清单
2.1.3 强名称程序集
强名称是由程序集的标识加上公钥和数字签名组成的。其中,程序集的标识包括简单文本名称、版本号和区域性信息(如果提供的话)。强名称是使用相应的私钥,通过程序集文件(包含程序集清单的文件,因而也包含构成该程序集的所有文件的名称和散列)生成的。强名称相同的程序集应该是相同的。
通过签发具有强名称的程序集,可以确保名称的全局唯一性,保护程序集的版本,提供可靠的完整性检查。
说明 如果一个具有强名称的程序集以后引用了具有简单名称的程序集(后者没有这些好处),则将失去使用具有强名称的程序集所带来的好处,并依旧会产生 DLL 冲突。因此,具有强名称的程序集只能引用其他具有强名称的程序集。
想要给一个程序集添加强名称,可以使用程序集链接器(AL.exe)或者强名称属性。
在为程序集创建强名称之前必须先创建一个公钥/私钥对。这一对加密公钥/私钥用于在编译过程中创建强名称程序集。您可以使用强名称工具 (Sn.exe) 来创建密钥对。密钥对文件通常具有snk 扩展名。图2-6为创建一个名为hello.snk的密钥文件。
图2-6 生成密钥文件
我们还可以从已经创建的密钥对文件中提取公钥,放到单独的文件中,如图2-7所示:
图2-7 提取公钥
在密钥文件创建完成之后,下一步就是利用密钥创建强名称程序集,这里我们使用程序集链接器(AL.exe)对前面生成的hh.netmodule模块使用强名称,如图2-8所示:
图2-8 为程序集添加强名称
图2-8中的开关/out:hh.dll hh.netmodule是把hh.netmodule模块输出为hh.dll文件,/keyf:hello.snk指定强名称的密钥文件。可以通过ILDasm查看hh.dll的强名称签名,如图2-9所示。
图2-9 hh.dll强名称签名的公钥
一个程序集具有了强名称之后,接下来的问题就是如何引用这个具有强名称的程序集。通常有两种方式可供选择:编译时引用和运行时引用。
用C#编译器引用hello.dll的方式如下所示:
csc /t:library showhello.cs /reference:hello.dll
运行时引用实际是反射的方式加载程序集,反射将会在2.2节介绍。下面给出运行时引用的简单示例:
Assembly.Load("hello,Version=4.0.0.0,Culture=neutral,PublicKeyToken= B77A5C561934E089");
关于强名称程序集暂时介绍这些,后面的章节还有很多内容会提及强名称程序集,下一节会介绍与此相关的共享程序集。
2.1.4 共享程序集
共享程序集的一个前提条件就是该程序集必须要有强名称。那么什么是共享程序集呢?要了解共享程序集,先从私有程序集开始。
通常建立的exe或者dll程序集都是私有程序集,当在其他客户应用程序中使用这类程序集时,只需要添加引用。当程序集被多个应用程序域使用时,每个应用程序域需要复制该程序集,进程中也将存在该程序集的多个副本。
相对于私有程序集的是共享程序集,它使多个应用程序域能够访问同一个程序集。特别地,内存中只存在该程序集的同一份副本,这种非特定于域的代码共享极大地节省了内存 资源占用。在大多数情况下,共享程序集安装在全局程序集高速缓冲存储器(Global Assembly Cache,GAC)中,而不存在于应用程序相关目录下,对它的引用不会产生文件复制,因此也不会产生额外的副本。下面了解共享程序集的创建、安装及使用。
创建共享程序集的第一步是为该程序集添加强名称(详见2.1.3节),然后该做的就是在GAC中安装共享程序集。
.NET提供的命令行工具gacutil.exe可以将具有强名称的程序集添至全局程序集缓存。命令格式为:
gacutil -I <程序集名称>
其中,"程序集名称"是要在全局程序集缓存中安装的程序集的名称。
下面的示例语句将文件名为 hello.dll 的程序集安装到全局程序集缓存:
gacutil -i hello.dll
在客户应用程序中使用共享程序集的方法与私有程序集一样简单。创建客户应用程序后,以与引用私有程序集相同的方式引用共享程序集,在应用程序代码中包含共享程序集命名空间(using语句),然后,就可以像使用本地对象一样使用共享程序集的公共对象了。
注意 安装全局程序集破坏了.NET简化应用程序安装、部署、移动的策略,因为它本身是注册式安装。
2.1.5 创建多文件程序集
创建一个多模块程序集可划分为两种情况,一是由同一语言编译器创建的不同模块的合并,二是由不同语言编译器创建的模块的合并。下面分别讨论这两种情况。
首先创建一个Module1.cs文件,如代码清单2-3所示。
代码清单2-3 Module1.cs源码
public class Module1 { public Module1() { } public int Add(int m,int n) { return m + n; } }
使用C#编译器csc.exe编译输出文件Module1.netmodule。如图2-10所示。
图2-10 编译生成Module1.netmodule
然后创建一个MainModule.cs文件,如代码清单2-4所示。
代码清单2-4 MainModule.cs源码
public class MainModule { public int Mul(int m,int n) { return m *n; } }
接下来把这段代码编译成dll类型的程序集文件,把Module1.netmodule模块添加到该程序集中。如图2-11所示。
图2-11 生成MainModule.dll
图2-11中的命令行开关/addmodule:Module1.netmodule把模块Module1.netmodule添加到程序集MainModule.dll。下面通过ILDasm来查看相关信息,如图2-12所示。
图2-12 关于Module1.netmodule的信息
如果某个程序想引用MainModule.dll文件,则必须保证Module1.netmodule文件的存在和访问权限,否则编译器会报错,如图2-13所示。
图2-13 模块丢失的错误信息
对于不同编译器生成的模块,而使用的编译器又不支持类似C#编译器中类似于/addmodule的开关,只能选用程序集链接器(AL.exe)来合并各模块。下面创建含有3个托管模块的程序集,如图2-14所示。
图2-14 包含3个托管模块的程序集简易示例
首先使用语言编译器生成模块1为M1.netmodule,模块2为M2.netmodule,然后使用AL.exe 生成包含程序集清单的Main.dll文件,命令如下:
al /out:Main.dll /t:M1.netmodule M2.netmodule
该程序集包含了3个文件,AL.exe不能将多个文件合并成一个文件。
--------------------------------------------------注:本文改编自《.NET 安全揭秘》第2章
作者:玄魂
出处:http://www.cnblogs.com/xuanhun/
原文链接:http://www.cnblogs.com/xuanhun/
更多内容,请访问我的个人站点 对编程,安全感兴趣的,加qq群:hacking-1群:303242737,hacking-2群:147098303,nw.js,electron交流群 313717550。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
关注我: