跨 AppDomain 调用会破坏运行时

Posted

技术标签:

【中文标题】跨 AppDomain 调用会破坏运行时【英文标题】:Cross-AppDomain call corrupts the runtime 【发布时间】:2018-01-20 14:08:40 【问题描述】:

这原本是一个更长的问题,但现在我已经构建了一个更小的可用示例代码,所以原文不再相关。

我有两个项目,一个包含一个无成员的结构,名为TestType。该项目被主项目引用,但程序集不包含在可执行目录中。主项目创建一个新的应用程序域,在其中使用包含的程序集的名称注册 AssemblyResolve 事件。在主应用程序域中,处理相同的事件,但它手动从项目资源加载程序集。

然后,新的应用程序域会构建自己的 TestType 版本,但比原来的具有更多字段。主应用域使用虚拟版本,新应用域使用生成的版本。

当调用签名中包含 TestType 的方法时(即使只是简单地返回就足够了),它似乎只是破坏了运行时的稳定性并破坏了内存。

我使用的是 .NET 4.5,在 x86 下运行。

DummyAssembly:

using System;

[Serializable]
public struct TestType



主要项目:

using System;
using System.Reflection;
using System.Reflection.Emit;

internal sealed class Program

    [STAThread]
    private static void Main(string[] args)
    
        Assembly assemblyCache = null;

        AppDomain.CurrentDomain.AssemblyResolve += delegate(object sender, ResolveEventArgs rargs)
        
            var name = new AssemblyName(rargs.Name);
            if(name.Name == "DummyAssembly")
            
                return assemblyCache ?? (assemblyCache = TypeSupport.LoadDummyAssembly(name.Name));
            
            return null;
        ;

        Start();
    

    private static void Start()
    
        var server = ServerObject.Create();

        //prints 155680
        server.TestMethod1("Test");
        //prints 0
        server.TestMethod2("Test");
    


public class ServerObject : MarshalByRefObject

    public static ServerObject Create()
    
        var domain = AppDomain.CreateDomain("TestDomain");
        var t = typeof(ServerObject);
        return (ServerObject)domain.CreateInstanceAndUnwrap(t.Assembly.FullName, t.FullName);
    

    public ServerObject()
    
        Assembly genAsm = TypeSupport.GenerateDynamicAssembly("DummyAssembly");

        AppDomain.CurrentDomain.AssemblyResolve += delegate(object sender, ResolveEventArgs rargs)
        
            var name = new AssemblyName(rargs.Name);
            if(name.Name == "DummyAssembly")
            
                return genAsm;
            
            return null;
        ;
    

    public TestType TestMethod1(string v)
    
        Console.WriteLine(v.Length);
        return default(TestType);
    

    public void TestMethod2(string v)
    
        Console.WriteLine(v.Length);
    


public static class TypeSupport

    public static Assembly LoadDummyAssembly(string name)
    
        var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name);
        if(stream != null)
        
            var data = new byte[stream.Length];
            stream.Read(data, 0, data.Length);
            return Assembly.Load(data);
        
        return null;
    

    public static Assembly GenerateDynamicAssembly(string name)
    
        var ab = AppDomain.CurrentDomain.DefineDynamicAssembly(
            new AssemblyName(name), AssemblyBuilderAccess.Run
        );

        var mod = ab.DefineDynamicModule(name+".dll");

        var tb = GenerateTestType(mod);

        tb.CreateType();

        return ab;
    

    private static TypeBuilder GenerateTestType(ModuleBuilder mod)
    
        var tb = mod.DefineType("TestType", TypeAttributes.Public | TypeAttributes.Serializable, typeof(ValueType));

        for(int i = 0; i < 3; i++)
        
            tb.DefineField("_"+i.ToString(), typeof(int), FieldAttributes.Public);
        

        return tb;
    

虽然 TestMethod1TestMethod2 都应该打印 4,但第一个会访问内存的一些奇怪部分,并且似乎破坏调用堆栈足以影响调用到第二种方法。如果我删除对第一个方法的调用,一切都很好。

如果我在 x64 下运行代码,第一个方法会抛出 NullReferenceException

这两个结构的字段数量似乎很重要。如果第二个结构总体上比第一个大(如果我只生成一个字段或不生成),那么一切都可以正常工作,如果 DummyAssembly 中的结构包含更多字段。这让我相信 JITter 要么错误地编译了方法(不使用生成的程序集),要么调用了不正确的本机版本的方法。我检查了typeof(TestType) 返回的类型的正确(生成)版本。

总而言之,我没有使用任何不安全的代码,所以这不应该发生。

【问题讨论】:

