CLR via C#-----生成,打包,部署和管理应用程序及类型

1. .Net Framework部署目标

Windows多年来一直因为不稳定和过于复杂而口碑不佳。

  • 安装新应用程序时,可能会莫名其妙破坏另一个已经安装好的应用程序,即所谓的“DLL hell”
  • 安装的复杂性,大多数应用程序安装时会影响系统的全部组件
  • 安全性,安装时会带来各种文件,其中许多是由不同公司开发的

2.将类型生成到模块中

public sealed class Program
{
	public static void Main()
	{
		System.Console.WriteLine("Hi");
	}
}

为了生成这个示例应用程序,请将代码放到一个源代码文件中(假定为Program.cs)然后在命令行执行以下命令

csc.exe /out:Program.cs /t:exe /r:MSCorLib.dll Program.cs

这个命令指示C#编译器生成名为Program.exe的可执行文件(/out:Program.exe)。生成的文件是Win32控制台应用程序(/t[arget]:exe)。
由于C#编译器会自动引用MSCorLib.dll以及/out:Program.exe和/t:exe是默认设定,所以可以简化成

csc.exe Program.exe

如果因为某个原因不想自动引用MSCorLib.dll程序集,可以使用/nostdlib开关,如下但是会报错

csc.exe /out:Program.exe /t:exe /nostdlib Program.exe

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

执行CSS.exe时,编译器打开响应文件,并使用其中包含的所有开关,感觉就像是这些开关直接在命令行上传递给CSC.exe。

响应文件是包含编译器命令行开关的文本文件

CSC.exe运行时,会在CSC.exe所在的目录查找全局CSC.rsp文件,想应用于自己所有项目的设置应放到其中。本地和全局响应文件中的某个设置发生冲突,将以本地设置为准。指定/noconfig命令行开关,编译器将忽略本地和全局CSC.rsp文件。

3.元数据概述

可以查看CorHdr.h头文件定义的IMAGE_COR20_HEADER来了解CLR头的具体格式

元数据是由几个表构成的二进制数据块。有三种表,分别是定义表(definition table),引用表(reference table)和清单表(manifest table)。
<常用的元数据定义表>

元数据定义表名称说明
ModuleDef总是包含对模块进行标识的一个记录项。该记录项包含模块文件名和扩展名(不含路径),以及模块版本ID(形式为编译器创建的GUID)。这样可在保留原始名称记录的前提下自由重命名文件。但强烈反对重命名文件,因为可能妨碍CLR在运行时正确定位程序集
TypeDef模块定义的每个类型在这个表都有一个记录项。每个记录都包含类型的名称,基类型,一些标志(public,private等)以及一些索引,这些索引指向MethodDef表中该类型的方法,FileDef表中该类型的字段,PropertyDef表中该类型的属性以及EventDef表中该类型的事件
MethodDef模块定义的每个方法在这个表都有一个记录项。每个记录项都包含方法的名称,一些标志(private,public,virtual,abstract,static,final等),签名以及方法的IL代码在模块的偏移量。每个记录项还引用了ParamDef表中的一个记录项,后者包括与方法参数有关的更多信息
FieldDef模块定义的每个字段在这个表中都有一个记录项。每个记录项都包含标志(private,public等),类型和名称
ParamDef模块定义的每个参数在这个表中都有一个记录项。每个记录项都包含标志(in,out,retval等),类型和名称
PropertyDef模块定义的每个属性在这个表中都有一个记录项。每个记录项都包含标志,类型和名称
EventDef模块定义的每个事件在这个表中都有一个记录项。每个记录项都包含标志和名称

<常用的引用元数据表>

引用元数据表名称说明
AssemblyRef模块引用的每个应用程序在这个表中都有一个记录项。每个记录项都包含绑定该程序集所需的信息:程序集名称(不包含路径和扩展名),版本号,语言文化(culture)以及公钥token(根据发布者的公钥生成的一个小的哈希值,标识了所引用程序集的发布者)。每个记录项还包含一些标志(flag)和一个哈希值。该哈希值本应作为所引用程序集的二进制数据的校验和来使用。但是,目前CLR完全忽略该哈希值,未来的CLR可能同样如此
ModuleRef实现该模块所引用的类型的每个PE模块在这个表中都有一个记录项。每个记录项都包含模块的文件名和扩展名(不含路径)、可能是别的模块实现了你需要的类型,这个表的作用便是建立同那些类型的绑定关系
TypeRef模块引用的每个类型在这个表中都有一个记录项。每个记录都包含类型的名称和一个引用(指向类型的位置)。如果类型在另一个类型中实现,引用指向一个TypeRef记录项。如果类型在同一个模块中实现,引用指向一个ModuleRef记录项。如果类型在调用程序集内的另一个模块中实现,引用指向一个ModuleRef记录项。如果类型在不同的程序集中实现,引用指向一个AssemblyRef记录项
MemberRef模块引用的每个成员(字段和方法,以及属性方法和事件方法)在这个表中都有一个记录项。每个记录项都包含成员的名称和签名,并指向对成员进行定义的那个类型的TypeRef记录项

