调用Assembly.Load(byte[])方法是否会触发AppDomain.AssemblyResolve事件?

20
假设我有一个处理程序来处理 AppDomain.AssemblyResolve 事件,在处理程序中构建了一个字节数组并调用方法Assembly.Load(byte[])。这个方法本身会导致AssemblyResolve事件再次被引发,从而导致我的处理程序被重新进入吗?
我的问题不仅限于可以使用C#编译器生成的程序集,它们可以包含由CLR支持的任意元数据和可执行代码。
我进行了一些实验,并没有发现任何这种情况发生的情况。我尝试加载需要其他引用的程序集,尝试将CAS属性添加到已加载程序集,其解码需要另一个程序集,尝试加载具有模块初始化器(全局.cctor方法)的程序集。在没有情况下,我观察到AssemblyResolve事件从Assembly.Load(byte[])方法内部引发,它只会在某些代码稍后尝试访问已加载程序集中的类型、方法或属性时发生。但是我可能会错过某些东西。

1
如果您在具有对第三个程序集的静态依赖关系的程序集中添加一些 assembly: 属性,会发生什么? - chillitom
是的,从技术上讲是可能的 - Hans Passant
正如我在问题中提到的那样,我尝试使用模块初始化程序,但它的行为类似于特殊的<Module>类型的静态构造函数,该类型包含在模块中声明的所有全局成员。只有当第一次访问某个全局成员时,才会调用模块初始化程序。@HansPassant - Vladimir Reshetnikov
@chillitom 我试过了,但是没有成功。通常情况下,属性只是被动数据,除非它们被显式读取。也许有一些特殊的属性(我不知道)会在程序集加载时自动被CLR读取,然后我可能可以为这样的属性提供一个枚举参数,其中相应的枚举类型在尚未解析的引用程序集中声明... - Vladimir Reshetnikov
我在IronScheme中经常使用它,但我从未见过它发生过(或者我可能在过去的7年中解决了它; p)。 - leppie
5个回答

3

模块初始化器是我能想到的唯一问题制造者。下面是一个C++/CLI的简单示例:

#include "stdafx.h"
#include <msclr\gcroot.h>

using namespace msclr;
using namespace ClassLibrary10;

class Init {
    gcroot<ClassLibrary1::Class1^> managedObject;
public:
    Init() {
        managedObject = gcnew ClassLibrary1::Class1;
    }
} Initializer;

当通过模块初始化程序加载模块时,Init()构造函数会被调用,此过程在初始化C运行时后立即执行。不过,在你的特定情况下,你不需要处理这种代码,因为Assembly.Load(byte[])不能加载混合模式程序集。

这并不是由模块初始化程序引起的限制。它们是在CLR v2.0中添加的,旨在完成类似的工作,即在开始执行任何托管代码之前让语言运行时初始化自己。你遇到这样的代码的可能性应该非常非常低。你会看到它的 :)


如果我正确理解你的陈述,那么.NET中何时才有圣诞老人呢? :) - jgauffin
2
样本对我来说有点奇怪 - 确实在加载后创建类将触发程序集解析,但我不认为创建类是Load调用的一部分。也就是说,仅保留Load调用将根本不会触发解析。很可能我完全误解了问题。 - Alexei Levenkov
1
@Hans 显然,如果我_明确地_访问已加载程序集中的某些代码(我在问题中提到了这一点,您的示例也证明了这一点),则可以引发AssemblyResolve事件。但是,在我的问题中,我关心的是CLR在调用Assembly.Load(byte[])之前可能会执行的_隐式_访问。 - Vladimir Reshetnikov
1
@AlexeiLevenkov 我认为你正确理解了问题,你的反对意见是有道理的。 - Vladimir Reshetnikov

2

你提到 -

我从未观察到在Assembly.Load(byte[])方法内部引发了AssemblyResolve事件,只有当稍后的某些代码尝试访问已加载程序集中的类型、方法或属性时才会发生。但是我可能漏掉了一些东西。

需要注意的要点 -

  1. 在执行代码时,如果代码中引用了某个类型并且CLR检测到包含该类型的程序集未加载,则CLR将加载该程序集。您的观察结果是正确的。

  2. AssemblyResolve是AppDomain类型中定义的事件。因此,无法从Assembly.Load(byte[])内部引发此事件。

因此,如果您已在正在运行的应用程序域中注册了AssemblyResolve事件并调用了Assembly.Load(byte[]),则它将在当前域中加载程序集。

现在,当调用来自已加载程序集的任何类型时,例如调用另一个在其他程序集中定义的类型,AppDomain将调用AssemblyResolve事件以尝试加载该程序集


1
你能澄清一下你的第二点吗?不同类型的定义如何防止事件被触发? - Vladimir Reshetnikov
当你说“事件从Assembly.Load内部引发”时,我严格地理解了你的陈述。这个说法是不正确的,因为AssemblyResolve事件只能从AppDomain类型代码内部引发(事件可以从定义它的类型引发,并且您可以订阅事件处理程序以在引发事件时调用它)。 - Dinesh
谢谢。我的意思是“从Assembly.Load方法内部或任何它直接或间接调用的代码中”。在AppDomain类中有一个私有方法OnAssemblyResolveEvent,其中有一条注释:“VM调用此方法”。看起来,具有本地实现的Assembly类方法可能会导致VM调用该方法。 - Vladimir Reshetnikov
堆栈跟踪清楚地显示,虽然 Assembly.LoadappDomain.Load 并没有直接调用 AssemblyResolve,但它确实存在于那里(在加载尝试和解析调用之间有一些非托管代码)。 - Łukasƨ Fronczyk

