【问题标题】:How to load an assembly from byte array into a non-default AppDomain without accessing the app directory?如何在不访问应用程序目录的情况下将程序集从字节数组加载到非默认 AppDomain?
【发布时间】:2019-09-25 04:33:22
【问题描述】:

我有一个内存程序集 MyAssembly(类库),用于我的主程序集 MyApp.exe

byte[] assemblyData = GetAssemblyDataFromSomewhere();

(对于测试,GetAssemblyDataFromSomewhere 方法可以对现有的程序集文件执行File.ReadAllBytes,但在我的真实应用程序中没有文件。)

MyAssembly 仅具有 .NET Framework 引用,不依赖于任何其他用户代码。

我可以将此程序集加载到当前(默认)AppDomain

Assembly.Load(assemblyData);

// this works
var obj = Activator.CreateInstance("MyAssembly", "MyNamespace.MyType").Unwrap();

现在,我想将此程序集加载到不同的AppDomain 并在那里实例化该类。 MyNamespace.MyType 派生自 MarshalByRefObject,因此我可以跨应用程序域共享实例。

var newAppDomain = AppDomain.CreateDomain("DifferentAppDomain");

// this doesn't really work...
newAppDomain.Load(assemblyData);

// ...because this throws a FileNotFoundException
var obj = newAppDomain.CreateInstanceAndUnwrap("MyAssembly", "MyNamespace.MyType");

是的,我知道AppDomain.Load docs中有一条注释:

此方法应仅用于将程序集加载到当前应用程序域中。

是的,它应该用于那个,但是...

如果当前AppDomain 对象代表应用程序域A,并且从应用程序域B 调用Load 方法,则程序集被加载到两个应用程序域.

我可以忍受。如果程序集将被加载到两个应用程序域中,对我来说没有问题(因为我实际上将它加载到默认应用程序域中)。

我可以看到该程序集已加载到新的应用程序域中。有点。

var assemblies = newAppDomain.GetAssemblies().Select(a => a.GetName().Name);
Console.WriteLine(string.Join("\r\n", assemblies));

这给了我:

mscorlib
MyAssembly

但是尝试实例化类总是会导致FileNotFoundException,因为 CLR 会尝试从文件加载程序集(尽管它已经加载,至少根据 AppDomain.GetAssemblies)。

我可以在MyApp.exe

newAppDomain.AssemblyResolve += CustomResolver;

private static Assembly CustomResolver(object sender, ResolveEventArgs e)
{
    byte[] assemblyData = GetAssemblyDataFromSomewhere();
    return Assembly.Load(assemblyData);
}

这可行,但这会导致第二个应用程序域从文件加载调用程序集 (MyApp.exe)。发生这种情况是因为该应用程序域现在需要来自调用程序集的代码(CustomResolver 方法)。

我可以将应用程序域创建逻辑和事件处理程序移动到不同的程序集中,例如MyAppServices.dll,因此新的应用程序域将加载该程序集而不是 MyApp.exe

但是,我想不惜一切代价避免文件系统访问我的应用程序目录:新的应用程序域不得从文件中加载任何用户程序集。

我也试过AppDomain.DefineDynamicAssembly,但也没有用,因为返回值的类型System.Reflection.Emit.AssemblyBuilder既不是MarshalByRefObject,也不是[Serializable]

有没有办法将程序集从字节数组加载到非默认 AppDomain 而不将调用程序集从文件加载到该应用程序域?实际上,我的应用程序目录没有任何文件系统访问权限?

