【.Net】AppDomain

文章目录

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

  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:

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

    功能:

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

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

    功能:

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

    》DefaultDomain1: DefaultDomain

    》 DefaultDomain2:

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

    功能:

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

在AppDomain中调用

方法一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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));
}
}

方法二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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));
}
}

如何使用:

1
2
3
4
5
6
Type proxyType = typeof(Sandboxer);
MarshalByRefObject proxy =
(MarshalByRefObject)domain.
CreateInstanceFrom(
proxyType.Assembly.Location,
proxyType.FullName).Unwrap();

JIT On Another AppDomain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 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 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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 的程序集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/// <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 调用原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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);
}