一文看懂C#中应用程序集的装载过程

dotNET全栈开发

共 5882字,需浏览 12分钟

 ·

2020-12-28 01:08

了解程序集如何在C#.NET中加载

我们一直在处理库和NuGet软件包不管是好是坏,高级.NET开发人员都需要了解.NET运行时如何加载程序集。

这些库依赖于其他流行的库,并且有很多共享的依赖项。有了足够大的依赖关系网络,您最终将陷入冲突或困境。处理此类问题的最佳方法是了解该机制在内部的工作方式。

在本文中,您将看到.NET进程如何以及何时加载引用的程序集。

您将了解加载了哪个库版本,当有多个可用版本时会发生什么,以及为什么有时由于版本冲突而出现问题。

您将看到如何调试这些类型的问题,查看程序集绑定日志(融合日志)以及一些解决冲突的方法。

程序集,模块和引用

让我们从围绕.NET流程的一些基本术语开始。

一个装配在.NET是一个DLL或EXE文件。Visual Studio解决方案中的每个项目都被编译为一个程序集。

每个程序集可以包含多个模块,但是实际上,我们几乎总是在一个程序集中有一个模块,该模块的名称与该程序集相同。

在Visual Studio中启动进程或单击F5时,将执行启动项目程序集。除了.NET Framework或.NET Core程序集之外,它将是第一个加载的程序集。

之后,该过程将根据需要在运行时加载其他程序集。仅当需要调用该程序集的方法或使用该程序集的类型时,它才会延迟加载程序集。

这里是为一个简单的“ Hello World” .NET Framework项目加载的模块(出于我们所有的意图和目的,模块和程序集都是相同的)。MyStartup.dll是此处的启动项目:

.NET Core项目启动时加载的模块


当您从另一个项目引用一个项目时,在构建时,被引用项目的DLL或EXE被复制到启动项目的Bin文件夹中。

通常是Bin \ DebugBin \ Release。在运行时,当您第一次使用引用的项目中的类型时,CLR在应用程序目录中查找具有与期望的名称和版本相同的DLL文件。然后将程序集加载到流程中。这也称为绑定到装配件。

这是一个例子:

假设我们有一个名为MyStartup的简单控制台应用程序,它引用了另一个名为Lib1的项目。MyStartup使用Lib1程序集中的某些类。

MyStartup中

class Program{    static void Main(string[] args)    {        int a = int.Parse(Console.ReadLine());        int b = int.Parse(Console.ReadLine());        Console.WriteLine("A + B = " + Add(a, b));    }
private static int Add(int a, int b) { var calculator = new Lib1.Calculator(); return calculator.Sum(a, b); }}

Lib1中

public class Calculator{    public int Sum(int a, int b)    {        return a + b;    }}

输入Main方法时,尚未加载Lib1程序集。但是,在输入Add方法时,CLR尝试解析Calculator类型,找出它在引用的程序集Lib1中,然后尝试加载该程序集。

.NET中的程序集绑定

当CLR需要加载程序集时,逻辑实际上比在Bin文件夹中查找要复杂一些。这是执行的实际逻辑(有关详细说明,请参见Microsoft文档[1]):

1.根据配置文件(app.configweb.config)确定需要加载的程序集的版本。该配置文件的名称为(在生成之后) [executable name].exe.configweb.config。绑定重定向在这里发挥了作用(稍后会详细介绍)。2.查看程序集是否已加载。如果加载了其他版本,则将抛出FileLoadException,除非它是一个可以同时加载多个版本的强命名程序集。3.如果它是强名称程序集,请检查全局程序集缓存[2](GAC)。GAC是机器上共享多个应用程序部件的地方。如果需要的话,程序集会缓存。它只能存储强命名程序集。它可以存储同一程序集的不同版本。您可以使用gacutil.exe[3]自己将其安装到GAC 。4.如果它是一个强名称的程序集,并且配置文件包含<codeBase>节点,那么它将检查那里的程序集位置。如果该<codeBase>节点存在并且找不到程序集,FileNotFoundException则将引发a。5.根据启发式算法检查程序集DLL或EXE。此过程称为“探测”。算法如下:1.检查文件夹[application base] / [assembly name].dll。应用程序库是应用程序可执行文件所在的位置。通常,您的Bin \ Debug或Bin \ Release文件夹。2.检查一下 [application base] / [assembly name] / [assembly name].dll3.如果为引用的程序集指定了区域性信息,则仅检查以下目录: [application base] / [culture] / [assembly name].dll [application base] / [culture] / [assembly name] / [assembly name].dll4.如果该<probing>节点存在于配置文件中,则它将在该privatePath节点的属性指定的文件夹中查找程序集。

他们为什么要使所有事情变得如此困难,对吗?

实际上,这种逻辑非常有助于我们发展,而不会使事情变得困难。它的存在是为了实现一些重要目标:

为了确保您引用的是特定的程序集和版本,则将加载该确切版本。否则,将引发异常。而且,如果您知道自己在做什么,则可以在配置文件中指定覆盖规则(绑定重定向)。为了灵活地在您要加载的程序集中进行。例如,如果要根据不同的区域性(语言)加载不同的程序集,则可以轻松地做到这一点。或者,如果您要根据客户配置加载不同的程序集,那也可以。为了安全起见,我们使用了全称的程序集。他们确保您不能“伪造”程序集。例如,如果某个进程希望加载Lib1 v4.5,那么您将无法加载具有相同名称和版本的恶意软件程序集。加载时会引发异常。这就是为什么在计算机上所有进程都共享的GAC只接受强名称程序集的原因。

