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

2023-12-24 11:38

本文主要是介绍【深入理解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#的出路问题了,因为在博客的写作过程中,越来越喜欢这些底层的东西,短期内确实发挥不了多大作用,但觉得特别开心,因为发现了很多原理性东西,感觉知其所以然,非常开心,继续加油吧!

这篇关于【深入理解CLR 四】共享程序集和强命名程序集的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/531650

相关文章

linux如何复制文件夹并重命名

《linux如何复制文件夹并重命名》在Linux系统中,复制文件夹并重命名可以通过使用“cp”和“mv”命令来实现,使用“cp-r”命令可以递归复制整个文件夹及其子文件夹和文件,而使用“mv”命令可以... 目录linux复制文件夹并重命名我们需要使用“cp”命令来复制文件夹我们还可以结合使用“mv”命令总

Python脚本实现图片文件批量命名

《Python脚本实现图片文件批量命名》这篇文章主要为大家详细介绍了一个用python第三方库pillow写的批量处理图片命名的脚本,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下... 目录前言源码批量处理图片尺寸脚本源码GUI界面源码打包成.exe可执行文件前言本文介绍一个用python第三方库pi

将java程序打包成可执行文件的实现方式

《将java程序打包成可执行文件的实现方式》本文介绍了将Java程序打包成可执行文件的三种方法:手动打包(将编译后的代码及JRE运行环境一起打包),使用第三方打包工具(如Launch4j)和JDK自带... 目录1.问题提出2.如何将Java程序打包成可执行文件2.1将编译后的代码及jre运行环境一起打包2

在不同系统间迁移Python程序的方法与教程

《在不同系统间迁移Python程序的方法与教程》本文介绍了几种将Windows上编写的Python程序迁移到Linux服务器上的方法,包括使用虚拟环境和依赖冻结、容器化技术(如Docker)、使用An... 目录使用虚拟环境和依赖冻结1. 创建虚拟环境2. 冻结依赖使用容器化技术(如 docker)1. 创

java父子线程之间实现共享传递数据

《java父子线程之间实现共享传递数据》本文介绍了Java中父子线程间共享传递数据的几种方法,包括ThreadLocal变量、并发集合和内存队列或消息队列,并提醒注意并发安全问题... 目录通过 ThreadLocal 变量共享数据通过并发集合共享数据通过内存队列或消息队列共享数据注意并发安全问题总结在 J

深入解析Spring TransactionTemplate 高级用法(示例代码)

《深入解析SpringTransactionTemplate高级用法(示例代码)》TransactionTemplate是Spring框架中一个强大的工具,它允许开发者以编程方式控制事务,通过... 目录1. TransactionTemplate 的核心概念2. 核心接口和类3. TransactionT

深入理解Apache Airflow 调度器(最新推荐)

《深入理解ApacheAirflow调度器(最新推荐)》ApacheAirflow调度器是数据管道管理系统的关键组件,负责编排dag中任务的执行,通过理解调度器的角色和工作方式,正确配置调度器,并... 目录什么是Airflow 调度器?Airflow 调度器工作机制配置Airflow调度器调优及优化建议最

NFS实现多服务器文件的共享的方法步骤

《NFS实现多服务器文件的共享的方法步骤》NFS允许网络中的计算机之间共享资源,客户端可以透明地读写远端NFS服务器上的文件,本文就来介绍一下NFS实现多服务器文件的共享的方法步骤,感兴趣的可以了解一... 目录一、简介二、部署1、准备1、服务端和客户端:安装nfs-utils2、服务端:创建共享目录3、服

一文带你理解Python中import机制与importlib的妙用

《一文带你理解Python中import机制与importlib的妙用》在Python编程的世界里,import语句是开发者最常用的工具之一,它就像一把钥匙,打开了通往各种功能和库的大门,下面就跟随小... 目录一、python import机制概述1.1 import语句的基本用法1.2 模块缓存机制1.

深入理解C语言的void*

《深入理解C语言的void*》本文主要介绍了C语言的void*,包括它的任意性、编译器对void*的类型检查以及需要显式类型转换的规则,具有一定的参考价值,感兴趣的可以了解一下... 目录一、void* 的类型任意性二、编译器对 void* 的类型检查三、需要显式类型转换占用的字节四、总结一、void* 的