要查看元数据表,可以使用一下命令

ILDasm Program.exe

在ILDasm中选择“视图”|“统计”,会显示一下信息
IL视图统计
对于这个如此小的Program.exe应用程序,PE头和元数据占了相当大的比重,事实上,IL代码只有区区20个字节,当然随着应用程序规模的增大,元数据和头信息在整个文件中的比重越来越大。

顺便说一下,ILDasm.exe的一个bug会影响显示的文件长度。尤其不要相信Unaccounted信息

4.将模块合并成程序集

CLR总是首先加载包含“清单”元数据的文件,再根据“清单”来获取程序集中的其他文件的名称。下面列出了程序集的重要特点

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

除了包含清单元数据表的文件,程序集其他单独的文件并不具备上述特点

使用程序集,可重用类型的逻辑表示和物理表示就可以分开

使用多文件程序集的理由

  • 不同的类型用不同的文件,使文件能以“增量”方式下载。另外,将类型划分到不同的文件中,可以对购买和安装的应用程序进行部分或分批打包/部署
  • 可在程序集中添加资源或数据文件。例如,假定一个类型的作用是计算保险信息,需要访问精算表才能完成计算。在这种情况下,不必在自己的源代码嵌入精算表。相反,可以使用一个工具(比如程序集链接器AL.exe),使数据文件成为程序集的一部分。顺便说一句,数据文件可以为任意格式-包括文本文件,Microsoft Office Excel电子表格文件以及Microsoft Office Word表格等-只要应用程序知道如何解析
  • 程序集包含各个类型可以用不同的编程语言来实现。例如,一些类型可以用C#实现,一些用Visual Basic实现,其他则用其他语言实现。编译器用C#写的类型时,编译器会生成一个模块。编译用Visual Basic写的类型时,编译器会生成另一个模块。然后可以用工具将所有模块合并成单个程序集。其他开发人员在使用这个程序集时,只知道这个程序集包含了一系列类型,根本不知道,也不用知道这些类型分别用什么语言写的。顺便说一句,如果愿意,可以对每个模块都运行ILDasm.exe,获得相应的IL源代码文件。然后运行ILAsm.exe,将所有IL源代码的文件都传给它。随后,ILAsm.exe会生成包含全部类型的单个文件。该技术的前提是源代码编译器能生成纯IL代码

<清单元数据表>

清单元数据表名称说明
AssemblyDef如果模块标识的是程序集,这个元数据表就包含单一记录项来列出程序集名称(不包含路径和扩展名),版本(major,minor,build和revision),语言文化(culture),一些标志(flag),哈希算法以及发布者公钥(可为null)
FileDef作为程序集一部分的每个PE文件和资源文件在这个表中都有一个记录项(清单本身所在文件除外,该文件在AssemblyDef表的单一记录项中列出)。在每个记录项,都包含文件名和扩展名(不包含路径),哈希值和一下标志(flag)。如果程序集中只包含它自己的文件,FileDef表将无记录
ManifestResourceDef作为程序集一部分的每个资源在这个表中都有一个记录项。记录项中包含资源名称,一些标志(如果在程序集外部可见,就为public;否则为private)以及FileDef表的一个索引(指出资源或流包含在哪个文件中)。如果资源不是独立文件(比如.jpg或.gif),那么资源就是包含在PE文件中的流。对于嵌入资源,记录项还包含一个偏移量,指出资源流在PE文件中的起始位置
ExportedTypesDef从程序集的所有PE模块中导出的每个public类型在这个表中都有一个记录项。记录项中包含类型名称,FileDef表的一个索引(指出类型由程序集的哪个文件实现)以及TypeDef表的一个索引。注意,为节省空间,从清单所在文件导出的类型不再重复,因为可通过元数据的TypeDef表获取类型信息

