Assembly Binding and Fusion

在.NET的开发中,偶尔会遇到assembly找不到的问题,相信.NET程序员对下面的错误信息都不陌生。大多数时候,我们去bin目录检查,把缺失的dll文件拷贝到bin目录,问题就解决了。但也有例外的时候,因此搞清楚.NET CLR是如何寻找assembly就很有必要了。
Could not load file or assembly

如果你仔细阅读错误页面的信息,你会发现其实.NET已经给我们指明了解决问题的办法。
Could not load file or assembly

这里面有两个重要的概念,一个是Assembly Binding,另一个是Fusion

Assembly Binding

Binding 这个词翻译成中文“绑定”实在是巧妙的很,读音和含义都很贴合。
Binding

Assembly Binding,即程序集绑定,指的是.NET CLR在加载应用程序时试图寻找和解析程序集引用(assembly reference)所执行的一系列动作。寻找程序集的这个动作,也有一个术语来描述,即Probing。如果程序集绑定失败,CLR就会抛出FileNotFoundException

Initiating the Bind

首先,程序集绑定起始于CLR遇到一个程序集引用。这个引用分为两种:

  • 静态引用(static reference)
    静态引用是在编译时写入到程序集manifest中的记录,可以使用ILDASM来查看。我们以Newtonsoft.Json.dll这个程序集为例:
    manifest
    所有标记为extern的assembly都是当前assembly所引用的程序集。当CLR试图加载Newtonsoft.Json.dll时,它不会立即加载这些引用的程序集,而是会在遇到相关的代码调用时才会加载,这时候才会发起一个Assembly Binding Request。
  • 动态引用(dynamic reference)
    动态引用是使用反射机制,如Assembly.Load这样的方法来动态的加载程序集。

在静态引用中,编译器会将引用的程序集的全名,即name、version、culture以及public key token写入到程序集的manifest中。如果目标程序集不是强命名的,public key token则会省略掉。在动态引用中,你可以只指定程序集的name, 但这会影响后续的寻找过程。

在CLR开始寻找目标assembly之前,以下的配置文件还会影响绑定请求:

  • Application configuration file,应用程序配置文件
  • Publisher policy file,发布者策略文件
  • Machine configuration file,计算机配置文件

这些文件遵循相同的语法,并提供绑定重定向、代码位置和特定程序集的绑定模式等信息。在这些配置文件中,bindingRedirect配置节会将程序集版本重定向到另一个版本。

Binding Redirection

在一个大型的项目中,多个不同的外部组件很有可能依赖同一个核心组件,比如Newtonsoft.Json.dll,但是它们依赖的版本却不一样。在项目编译后,同一个assembly在bin目录只能保留一份,这会导致运行时错误。bindingRedirect能很好的解决这个问题。当然这样设置的前提是新旧版本的接口是没有变化的,否则一样会发生运行时错误。

1
2
3
4
5
6
7
8
9
10
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.0" newVersion="10.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

Probing

Probing探测追根究底的意思。在CLR开始Probe指定的assembly之前,它还会做一些检查:

  1. Checking for Previously Referenced Assemblies,检查以前引用的程序集 
    检查目标assembly是否已经加载,如果没有则继续下一步
  2. Checking the Global Assembly Cache,检查全局程序集缓存 
    如果目标assembly是强命名的,那么去GAC目录中去查找,如果没有找到则继续下一步
  3. Locating the Assembly through Codebases,通过基本代码定位程序集 
    配置文件定义了<codeBase>配置节,如果能找到相匹配的版本,则从中指定的位置下载assembly

我们常说assembly是加载到内存,具体到.NET Application而言,assembly是加载到Application DomainApplication Domain是.NET应用程序进程中的一种隔离机制。

经过了上述三步,如果还没有找到目标assembly,CLR将会开始探测的过程。

CLR使用以下4个条件来探测程序集:

  1. Application base,应用程序根目录,即正在执行应用程序的位置
  2. Culture,区域语言,即被引用的程序集的区域语言属性
  3. Name,名称,即被引用的程序集的名称
  4. The privatePath attribute of the <probing> element,配置节中定义的 privatePath 属性,这是根目录下用户定义的子目录列表

