版本号

【CLR】程序集查找与GAC_全局程序集缓存

如图:


  • AssemblyVersion程序集版本号:存储在AssemblyDef清单元数据表中,虽然在文件属性面板中不显示,但是这个版本号对于CLR来说很重要,绑定强明明程序集时会用到这个,它唯一地标识了程序集。当A程序集引用了B程序集时,会将B程序集的AssemblyVersion嵌入到自己的AssemblyDef清单中,这样当CLR加载B的时候,就能准确的知道是程序集B的哪个版本。
  • AssemblyFileVersion文件版本号:即属性面板中的文件版本,存储在Win32版本资源中,这个号仅供参考,CLR既不会检查,也不关心这个版本号。
  • AssemblyInformationVersion:属性面单中的产品版本,存储在Win32版本资源中,当Assembley.cs中未设置此项时,属性中的产品版本将于AssemblyFileVersion保持一致。也是仅供参考,CLR不检查不适用。没什么用。

版本号的格式:major主版本号.minor次版本号.build内部版本号.reversion修订号。

前两个版本号构成了公众对版本的理解。

如果公司每天都编译这个程序集,那build号每天都需要递增。如果一天内需要多次编译程序集,则保持同一个build一致的情况下,增加修订号。

程序集的查找

很多开发者在编写代码过程都有这么一个需求,喜欢把引用第三方的一些dll放到根目录单独的一个文件夹中,使看起来更有条例一点。

比如我们有个控制台程序ConsoleTest,引用一大堆第三方dll,想把这些dll都放到与ConsoleTest.exe同级的一个文件夹subLib1和subLib2里,大致结构可能如下:

  • 根目录

    • ConsoleTest.exe
    • ConsoleTest.exe.config
    • subLib1

      • a.dll
      • b.dll

    • subLib2

      • c.dll
      • d.dll


此时运行程序的话,因为查找不到引用的dll,会报FileNotFoundException。所以需要在​​ConsoleTest.exe.config​​文件中做如下配置(必须是在应用程序主程序集文件名的config文件中配置)。

<configuration>
<runtime>
<assemblyBinding xmlns:"urn:schemas-microsoft-com:asm.v1">
<probing privatePath="subLib1;subLib2" />
</assemblyBinding>
</runtime>
</configuration>

​probing​​就制定了要查找的字目录,privatePath只能是根目录下的相对目录,不能指向根目录意外的目录。

CLR查找程序集顺序

这里假如CLR要查找一个AsmName.dll的程序集,而且程序的根目录叫AppDir,subLib1和subLib2已经在privatePath中配置过。查找顺序如下:


  • AppDir\AsmName.dll
  • AppDir\AsmName\AsmName.dll(上一步没找到,就往AsmName文件夹内找AsmName.dll文件)
  • AppDir\subLib1\AsmName.dll
  • AppDir\subLib1\AsmName\AsmName.dll
  • AppDir\subLib2\AsmName.dll
  • AppDir\subLib2\AsmName\AsmName.dll
  • 如果再以上各个目录中都没找到目标程序集,则会用exe后缀替换dll后缀继续查找
  • AppDir\AsmName.exe
  • AppDir\AsmName\AsmName.exe
  • AppDir\subLib1\AsmName.exe
  • AppDir\subLib1\AsmName\AsmName.exe
  • AppDir\subLib2\AsmName.exe
  • AppDir\subLib2\AsmName\AsmName.exe