假定有两个源代码文件

  • RUT.cs,其中包含不常用类型
  • FUT.cs,其中包含常用类型
    将不常用类型编译到一个单独的模块。这样一来,如果程序集的用户永远不使用不常用类型就不需要部署这个模块
csc /t:module RUT.cs

接着将常用类型编译到另一个模块。该模块将成为程序集清单的宿主,因为这些类型会经常用到。事实上,由于该模块现在代表整个程序集在此将输出文件改为MultiFileLibrary.dll而不是默认的FUT.dll

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

最终创建如下所示的两个文件
<含有两个托管模块的多文件程序集,清单在其中一个模块中>
含有两个托管模块的多文件程序集,清单在其中一个模块中

元数据token是一个4字节的值。(以0x26000001位示例)。其中高位字节指明token的类型(0x01=TypeRef,0x02=TypeDef,0x23=AssemblyRef,0x26=File(文件定义),0x27=ExportedType)。三个低位字节指对应的元数据表中的行。例如,0x26000001这个实现token引用的是File表的第一行。大多数表的行从1而不是0开始编号。TypeDef表的行号实际从2开始。

ClR并非一上来就加载所有可能用到的程序集。只有在调用的方法确实引用了未加载程序集中的类型时,才会加载程序集。

使用Visual Studio IDE将程序集添加到项目中,打开解决方案资源管理器,右击想添加引用的项目,选择"添加引用"来打开"引用管理器"对话框
Visual Studio引用管理器

