C#编译基础知识(三)

本文章我们将来重点介绍强命名程序集,强命名程序集的出现其实是为解决版本控制问题,比如说,在新版程序集发布后,我们希望在系统中对旧程序集的引用继续保留,而有些地方又可以引用新的程序集,再比如说不同的公司提供了不同功能的程序集,这些类库存放在一个公共目录,有时候可能会出现名称相同的情况。使用强命名程序集可以解决这些问题,一个强命名的程序集是靠公钥标示、程序集版本号、区域属性、程序集名称这四个属性来唯一标识的,这样一来,新发布的库文件版本与前面发布的不同,不同的版本引用可以在元数据里面标识,相互不会受到影响,而且在99.99%的情况下不同公司生成的程序集这四个属性不会全都一样,可以大大降低应用程序部署的风险。

一 强命名程序集的各个部分



版本号

[assembly: AssemblyVersion("1.0.0.2")]

众所周知,程序集的版本是通过程序集级别的定制Attribute来标识的,一个版本号总是包含4个部分,分别是 Major Version(主版本号),MinorVersion(次版本号),Build(构建),Revision(修订),可以按照自己的版本号规划自行指定。构建版本号或修订版本号可以自动生成,此时这里的AssemblyVersion属性值可以是1.0.0.*或1.0.*,当使用这种方式时,自动生成的Build为从2000-01-01起的天数,Revision为从凌晨00:00:00 起的秒数/2。我们举个简单的例子。

using System;
using System.Reflection;

[assembly: AssemblyVersion("1.0.*")]
public class a{
    public static void Main()
    {
        DateTime dtBase=new DateTime(2000,1,1);
        DateTime dtWeeHours=new DateTime(DateTime.Now.Year,DateTime.Now.Month,DateTime.Now.Day,0,0,0);  

        Console.WriteLine("Build版本号:" + DateTime.Now.Subtract(dtBase).Days);
        Console.WriteLine("Revision版本号:" + (int)DateTime.Now.Subtract(dtWeeHours).TotalSeconds / 2);
    }
}

运行之后的结果如下

dll的版本号如下(修订号相隔1是因为编译时间和运行时间不同,如果用版本号去反算生成时间,就不会有这个问题)

  

这样一来就很明确了,Build版本号对应到生成的日期,Revision对应生成的时间,只要两台机器的时间不出问题,自动生成的dll版本永不会重复。在实际项目中我们肯定是把版本信息(上面那段属性声明)放到一个单独的文件中,vs新建项目时会默认生成一个AssemblyInfo.cs文件,这个文件里面就包含这个属性,当然还包含了很多其他的内容,像上面截图中的产品版本,内部名称,文件版本等等,大家可以去了解下。

 区域性

[assembly: AssemblyCultureAttribute("en-Us")]

程序集的区域性是通过上面的定制Attribute来标识的,区域性可以用来标识程序集的语言,如果不指定,则认为是语言中性。为了让应用程序支持多语言,按照微软的官方提倡的做法,应该是把与语言文化有关的内容放到一个单独的资源文件中,分不同的语言提供多个不同的dll,不同的语言之间用区域性来标识,在主程序调用时通过反射的方式来加载(注意不是直接引用,这样一来可以更加方便的部署和发布)。正因为如此,如果某个程序集直接引用了一个区域性不为中性的dll,则会抛出一个警告“CS1607: 程序集生成 -- 引用的程序集“××××”是本地化附属程序集”。

区域性不为中性的程序集被称作附属程序集,可执行文件不能为附属程序集,如果标识的区域性不为空,则编译时会报错“ error CS0647: 发出“System.Reflection.AssemblyCultureAttribute”属性时出错 -- “可执行文件不能是附属程序集,区域性应始终为空””。 除此之外,在运行时加载附属程序集也会与语言中性程序集有所不同,这一点我们在后面应用程序配置中再做深入说明。

  公钥标记

<add assembly="System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>

以上内容是从Web.Config中截取到的,最后的那一部分,PublicKeyToken=B77A5C561934E089,等号后面的那部分就是公钥标识,它其实是嵌入在程序集中公钥经过SHA-1散列运算得到的密文的最后8个字节。在继续讲公钥标记前,我们先来了解一下程序集生成时使用公钥/私钥对进行签名的一个过程。

1 生成一个公钥私钥对,这个公钥的大小可以指定

  sn -k d:\liuzh.snk

2 生成时使用keyfile开关指定密钥文件

  csc /out:d:\apple.dll /t:library /keyfile:d:\liuzh.snk d:\apple.cs

