阅读这篇文章你会了解到:

  1. AppDomain 是什么
  2. SystemDomain、SharedDomain、DefaultDomain
  3. 如何创建 AppDomain
  4. 如何跨 AppDomain 加载程序集并调用指定接口

相关文章:

The Truth About .NET Objects And Sharing Them Between AppDomains - Geeks with Blogs

AppDomain & Assembly

应用程序域是 .NET 中用于提供隔离的执行环境的一个概念,它允许在同一个进程中运行多个应用程序域,每个应用程序域都有自己独立的代码、数据和配置。应用程序域之间是相互隔离的,这种隔离提供了更高的安全性和稳定性,并允许动态加载和卸载程序集。

关于 AppDomain 我的理解是,可以参考系统和进程。一个系统由多个进程,一个进程可以有多个AppDomain。AppDomain 之间也和进程一样是隔离的。但不同的是,AppDomain 又大致分为三类,SystemDomain、ShreadDomain、DefaultDomain 官方并没有对这三个 AppDomain 进行说明。这里我就个人理解说明一下:

Windbg 调试可以使用 !DumpDomain 查看域信息,可以转储一个 w3wp 的 dump 调试一下看看。这里以 w3wp 为例。

  • SystemDomain:

    image-20240730094819438

    描述: SystemDomain 是 .NET 运行时启动时创建的第一个应用程序域。它主要用于加载和执行与 .NET 运行时自身相关的核心库和代码,如 mscorlib.dll,该库包含了 .NET Framework 的基本类。

    功能:

    • 管理 .NET 运行时的启动和初始化。
    • 加载和执行 .NET 运行时的核心组件。
    • 提供一个安全的隔离环境,以确保 .NET 运行时的稳定性。
  • SharedDomain:

    image-20240730095322670

    描述: SharedDomain 是一个特殊的应用程序域,用于共享 .NET 运行时中某些全局静态数据。这个域通常用于托管全局的和通用的类型,例如共享类型和静态字段,以减少内存使用和提高性能。

    功能:

    • 提供一个全局共享的区域,用于存储共享的静态数据和类型。
    • 优化内存使用和性能,通过减少冗余的静态数据。
  • DefaultDomain:

    》DefaultDomain1: DefaultDomain

    image-20240730095428445》 DefaultDomain2: image-20240730095631993

    描述: DefaultDomain 是在 .NET 运行时启动之后创建的第一个应用程序域,用于加载并执行应用程序代码。每个 .NET 进程启动时,都会自动创建一个 DefaultDomain

    功能:

    • 加载并执行应用程序代码。
    • 提供一个隔离的执行环境,用于托管应用程序的代码和资源。
    • 支持跨应用程序域的代码和资源隔离,提高安全性和稳定性。

在AppDomain中调用

方法一:

public class SimpleAssemblyLoader : MarshalByRefObject
{
    public void Load(string path)
    {
        ValidatePath(path);
 
        Assembly.Load(path);
    }
 
    public void LoadFrom(string path)
    {
        ValidatePath(path);
 
        Assembly.LoadFrom(path);
    }
 
    private void ValidatePath(string path)
    {
        if (path == null) throw new ArgumentNullException("path");
        if (!System.IO.File.Exists(path))
            throw new ArgumentException(String.Format("path \"{0}\" does not exist", path));
    }
}

方法二:

public class SimpleAssemblyLoader : MarshalByRefObject
{
    public void Load(string path)
    {
        ValidatePath(path);
 
        Assembly.Load(path);
    }
 
    public void LoadFrom(string path)
    {
        ValidatePath(path);
 
        Assembly.LoadFrom(path);
    }
 
    private void ValidatePath(string path)
    {
        if (path == null) throw new ArgumentNullException("path");
        if (!System.IO.File.Exists(path))
            throw new ArgumentException(String.Format("path \"{0}\" does not exist", path));
    }
}

如何使用:

Type proxyType = typeof(Sandboxer);
MarshalByRefObject proxy =
    (MarshalByRefObject)domain.
    CreateInstanceFrom(
    proxyType.Assembly.Location,
    proxyType.FullName).Unwrap();

