.Net Framework部署目标

什么是DLL Hell?

Windows早期并没有很严谨的DLL版本管理机制,以致经常发生安装了某软件后,因为其覆盖了系统上原有的同一个DLL文件,而导致原有可运行的程序无法运行。但还原回原有的DLL文件之后,所新安装的软件就无法运行。若影响到系统所使用的重要DLL时也可能让系统容易死机甚至无法正常启动。

別再掉進DLL地獄的陷阱裡(DLL Hell)~.NET解決之道 資策會數位教育研究所講師 王芳芳

总结:

  1. The .NET Framework NET Assembly 自描述与版本管理功能让 zero-impact(零影响) 的部署安装成为可能,同時也终结了DLL Hell 。

  2. Application-Private Assemblies (or 被隔离的assembly) 只能被一个应用程式所使用- 它不会被其他的应用程式所影响。 隔离的assembly 让程式开发者对应用程式有着绝对的控制权,开发好的Application-Private Assemblies只要部署在和应用程式同一目录即可。

  3. 透过Side by side execution(并行执行)的技术,应用程式只要安装成功之后,就不用担心DLL更新版本,或规格的改变, 它允许一个assembly 的多个版本在一个机器上同时被安装并执行, 而且每一个应用程式都可以要求和不同的Assembly 版本系结。

  4. The .NET Framework 纪录应用程式版本资讯,并在执行应用程式时使用此资讯载入应用程式所需依赖的正确版本的Assemblies。

将类型生成到模块中

public sealed class Program{
        public static void Main()
        {
          // 由于引用了Console类的WriteLine方法
          // 要顺利通过编译,必须向C#编译器提供一组程序集
          // 使他能解析对外部类型的引用
          System.Console.WriteLine("Hi");
        }
}

System.Console是Microsoft实现好的类型,用于实现这个类型的各个方法的IL代码存储在MSCorLib.dll

此处”r“意为reference

因此需要添加r:MSCorLib.dll 开关命令,完整编译命令行应如下:
csc.exe /out:Program.exe /t:exe /r:MSCorLib.dll Program.cs

但由于其他命令均为默认命令,本例中的编译命令行可以简化为
csc.exe Program.cs

如果不想C#编译器自动引用MSCorLib.dll程序集,可以使用/nostdlib开关。

生成三种应用程序的编译器开关

此处”t“意为target

  • 生成控制台用户界面(Console User Interface, CUI)应用程序使用/t:exe开关;
  • 生成图形用户界面(Graphical User Interface, GUI)应用程序使用/t:winexe开关;
  • 生成Windows Store应用程序使用/t:appcontainerexe开关;

集合开关命令的文件:响应文件

编译时可以指定包含编译器设置命令的响应文件,例如:假定响应文件MyProject.rsp包含以下文本

// MyProject.rsp
/out:MyProject.exe
/target:winexe

为了让CSC.exe使用该响应文件,可以像下面这样调用它

csc.exe @MyProject.rsp CodeFile1.cs CodeFile2.cs

C#支持多个响应文件,其先后顺序服从就近原则,优先级为控制台命令>本地>全局

.NET Framework具有一个默认的全局CSC.rsp文件,在运行CSC.exe进行编译时会自动调用,全局CSC.rsp文件中列出了所有的程序集,就不必使用C#的/reference开关显式引用这些程序集,这会对编译速度有一些影响,但不会影响最终的程序集文件,以及执行性能,开发者也可以自己为全局CSC.rsp添加命令开关,但这可能为在其他机器上重现编译过程带来麻烦。

另外,指定/noconfig开关后,编译器将忽略本地和全局CSC.rsp文件。

什么是元数据? 元数据概述

元数据(英语:metadata),又称诠释数据、中介数据、中继数据、后设数据等,为描述其他数据信息的数据.

元数据概述:元数据是一种二进制信息,用以对存储在公共语言运行库可移植可执行文件 (PE) 文件或存储在内存中的程序进行描述。将您的代码编译为 PE 文件时,便会将元数据插入到该文件的一部分中,而将代码转换为 Microsoft 中间语言 (MSIL) 并将其插入到该文件的另一部分中。在模块或程序集中定义和引用的每个类型和成员都将在元数据中进行说明。当执行代码时,运行库将元数据加载到内存中,并引用它来发现有关代码的类、成员、继承等信息。

首先回顾一下托管模块(Managed Module)托管模块是一个需要CLR才能执行的标准WindowsPE(Portable executable,简称PE)文件

  • PE32(+)头 :PE 文件主要部分的索引和入口点的地址。运行库使用该信息确定该文件为 PE 文件并确定当将程序加载到内存时执行从何处开始。
  • CLR表头:是一个小的信息块,是托管模块特有的,包含生成时所面向的版本号、一些标志、和一个MethodDef token用来指定模块的入口方法,最后,CLR头还包含模块内部的一些元数据表的大小的偏移量
  • 中间语言(IL)代码 : 编译器在编译源代码时产生的指令。CLR在运行时会将IL代码编译成本地CPU指令
  • 元数据: 元数据表和堆,是由三种表构成的二进制数据块,这三种表分别为定义表(definiton talbe)引用表(reference table)清单表(mainfest table)。运行库使用该部分记录您的代码中每个类型和成员的信息。本部分还包括自定义属性和安全性信息。