探测应用程序根目录和区域性目录

CLR将首先探测应用程序根目录。如果应用程序根目录中不存在引用的程序集且未提供区域性信息,则CLR将搜索具有程序集名称的所有子目录。 探测的目录包括:

[application base] / [assembly name].dll
[application base] / [assembly name] / [assembly name].dll

如果指定了被引用程序集的区域性信息,则只探测以下目录:
[application base] / [culture] / [assembly name].dll
[application base] / [culture] / [assembly name] / [assembly name].dll

使用privatePath属性进行探测

除了上述的目录之外,如果<probing>配置节中定义了privatePath属性,则CLR还会探测 privatePath 属性指定的目录列表。使用privatePath属性指定的目录必须是应用程序根目录的子目录。根据程序集绑定请求中是否包含区域性信息,探测的目录会所有不同。

如果不包括区域性信息,则探测以下目录:
[application base] / [binpath] / [assembly name].dll
[application base] / [binpath] / [assembly name] / [assembly name].dll

如果包括区域性,则探测以下目录:
[application base] / [binpath] / [culture] / [assembly name].dll
[application base] / [binpath] / [culture] / [assembly name] / [assembly name].dll

binpath 是在privatePath中指定的目录。

PrettyBin

常规的.NET Application在引用了很多外部的assembly之后,编译后的目录就会显得非常的混乱。但你又不能把那些依赖的dll文件放到其他的位置。PrettyBin 就是一个利用privatePath的这个特性来使bin目录干净的一个NuGet Package。

使用非常简单:Install-Package PrettyBin

在安装过程中,它会在 Application configuration file 中加入以下配置信息, 并且将所有的dll文件移动到libs目录下。

1
2
3
4
5
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="libs" />
</assemblyBinding>
</runtime>

Fusion

在上面提到的错误信息中,我注意到了一个注册表的路径:HKLM\Software\Microsoft\Fusion。让我比较疑惑的是Fusion是个什么东西?

经过一番搜索,我了解到Fusion原来是CLR中实现Probing这部分逻辑的代码代号(Codename)。

微软在2006年公开了.NET Framework 2.0大部分的CLR和BCL代码,在这个代码库里我们可以找到这部分的代码。现在微软公布的代码库地址已经不可用了,但我在GitHub上找到了一份,并且Fork了一份
SSCLI

在这份代码库中,我们还可以看到很多熟悉的CLR组件和相关的工具,例如jitilasmildasm等等。

Fusion Log Viewer

知道了Fusion的由来,Fusion Log Viewer的用途也就很明确了。

默认情况下,Assembly binding logging 是关闭的,启用的方式有两种:

  1. 在注册表中添加以下配置信息:
    reg add "HKLM\Software\Microsoft\Fusion" /v EnableLog /t REG_DWORD /d 1 /f
  2. 打开Developer Command Prompt,输入fuslogvw,在Settings中选择Log in exception text:
    fuslogvw

    在打开Developer Command Prompt时,需要右键选择Run as Administrator

启用Assembly binding logging后,再次打开页面就可以看到程序集绑定的log了。从log中我们可以看到CLR从多个位置尝试加载Newtonsoft.Json.dll。
fusion log

此外,我们还可以将log信息保存到磁盘上,方便以后分析问题。
fusion log

对于ASP.NET程序而言,需要重启IIS或者Recycle AppPool才能使上述修改生效。

Process Explorer

有了Assembly binding logging和Fusion Log Viewer的帮助,相信你可以很快的解决文章开头出现的问题。当ASP.NET程序正常运行后,我们还可以通过Process Explorer这个工具来验证指定的Assembly的确加载到AppDomain了。
.NET assembly loaded

总结

本文从.NET开发过程的一个运行时错误出发,详细阐释了Assembly Binding的机制以及Fusion Log Viewer这个工具,希望对你今后的.NET开发有帮助。

相关链接