在大多数应用程序中,您无需记住程序集加载和探测的复杂逻辑。您无需了解或考虑GAC,全名程序集或操作配置文件。

几乎根本不需要考虑库的版本,因为可能的冲突通过称为“绑定重定向”的机制自动解决了。


绑定重定向

如果有一件事对于了解这笔交易非常重要,那就是绑定重定向。能够告诉运行时它将实际加载哪个版本,而不管其引用的版本如何。

这是一个示例:您的流程有两个项目(模块):项目A和项目B。项目A引用log4net.dll v1.1,项目B引用log4net.dll v1.2。两个log4net DLL文件都复制到输出文件夹,但是只能有一个log4net.dll文件。

假设复制到输出文件夹的文件是log4net.dll v1.2。假设到达的第一个代码是Project A中的代码,该代码引用了log4net v1.1。运行时将在输出文件夹中查找,找到不同版本的log4net,并失败FileLoadException

还有另一种可能。假设首先执行了项目B中的代码,并且在尝试使用log4net时,它成功加载了log4net.dll v1.2。片刻之后,Project A中的代码将尝试使用log4net v1.1,请参见该程序集已经加载了其他版本,并抛出FileLoadException

如果您知道哪个log4net版本将在输出文件夹中,在这种情况下可以做的就是告诉运行时应该使用哪个版本。只需app.config在该runtime部分的文件中添加以下几行:

<?xml version="1.0" encoding="utf-8" ?><configuration>  ...  <runtime>    ...    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">      <dependentAssembly>        <assemblyIdentity name="log4net"                           publicKeyToken="669e0ddf0bb1aa2a" culture="neutral" />        <bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="1.2.0" />      </dependentAssembly>    </assemblyBinding>  </runtime>  ...</configuration>

这意味着,只要运行时想绑定到版本范围为0.0.0.0to的程序集log4net 5.0.0.0,它就会尝试绑定到version 1.2.0

实际上,您不必手动添加这些重定向,因为它们是自动添加的。如果转到启动项目的“属性”,则会看到以下设置:



图片-20200711101325128


默认情况下选中此选项。它会自动检测版本冲突并在.config文件中生成绑定重定向。

当问题开始发生时

乍一看,绑定重定向可能看起来像是对所有问题的答案,但事实并非如此。使用绑定重定向时,基本上使用的库版本与预期不同。如果删除方法怎么办?或方法的签名已更改?在这种情况下,调用该方法时,程序将因运行时错误而失败。毕竟,创建版本是有原因的。

如果确实存在此类问题,则有解决方法。查看我的文章:如何解决.NET引用和NuGet软件包版本冲突[4]

故障排除

当您有一个FileLoadException或类似的东西时,我建议做的第一件事是查看Visual Studio中的“模块”窗口。在这里,您将看到所有已加载的模块,并确定您要加载的程序集是否已加载,使用哪个版本以及从哪个路径加载。

除此之外,您还可以查看程序集绑定日志,也称为融合日志。这些日志将显示在程序集绑定尝试过程中到底发生了什么。您将看到运行时查找的程序集版本,运行时查找的文件夹以及故障点。

有几种查看融合日志的方法。首先,您必须启用它们,因为默认情况下它们是禁用的。您可以通过将HKLM\Software\Microsoft\Fusion\ForceLog值设置为1并将HKLM\Software\Microsoft\Fusion\LogPath值设置为来在注册表中手动启用它们C:\FusionLogs。日志将自动出现。或者,您可以使用Fusion Log Viewer,该软件应以方式安装在PC上fuslogvw.exe。我建议使用“一切窗口”搜索之[5]类的程序来查找它。确保以管理员权限运行融合日志查看器,以便能够启用和禁用日志。最近更流行的一种更现代的工具是Fusion ++[6]

边注

也许您不需要,但是我以前讨厌不得不处理这类问题。例如一个逻辑上的问题,让我构建一些东西,甚至解决一个生产错误,但其他问题都好说,唯独这个……。

在这件事上别无选择,我不得不艰难地学习程序集绑定的内部工作。我发现,就像其他所有内容一样,一旦您理解了某些内容,它就会变得不那么可怕,甚至变得不再那么有趣了。

因此,我希望本文对您有意义,并会在我走过的道路上为您提供快速帮助。

References

[1] Microsoft文档: https://docs.microsoft.com/en-us/dotnet/framework/deployment/how-the-runtime-locates-assemblies
[2] 全局程序集缓存: https://docs.microsoft.com/en-us/dotnet/framework/app-domains/gac
[3] gacutil.exe: https://docs.microsoft.com/en-us/dotnet/framework/tools/gacutil-exe-gac-tool
[4] 如何解决.NET引用和NuGet软件包版本冲突: https://michaelscodingspot.com/how-to-resolve-net-reference-and-nuget-package-version-conflicts/
[5] 一切窗口”搜索之: https://www.voidtools.com/
[6] Fusion ++: https://github.com/awaescher/Fusion/

回复 【关闭】广
回复 【实战】获取20套实战源码
回复 【被删】
回复 【访客】访
回复 【小程序】学获取15套【入门+实战+赚钱】小程序源码
回复 【python】学微获取全套0基础Python知识手册
回复 【2019】获取2019 .NET 开发者峰会资料PPT
回复 【加群】加入dotnet微信交流群

副业刚需,没有人能拒绝这个网站!


终于GitHub App 已支持简体中文!


浏览 25
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报