将清单程序集动态加载到 appdomain

Posted

技术标签:

【中文标题】将清单程序集动态加载到 appdomain【英文标题】:Load manifest assemblies dynamically into appdomain 【发布时间】:2021-07-11 13:18:42 【问题描述】:

我有 2 个项目:

Main (Console)
Util (Class library)

我想要两全其美,但我不确定这是否可能。

假设我的 Util 库中有这个类:

public static class Helper

    public static int AnswerToUniverse()
    
        return 42;
    

我通过 Visual Studio Project -> Add Reference 将类库 Util 添加为 Main 的依赖项。这会将其放入 Main 的清单中,并允许我静态使用 Util 库中公开的所有类型,以及所有 Intellisense 优点。

我一直认为运行时会在启动时立即加载这些静态链接的程序集,但似乎如果我在我的 Main 项目中从 Util 中删除所有使用但保留它作为参考,然后删除 .dll 程序集文件从Main 的工作目录中,它仍然运行良好,所以我得出结论,它在第一次使用时就被加载了。

现在,我的工作目录中不会有程序集文件 Util,但我确实有它的原始字节,所以我想将 Util 程序集的内容作为原始字节动态加载到应用程序域在第一次使用其任何方法之前在运行时。

我可以删除静态依赖,然后使用反射,例如:

var assembly = AppDomain.CurrentDomain.Load(rawBytes);
assembly.GetType("Util.Helper").GetMethod("AnswerToUniverse") // etc.. etc..

但我仍然非常希望能够做到:

var answer = Helper.AnswerToUniverse();

我已经尝试过挂钩 AppDomain.CurrentDomain.AssemblyResolve,但这似乎不会在静态引用的程序集上被调用。

测试代码:

Util、Helper.cs:

namespace Util

    public static class Helper
    

        public static int AnswerToUniverse()
        
            return 42;
        
    

主要,Program.cs:

using Util;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace Main

    class Program
    
        static void Main(string[] args)
        
            AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
            Console.WriteLine(Helper.AnswerToUniverse());
        

        private static System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        
            File.WriteAllText("test.txt", "assemblyresolved");
            Console.WriteLine("resolved.");
            Console.Out.Flush();
            return null;
        
    

每次我在工作目录中没有Util 程序集文件的情况下运行Main 时,我都会得到一个FileNotFoundException,这是否可以以某种方式拦截,所以我可以返回从原始字节加载的程序集,还是我使用反射卡住了如上图?

【问题讨论】:

@HansPassant 这是团队正在创建的 CTF 挑战。我知道这不是最佳实践。 Main 应该从字节数组中动态加载 Util,而字节数组又来自 CTF 的先前步骤。如果我在不删除静态引用的情况下无法加载它,因此必须使用GetMethod 等,那么就这样吧,但我想知道是否有办法保持静态类型检查并从字节加载。 @HansPassant 是的,大卫的回答似乎符合您的描述,谢谢! 【参考方案1】:

我想将 Util 程序集的内容作为原始字节动态加载到应用程序域中

我已经尝试过挂钩 AppDomain.CurrentDomain.AssemblyResolve

这是正确的方法。当 JITter 需要编译目标程序集中的任何类型时,将调用 AssemblyResolve。

由于 JIT 会触发此操作,因此您必须在 JITter 尝试编译对目标程序集的任何引用之前挂钩 AssemblyResolve。所以这个

static void Main(string[] args)

    AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
    Console.WriteLine(Helper.AnswerToUniverse());

不起作用,因为 Main 的编译会触发 Helper 程序集的加载,但您还没有挂钩 AssemblyResolve。

因为

加载器创建一个存根并将其附加到一个类型中的每个方法,当 类型被加载并初始化。当一个方法被调用时 第一次,存根将控制权交给 JIT 编译器,它 将该方法的 MSIL 转换为本机代码并修改 stub 直接指向生成的本机代码。所以, 对 JIT 编译方法的后续调用直接转到本机 代码。

Compilation by the JIT Compiler

将Helper的第一次使用推入一个单独的方法就足够了(并防止该方法被“内联”到Main中),因此可以在第一次需要Helper之前编译Main开始执行。例如

static void Main(string[] args)

    AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
    Run();


[MethodImpl(MethodImplOptions.NoInlining)]
static void Run()

   Console.WriteLine(Helper.AnswerToUniverse());

【讨论】:

没错,但是如果我通过Project -> Add Reference 添加程序集然后删除物理文件,则无法调用我的回调。 完成!编辑了我的帖子。在这个测试用例中,控制台会记录任何内容,也不会创建文件。 查看更新的答案。您需要在 JITter 需要 Helper 程序集之前挂钩 AssemblyResolve。 行得通!太棒了,谢谢。我认为在任何时候都使用它就足够了,我不知道在这种情况下它是 per-method。

以上是关于将清单程序集动态加载到 appdomain的主要内容,如果未能解决你的问题,请参考以下文章

C# 动态加载/卸载程序集

C# 通过 AppDomain 应用程序域实现程序集动态卸载或加载

C# 通过 AppDomain 应用程序域实现程序集动态卸载或加载

从 byte[] 动态加载程序集

C#动态加载dll 时程序集的卸载问题

vb.net编程,如何使用 appdomain 实现某进程DLL动态加载和卸载?