1

MSDN文档中提到:

AssemblyResolve事件的工作原理:

当您为AssemblyResolve事件注册处理程序时,每当运行时无法按名称绑定到程序集时,都会调用处理程序。例如,从用户代码调用以下方法可能会导致引发AssemblyResolve事件:

  1. 一个AppDomain.Load方法重载或Assembly.Load方法重载,其第一个参数是表示要加载的程序集的显示名称的字符串(即Assembly.FullName属性返回的字符串)。

  2. 一个AppDomain.Load方法重载或Assembly.Load方法重载,其第一个参数是标识要加载的程序集的AssemblyName对象

它没有提到重载接收byte[]。我在参考源代码中查找,似乎接受stringLoad内部调用了一个名为InternalLoad的方法,在调用本地LoadImage之前调用CreateAssemblyName,其文档说明如下:

创建AssemblyName。如果已引发AssemblyResolve事件,则填充程序集。

internal static AssemblyName CreateAssemblyName(
           String assemblyString, 
           bool forIntrospection, 
           out RuntimeAssembly   assemblyFromResolveEvent)
{
        if (assemblyString == null)
           throw new ArgumentNullException("assemblyString");
        Contract.EndContractBlock();

        if ((assemblyString.Length == 0) ||
            (assemblyString[0] == '\0'))
            throw new ArgumentException(Environment.GetResourceString("Format_StringZeroLength"));

        if (forIntrospection)
            AppDomain.CheckReflectionOnlyLoadSupported();

        AssemblyName an = new AssemblyName();

        an.Name = assemblyString;
        an.nInit(out assemblyFromResolveEvent, forIntrospection, true); // This method may internally invoke AssemblyResolve event.
        
        return an;

byte[]重载没有这个问题,它只是在QCall.dll内部调用本地的nLoadImage函数。这可能解释了为什么ResolveEvent没有被调用。


1
如果您执行以下代码:

            var appDomain = AppDomain.CreateDomain("some domain");
            var assembly = appDomain.Load(someAssemblyBytes);

将会生成一个AssemblyResolve事件。这是因为该程序集已加载到两个域中。第一个域是您期望的域,即appDomain,并且该域的程序集解析器未被调用。第二个域是您当前的应用程序域,因为您尝试从中访问该程序集,而它在那里不存在。 因此,如果您从与appDomain不同的域执行appDomain.Load(someAssemblyName);,则将生成两次解析事件,每个域一次。
在这种情况下,如果您想要省略AssemblyResolve尝试(它将失败或也将加载程序集到主域中),您必须创建一个代理类,该类派生自MarshalByRefObject,在两个域中都可见的程序集中使用此代理类来包含代理类。例如:
    internal class AssemblyVersionProxy : MarshalByRefObject
    {
        public Version GetVersion(byte[] assemblyBytes)
        {
            var assembly = Assembly.Load(assemblyBytes);
            var version = new AssemblyName(assembly.FullName).Version;
            return version;
        }

    }

并使用它:

        public Version GetAssemblyVersion(byte[] assemblyBytes)
        {
            var appDomain = AppDomain.CreateDomain(Guid.NewGuid().ToString());

            try
            {
                var proxyType = typeof(AssemblyVersionProxy);

                var proxy = (AssemblyVersionProxy)appDomain.CreateInstanceAndUnwrap(
                    proxyType.Assembly.FullName, proxyType.FullName,
                    false, BindingFlags.CreateInstance, null,
                    new object[0], null, new object[0]
                );

                var version = proxy.GetVersion(assemblyBytes);
                return version;
            }
            finally
            {
                AppDomain.Unload(appDomain);
            }
        }

1
据我所知,Assembly.Load或通过其他方式加载程序集不会执行由C#编译器生成的任何构造函数(包括静态构造函数)。因此,您将无法在常见的程序集中重新进入AssemblyResolve
正如您在问题中提到的那样,模块初始化程序在Load调用期间不会被执行。在CLI规范的保证列表中有所涵盖 - 摘录可以在Junfeng Zhang的Module Initializers中找到。

B. 在访问模块中定义的任何类型、方法或数据之前,模块的初始化程序方法将被执行。

通常有关“在任何类型构造函数之前运行代码”的相关SO问题,例如程序集加载时初始化库。请注意,.Net:当程序集加载时运行代码由Marc Gravell回答,指出由于安全限制可能无法实现。

谢谢。我并不打算将我的问题仅限于可以使用C#编译器生成的程序集。我已经更新了问题以使其更加清晰明了。 - Vladimir Reshetnikov
2
你所提到的问题的答案似乎没有提供任何方法在调用Assembly.Load(byte[])时立即执行代码。除非它们被显式读取,否则属性是被动数据,而模块初始化程序(我在问题中提到过)仅在尝试访问全局声明的成员之前调用。也许有一些特殊的属性在程序集加载时会被CLR自动读取,然后我可以为这样的属性提供一个枚举参数,其中枚举类型在引用的程序集中声明... - Vladimir Reshetnikov
@VladimirReshetnikov - 根据http://einaregilsson.com/module-initializers-in-csharp/,似乎在<Module>类中添加静态构造函数是最接近你所寻找的东西。我个人没有经验,不知道它何时被调用。 - Alexei Levenkov
我尝试过这个,但它在程序集加载时没有被调用。 - Vladimir Reshetnikov
@VladimirReshetnikov - 我猜,根据 Hans Passant 提供的更好的模块初始化程序链接,模块初始化程序具有与静态构造函数相同的保证,只是在其之前运行。因此,不在 Load 上运行是有意义的... - Alexei Levenkov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接