JIT On Another AppDomain

 ObjectHandle handle = Activator.CreateInstanceFrom(
     domain, typeof(Process).Assembly.ManifestModule.FullyQualifiedName,
     typeof(Process).FullName
 );
 
 //TODO 尝试 JIT 
 ObjectHandle handle1 = domain.CreateInstanceFrom(
     typeof(Process).Assembly.ManifestModule.FullyQualifiedName,
     typeof(Process).FullName
 );
 Process DomainProcess = (Process)handle.Unwrap();
 var Methods = DomainProcess.GetType().GetRuntimeMethods().ToList();
 var StartWithCreateProcess = Methods.Where(_ => _.Name == "StartWithShellExecuteEx").FirstOrDefault();
 var StartWithShellExecuteEx = Methods.Where(_ => _.Name == "StartWithCreateProcess").FirstOrDefault();
 
 var methodhandle = StartWithCreateProcess.MethodHandle;
 var methodAttr = StartWithCreateProcess.Attributes;
 var methodType = methodhandle.GetType();
if(false)
 {
     try
     {
         StartWithCreateProcess.Invoke(null, null);
         StartWithShellExecuteEx.Invoke(null, null);
 
     }
     catch (Exception ex)
     {
         int error = Marshal.GetLastWin32Error();
         Natives.OutputDebug(ex.StackTrace + $"error:{error}, " + ex.Message);
     }
 }
 // RuntimeHelpers.PrepareMethod(StartWithShellExecuteEx);
 
 if (!((methodAttr & MethodAttributes.Abstract) == MethodAttributes.Abstract || methodType.ContainsGenericParameters))
 {
     RuntimeHelpers.PrepareMethod(methodhandle);
 }
 methodhandle = StartWithCreateProcess.MethodHandle;

创建 AppDomain 并传递数据

如下所示,循环创建 AppDomain 并传递数据到所创建的 AppDomain 中。

class Program {
  /// <summary>
  /// Show how to pass an object by reference directly into another appdomain
  /// without serializing it at all.
  /// </summary>
  /// <param name="args"></param>
  [LoaderOptimization(LoaderOptimization.MultiDomainHost)]
  static public void Main(string[] args) {
    for (int i = 0; i < 10000;
         i++)  // try it often to see how the AppDomains do behave
    {
      // To load our assembly appdomain neutral we need to use MultiDomainHost
      // on our hosting and child domain If not we would get different Method
      // tables for the same types which would result in InvalidCastExceptions
      // for the same type.
      // Prerequisite for MultiDomainHost is that the assembly we share the data
      // is a) Installed into the GAC (which requires as strong name as well) If
      // you would use MultiDomain then it would work but all AppDomain neutral
      // assemblies will never be unloaded.
      var other = AppDomain.CreateDomain(
          "Test" + i.ToString(), AppDomain.CurrentDomain.Evidence,
          new AppDomainSetup {
            LoaderOptimization = LoaderOptimization.MultiDomainHost,
          });
 
      // Create gate object in other appdomain
      DomainGate gate = (DomainGate)other.CreateInstanceAndUnwrap(
          Assembly.GetExecutingAssembly().FullName,
          typeof(DomainGate).FullName);
 
      // now lets create some data
      CrossDomainData data = new CrossDomainData();
      data.Input = Enumerable.Range(0, 10).ToList();
 
      // process it in other AppDomain
      DomainGate.Send(gate, data);
 
      // Display result calculated in other AppDomain
      Console.WriteLine("Calculation in other AppDomain got: {0}",
                        data.Aggregate);
 
      AppDomain.Unload(other);
      // check in debugger now if UnitTests.dll has been unloaded.
      Console.WriteLine("AppDomain unloaded");
    }
  }

使用 LoaderOptimzation.MultiDomainHost 卸载在另一个 AppDomain 没有 GC 的程序集。同时必须从 GAC 加载自定义的 CrossDomainData 的程序集。

/// <summary>
/// Enables sharing of data between appdomains as plain objects without any
/// marsalling overhead.
/// </summary>
class DomainGate : MarshalByRefObject {
  /// <summary>
  /// Operate on a plain object which is shared from another AppDomain.
  /// </summary>
  /// <param name="gcCount">Total number of GCs</param>
  /// <param name="objAddress">Address to managed object.</param>
  public void DoSomething(int gcCount, IntPtr objAddress) {
    if (gcCount != ObjectAddress.GCCount) {
      throw new NotSupportedException(
          "During the call a GC did happen. Please try again.");
    }
 
    // If you get an exception here disable under Projces/Debugging/Enable
    // Visual Studio Hosting Process The appdomain which is used there seems to
    // use LoaderOptimization.SingleDomain
    CrossDomainData data =
        (CrossDomainData)PtrConverter<Object>.Default.ConvertFromIntPtr(
            objAddress);
    ;
 
    // process input data from other domain
    foreach (var x in data.Input) {
      Console.WriteLine(x);
    }
 
    OtherAssembliesUsage user = new OtherAssembliesUsage();
 
    // generate output data
    data.Aggregate = data.Input.Aggregate((x, y) => x + y);
  }
 