你需要真正的minimal reproducible example 才能让别人看到你的问题......旁注:事实上你有 struct Vect 没有任何值类型字段,因为你大概会感到困惑对 .Net 内部有很好的理解,包括与从字节加载程序集相关的程序集标识问题...... 为什么不把你的结构定义为不安全的结构双坐标[DIMENSION_COUNT]; ?然后你可以把它的地址作为一个 long 或其他东西传递给另一个 AppDomain,只要它存在于同一个进程中,它就可以很好地读取它。 @AlexeiLevenkov 我提供的代码可用于验证问题,并包含重现问题所需的所有代码。虚拟 Vect 类型只需存储坐标并将它们序列化到动态应用程序域。它最初也有一个索引器,但我删除了它以减少此处代码的大小。真正的向量操作发生在动态类型上,它当然有固定数量的 double 字段 (vtyp.DefineField),您可以使用 dynamic 访问这些字段,方法是我现在添加到问题中。 @hoodaticus 我想访问在动态应用程序域中使用Vect 的任何方法或属性,而无需在调用站点解决序列化问题。我也可以将double[] 直接传递给该方法。此外,DIMENSION_COUNT 不能是编译类型常量,因为用户必须能够在运行时更改它。 嗯,我想知道 new double[0] 可能隐藏了什么错误。例外是你的朋友。 【参考方案1】:

我能够在使用最新框架的机器上重现此问题。

我在默认 appdomain 的程序集解析中添加了版本检查:

if (name.Name == "DummyAssembly" && name.Version.Major == 1)

我得到了以下异常:

System.Runtime.Serialization.SerializationException: Cannot find assembly 'DummyAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'



Server stack trace:
   w System.Runtime.Serialization.Formatters.Binary.BinaryAssemblyInfo.GetAssembly()
   w System.Runtime.Serialization.Formatters.Binary.ObjectReader.GetType(BinaryAssemblyInfo assemblyInfo, String name)
   w System.Runtime.Serialization.Formatters.Binary.ObjectMap..ctor(String objectName, String[] memberNames, BinaryTypeEnum[] binaryTypeEnumA, Object[] typeInformationA, Int32[] memberAssemIds, ObjectReader objectReader, Int32 objectId, BinaryAssemblyInfo assemblyInfo, SizedArray assemIdToAssemblyTable)
   w System.Runtime.Serialization.Formatters.Binary.__BinaryParser.ReadObjectWithMapTyped(BinaryObjectWithMapTyped record)
   w System.Runtime.Serialization.Formatters.Binary.__BinaryParser.ReadObjectWithMapTyped(BinaryHeaderEnum binaryHeaderEnum)
   w System.Runtime.Serialization.Formatters.Binary.__BinaryParser.Run()
   w System.Runtime.Serialization.Formatters.Binary.ObjectReader.Deserialize(HeaderHandler handler, __BinaryParser serParser, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage)
   w System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream serializationStream, HeaderHandler handler, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage)
   w System.Runtime.Remoting.Channels.CrossAppDomainSerializer.DeserializeObject(MemoryStream stm)
   w System.Runtime.Remoting.Messaging.SmuggledMethodReturnMessage.FixupForNewAppDomain()
   w System.Runtime.Remoting.Channels.CrossAppDomainSink.SyncProcessMessage(IMessage reqMsg)

Exception rethrown at [0]:
   w System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
   w System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
   w ServerObject.TestMethod1(TestType& result, String v)

这里使用二进制格式化程序进行Marshaling,它从不同的AppDomains中找到不同大小的值类型。请注意,当您调用TestMethod1 时,它会尝试使用版本0.0.0.0 加载您的DummyAssembly,然后您将之前缓存的虚拟版本1.0.0.0 传递给它,其中TestType 具有不同的大小。

由于结构的大小不同,当您从方法中按值返回时,AppDomains 之间的封送处理出现问题并且堆栈变得不平衡(可能是运行时中的错误?)。通过引用返回似乎没有问题(引用的大小始终相同)。

使两个程序集中的结构大小相等/通过引用返回应该可以解决这个问题。

【讨论】:

谢谢。由于我无法限制结构的大小,我可能只能使用引用。

以上是关于跨 AppDomain 调用会破坏运行时的主要内容,如果未能解决你的问题,请参考以下文章

跨 appdomain 的静态类变量

如何识别代码是在 ASP.Net 还是常规 AppDomain 下运行 [重复]

跨 AppDomain 边界的垃圾收集对象

跨AppDomain通信问题

如何从其他 appdomain 获取 UI 线程调度程序?

沙盒 AppDomain 跨程序集异常处理