【问题讨论】:

  • "这可行,但这会导致第二个应用程序域从文件加载调用程序集 (MyApp.exe)。这是因为应用程序域需要代码(类型)形式程序集" ..."我想不惜一切代价避免这种情况:新的应用程序域不得从文件中加载任何用户程序集。" 那么,在某些时候你的第二个应用程序域需要以某种方式加载 MyApp.exe,不是吗?您自己是这么说的,因为您加载到第二个应用程序域中的一些汇编代码似乎需要来自 MyApp.exe 的类型。我的意思是,如果它需要来自其他程序集的类型,你希望它做什么......?
  • @elgonzo,MyApp.exe 文件很大,因为有很多嵌入式资源。它在启动时首先加载,是的,但我需要避免任何进一步的加载。 MyAssembly 没有引用 MyApp.exe 并且没有其他对我的程序集的引用。只有当我连接 AssemblyResolve 事件时才会发生这种情况 - 所以我不能这样做。
  • 我是否正确理解您,AssemblyResolve 处理程序是在您的 MyApp.exe 中声明的方法(匿名或非匿名),从而将 MyApp.exe “拉”到第二个应用程序域中?
  • @elgonzo,正确。我稍微编辑了我的问题以使其更清楚。
  • 我猜你需要从你的 MyApp.exe 中“提升”那个处理程序。要么将其作为 GetAssemblyDataFromSomewhere 返回的程序集的一部分(如果可行),要么只创建(还)另一个程序集(动态地在内存中或作为单独的 DLL,具体取决于您的要求)。有几种方法可以触发第二个应用程序域中的 AssemblyResolve 处理程序的事件订阅。从第一个应用程序域显式设置它,或者在第二个应用程序域中使用(静态?)方法或类似类型的方法,该方法将通过反射从第一个应用程序域调用...

标签: c# .net .net-assembly appdomain assembly-loading


【解决方案1】:

您的第一个问题是将程序集加载到第二个AppDomain 的方式。

您需要在AppDomains 之间加载/共享一些类型。如果程序集尚未加载到第一个 AppDomain 中,则无法从第一个 AppDomain 将程序集加载到第二个 AppDomain 中(如果您将程序集字节加载到第一个 AppDomain uisng .Load(...) 中,它也将不起作用).
这应该是一个很好的起点:
假设我有一个名为 Models 的类库,其中单个类 Person 如下:

namespace Models
{
    public class Person : MarshalByRefObject
    {
        public void SayHelloFromAppDomain()
        {
            Console.WriteLine($"Hello from {AppDomain.CurrentDomain.FriendlyName}");
        }
    }
}

和控制台应用程序如下(Models 类库不是项目中的引用)

namespace ConsoleApp
{
    internal class Program
    {
        [LoaderOptimizationAttribute(LoaderOptimization.MultiDomain)]
        public static void Main(String[] args)
        {
            CrossAppDomain();
        }

        private static Byte[] ReadAssemblyRaw()
        {
            // read Models class library raw bytes
        }

        private static void CrossAppDomain()
        {
            var bytes = ReadAssemblyRaw();
            var isolationDomain = AppDomain.CreateDomain("Isolation App Domain");

            var isolationDomainLoadContext = (AppDomainBridge)isolationDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, "ConsoleApp.AppDomainBridge");
            // person is MarshalByRefObject type for the current AppDomain
            var person = isolationDomainLoadContext.ExecuteFromAssembly(bytes);
        }
    }


    public class AppDomainBridge : MarshalByRefObject
    {
        public Object ExecuteFromAssembly(Byte[] raw)
        {
            var assembly = AppDomain.CurrentDomain.Load(rawAssembly: raw);
            dynamic person = assembly.CreateInstance("Models.Person");
            person.SayHelloFromAppDomain();
            return person;
        }
    }
}

它的工作方式是从 ConsoleApp 项目中创建 AppDomainBridge 的实例,该项目同时加载到 AppDomains 中。现在这个实例存在于第二个AppDomain 中。然后您可以使用AppDomainBridge 实例将程序集实际加载到第二个AppDomain 并跳过与第一个AppDomain 相关的任何事情。
这是我执行代码(.NET Framework 4.7.2)时控制台的输出,因此Person 实例位于第二个AppDomain 中:


您的第二个问题是在 AppDomain 之间共享实例。

AppDomains 共享相同代码的主要问题是需要共享相同的 JIT 编译代码(方法表、类型信息...等)。
来自docs.microsoft