以上查找是基于中性语言文化的查找,如果向AsmName.dll应用了"en-US"语言文化,那么就会这么查找


  • AppDir\en-US\AsmName.dll
  • AppDir\en-US\AsmName\AsmName.dll
  • AppDir\en-US\subLib1\AsmName.dll
  • AppDir\en-US\subLib1\AsmName\AsmName.dll
  • AppDir\en-US\AsmName.exe
  • AppDir\en-US\AsmName\AsmName.exe
  • AppDir\en-US\subLib1\AsmName.exe
  • AppDir\en-US\subLib1\AsmName\AsmName.exe
  • 如果没en-US文件夹或者文件夹内找不到,则会继续找en文件夹
  • AppDir\en\AsmName.dll
  • AppDir\en\AsmName\AsmName.dll
  • AppDir\en\subLib1\AsmName.dll
  • AppDir\en\subLib1\AsmName\AsmName.dll
  • AppDir\en\AsmName.exe
  • AppDir\en\AsmName\AsmName.exe
  • AppDir\en\subLib1\AsmName.exe
  • AppDir\en\subLib1\AsmName\AsmName.exe

强命名程序集与弱命名程序集

二者的结构完全相同,都有PE头、CLR头、元数据、清单表、IL。不同的是强程序集使用发布者的公钥/私钥进行了签名,对程序集进行了唯一性的标识。

弱程序集只能私有部署,即部署到应用程序的根目录或者字目录。强程序集可以私有部署或全局部署,即也可以部署到一些公有位置,CLR默认会检查这些位置。

Dll hell问题

全局部署会引起dll hell(dll 地狱)问题:两个公司可能会生成同名的dll,然后这两个强程序集都复制到相同的公有位置,最后一个复制的就是“老大”,造成所有正在使用原程序集的软件无法工作。

所以只根据文件名来区分程序集是不够的,CLR必须支持对程序集进行唯一性标识的机制,这就是强命名程序集。强程序集根据以下四个特征进行唯一性标识:


  1. 文件名(不含扩展名)
  2. 版本号(AssemblyVersion)
  3. 语言文化
  4. 公钥标记(public key token):从公钥派生出的小哈希值

如下就是两个程序集:

ConsoleFrame, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5csdfgsdfg3434dsqfg
ConsoleFrame, Version=1.0.0.0, Culture="en-US", PublicKeyToken=b77a5csdfgsdfg3434dsqfg

弱程序集的public key token为null

ConsoleFrame, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

对弱程序集签名,变成强程序集

右键项目-属性,选择Signing-Sign the assembly即可。

【CLR】程序集查找与GAC_程序集查找_02

然后在另一个项目中,引用ConsoFrame程序集,可以看到,已经生成了PublicKeyToken。

【CLR】程序集查找与GAC_c#_03

在这里我们新建另一个程序集ConsoleFrame2,然还用同一个AA.pfx公钥进行签名,可以看到ConsoleFrame与ConsoleFram2的PublicKeyToken完全一致:

【CLR】程序集查找与GAC_程序集查找_04

生成强程序集时,公钥也会被嵌到dll里。

全局程序集缓存GAC

之前有讲过强程序集可以放到一个公共的位置,供所有的程序访问,这个位置就是GAC(Global Assembly Cache)。这个位置不是固定的,不同的CLR版本可以有不同的位置,但一般在以下目录都能发现:

​%SystemRoot%\Microsoft.NET\Assembly​

GAC目录下有很多子目录,子目录的名称是用算法生成的,不要自己手动把dll复制到某个目录里去,没用。应该使用GACUtil.exe工具复制,一般电脑里都有这个,使用Everything工具全局搜即可。

至此,dll hell问题得到解决。

在你的项目中如何引用强程序集?


  1. 你知道要引用dll的路径,直接项目上右键-添加引用即可。
  2. 你不知道引用dll的路径,那编译器CSC.exe会从以下位置开始查找:

    • 工作目录
    • CSC.exe所在的目录,目录中包含CLR的各种dll文件
    • 编译时使用/lib参数指定的目录
    • 使用LIB环境变量指定的目录


注意:编译查找dll进行编译的位置可不是程序运行时加载dll的位置,这是两码事。

到这里你应该理解到了.NET为什么会有​​.NET SDK​​和​​.NET Runtime​​两种包可供下载安装了,如果只是运行.net程序而不进行开发那么只下载runtime就行。安装SDK时会安装同一程序集的两套拷贝,一份放到编译器的目录,另一份放到GAC里,而安装runtime只需要放到GAC就行。