在使用/keyfile:开关后,在生成时编译器会做如下事情:
  1 对程序集FileDef清单中的各个文件进行hash运算(默认是SHA-1,使用AL.exe生成时可以用/algid来覆盖),再使用私钥(私钥来自于liuzh.snk)对该散列值进行加密,得到密文。

  2 将公钥(公钥也来自于liuzh.snk)和1中的密文嵌入到程序集中。

下图展示了这一过程

微软保证了任何情况下sn生成的公钥/私钥的内容都不会相同,也就是说,嵌入到程序集中的公钥都是唯一的,在引用该程序集时,假如在引用的程序集中记录下被引用的程序集的完整的公钥会使得文件特别的大(比如说,任何一个 程序集都包会含了对mscorlib.dll的引用,因为任何类都继承于Object)。下图展示了生成的程序集中的公钥

由于引用的程序集只能记录被引用程序集公钥散列运算后的部分内容,这样一来就存在一个问题,就是可能有两个程序集最后的公钥标识一致,但是概率会很低(小于1/264),加上程序集的唯一性还有其他内容来区分,所以这不会出现什么问题。

顺便提一下,进行过公钥/私钥签名的程序集可以从一定程序上防止篡改,在运行时CLR会用公钥将使用私钥加密后的密文进行解密,将解密后的密文与程序集中各模块hash后的散列进行对比,如果不一致,则可以判定被篡改了。之所以说是一定程序上,是因为这种保护机制很脆弱,这点我们在后面进行说明。

程序集的名称

程序集的名称默认就是/out开关指定的文件的名称(不带扩展名)。使用vs时,这个名称可以在项目属性中指定,正因为在生成时程序集名称与文件名保持一致,所以你在项目属性中指定的是什么名称,最后生成的dll,exe就会使什么名称

2 全局程序集缓存



如果一个程序集需要被多个应用程序访问(如微软提供的类库),那么必须把它放到一个已知的目录,而且CLR在检测到对该程序集的一个引用时,需要知道自动检查目录。这个已知的目录就叫做全局程序集缓存(Global Assembly Cache,GAC),放到GAC中的程序集称为共享程序集,因为它可以被其他应用程序共享,仅在程序运行目录下的,我们称之为私有程序集。对于.net 3.5 以前版本,这个目录位于C:\Windows\Assembly,对于.net4.0,它位于C:\Windows\Microsoft.NET\Assembly。

GAC的目录是结构化的,其中包含了许多子目录,并有一个算法来生成这些子目录,永远不要手动将程序集文件手动复制到GAC,相反,应该使用工具来完成,这个工具是GACUtil.exe,可以在命令行查看它的用法。它包含了比较常用的功能如安装,卸载,删除,重新安装,筛选等。注意,只能将一个强命名的程序集安装到GAC中,如果不是强命名(没有进行过签名),在安装时就会提示“将程序集添加到缓存失败:试图安装没有强名称的程序集”,在server2003下,这个文件夹是这样子的。

这个目录里面几乎包含了应用程序在运行时的所需要的所有程序集,在安装.net framework时,实际会安装程序集文件的两个拷贝,一套安装在编译目录,另一套安装在GAC中。之所以要安装两套,是因为在编译时CSC.exe并不会去GAC中寻找你引用的dll,为什么编译器不去GAC中寻找呢,是因为在编译时/reference必须要知道具体的路径,而GAC的目录是不公开的,一个可能的替代方案是在编译时指定程序集的强名称,如System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089,但是这种方案很明显都没有直接部署两套dll来得方便。

将程序集部署到GAC中是对程序集注册的一种手段,虽然没有记录到注册表中,但是很明显破坏了我们的一个基本目标,简单安装,备份,还原,移动和卸载,所以如果我们不是提供类库供他人使用的话,还是应该直接把dll放到程序运行目录下

3 小结



进一步了解程序集相关信息可以加强我们理解各种dll的引用方式及项目属性中的配置原理,.net安装目录下各种dll及实用程序的功能,以在程序编译或运行时,快速定位和解决问题。

C#编译基础知识(三)

时间: 2024-10-05 09:09:58

C#编译基础知识(三)的相关文章

20_Shell语言———VIM编辑器基础知识三之窗口属性定制、配置文件及查找替换功能

Vim编辑器可以让用户按照需求来定制一些使用属性. 一.窗口属性定义 1)显示行号 行号不是内容,只是用来帮助用户确认文本所在的行.在vim编辑器中,如果要显示行号,可以在末行模式下输入: set number 如果想关闭,则可以在功能名称前面加上no,即: set nonumber 命令可以被简写,如set number 可以简写为 set nu:set nonumber 可以简写为 set nonu. 注意,上述设定仅对当前vim的进程有效,一旦当前进程关闭,这些设定就会失效,如果要使设定永