如果程序集要包含由不同编译器生成的模块(而且这些编译器不支持与C#编译器的/addmodule开关等价的机制),或者在生成时不清楚程序集的打包要求,程序集链接器(AL.exe)就显得相当有用,还可用AL.exe生成只包含资源的程序集,也就是附属程序集(satellite assembly),它们通常用于本地化

AL.exe使用程序能生成EXE文件,或者只生成包含清单(对其他模块中的类型进行描述)的DLL PE文件。为了理解AL.exe的工作原理,让我们改变一下MultiFileLibrary.dll程序集的生成方式

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

下图展示了执行了这些命令后的结果
<三个托管模块构成的多文件程序集,其中一个含清单>
三个托管模块构成的多文件程序集,其中一个含清单
调用AL.exe时添加/main命令行开关,可指定模块的那个方法是入口

csc /t:module /r:MultiFileLibrary.dll Program.cs
al /out:Program.exe /t:exe /main:Program.Main Program.netmodule

AL.exe还会生成一个小的全局函数,名为_EntryPoint,其中包含以下IL代码

.method privatescope static void _EntryPoint$PST06000001() cil maanged
{
	.entrypoint
	// Code size     8 (0x8)
	.maxstack  8
	IL_0000:tail
	IL_0002:call void [.module ' Program.netmodule'] Program::Main()
	IL_0007:ret
}  // end of method 'Global Functions'::_EntryPoint

用AL.exe创建程序集时,可以/embed[resource]开关将文件作为资源文件添加到程序集,该开关获取任意文件并将文件内容嵌入最终的PE文件,清单的ManifestResourceDef表会更新以反映新资源的存在。AL.exe还支持/link[resource],它同样获取包含资源的文件,但只更新清单的ManifestResourceDef和FileDef表以反映新资源的存在,支出资源包含在程序集的哪个文件中。资源文件不会嵌入程序集PE文件中;相反,它保持独立,而且必须和其他程序集文件一起打包部署。

AL.exe或CSC.exe生成PE文件程序集时还会在PE文件中嵌入标准的Win32版本资源。可查看文件属性来检查该资源。在应用程序代码中调用System.Diagnostics.FileVersionInfo的静态方法GetVersionInfo,并传递程序集的路径作为参数,就可以获取并检查这些信息。

5.程序集版本资源信息

<版本资源字段和对应的AL.exe开关/定制特性>

版本资源AL.exe开关定制特性/说明
FILEVERSION/fileversionSystem.Reflection.AssemblyFileVersionAttribute
PRODUCTVERSION/productversionSystem.Reflection.AssemblyInformationalVersionAttribute
FILEFLAGSMASK(无)总是设为VS_FFI_FILEFLAGMASK(在WinVer.h中定义为0x0000003F)
FILEFLAGS(无)总是0
FILEOS(无)目前总是VOS_WINDOWS32
FILETYPE/target如果指定了/target:exe或/target:winexe,就设为VFT_APP;如果指定了/target:library,就设为VFT_DLL
FILESUBTYPE(无)总是设为VFT2_UNKNOWN(该字段对于VFT_APP和VFT_DLL无意义)
AssemblyVersion/versionSystem.Reflection.AssemblyVersionAttribute
Comments/descriptionSystem.Reflection.AssemblyDescriptionAttribute
CompanyName/companySystem.Reflection.AssemblyCompanyAttribute
FileDescription/titleSystem.Reflection.AssemblyTitleAttribute
FileVersion/versionSystem.Reflection.AssemblyFileVersionAttribute
InternalName/out设为指定的输出文件的名称(无扩展名)
LegalCopyright/copyrightSystem.Reflection.AssemblyCopyrightAttribute
LegalTrademarks/trademarkSystem.Reflection.AssemblyTrademarksAttribute
OriginalFilename/out设为输出文件的名称(无路径)
PrivateBuild(无)总是空白
ProductName/productSystem.Reflection.AssemblyProductAttribute
ProductVersion/productversionSystem.Reflection.AssemblyInformationalVersionAttribute
SpecialBuild(无)总是空白

可直接打开AssemblyInfo.cs文件修改自己的程序集特有信息,Visual Studio还提供了对话框帮助编辑该文件
<程序集信息>
程序集信息
<版本号格式>

major(主版本号)minor(次版本号)build(内部版本号)revision(修订号)
示例257192

上表展示了一个示例版本号2.5.719.2。前两个编号构成了公众对版本的理解。公众会将这个例子看成是程序集的2.5版本。第三个编号719是程序集的build好。如果公司每天都生成程序集,那么每天都应该递增这个build号。最后一个编号2指出当前build的修订次数。如果因为某个原因,公司某一天必须生成两次程序集(可能是为了修复一个造成其他什么事情都干不了的hot bug),revision号就应该递增。

程序集有三个版本号

  • AssemblyFileVersion
    这个版本号存储在win32版本资源中。它仅供参考,CLR集不检查,也不关心这个版本号。通常,可以先设置好版本号的major/minor部分,这是希望公众看到的版本号,,然后每生成一次就递增build和revision部分。理想情况是Microsoft的工具(比如CSC.exe或者AL.exe)能自动更新build和revision号(根据生成时的日期和时间)。但实际情况并非如此。在window资源管理器能看到这个版本号。对客户系统进行故障诊断时,可根据它识别程序集的版本是多少。
  • AssemblyInformationalVersion
    这个版本号也存储在win32版本资源中,同样仅供参考。CLR既不检查也不关心它。这个版本号的作用是指出包含该程序集的产品的版本。例如,产品的2.0版本可能包含几个程序集,其中一个程序集标记为版本1.0,因为它是新开发的,在产品的1.0版本中不存在。通常,可以设置这个版本号的major和minor部分来代表产品的公开版本号。以后每次打包所有程序集来生成完整产品,就递增build和revision部分
  • AssemblyVersion
    这个版本号存储在AssemblyDef清单元数据表中。CLR在绑定到强命名程序集时会用到它。这个版本号很重要,它唯一性的标识了程序集。开始开发程序集时应该设置好major/minor/build/revision部分。而且除法要开发程序集的下一个可部署版本,否则不应变动。如果程序集A引用了程序集B,程序集B的版本会嵌入程序集A的AssemblyRef表。这样一来,当CLR需要加载程序集B时,就准确的知道当初生成和测试的是程序集B的哪个版本。

6.语言文化

除了版本号,程序集还将语言文化(culture)作为其身份标识的一部分。
<程序集语言文化标记的例子>

主标记副标记语言文化
de(无)德语
deAT奥地利德语
deCH瑞士德语
en(无)英语
enGB英国英语
enUS美国英语

如果应用程序包含语言文化特有的资源,Microsoft强烈建议专门创建一个程序集来包含代码和应用程序的默认(或备用)资源。

7.简单应用程序部署(私有部署的程序集)

下面展示如何打包和部署程序集使用户能运行应用程序
Window Store应用对程序集打包有一套很严格的规则,Visual Studio会将应用程序所有必要的程序集打包成一个.appx文件。该文件要么上传到Windows Store,要么side-load到机器。用户安装.appx文件时,其中包含的所有程序集都进入一个目录。CLR从该目录加载程序集,window则在用户的"开始"屏幕添加应用程序磁贴。如果其他用户安装相同的.appx文件,程序集会使用之前安装好的,新用户只是在"开始"屏幕添加了一个磁贴。用户卸载window store应用时,系统从"开始"屏幕删除磁贴。如果没有其他用户安装该应用,window删除目录和其中所有的程序集。注意,不同用户可以安装同一个window store应用的不同版本。对于非window store的桌面应用,程序集的打包方式没有任何特殊要求。打包一组程序集最简单的方式就是直接复制所有文件。例如,可将所有程序集文件放到一张光盘上,将光盘分发给用户,执行上面的批处理程序,将光盘上的文件复制到用户硬盘上的一个目录。由于已经包含了所有依赖的程序集和类型,所以用户能直接运行应用程序,“运行时"会在应用程序目录查找引用的程序集。不需要对注册表进行任何修改就能运行程序。要卸载应用程序,删除所有文件即可。也可使用其他机制打包和安装程序集文件,比如使用.cab文件(从Internet下载时使用,旨在压缩文件并缩短下载时间)。还可将程序集文件打包成一个MSI文件,以便有windows Installer服务(MSIExec.exe)使用。使用MSI文件可实现程序集的"按需安装”——CLR首次尝试加载一个程序集时才安装它。当然也可使用Visual Studio内建的机制发布应用程序。具体做法是打开项目属性页并点击"发布"标签。利用其中的选项,可以让Visual Studio服务器或者文件路径。这个MSI文件还能安装必备组件,比如.Net Framework或Microsoft SQL Server Express Edition。最后,利用ClickOnce技术,应用程序还能自动检查更新并在用户的机器上安装更新。

每个程序集都用元数据注明了自己引用的程序集,不需要注册表设置,在COM中,类型是在注册表中登记的。

8.简单管理控制(配置)

为了实现对应用程序的管理控制,可在应用程序目录放入一个配置文件。应用程序的发布者可创建并打包该文件。安装程序会将配置文件安装到应用程序的基目录。另外,计算机管理员或最终用户也能创建或修改该文件。CLR会解析文件内容来更改程序集文件的定位和加载策略。配置文件包含XML代码,它既能和应用程序关联也能和机器关联。由于使用的是一个单独的文件(而不是注册表设置),用户可以方便地备份文件,管理员也能将应用程序方便地复制到其他机器——只要把必要的文件复制过去,管理员策略就会被复制过去。

假定应用程序的发布者想把MultiFileLibrary的程序集文件部署到和应用程序的程序集文件不同的目录,要求目录结构如下

AppDir目录(包含应用程序的程序集文件)
	Program.exe
	Program.exe.config
	AuxFiles子目录(包含MultiFileLibrary的程序集文件)
		MultiFileLibrary.dll
		FUT.netmodule
		RUT.netmodule

由于MultiFileLibrary的文件不在应用程序的基目录,所以CLR无法定位并加载这些文件。运行程序将抛出System.IO.FileNotFoundException异常。为了解决问题,发布者创建了XML格式的配置文件,把它部署到应用程序的基目录。文件名必须是应用程序主程序集文件的名称,附加.config扩展名,也就是Program.exe.config。配置文件内容如下。

<configuration>
	<runtime>
		<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.vl">
			<probing privatePath="AuxFiles">
		</assemblyBinding >
	</runtime>
</configuration>

CLR尝试定位程序集文件时,总是先在应用程序基目录查找。如果没有找到,就查找AuxFiles子目录。可为probing元素的privatePath特性指定多个以分号分隔的路径。每个路径都相对于应用程序基目录。不能用绝对或相对指定在应用程序基目录外部的目录。这个设计的出发点是应用程序能控制它的目录及子目录,但不能控制其他目录。
这个XML配置文件的名称和位置取决于应用程序的类型。

  • 对于可执行应用程序(EXE),配置文件必须在应用程序的基目录,而且必须采用EXE文件全名作为文件名,再附加.config扩展名
  • 对于Microsoft.ASP.NET Web窗体应用程序,文件必须在Web应用程序的虚拟根目录中,而且总是命名为Web.config。除此之外,子目录可以包含自己的Web.config,而且配置设置会得到继承。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值