元数据描述的信息

元数据以非特定语言的方式描述在代码中定义的每一类型和成员。元数据存储以下信息:

  • 程序集的说明
    • 标识(名称、版本、区域性、公钥)。
    • 导出的类型
    • 该程序集所依赖的其他程序集。
    • 运行所需的安全权限。
  • 类型的说明
    • 名称、可见性、基类和实现的接口。
    • 成员(方法、字段、属性、事件、嵌套的类型)。
  • 属性
    • 修饰类型和成员的其他说明性元素。

定义表(definiton talbe)

代码中定义的任何东西都将在上表中的某个表创建一个记录项。

定义表名称 说明
ModuleDef 包含模块文件名,扩展名(不含路径),编辑器创建的GUID 的 记录项.
TypeDef 每个类型都在这个表中有一个记录项,包含类型的名称,基类,标志(public/private等),一些索引.这些索引指向MethodDef中属于该类型的方法、FieldDef表中该类的字段、PropertyDef表中该类型的属性以及EventDef表中该类型的时间.
MetodDef 每个方法(包括入口方法)都在这个表中有一个记录项, 包含方法的名称,标志,签名,以及方法的IL代码在模块中的偏移量(位置),每个记录项还引用了ParamDef表中的一个记录项,后者包括与方法参数有关的更多信息。
FieldDef 模块定义的每一个字段在这个表中都有一个记录项。每个记录项都包括标志、类型和名称。
ParamDef 关于参数的记录项
PropertyDef 模块定义的每个属性在这个表中都有一个记录项。每个记录项都包含标志、类型和名称。
EventDef 模块定义的每个事件在这个表中都有一个记录项。每个记录项都包含标志和名称。

引用表(reference table)

引用表名称 说明
AssemblyRef 引用的每个程序集的记录项,每个记录项都包含绑定(bind)该程序集所需的信息:程序集名称(不包含路径和扩展名)、版本号、语言文化及公钥Token(根据发布者的公钥生成一个小的哈希值,标识了所引用程序集的发布者)。
ModuleRef 实现该模块所引用的类型的每个PE模块在这个表中都有一个记录项。每个记录项都包含模块的文件名和扩展名(不含路径),如果存在别的模块实现了你需要的类型,这个表的作用便是同哪些类型建立绑定关系
TypeRef 模块引用的每一个引用类型…..
MemberRef 模块引用的每个成员(字段和方法,以及属性方法和事件方法)在这个表中都有一个记录项。每个记录项都包含成员的名称和签名,并指向对成员进行定义的那个类型的TypeRef记录项

清单表(mainfest table)

引用表名称 说明
AssemblyDef 如果模块标识的是程序集,这个元数据表就包含单一记录项来列出程序集名称(不包含路径和扩展名)、版本(major,minor,build和revision)、语言文化、标志、哈希算法以及发布者公钥(可为null)
FileDef 每个PE文件和资源文件在这个表中都有一个记录项(清单本身所在的文件除外,该文件在AssemblyDef表的单一记录项中列出) 在每个记录项中,都包含文件名和扩展名(不含路径)、哈希值和一些标志。如果程序集只包含他的主模块,不包含其他非主模块和资源文件。FileDef将无记录
ManifestResourceDef 每个资源在这个表中都有一个记录项 .记录项中包含资源名称、一些标志(如果程序集外部可见,就为public,否则为private)以及FileDef表的一个索引(指出资源或流包含在哪个文件中)。如果资源不是独立文件(比如.jpg或者.gif文件),那么资源就是包含在PE文件中的流。对于嵌入资源,记录项还包含一个偏移量,指出资源流在PE文件中的起始位置
ExportedTypesDef 从程序集的所有PE模块中导出的每个public类型在这个表中都有一个记录项。

将模块合并成程序集

Microsoft为何考虑要引入程序集这一概念?

这是因为使用程序集,可重用类型逻辑表示物理表示就可以分开。

  • 物理上,可以将常用的类型放在一个文件中,不常用的程序放在另一些文件中,只在使用时加载,
  • 在逻辑上,这些程序仍然被组织于同一程序集中,不需要编写额外的代码显式进行链接。

程序集(Assembly)是什么组成的?

程序集(Assembly)是一个或多个类型定义文件及资源文件的集合。在程序集的所有文件中,有一个文件容纳了清单(Manifest),如上一节一开始所述,清单也是元数据的组成部分之一,表中主要包含作为程序集组成部分的那些文件的名称。此外还描述程序集的版本、语言文化、发布者、公开导出类型以及构成程序集的所有文件。

CLR操作的是程序集,对于程序集,有以下几点重要特性:

  • 程序集定义了可重用的类型。
  • 程序集用一个版本号标记。
  • 程序集可以关联安全信息。

程序集是进行重用、版本控制和应用安全性设置的基本单元。