  public static void Send(DomainGate gate, object o) {
    var old = GCSettings.LatencyMode;
    try {
      GCSettings.LatencyMode =
          GCLatencyMode.Batch;  // try to keep the GC out of our stuff
      var addandGCCount = ObjectAddress.GetAddress(o);
      gate.DoSomething(addandGCCount.Value, addandGCCount.Key);
    } finally {
      GCSettings.LatencyMode = old;
    }
  }
}

获取不同 AppDomain 下的程序集地址

参考文章

Jitex/src/Jitex/Utils/ModuleHelper.cs at 89e3faa23b1a38933c46915c6ad5b1c6964824dd · Hitmasu/Jitex

原理

主要通过 Module. m_pData 获取程序加载地址。不同 AppDomain 下程序集的加载地址是不同的。

JIT 调用原理

private static int GetInvocationFlags(MethodInfo method)
{
    if (method == null) return 0;
 
    try
    {
        // 运行时实际类型(通常是内部的 RuntimeMethodInfo):
        Type runtimeType = method.GetType();
        // 尝试读取非公开字段 m_invocationFlags(在当前 CLR 实现中存在)
        var f = runtimeType.GetField("m_invocationFlags", BindingFlags.NonPublic | BindingFlags.Instance);
        if (f != null)
        {
            object val = f.GetValue(method);
            if (val != null)
            {
                // boxed enum -> 转为 int
                return Convert.ToInt32(val);
            }
        }
    }
    catch
    {
        // 忽略反射失败,走下一步回退方案
    }
 
    try
    {
        // 回退:读取 MethodDesc 的 m_dwFlags(第 6 字节),已有代码中也使用类似方式
        IntPtr handle = method.MethodHandle.Value;
        byte b = Marshal.ReadByte(IntPtr.Add(handle, 6));
        return (int)b;
    }
    catch
    {
        return 0;
    }
}
 
// 如下所示为修改标志位并执行 PrePareMethod 的逻辑
// OldMethod 获取逻辑如下所示
// this.OldMethod = Clazz.GetMethod(TargetMethodName, this.oldbindingfalgs);
 
// InvocationFlags
uint INVOCATION_FLAGS_INITIALIZED = 0x00000001;
 
// MethodDescClassification
byte mdcRequiresInheritanceCheck = 0x80;
byte mdcStatic = 0x20;
 
byte bTargetMDC = 0;
// 读取  m_dwFlags 字段,判断方法是否已经被初始化
byte bOriginMDC = 0;
 
bool bChangeMDC = false;
if (this.OldMethod != null)
{
    uint flags = (uint)GetInvocationFlags(this.OldMethod);
    Natives.OutputInfo("OldMethod InvocationFlags: " + flags);
 
    if((INVOCATION_FLAGS_INITIALIZED & flags) == INVOCATION_FLAGS_INITIALIZED)
    {
        bOriginMDC = Marshal.ReadByte(this.OldMethod.MethodHandle.Value + 6);
        int iNextFlag = Marshal.ReadInt32(this.OldMethod.MethodHandle.Value + 8);
        if (iNextFlag < 0xFF &&
            (bOriginMDC & mdcRequiresInheritanceCheck) == mdcRequiresInheritanceCheck)
        {
            bChangeMDC = true;
            Reporter.OutputInfo("The OldMethod has been initialized before hook, Method:" + this.HookMethodName);
            bTargetMDC = bOriginMDC;
            bTargetMDC &= 0x0F; // 保留其他标志位不变
            bTargetMDC |= mdcStatic;
            Marshal.WriteByte(this.OldMethod.MethodHandle.Value + 6, (byte)bTargetMDC);
        }
    }
}
 
RuntimeHelpers.PrepareMethod(OldMethod.MethodHandle);
if(bChangeMDC)
{
    Marshal.WriteByte(this.OldMethod.MethodHandle.Value + 6, bOriginMDC);
}