【深入理解CLR 四】共享程序集和强命名程序集

简介: 【深入理解CLR 四】共享程序集和强命名程序集

上一篇博客我主要介绍了类型如何生成托管模块、托管模块如何链接成程序集等生成、打包、部署及管理等知识传送门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即可**用私钥对程序集进行签名,并且将公钥嵌入到清单中。**具体实现分为以下几步:

  1. FileDef清单元数据表列出构成程序集的所有文件
  2. 每将一个文件添加到清单,都对文件内容哈希处理,哈希值和文件名一道存储到FileDef中。
  3. 生成包含清单的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就可以验证,验证过程如下:

  1. CLR对程序集包含清单的文件,用公钥解除签名获得Hash值,确保来源可靠
  2. CLR对程序集包含清单的文件使用Hash处理,比对步骤1的值,如果一致则说明未经修改
  3. CLR对程序集的其它文件每一个都进行Hash处理并且与清单文件中国的FileDef做比较,任何一个Hash值不相同都会导致程序集无法被安装到GAC。

优点:GAC目录下的安全检查仅在安装时执行一次,性能损耗较小。缺点:因为在GAC安装程序集,所以违反了简单部署的原则。

###非GAC目录检查

对于非GAC目录安装的程序集(应用程序基目录或者通过配置文件codeBase元素指定的路径中),CLR会在程序集加载后比较Hash值,所以每次加载程序集时都会执行安全策略,损耗性能。

优点:应用程序每次执行,每次加载程序集时都会执行安全策略,损耗性能。缺点:可以达成简单部署的目标。

###应用程序绑定到程序集

应用程序绑定到程序集时,依据如下顺序定位程序集位置:

  1. CLR依据程序集的强名称(编译时获取AssemblyRef中标明,来自CLR目录)在GAC中定位该程序集,匹配被引用程序集(GAC中的目录)的签名,这样可以保证运行时和编译时的发布者唯一。
  2. GAC中找不到去应用程序基目录找。
  3. 应用程序基目录没有,则访问配置文件的codeBase元素标注的私有路径
  4. 如果由MSI按照,CLR要求MSI定位程序集。
    ##延迟签名策略
    在开发和测试阶段,频繁的访问私钥是一件麻烦事(私钥一般被公司严密保护),所以采用延迟签名策略,运行公司生成只有公钥的程序集。
    ###有何不同
    延迟签名和正常的签名的相同与不同之处在于:
  5. 延迟签名任然具有公钥,在AssemblyDef和引用该程序集的AssemblyRef里没有任何影响。
  6. 延迟签名仍然可以存储到GAC(要求关闭防篡改机制)
  7. 延迟签名在开发和测试阶段没有私钥,可能被篡改

###如何操作

创建过程与正常签名类似,唯一的不同是前期PE文件中不嵌入数字签名(因为没有私钥嘛),但实用程序会依据公钥大小判断需要预留多大空间来给RSA数字签名预留空间,具体步骤如下:

  1. 开发和测试期间,csc /keyfile:MyCompany.PublicKey /delaysign MyAssembly.cs使用该命令仅加入公钥,并指明要采用延迟签名策略。
  2. 关闭每一台机器的安全策略SN.exe -Vr MyAssembly.dll以确保程序集能被安装到GAC
  3. 在打包和部署阶段,打开安全策略SN.exe -Vu MyAssembly.dll
  4. 获取公司私钥并按照到GACSN.exe -Ra MyAssembly.dll MyCompany.PrivateKey

另外执行延迟签名的好处在于,由于不经过hash计算,可以执行混淆程序。

#强命名程序集的部署和引用

如前所述,强命名程序集既可以私有部署看,又可以全局部署。

##全局程序集缓存

前边提到的全局共享目录,也就是GAC,一般放在:

在该目录下经过算法结构化存放程序集,子目录自动生成,即使同名,只要不同公司,一样可以区别开来,不会发生相互覆盖现象。

注意:千万不能随意手动复制,因为目录是自动生成的而且GAC目录只能存放强命名程序集

##私有部署强命名程序集

私有部署即是部署到程序的基目录及其子目录

存在以下不合理的历史策略:少数应用程序共享的程序集目录,配置文件的codeBase元素指向的路径既不在GAC,也不在应用程序内部,而是一个第三方。这样做有一个很大的问题就是:每个应用程序都不能决定何时删除该程序集。

##引用强命名程序集

编译时和运行时强命名程序集有两套(Microsoft),一套安装在CLR目录(方便编译时找路径),一套安装在GAC(方便运行时加载程序集)。

  1. CLR目录下:编译使用,只有元数据,与机器无关
  2. GAC目录下:运行使用,包括元数据和IL代码以及针对特定CPU架构的优化,运行存在多个程序集拷贝,每一个架构都有一个专门的子目录存放拷贝。

在编译时的程序集搜索顺序如下:

  1. 工作目录。
  2. CLR所在目录。
  3. /lib编译器开关指定目录。
  4. LIB环境变量指定的任何目录。

运行时的程序集搜索顺序在下一部分介绍。

#运行时解析类型引用

还是使用上一篇博文里举的例子:

namespace TML
{
    class Program
    {
        static void Main(string[] args)          //成员方法入口
        {
            Console.WriteLine("Hello World!");   //引入的外部类型Console
        }
    }
}

