上一篇博客我主要介绍了类型如何生成托管模块、托管模块如何链接成程序集等生成、打包、部署及管理等知识传送门https://blog.csdn.net/sinat_33087001/article/details/80347193,本篇博客介绍两种程序集:强命名程序集和弱命名程序集,阅读本篇博客前,我先抛出3个问题,如果你想了解问题的解决,那么这篇博客就对你胃口啦:
1. CLR如何验证程序集的安全性
2. 如何解决版本之间不兼容,以至于引入新dll会导致其它不可用
3. 运行时类型引用是如何被解析的
其中前两个问题再加上上一章解决的“简单部署”问题,**.NetFrameWork的三大部署目标就都能实现了(1,简单部署 2,安全性可验证 3,dll Hell问题)。**先梳理下本文的行文脉络,最佳食用方式。主线即是围绕强弱命名程序集展开的,而以强命名程序集为主。
- 强命名程序集如何得到强名称、全局程序集缓存是什么
- 强命名程序集怎么实现防篡改、延迟签名策略是什么
- 强明明程序集如何部署、如何被引用
- 运行时(JIT编译)如何解析类型引用(运行时发生了什么)
- 加入全局之后的高级管理控制(对应于上一篇博文提到的简单管理控制)
#两种命名程序集
程序集共有两种命名方式:强命名程序集和弱命名程序集。程序集有两种部署方式:私有部署和全局部署。
##两种程序集
强命名程序集:使用发布者的公钥/私钥对进行了签名,由于程序集被唯一标识,所以强命名程序集可以被部署到用户机器的任何地方,甚至可以被部署到Internet上。
弱命名程序集:与强命名程序集结构完全相同,区别仅在于没有公钥/私钥对,所以只能私有部署。
##两种部署
私有部署:程序集部署到应用程序基目录或者某个子目录。
全局部署:多个应用程序共享的程序集部署即是全局部署,.NetFrameWork随带的程序集就是典型的全局部署,这就可以解释为什么有些应用程序运行需要.NetFrameWork的支持,因为它们某些功能可能用到了.NetFrameWork的程序集。
##程序集与部署
两种程序集各自可以适用的部署方式如下所示:
可以看得出强命名程序集既可以进行私有部署,也可以进行全局部署,但为了实现“简单部署”,最好只使用私有部署,后边我会详细介绍。
##为程序集分配强名称
程序集要想变为强命名程序集,就得需要唯一性标识,这个唯一性标识就是公钥/私钥对,不仅可以解决唯一性问题,还可以解决安全策略、检查完整性,允许分配授权
###问题所在
要想全局部署,多个应用程序共享的强命名程序集,就得有个公认的目录来存放,这样多个应用程序才能找得到。而多个公司可能会生成同名文件,同名文件放到公认目录会导致覆盖,所以有时候会遇到这种现象:A公司的软件里有个tml.dll文件,B公司的软件装到电脑上的时候恰好也有个tml.dll文件,恰好也是强命名程序集,然后放到公共目录覆盖了A公司的,然后A公司的软件就不可用了**(这就叫DLL hell).**
###唯一性标识
强命名程序集具有四个重要特性来进行唯一性标识:文件名(不计扩展名)、版本号、语言文化、公钥。其实可以发现,前三个标识没有什么用,只有最后一个公钥才能唯一标识。
可以看到前三条的公钥token是同一个公司的,最后一个和第一个前三项完全相同,但是是不同的程序集,说明标识策略即是最后的公钥token(公钥标记)。公钥的不同基于不同的公司!
###创建强命名程序集
创建强命名程序集分为两步:1,创建公钥/私钥对,2,编译程序集
####创建公钥/私钥对
创建公钥/私钥对分为以下几个步骤:
1,SN -k MyCompany.snk
首先创建一个公钥/私钥对文件
2,SN -p MyCompany.snk MyCompany.PublicKey sha256
从MyCompany.snk中提取出只含公钥的文件并且采用SHA256算法
3,SN -tp MyCompany.PublicKey
使用该命令生成公钥标记(64位哈希值),注意不同的公钥可能会生成相同的公钥标记
以上三行命令分别对应如下输出:
####编译程序集
使用命令csc /keyfile: MyCompany.snk Program.cs
即可**用私钥对程序集进行签名,并且将公钥嵌入到清单中。**具体实现分为以下几步:
- FileDef清单元数据表列出构成程序集的所有文件
- 每将一个文件添加到清单,都对文件内容哈希处理,哈希值和文件名一道存储到FileDef中。
- 生成包含清单的PE文件(程序集的宿主,包含清单文件)后,对PE文件完整内容(除Authenticode Signature、程序集强名称数据以及PE头校验和)进行哈希处理。
(注意:强名称(文件名,程序集版本号,语言文化,公钥)也不会哈希处理哦,包括公钥)
由上图可知:哈希值用私钥签名,得到的RSA数字签名存储到PE文件的一个保留区域(哈希处理时也会忽略),同时发布者公钥也会嵌入PE文件的AssemblyDef清单元数据表。
AssemblyRef表为了节省空间,存储的都是公钥标识(因为几个公钥进过哈希计算可能会得到相同的公钥标记,所以CLR不会用公钥标记做安全或信任决策),AssemblyDef存储的是完整公钥,目的就是为了防篡改
注:在VS里可以从项目属性–签名–为程序集签名来选择
#安全策略
这里的安全策略采用了公钥/私钥对的形式,关于公钥/私钥如何做到安全通信加密,通信双方如何操作原理下边这篇博文讲的比较详细:
关于公钥/私钥http://www.blogjava.net/yxhxj2006/archive/2012/10/15/389547.html
##强命名程序集如何防篡改
###GAC目录检查
GAC也就是全局缓存,后边会介绍到,这里只要知道是共享程序集存放的位置就好了。用私钥对程序集签名,并将公钥和签名嵌入程序集中,CLR就可以验证,验证过程如下:
- CLR对程序集包含清单的文件,用公钥解除签名获得Hash值,确保来源可靠
- CLR对程序集包含清单的文件使用Hash处理,比对步骤1的值,如果一致则说明未经修改。
- CLR对程序集的其它文件每一个都进行Hash处理并且与清单文件中国的FileDef做比较,任何一个Hash值不相同都会导致程序集无法被安装到GAC。
优点:GAC目录下的安全检查仅在安装时执行一次,性能损耗较小。缺点:因为在GAC安装程序集,所以违反了简单部署的原则。
###非GAC目录检查
对于非GAC目录安装的程序集(应用程序基目录或者通过配置文件codeBase元素指定的路径中),CLR会在程序集加载后比较Hash值,所以每次加载程序集时都会执行安全策略,损耗性能。
优点:应用程序每次执行,每次加载程序集时都会执行安全策略,损耗性能。缺点:可以达成简单部署的目标。
###应用程序绑定到程序集
应用程序绑定到程序集时,依据如下顺序定位程序集位置:
- CLR依据程序集的强名称(编译时获取AssemblyRef中标明,来自CLR目录)在GAC中定位该程序集,匹配被引用程序集(GAC中的目录)的签名,这样可以保证运行时和编译时的发布者唯一。
- GAC中找不到去应用程序基目录找。
- 应用程序基目录没有,则访问配置文件的codeBase元素标注的私有路径
- 如果由MSI按照,CLR要求MSI定位程序集。
##延迟签名策略
在开发和测试阶段,频繁的访问私钥是一件麻烦事(私钥一般被公司严密保护),所以采用延迟签名策略,运行公司生成只有公钥的程序集。
###有何不同
延迟签名和正常的签名的相同与不同之处在于: - 延迟签名任然具有公钥,在AssemblyDef和引用该程序集的AssemblyRef里没有任何影响。
- 延迟签名仍然可以存储到GAC(要求关闭防篡改机制)
- 延迟签名在开发和测试阶段没有私钥,可能被篡改
###如何操作
创建过程与正常签名类似,唯一的不同是前期PE文件中不嵌入数字签名(因为没有私钥嘛),但实用程序会依据公钥大小判断需要预留多大空间来给RSA数字签名预留空间,具体步骤如下:
- 开发和测试期间,
csc /keyfile:MyCompany.PublicKey /delaysign MyAssembly.cs
使用该命令仅加入公钥,并指明要采用延迟签名策略。 - 关闭每一台机器的安全策略
SN.exe -Vr MyAssembly.dll
以确保程序集能被安装到GAC - 在打包和部署阶段,打开安全策略
SN.exe -Vu MyAssembly.dll
- 获取公司私钥并按照到GAC
SN.exe -Ra MyAssembly.dll MyCompany.PrivateKey
另外执行延迟签名的好处在于,由于不经过hash计算,可以执行混淆程序。
#强命名程序集的部署和引用
如前所述,强命名程序集既可以私有部署看,又可以全局部署。
##全局程序集缓存
前边提到的全局共享目录,也就是GAC,一般放在:
在该目录下经过算法结构化存放程序集,子目录自动生成,即使同名,只要不同公司,一样可以区别开来,不会发生相互覆盖现象。
注意:千万不能随意手动复制,因为目录是自动生成的而且GAC目录只能存放强命名程序集
##私有部署强命名程序集
私有部署即是部署到程序的基目录及其子目录
存在以下不合理的历史策略:少数应用程序共享的程序集目录,配置文件的codeBase元素指向的路径既不在GAC,也不在应用程序内部,而是一个第三方。这样做有一个很大的问题就是:每个应用程序都不能决定何时删除该程序集。
##引用强命名程序集
编译时和运行时强命名程序集有两套(Microsoft),一套安装在CLR目录(方便编译时找路径),一套安装在GAC(方便运行时加载程序集)。
- CLR目录下:编译使用,只有元数据,与机器无关
- GAC目录下:运行使用,包括元数据和IL代码以及针对特定CPU架构的优化,运行存在多个程序集拷贝,每一个架构都有一个专门的子目录存放拷贝。
在编译时的程序集搜索顺序如下:
- 工作目录。
- CLR所在目录。
- /lib编译器开关指定目录。
- LIB环境变量指定的任何目录。
运行时的程序集搜索顺序在下一部分介绍。
#运行时解析类型引用
还是使用上一篇博文里举的例子:
namespace TML { class Program { static void Main(string[] args) //成员方法入口 { Console.WriteLine("Hello World!"); //引入的外部类型Console } } }
运行时执行步骤如下:
- 初始化CLR
- CLR读取程序集的CLR头,查找标识应用程序的入口方法Main的MethodDef
- 检索MethodDefy元数据表找到方法的IL代码在文件中的偏移量
- 将IL代码使用JIT编译器编译(提前需要验证)为本机代码
- 执行本机代码
在第三步,CLR会检测所有类型和成员引用,顺序如下:
- IL call 引用了元数据token0A000003,表示MemberRef中的记录项3
- 检查该项,发现字段引用了TypeRef表中记录项
- 检查后发现该类型System.Console非本程序集,所以被引到AssemblyRef,定位到了程序集
总体流程见下图,如果被引用类来自本程序集内部,则参见左边两条分支:
#高级管理控制
与私有部署不同,全局部署的时候,为了解决版本冲突问题,需要高级管理控制。正因为高级管理控制,程序集才能同时存在多版本。
##版本控制
CLR通过配置文件定位程序集
可以发现主要有以下几个元素:
- probing:对于弱命名程序集,直接检查私有路径,对于强命名程序集,会依据GAC—codeBase指定路径—私有路径的顺序检查程序集
- 第一个dependentAssembly及其子元素表明:依据codeBase元素,重定向该程序集版本1.0.0.0为2.0.0.0,codebase已经提供了2版本的地质
- codeBase指明了更新地址,如果是弱命名程序集,codeBase只能指向应用程序基目录的子目录
- 第二个dependentAssembly及其子元素表明:查找到3-3.5版本后,统一重定向到版本4.0
- publisherPolicy元素,表明忽略TypeLib发布者的策略文件
执行流程如下:
- CLR定位程序集,进行指定重定向跳转。
- 加载发布者策略(若为yes),执行发布者的命令跳转到希望版本。
- 定位到后从GAC加载,GAC没有,则从codeBase指定url加载
注意,CLR默认不加载新版本程序集,如果管理员希望所有应用程序使用发布者更新,则修改Machine.config中的文件,关于该文件,这篇博客有详细说明:
##发布者策略控制
由发布者告诉用户该使用什么版本,也可以用来修复bug。
###发布者策略文件
发布者策略配置文件如下,不存在probing和publisherPolicy元素。该示例指明一旦发现1.0版本的引用个,就执行2.0版本,当然如果2.0版本bug更多,可以直接选择publisherPolicy的no。
###包含发布者策略文件的程序集
使用al.exe来生成程序集,使用如下命令:
从上到下四个命令依次为:
- /out 创建PE文件(只包含清单),要应用Policy发布者策略,适用版本1.0,应对程序集为SomeClassLibrary.dll。
- /version 表示发布者策略程序集的版本。
- / keyfile表示要对发布者策略程序集进行加密
- /linkresource表示将配置文件作为程序集的一个单独文件。
注意:发布者策略程序集随同新的SomeClassLibrary.dll程序集一起打包部署到用户机器,发布者策略程序集必须安装到GAC
整个博文终于梳理完了,写到一半网速太差csdn保存奔溃,导致后半部分又重写了一遍,那种感觉真是万分痛心,意识到了自己千辛万苦写的东西不易啊。最近不再想C#的出路问题了,因为在博客的写作过程中,越来越喜欢这些底层的东西,短期内确实发挥不了多大作用,但觉得特别开心,因为发现了很多原理性东西,感觉知其所以然,非常开心,继续加油吧!