计算机科学基础知识(三)静态库和静态链接

三.将relocatable object file静态链接成可执行文件 将relocatable object file链接成可执行文件分成两步,第一步是符号分析(symbol resolution),第二步是符号重新定位(Relocation).本章主要描述这两个过程,为了完整性,静态库的概念也会在本章提及. 1.为什么会提出静态库的概念? 程序逻辑有共同的需求,例如数学库.字符串库等,如果每个程序员在撰写这些代码逻辑的时候都需要自己重新写那么该是多么麻烦的事情,而且容易出错,如果有现成的,

Dapper基础知识三

在下刚毕业工作,之前实习有用到Dapper?这几天新项目想用上Dapper,在下比较菜鸟,这块只是个人对Dapper的一种总结. Dapper,当项目在开发的时候,在没有必要使用依赖注入的时候,如何做到对项目的快速开发这里对Dapper做一个小的进阶. 结合上一篇的博客,就可以使用了. public class Demo { public string name { get; set; } public string DapperTest { get; set; } } public class

【基础知识三】线性模型

一.基本形式 通过属性的线性组合来进行预测, 许多非线性模型可以在线性模型的基础上,引入层级结构或高维映射而得. 二.线性回归 最小二乘法:求解ω和b: 多元线性回归:样本由多个属性描述,即x为多维向量: 若矩阵不满秩产生多个解,解决方法:引入正则化项: 三.对数/逻辑线性回归 广义线性模型: g(.)条件:连续且充分光滑(单调可微) 为了预测值连续,引入Sigmoid函数 得到, 极大似然估计:求解ω和b 四.线性判别分析LDA 也叫"Fisher判别" 将样例投影到一条直线上,使同

KnockoutJS基础知识(三)

对于knockoutJS来讲,模板绑定和Mapping插件绑定是十分重要的功能,虽然模板绑定在我工作中用的及其少,但模板绑定的重要性不可忽视,在其他前端框架中,如Angular.Vue等等,模板存在的意义十分重要,Mapping插件使得我们能够脱离手工绑定,及其方便我们快速绑定达到预期效果. KnockoutJS模型绑定更多用法:https://knockoutjs.com/documentation/template-binding.html 本文地址:https://www.cnblogs.

3. K线基础知识三

1. 阴线 证券市场上指开盘价高于收盘价的K线,K线图上一般用淡蓝色标注,表示股价下跌,当收盘价低于开盘价,也就是股价走势呈下降趋势时,我们称这种形态的K线为阴线. 中间部分实体为蓝色,此时,上影线的长度表示最高价和开盘价之间的价差.实体的长短代表开盘价比收盘价高出的幅度.下影线的长度则有收盘价和最高价之间的价差大小所决定. 2. 小阴星 小阴星的分时走势图与小阳星相似,只是收盘价格略低于开盘价格.表明行情疲软,发展方向不明. 3. 小阴线 表示空方呈打压态势,但力度不大. 4. 光脚阴线 光脚

Linux基础知识三

1.列出当前系统上所有已经登录的用户的用户名,注意:同一个用户登录多次,则只显示一次即可. 查看系统用户的命令为: (1)getent passwd 查看系统上的所有用户信息 (2)w 查看登录用户正在使用的进程信息,该命令所使用的信息来源于/var/run/utmp文件 w命令输出的信息包括: -用户名称 -用户的机器名称或tty号 -远程主机地址 -用户登录系统的时间 -空闲时间(作用不大) -附加到tty(终端)的进程所用的时间(JCPU时间) -当前进程所用时间(PCPU时间) -用户当

cocos2d0基础知识三个音符

1.触摸屏事件: bool HelloWorld::init() { //省略的代码的最后位 this->schedule(schedule_selector(HelloWorld::usecreatesprite),2);//定时器 this->setTouchEnabled(true);/CCLayer是能够对应点击对象的,默认情况是没开启的,我们通过this->setTouchEnabled(true)来设置是否接受触摸事件. return true; } void HelloWo

SQL Server基础知识三十三问 (15-21)

15. 存储过程可以调用自己么, 或者说可能有递归的存储过程么? SP nesting最多可以到多少层? 答: 可以的. 因为Transact-SQL 支持递归, 你可以编写可以调用自己的存储过程. 敌对可以被定义为一种解决问题的方法, 其中问题的解决是通过不断的对问题的子集调用自己而达成的. 当一个存储过程调用另一个存储过程或执行CLR的routine, type, 或aggregate时, 就会形成嵌套(nest). 你最多可以嵌套存储过程或托管代码的层级为32层.   16. 什么是log