【深入理解CLR】 三.生成、打包、部署和管理应用程序及类型
上一篇博文https://blog.csdn.net/sinat_33087001/article/details/80185199讲了CLR的执行模型,从整体流程把控方面介绍了CLR的执行过程,对于一些细节没有进行说明,例如:类怎么被编译为模块文件的、模块文件又是怎么被合并为程序集等问题,还有诸如元数据是如何工作的、程序集的版本资源信息怎么看等操作。本篇博文可以说是对上一篇细节的补充说明吧。
这里扯个题外话,从上两篇博文的阅读量来看,再和之前我写的java相关博文阅读量比起来,简直惨不忍睹,看来大家对于.Net的热情确实消退了不少,感觉微软因为开源和高收费失去了中国市场的十年吧,希望.Net Core的出现,和微软云编译,模糊化PC的战略能打赢下一个十年,毕竟感觉微软是非常伟大的一家公司。对照最近5.15的TNT和中兴的休克,真的能感受到科技PPT和科技创新之间真正的差距,我们还有很长的路要走啊。
闲言少叙,书归正传,依照惯例,本篇博文的最佳食用方式如下:本篇博文主线是1、类如何编译为托管模块———–2、托管模块如何集成为程序集,实际上也就是上篇博文讲到的第一部分操作,将源代码编译为面向CLR的程序集的全部细节过程,附带将详细讨论元数据有哪些内容,另外,你将会了解到一些原理性知识,
- 为何现在删程序好删多了,不用管注册表什么的,而之前特别麻烦。
- CLR如何解析和定位要执行的应用程序集,即使你把qq软件里的远程连接dll扔到微信文件夹下,只要通过配置依然安然无恙。
- 软件为何可以具备那么多语言,怎么加载的
- 程序集的版本资源信息又是如何生成的
等等知识,数不胜数,接下来通过主线来串联本篇博文涉及到的知识.
将类型生成到模块中
按照上一篇博客的理解,我们得现将源代码编译成托管模块,这个过程实质上就是把类型生成到托管模块中去依照Jeffrey举的一个小小例子来分析:
namespace TML
{
class Program
{
static void Main(string[] args) //成员方法入口
{
Console.WriteLine("Hello World!"); //引入的外部类型Console
}
}
}
以上所示也就是源代码啦,编写完是一个Program.cs这样子的源代码文件。
生成基本流程
拿到源代码文件Program.cs之后做如下处理用来生成Program.exe(可执行文件,换言之可被CLR执行文件):
执行如下命令 csc.exe /out:Program.exe /t:exe /r:MScorLib.dll Program.cs
下面解释这几个输出参数:
- /out
输出文件名,这里文件名为Program.exe
- /t:exe
生成控制台可执行文件 (/target:exe的缩写: )
- /r
从指定程序集文件引用元数据 (/reference:<文件列表> 的 缩写:) ,这里引用的程序集文件是MScorLib.dll
该篇文章有详细配置和参数说明 https://blog.csdn.net/cy88310/article/details/4792118
由于C#编译器默认执行命令 /out:Program.exe /t:exe,所以以上命令可以简写为 csc.exe /r:MScorLib.dll Program.cs
这里为什么要引用MScorLib.dll 程序集呢?因为这里引用了外部类型System.Console的WriteLine方法,而该类型来源于此文件集,MScorLib.dll 是核心类型,所以C#编译器会自动引用该程序集。所以以上命令又可以简写为 csc.exe Program.cs
当然如果不想默认引用该程序集,使用如下命令:
/nostdlib[+|-]
不引用标准库(mscorlib.dll) ,这个时候就会报错,因为没有引用标准库:csc.exe /out:Program.exe /t:exe /nostdlib Program.cs
该文件使用的System.Console类是在标准库中定义的。
以上这一大段的核心内容就是:编译器将cs文件编译为exe文件。cs文件里引用的一些外部类型要想使用必须引用外部程序集,那么就需要通过一些命令来引,/r,那么问题来了,如果我这个类文件要引100个程序集,得在命令行敲100个么?这就需要接下来介绍的神器响应文件来帮忙了
辅助神器–响应文件
为了避免输入命令的繁琐,不再使用/r开关显式的引用这些程序集,C#编译器支持响应文件,这货长这样:
这是我本机的csc.rsp文件,这个文件是全局性质的响应文件。那么在执行命令的时候,读取流程是什么呢?例如以下这行命令:
csc.exe @MyProject.rsp Program.cs
CSC在运行时经过如下过程:
- 首先检查全局响应文件csc.rsp(在csc.exe所在目录),若有加载
- 其次检查本地引入的响应文件(这里就是Myproject.rsp)若有加载
- 最后读取命令行手动显示指定的设置
汇总三方面命令得出最后的总的命令,若命令行与全局和本地文件冲突,以命令行为准,若本地和全局发生冲突,以本地为准,另外,指定/noconfig
命令行开关,编译器将忽略本地和全局csc.rsp
构建联系–元数据(定义表与引用表)
元数据是由几个表构成的二进制数据块,和JVM的class文件表类似,只是class文件在一张表(表里的一些项也是表)里聚合了名称和数量并且使用常量池来读取值,而元数据则分成3张表,元数据定义表描述了自身的信息,元数据引用表描述了引用信息,元数据清单表应用于程序集,托管模块没有该表。
元数据定义表
元数据定义表包含7张表,模块、类型、(方法、字段、参数、属性、事件)
- ModeleDef:每一个记录项包括模块文件名和扩展名、模块版本ID
- TypeDef每一个记录项包括类型的名称、基类型、一些标志、一些索引,这些标志标志了类的可访问性等信息,这些索引则指明了类的组成元素来自哪张表里的哪些项
- MethodDef每一个记录项包括方法的名称、一些标志、签名、方法的IL代码在模块中的偏移量、一个索引,这个索引指向了ParamDef表中的一个记录项,指明方法参数的信息
- FieldDef每一个记录项包括字段的名称、字段类型、一些标志,标志了字段的可访问性等
- ParamDef每一个记录项包括参数的名称、参数类型、标志,
- PropertyDef每一个记录项包括属性的名称、属性类型、标志,
- EventDef每一个记录项包括事件的名称、标志,
这里科普下:方法签名由方法名称和一个参数列表(方法的参数个数、顺序和类型)组成
通过以上信息可以看出,这些元数据定义表基本就清晰的描绘出了整个模块里各个类的一些细节,但仍然有一个问题,如果我一个类引用了另一个类,引用信息该怎么描述呢?我如果想查看被引用类属于哪个程序集,哪个模块该怎么办,这些就要用到元数据引用表。
元数据引用表
元数据应用表主要包括4张表:引用的程序集表、引用的模块表、引用的类型表、引用的成员表
- AssemblyRef每个记录项都包含:程序集的名称、版本号、语言文化、公钥token生成hash值(标识该程序集发布者)、标志、校验和hash值
- ModuleRef每个记录项都包含:所引用托管模块的文件名和扩展名
- TypeRef每个记录项都包含:所引用类型的名称和类型位置
- MemberRef每个记录项都包含:所引用的每个成员的名称、签名、指向对成员定义的那个类型的TypeRef记录项
反编译对照说明
上边说了这么多表,太混乱,这里还是用开篇代码Program.exe来描述下具体的元数据是怎样的,为方便显示,将定义表里对应的一些引用直接放到该项目下边说明。
- 首先是定义表:
这里就不讨论托管模块表,可以从类的定义表里看到如下信息,类名、基型、一些标志位、索引(这里直接整合到下边,不呈现MethodDef了)也就是方法的记录项,包括方法名称、一些标志、偏移量(RVA目前还不知道干啥的)、方法的签名返回类型。构造器方法里包括一个指向调用方法时构造对象的内存,这也解释了为什么可以通过this访问自己。 - 其次是引用表
程序集引用表,可以看出引用自程序集mscorlib也就是标准库
类型引用,引用了System.Console类型的方法WriteLine。这里有个困惑留下来,我本身用到的类型如何对应到引用的类型,难道仅凭类型名就可以么?这也太草率了吧。
除此之外ILDasm.exe还提供统计信息。
将模块合并为程序集
上一篇博文讲到了程序集,这里添加一些程序集的特点
- 程序集定义了可重用的类型
- 程序集用一个版本号标记
- 程序集可以关联安全信息
可以将程序集视为一个逻辑的DLL或EXE,一般由含有元数据的PE文件、IL代码文件和一些jpg、gif等资源文件构成。
如何配置应用程序去下载程序集文件呢?可以在应用程序配置文件中指定codeBase元素,在codeBase的URL中检查机器的下载缓存,如果有直接加载文件,没有的话去URL位置下载到缓存,如果URL都没有,直接抛出异常FileNotFoundException。
程序集的三大好处:增量下载、代码与资源文件分离、多语言封装
注意:对于常用到的那些设置和类型,虽然程序集是逻辑分组的,但您最好还是把它们放到一个文件里,这样可以让CLR缩短以下查找时间,提高下查找性能不是
构建联系–元数据(清单表)
合并的前提就是清单,只有通过清单,CLR才能知道程序集的具体信息,知道该加载哪些资源文件。换句话说清单就是程序集的自我介绍。
也许各位看着表头疼,一如前例,我简单介绍一下这个自描述清单表的各部分到底有什么:
- AssemblyDef :程序集名称、版本、语言文化、一些标志、哈希算法、发布者公钥(特别注意,这里不分什么项,描述自身程序集的就这些内容)
- FileDef:每个PE和资源文件都有记录项,文件名和扩展名、哈希值、标志
- ManifestResoutceDef:主管资源,资源有两类,独立的资源文件和嵌入PE的资源流,对于独立的资源文件,给个指向FileDef的引用就行,如果是嵌入的,还得给个偏移量,确定资源在PE文件中的起止位置。当然还包括资源名称、一些标志(主要是访问标志)
- ExportedTypesDef:主管导出类型,所有PE模块中的每个导出类型都都应该有个记录项,包含类型名称,指向FileDef表中的引用以及指向TypeDef的引用。也就是定位到托管模块再定位到类型
合并基本流程
一些开关
以下几个常用开关可以帮助我们编译指定文件:
-
/t:exe
生成控制台可执行文件 (/target:exe的缩写: ) -
/t:winexe
生成图形界面可执行文件 (/target:exe的缩写: ) -
/t:appcontainerexe
生成WindowsStore执行体 (/target:exe的缩写: ) -
/t:library
生成类库执行体 (/target:exe的缩写: ) -
/t:winmdobj
生成winmd执行体 (/target:exe的缩写: ) (不知道干嘛的 )
总之以上任何命令执行完都会生成程序集,换句话说,都包含清单文件、元数据PE+IL、资源文件(不一定有)三件套。可以直接让CLR操作,但如果加了这个开关:/t:module
这样生成的就是一个托管模块了(不一定准确啊),反正就是不含清单文件,不能直接执行了。要想执行这个模块,必须得把它加载到别的程序集里,使用这个命令/t:addmodule
。
工作流程
生成程序集文件
假定有两个源代码文件:Java.cs(包含不常用类型)、C#.cs(包含常用类型)。在有了之前的开关的基础上,这里同样用书中举的例子来说明情况吧:
1,首先编译一个非程序集模块:csc /t:module Java.cs
因为不常用,所以可以只编译为模块,用的时候再加载到程序集里去。
2,其次编译个常用的程序集模块,顺便再加上那个不常用的已经编译好的模块Java.netmodule
csc /out:C#biggerthanJava.dll /t:library /addmodule:Java.netmodule C#.cs
/t:addmodule
这个开关告诉编译器要将Java.netmodule文件添加到FileDef清单元数据表,并将Java.netmodule的公开导出类型添加到ExportTypesDefe清单元数据表,这儿没涉及到啥资源。和ManifestResoutceDef没啥关系。用图片表示就类似这样:
图片有些许错误哈,jeffrey也说明了,就像FUT,它是主模块,清单文件宿主,所以不应该出现自公开导出类型的。
这下可以在导出类型里看到引用token编号:0x26000001,引入题外话解释0x26啥意思:
0x26就是表示文件嘛,也就是表示引入文件的token号。
加载程序集文件
经过以上各个步骤,程序集文件C#biggerthanJava.dll 已经生成了,要想用,当然得用命令引进来嘛,/r:C#biggerthanJava.dll
注意所有要引入的文件都必须存在!
- 方法首次调用,CLR检测作为参数、返回值或局部变量被引用的类型
- CLR试图加载所有引用的程序集中含有的清单文件,如果发现和1中检测到的类型一致的,则登记,总之要登记完所有1中指出的所有文件。注意,咱1中用不到的也就别加载了,浪费性能
辅助神器–程序集链接器
这个东西的作用简而言之就是你先生成一堆module,用的时候它帮你链接成程序集,用这个方法,上述步骤就该这么来了: csc /t:module Java.cs
csc /t:module C#.cs
al /out:C#biggerthanJava.dll /t:library Java.netmodule C#.netmodule
生成的程序集就如下所示了:
要是想加入资源文件(咱前边说过资源文件分独立和嵌入两种),也可以直接用命令:/embed
加嵌入的和/link
加独立的。
程序集版本资源信息
程序集版本号组成
通过一些命令或者C#编译器都可以指定程序集的版本信息:这些信息点击任何一个dll的属性都可见:
你可以通过命令(这里就不再详细说了,网上都可以查到)或者VS来直接标明:
这里介绍下版本号,个人感觉java和C#的版本号也很像:
简单来说一下(个人理解啊):拿微信举例吧,微信添加了朋友圈功能,这是大改,主板本号加,朋友圈添加了三天可见,这是小改,次版本号加,这两个是公众感受的。咱开发的时候,对于朋友圈三天可见这个dll每改一天生成一次,build加一,如果一天生成两次了,说明第一次生成有bug,咱改个bug,修订号review加一。
程序集的三个版本号
- AssemblyFileVersion:简单理解就是文件每发生更新(主次版本号先设置好),版本号就加,机器自动加,windows资源管理器可见。
- AssemblyInformationalVersion:简单理解就是文件每发生更新(主次版本号先设置好),版本号就加,不同的是,内部和修订号人工打包标记
- AssemblyVersion:最重要的版本号!唯一标识程序集,存储在AssemblyDef清单元数据表里,也就是之前提到的清单元数据表里的版本号
语言文化
不能每一种语言都开发一种代码吧,所以生成程序集应该指定为语言文化中性,然后把各种语言文化打包为附属程序集(专门放资源文件和语言文化这类不含代码的文件的),这样,用到的时候直接加载(使用/c:text指定,这里目录名称要符合语言文化,也就是上述那张表)就好:举个例子
程序集的部署与控制
.NetFrameWork部署目标
在COM时代(虽然根本没经历过COM时代),但凭使用感受说话吧:
1,安装一个应用程序导入了一个dll,一连串的报错,其它dll受到影响了(DLL HILL问题)
2,安装和卸载复杂,垃圾注册表根本清不干净。
3,安全性不高,用户不能完全控制。
部署目标也就是针对以上三个问题吧:解决DLL hell问题,不依赖注册表,将安全权限完全赋予给用户。
简单应用程序部署
对于windows Store的官方程序
- vs会将所有必要程序集打包为一个.appx文件,用户安装该文件时,所有程序集进入一个目录
- CLR从目录加载程序集,windows在桌面添加磁贴,若其它用户安装该appx,windows只添加磁铁(因为程序集已经在第一次加载好了呀)
- 删除时,如果有很多用户安装,只删不想要的用户的磁贴,如果都不想要,则删程序集。
对于非官方开发者
- 只需要把全部代码复制粘贴到光盘里,所有引用的程序集都在里边,所以摆脱注册表了,这也许就是咱常说的离线安装包下载吧
- 用cab格式,打包成msi文件,联网按需下载。这也许就是在线安装吧
简单管理控制
管理程序集文件
其实主要是CLR定位和加载用的,假定发布者想把应用程序中一个程序集文件(MutiFileLibrary.dll)放到其它地方(也就是不在该应用程序所包含的程序集文件下),那么就需要配置文件:
结构类似这样,这样就可以通过Program.exe.config找到所有文件
该配置文件指明了,要去AuxFile子目录去查找,通过probing元素的privatePath特性。CLR先从应用程序基目录查找,找不到再按照配置路径(配置路径都应为相对基目录的路径)找。通常网站的配置文件就是Web.confg
管理CLR
通过Machine.config来管理CLR,每一个CLR均对应一个Machine.config
全文到这里就结束了,整篇博文所讲的是模块是如何生成,程序集又是如何生成的,CLR是如何加载程序集的(简述)。完成的过程中也在思考一个问题,微软的元数据结构似乎是优于java的class文件结构的,微软的版本号策略似乎也比java的结构清晰。但为何如今发展起来这么缓慢,究其原因,可能就是之前没开源吧,果然开源改变世界啊。最近也一直在思考职业规划,如果要在C#这条路上前进,无疑得使用微软全套(虽然很爽但窄),现在搞的比较好的Azure云势头似乎正在追赶亚马逊的Aws,未来的开发真的会迎来云编译时代么?感觉前景较好的是.netcore和uwp编程以及基于.netFramework的Azure。
上一篇: Spring源码分析之IoC容器初始化
下一篇: PyInstaller程序打包