对于一个程序集来说,除了包含清单元数据表的文件,程序集中的其他文件独立时不具备以上特点.

编译生成含有清单元数据表的PE文件

C#编译器都会生成程序集: /t: exe, /t: winexe, /t: appcontainerexe, /t: library 或者/t: winmdobj。这些开关会指示编译器生成含有清单元数据表的PE文件。

C#编译器还支持/t: module开关。这个开关指示编译器生成一个不包含清单元数据表的PE文件。这样生成的肯定是一个DLL PE文件。CLR要想访问其中的任何类型,必须先将该文件添加到一个程序集中。使用/t: module开关时,C#编译器默认为输出文件使用.netmodule扩展名。

例如:
将不常用类型编译到一个单独模块,这样一来如果程序集的用户永远不使用不常用类型,就不需要部署这个模块。

// 使用`/t: module`开关时,C#编译器默认为输出文件使用`.netmodule`扩展名。
csc /t:module 不常用类型.cs

不常用类型.netmodule这是一个标准的DLL PE文件,但是CLR不能但单独加载它。

将输出的文件名改为MultiFileLibrary.dll, 目标是生成库文件,添加不常用类型的模块, 编译FUI.cs

// 指定了/t: library开关,所以生成的是含有清单元数据表的DLL PE文件
// /addmodule:不常用类型.netmodule 开关告诉编译器不`常用类型.netmodule`文件是程序集的一部分,从而将其添加到FileDef清单元数据表,并将`不常用类型.netmodule`的公开导出类型添加到ExportedTypesDef清单源数据表。
csc /out:NultiFileLibray.dll /t:library /addmodule:不常用类型.netmodule FUT.cs

使用程序集链接器(AL.exe)生成程序集

除了使用C#编译器,还可以使用”程序集链接器“实用程序AL.exe来创建程序集。如果程序集要求包含由不同编译器生成的模块(而这些编译器不支持与C#编译器的/addmodule开关等家的几种机制),程序集连接器就显得相当有用。

AL.exe能生成EXE文件,或者生成只包含清单的DLL PE文件。程序集链接器不能将多个文件合并成一个文件。

csc /t:module RUT.cs
csc /t:module FUT.cs
al /out:MultiFileLibrary.dll /t:library FUT.netmodule RUT.netmodule

为程序集添加资源文件

  • 用AL.exe创建程序集时,可用/enbed [resource]开关将文件作为资源添加到程序集。该开关获取任意文件,并将文件内容嵌入最终的PE文件。也可用/Link [resource]开关获取资源文件,但只指出资源包含在程序集的哪个文件,并不嵌入到PE文件中;该资源文件独立,并必须与程序集文件一同被打包部署
  • C#编译器用/resource开关将资源嵌入PE文件,用/linkresource开关添加记录项引用资源文件。以上开关均会修改ManifestResourceDef清单表添加记录项,外部引用的开关还会修改FileDef表以指出资源包文件。

程序集版本资源信息

Visual Studio新建C#项目时会在一个Properties文件夹中自动创建AssemblyInfo.cs文件。可直接打开该文件并修改自己的程序集特有信息。

在应用程序代码中调用System.Diagnostics.FileVersionInfo的静态方法GetVersionInfo并传递程序集路径作为参数可以获取并检查这些信息。

// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("LentilToolbox")]
[assembly: AssemblyDescription("Licensed under the MIT license")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("LentilToolbox")]
[assembly: AssemblyCopyright("Copyright ©  2016 Lentil Sun")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

//将 ComVisible 设置为 false 将使此程序集中的类型
//对 COM 组件不可见。  如果需要从 COM 访问此程序集中的类型,
//请将此类型的 ComVisible 特性设置为 true。
[assembly: ComVisible(false)]

// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
[assembly: Guid("ac315d57-80ca-4e7a-b55c-064b94547552")]

// 程序集的版本信息由下列四个值组成:
//
//      主版本
//      次版本
//      生成号
//      修订号
//
//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值,
// 方法是按如下所示使用“*”: :
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.1.0.2")]
[assembly: AssemblyFileVersion("1.1.0.2")]
- major(主版本号) minor(次版本号) build(内部版本号) revision(修订号)
示例 2 5 719 2

注意:程序集有三个版本号,每个版本号都有不同的用途:

  • AssemblyFileVersion:这个版本号存储在Win32版本资源中供使用者参考,CLR既不检查,也不关心,这个版本号的作用是说明该程序集的版本
  • AssemblyInformationalVersion:同上,这个版本号存储在Win32版本资源中供使用者参考,CLR既不检查,也不关心,这个版本号作用是说明使用该程序集的产品的版本
  • AssemblyVersion:存储在AssemblyDef清单元数据表中,CLR在绑定到强命名程序集时会用到它。这个版本号很重要,它唯一性地标识了程序集。

语言文化

未指定具体语言文化的程序集成为语言文化中性(Culture neutral)

// 将程序集的语言文化设为瑞士德语
[assembly: AssemblyCulture("de-CH")]

CLR探测程序集文件会扫描的目录