运行时执行步骤如下:

  1. 初始化CLR
  2. CLR读取程序集的CLR头,查找标识应用程序的入口方法Main的MethodDef
  3. 检索MethodDefy元数据表找到方法的IL代码在文件中的偏移量
  4. 将IL代码使用JIT编译器编译(提前需要验证)为本机代码
  5. 执行本机代码

在第三步,CLR会检测所有类型和成员引用,顺序如下:

  1. IL call 引用了元数据token0A000003,表示MemberRef中的记录项3
  2. 检查该项,发现字段引用了TypeRef表中记录项
  3. 检查后发现该类型System.Console非本程序集,所以被引到AssemblyRef,定位到了程序集

总体流程见下图,如果被引用类来自本程序集内部,则参见左边两条分支:

#高级管理控制

与私有部署不同,全局部署的时候,为了解决版本冲突问题,需要高级管理控制。正因为高级管理控制,程序集才能同时存在多版本。

##版本控制

CLR通过配置文件定位程序集

可以发现主要有以下几个元素:

  1. probing:对于弱命名程序集,直接检查私有路径,对于强命名程序集,会依据GAC—codeBase指定路径—私有路径的顺序检查程序集
  2. 第一个dependentAssembly及其子元素表明:依据codeBase元素,重定向该程序集版本1.0.0.0为2.0.0.0,codebase已经提供了2版本的地质
  3. codeBase指明了更新地址,如果是弱命名程序集,codeBase只能指向应用程序基目录的子目录
  4. 第二个dependentAssembly及其子元素表明:查找到3-3.5版本后,统一重定向到版本4.0
  5. publisherPolicy元素,表明忽略TypeLib发布者的策略文件

执行流程如下:

  1. CLR定位程序集,进行指定重定向跳转。
  2. 加载发布者策略(若为yes),执行发布者的命令跳转到希望版本。
  3. 定位到后从GAC加载,GAC没有,则从codeBase指定url加载

注意,CLR默认不加载新版本程序集,如果管理员希望所有应用程序使用发布者更新,则修改Machine.config中的文件,关于该文件,这篇博客有详细说明:

https://blog.csdn.net/hanxuema2008/article/details/3344409

##发布者策略控制

由发布者告诉用户该使用什么版本,也可以用来修复bug。

###发布者策略文件

发布者策略配置文件如下,不存在probing和publisherPolicy元素。该示例指明一旦发现1.0版本的引用个,就执行2.0版本,当然如果2.0版本bug更多,可以直接选择publisherPolicy的no。

###包含发布者策略文件的程序集

使用al.exe来生成程序集,使用如下命令:

从上到下四个命令依次为:

  1. /out 创建PE文件(只包含清单),要应用Policy发布者策略,适用版本1.0,应对程序集为SomeClassLibrary.dll。
  2. /version 表示发布者策略程序集的版本。
  3. / keyfile表示要对发布者策略程序集进行加密
  4. /linkresource表示将配置文件作为程序集的一个单独文件。

注意:发布者策略程序集随同新的SomeClassLibrary.dll程序集一起打包部署到用户机器,发布者策略程序集必须安装到GAC

整个博文终于梳理完了,写到一半网速太差csdn保存奔溃,导致后半部分又重写了一遍,那种感觉真是万分痛心,意识到了自己千辛万苦写的东西不易啊。最近不再想C#的出路问题了,因为在博客的写作过程中,越来越喜欢这些底层的东西,短期内确实发挥不了多大作用,但觉得特别开心,因为发现了很多原理性东西,感觉知其所以然,非常开心,继续加油吧!

相关文章
|
2月前
|
C# 容器
C#中的命名空间与程序集管理
在C#编程中,`命名空间`和`程序集`是组织代码的关键概念,有助于提高代码的可维护性和复用性。本文从基础入手,详细解释了命名空间的逻辑组织方式及其基本语法,展示了如何使用`using`指令访问其他命名空间中的类型,并提供了常见问题的解决方案。接着介绍了程序集这一.NET框架的基本单位,包括其创建、引用及高级特性如强名称和延迟加载等。通过具体示例,展示了如何创建和使用自定义程序集,并提出了针对版本不匹配和性能问题的有效策略。理解并善用这些概念,能显著提升开发效率和代码质量。
83 4
|
Windows
命名空间“System”中不存在类型或命名空间名称“Windows”(是缺少程序集引用吗?)
命名空间“System”中不存在类型或命名空间名称“Windows”(是缺少程序集引用吗?)
命名空间“System”中不存在类型或命名空间名称“Windows”(是缺少程序集引用吗?)
|
IDE 编译器 C#
C#中的命名空间和程序集
C#中的命名空间和程序集
229 0
|
.NET
【Xamarin.Forms】XAML命名空间——将XAML名称空间声明为引用类型
XAML使用xmlns XML属性来进行名称空间声明。 本文将介绍XAML命名空间语法,并演示如何声明XAML命名空间以访问类型。 概观 有两个XAML名称空间声明总是在XAML文件的根元素中。
1291 0
|
编译器
相同命名空间相同类名的程序集间引发的致命错误
错误描述: 客户端post后台方法,返回500错误;检查后发现是该后台方法其中一行代码引起的,注释掉就正常;注释后断点调试,进到相应位置取消该行代码注释继续运行报错:“尝试应用代码更改时发生致命错误,需要终止调试。
991 0