JIT 编译的代码不能共享给加载到 load-from 上下文,使用 Assembly 类的 LoadFrom 方法,或 使用指定的 Load 方法的重载从图像加载 字节数组。

因此,当您从字节加载汇编时,您将无法完全共享类型信息,这意味着此时您的对象只是 MarshalByRefObject 用于第一个 AppDomain。这意味着您只能从MarshalByRefObject 类型执行和访问方法/属性(如果您尝试使用动态/反射没有关系 - 第一个AppDomain 没有实例的类型信息)。

您可以做的不是从ExecuteFromAssembly 返回对象,而是将AppDomainBridge 类扩展为对创建的Person 实例的简单包装,并使用它来委托第一个@987654347 的任何方法执行@ 到第二个,如果你真的需要它来达到这些目的。

【讨论】:

  • @dymanoid 我的错。我正在为 AppDomain 尝试不同的加载选项,而这段代码已经被遗忘了。我进行了编辑 - 它应该是 CrossAppDomain()。
  • 感谢您的回答。但是,“隔离应用程序域”中的 AppDomain.CurrentDomain.GetAssemblies() 向我显示 ConsoleApp.exe 程序集已从文件加载到该应用程序域 - 这就是我要避免的。
  • @dymanoid JITed 代码在应用程序域之间共享 - 因此它实际上只执行一次然后共享。我的意思是如果你现在有一个或多个 AppDomain 没有区别。
  • @dymanoid 我在 main 方法中添加了 LoaderOptimization 属性注释。这应该确保 AppDomain 之间的最大资源共享。我认为这是默认行为,但我可能是错的。直言不讳更好。
  • 不,这似乎没有帮助。如果我在启动后立即删除ConsoleApp.exe,则isolationDomain.CreateInstanceAndUnwrap 调用会为“ConsoleApp.exe”抛出FileNotFoundException
【解决方案2】:

我不太确定您要达到什么目标,但我会尝试以下方法。

总的来说,您的方法似乎还可以。您必须确保正确设置辅助 appdomain 的探测路径(尤其是 appbase 路径)。否则,.NET Fusion 将探测这些位置的依赖关系,并且您将尝试避免那些不需要的文件系统访问尝试。 (好吧,至少确保将这些路径配置为一些没有设置实际权限的临时文件夹)。

建议的解决方案

在任何情况下,您都可以尝试在您的 dynamic 中添加(我应该这样称呼它吗?)组装一个入口点(例如,某些 Bootstrap 类中的 Main 方法),和 在将程序集加载到辅助 AppDomain 后,尝试调用 AppDomain.ExecuteAssemblyByName

我会将您的CustomResolver 方法添加到Bootstrap 类,并在Main 方法中订阅AssemblyResolve

这样,当调用Main 方法时(希望它按预期工作),订阅AppDomain 的AssemblyResolve 不会触发融合。

我没有测试这个解决方案,这可能是一个很长的尝试,但更糟糕的是尝试。

附注: 我确实看到有关此方法的文档确实指出,运行时将首先尝试加载程序集(可能通过使用常规探测逻辑),但它没有说明程序集预加载到 AppDomain 中的情况在拨打电话之前。

备注

ExecuteAssemblyByName 方法提供与 ExecuteAssembly 方法类似的功能,但通过显示名称或 AssemblyName 而不是文件位置指定程序集。因此,ExecuteAssemblyByName 使用 Load 方法而不是 LoadFile 方法加载程序集。

程序集在 .NET Framework 标头中指定的入口点开始执行。

此方法不会创建新的进程或应用程序域,也不会在新线程上执行入口点方法。

Load 方法文档也没有提供明确的答案。

附言: 调用 Unwrap 方法可能会触发主 AppDomain 中的融合,因为为您的类创建了代理。我认为,此时您的主 AppDomain 将尝试定位该动态加载的程序集。您确定是辅助 AppDomain 引发了异常吗?

【讨论】:

  • 没有办法为我要加载的类库定义入口点。我编辑了我的问题以使其更加明显。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-13
相关资源
最近更新 更多