C#中对象的内存地址


78

我前段时间编写了一个函数(针对.NET 3.5),现在我已升级到4.0

我无法正常工作。

该函数是:

public static class MemoryAddress
{
    public static string Get(object a)
    {
        GCHandle handle = GCHandle.Alloc(a, GCHandleType.Pinned);
        IntPtr pointer = GCHandle.ToIntPtr(handle);
        handle.Free();
        return "0x" + pointer.ToString("X");
    }
}

现在,当我调用它时-MemoryAddress.Get(new Car(“ blue”))

public class Car
{
    public string Color;
    public Car(string color)
    {
        Color = color;
    }
}

我得到错误:

对象包含非原始或不可复制的数据。

为什么它不起作用了?

现在如何获取被管理对象的内存地址?


10
为什么呢 你想做什么?
SLaks 2011年

17
我正在尝试获取对象的地址。在确定是否实际复制内容(即按值传递)时,这非常有帮助(并且很有启发性)。
lejon 2011年

208
“因为我想知道它是如何工作的”已经足够了。
塞缪尔·梅亚姆

14
我同意“因为我想知道它是如何工作的”是一个足够好的理由-另外-如果您要尝试验证两个对象是同一实例(相对于精确副本),则可以使用该object.ReferenceEquals方法。它实际上并没有告诉您引用是什么,但是它确实返回一个布尔值,指示两个对象是否指向相同的堆位置。(希望对您
有所

4
GCHandle.ToIntPtr返回句柄本身的内部表示,而不是它指向的对象的地址。如果为同一对象创建多个句柄,GCHandle.ToIntPtr则每个句柄将返回不同的结果。它GCHandle.AddrOfPinnedObject返回句柄所指向的对象的地址。有关更多详细信息,请参见GCHandle.ToIntPtr与GCHandle.AddrOfPinnedObject
安托沙

Answers:


58

您可以使用GCHandleType.Weak代替Pinned。另一方面,还有另一种获取指向对象的指针的方法:

object o = new object();
TypedReference tr = __makeref(o);
IntPtr ptr = **(IntPtr**)(&tr);

要求使用不安全的块,非常非常危险,完全不应使用。☺


早在C#中不可能使用by-ref本地对象的时候,就有一种未记录的机制可以完成类似的任务– __makeref

object o = new object();
ref object r = ref o;
//roughly equivalent to
TypedReference tr = __makeref(o);

TypedReference是“泛型”的,这是一个重要的区别;它可以用于存储对任何类型的变量的引用。访问此类引用需要指定其类型,例如__refvalue(tr, object),如果不匹配,则引发异常。

要实现类型检查,TypedReference必须具有两个字段,一个具有变量的实际地址,一个具有指向其类型表示的指针。碰巧的是地址是第一个字段。

因此,__makeref首先用于获取对该变量的引用o。铸造(IntPtr**)(&tr)对待的结构的阵列(经由一个指针表示)IntPtr*(指针到一个通用的指针类型),通过指向它的指针访问。首先取消对指针的引用以获得第一个字段,然后再次对指针进行解引用以获得实际存储在变量中的值o–指向对象本身的指针。

但是,自2012年以来,我想出了一个更好,更安全的解决方案:

public static class ReferenceHelpers
{
    public static readonly Action<object, Action<IntPtr>> GetPinnedPtr;

    static ReferenceHelpers()
    {
        var dyn = new DynamicMethod("GetPinnedPtr", typeof(void), new[] { typeof(object), typeof(Action<IntPtr>) }, typeof(ReferenceHelpers).Module);
        var il = dyn.GetILGenerator();
        il.DeclareLocal(typeof(object), true);
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Stloc_0);
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Ldloc_0);
        il.Emit(OpCodes.Conv_I);
        il.Emit(OpCodes.Call, typeof(Action<IntPtr>).GetMethod("Invoke"));
        il.Emit(OpCodes.Ret);
        GetPinnedPtr = (Action<object, Action<IntPtr>>)dyn.CreateDelegate(typeof(Action<object, Action<IntPtr>>));
    }
}

这将创建一个动态方法,该方法首先固定对象(因此其存储不会在托管堆中移动),然后执行接收其地址的委托。在执行委托的过程中,该对象仍处于固定状态,因此可以安全地通过指针进行操作:

object o = new object();
ReferenceHelpers.GetPinnedPtr(o, ptr => Console.WriteLine(Marshal.ReadIntPtr(ptr) == typeof(object).TypeHandle.Value)); //the first pointer in the managed object header in .NET points to its run-time type info

这是固定对象的最简单方法,因为GCHandle要求该类型必须是可蓝变的才能固定。它的优点是不使用实现细节,未记录的关键字和内存黑客。


7
我发现这仅在一种情况下很有用:当我要启动流程的自转储时,并且在我想打印之前,我要向托管对象打印一堆内存地址,以便以后分析转储文件更加容易。
Andrew Arnott 2014年

我用它来验证字符串实习的概念,它很好用!
马里克·哈利勒

“非常非常危险”。如果仅读取对象的实际地址并将其存储在某处,是否有可能出错?(例如:作为唯一标识符)。假设您要遍历对象的层次结构,并希望确保仅访问它们一次。
tigrou

@tigrou垃圾回收器很可能会介入并在内存中移动对象,从而更改其地址。您应该考虑使用的对象的第一个唯一标识符是其引用,即简单地object。如果您想要一些数字,ObjectIDGeneratorGCHandle都可以用于生成对象的唯一编号。
IllidanS4支持莫妮卡

第三行不应该是:IntPtr ptr = *(IntPtr*)&tr??
Mohammad Omidvar '18

20

代替此代码,您应该调用GetHashCode(),它将为每个实例返回一个(唯一的)唯一值。

您也可以使用ObjectIDGeneratorclass,它保证是唯一的。


33
GetHashCode不是唯一的。对象ID生成器ID是唯一的,但是它阻止了对象的收集。
埃里克·利珀特

2
@Eric:1)出于实用目的,我认为(但从未检查过)它足够好。2)记录在任何地方吗?检查源,这是真的。
SLaks 2011年

4
关于(1)好,实际的情况取决于要解决的问题;仅9300个散列后,发生碰撞的机率> 1%。关于Re(2),它没有明确记录,但是正如您所注意到的,如果使用反射器分解实现,您将看到它所做的只是将对象粘贴到哈希表中。只要该哈希表是活动的,对象也是如此。该文档暗示,仅在序列化操作运行时,id生成器才应保持活动状态。
埃里克·利珀特

2
对象ID生成器+1,我不知道它是否存在。哈希不能与需要缓存的大量对象配合使用,这并不是我第一次遇到重复的Hashkey错误。不确定@EricLippert注释是否意味着ID生成器也会出现冲突。
亚伯(Abel)

2
如果覆盖Equals,则还必须覆盖GetHashCode -有时,您还需要基于引用的哈希,而不是基于相等的哈希。(即,同一对象的两个副本在这种情况下应给出两个不同的哈希值。)
BrainSlugs83 2014年

15

如果您确实不需要内存地址,而是需要一些方法来唯一标识托管对象,则有更好的解决方案:

using System.Runtime.CompilerServices;

public static class Extensions
{
    private static readonly ConditionalWeakTable<object, RefId> _ids = new ConditionalWeakTable<object, RefId>();

    public static Guid GetRefId<T>(this T obj) where T: class
    {
        if (obj == null)
            return default(Guid);

        return _ids.GetOrCreateValue(obj).Id;
    }

    private class RefId
    {
        public Guid Id { get; } = Guid.NewGuid();
    }
}

这是线程安全的,并且在内部使用弱引用,因此不会有内存泄漏。

您可以使用任何您喜欢的密钥生成方式。我在Guid.NewGuid()这里使用它是因为它简单且线程安全。

更新资料

我继续创建了一个Nuget包Overby.Extensions.Attachments,其中包含一些用于将对象附加到其他对象的扩展方法。有一个扩展名为GetReferenceId(),可以有效地执行此答案中的代码显示的内容。


一个人会这样使用它:System.Guid guid1 = Overby.Extensions.Attachments.AttachmentExtensions.GetReferenceId(myObject);
Al Lelopath

1
或作为扩展方法,如果您很简单。
罗尼·奥弗比

8

当您释放该句柄时,垃圾收集器可以自由移动固定的内存。如果您有一个指向应该固定的内存的指针,并且取消固定了该内存,那么所有的选择都将关闭。这在3.5中完全奏效可能只是运气。JIT编译器和4.0的运行时可能在对象寿命分析方面做得更好。

如果确实要执行此操作,则可以使用atry/finally来防止取消固定对象,直到使用完之后:

public static string Get(object a)
{
    GCHandle handle = GCHandle.Alloc(a, GCHandleType.Pinned);
    try
    {
        IntPtr pointer = GCHandle.ToIntPtr(handle);
        return "0x" + pointer.ToString("X");
    }
    finally
    {
        handle.Free();
    }
}

我同意您关于a取消固定对象后对象可能移动的评论,但是我不认为您的代码示例和OP的行为有所不同。两者都在计算pointer之前handle.Free()
Joh

@Joh:看来我起初看错了他的代码。从我们可以看到,看起来两者应该发挥相同的作用。我想知道他们是否这样做。如果我的行为有所不同,我将不得不怀疑JIT编译器在做什么。
Jim Mischel

4
是执行失败的“ GCHandle.Alloc(a,GCHandleType.Pinned)”。
lejon

4

这是我想到的一种简单方法,它不涉及不安全的代码或固定对象。也可以反向使用(地址中的对象):

public static class AddressHelper
{
    private static object mutualObject;
    private static ObjectReinterpreter reinterpreter;

    static AddressHelper()
    {
        AddressHelper.mutualObject = new object();
        AddressHelper.reinterpreter = new ObjectReinterpreter();
        AddressHelper.reinterpreter.AsObject = new ObjectWrapper();
    }

    public static IntPtr GetAddress(object obj)
    {
        lock (AddressHelper.mutualObject)
        {
            AddressHelper.reinterpreter.AsObject.Object = obj;
            IntPtr address = AddressHelper.reinterpreter.AsIntPtr.Value;
            AddressHelper.reinterpreter.AsObject.Object = null;
            return address;
        }
    }

    public static T GetInstance<T>(IntPtr address)
    {
        lock (AddressHelper.mutualObject)
        {
            AddressHelper.reinterpreter.AsIntPtr.Value = address;
            return (T)AddressHelper.reinterpreter.AsObject.Object;
        }
    }

    // I bet you thought C# was type-safe.
    [StructLayout(LayoutKind.Explicit)]
    private struct ObjectReinterpreter
    {
        [FieldOffset(0)] public ObjectWrapper AsObject;
        [FieldOffset(0)] public IntPtrWrapper AsIntPtr;
    }

    private class ObjectWrapper
    {
        public object Object;
    }

    private class IntPtrWrapper
    {
        public IntPtr Value;
    }
}

那是一个整洁的技巧。
Shimmy Weitzhandler

2

这对我有用...

#region AddressOf

    /// <summary>
    /// Provides the current address of the given object.
    /// </summary>
    /// <param name="obj"></param>
    /// <returns></returns>
    [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
    public static System.IntPtr AddressOf(object obj)
    {
        if (obj == null) return System.IntPtr.Zero;

        System.TypedReference reference = __makeref(obj);

        System.TypedReference* pRef = &reference;

        return (System.IntPtr)pRef; //(&pRef)
    }

    /// <summary>
    /// Provides the current address of the given element
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="t"></param>
    /// <returns></returns>
    [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
    public static System.IntPtr AddressOf<T>(T t)
        //refember ReferenceTypes are references to the CLRHeader
        //where TOriginal : struct
    {
        System.TypedReference reference = __makeref(t);

        return *(System.IntPtr*)(&reference);
    }

    [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
    static System.IntPtr AddressOfRef<T>(ref T t)
    //refember ReferenceTypes are references to the CLRHeader
    //where TOriginal : struct
    {
        System.TypedReference reference = __makeref(t);

        System.TypedReference* pRef = &reference;

        return (System.IntPtr)pRef; //(&pRef)
    }

    /// <summary>
    /// Returns the unmanaged address of the given array.
    /// </summary>
    /// <param name="array"></param>
    /// <returns><see cref="IntPtr.Zero"/> if null, otherwise the address of the array</returns>
    [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
    public static System.IntPtr AddressOfByteArray(byte[] array)
    {
        if (array == null) return System.IntPtr.Zero;

        fixed (byte* ptr = array)
            return (System.IntPtr)(ptr - 2 * sizeof(void*)); //Todo staticaly determine size of void?
    }

    #endregion

2
AddressOf(object obj)在您的代码中,返回堆栈分配TypedReference的地址,而不是对象地址。要获得对象地址,您必须按照此答案两次取消引用。
安托沙

0

切换分配类型:

GCHandle handle = GCHandle.Alloc(a, GCHandleType.Normal);

如果他切换GCHandleType,则将无法使用ToIntPtr方法。
安全猎犬

这是否不会更改我不希望其移动对象的GC指令?(但是更改代码确实可以编译,并且在运行时不会出错。)
lejon

1
您仍然可以在非固定对象上调用ToIntPtr()。但是它返回的ptr仅在.NET运行时内确实有效。它不再表示您可以直接写入的物理地址,而是在CLR中可用作对象的“引用”
Matt Warren

对于GCHandle固定或非固定的所有类型,GCHandle.ToIntPtr返回句柄本身的内部表示,而不是其指向的对象的地址。如果为同一对象创建多个句柄,GCHandle.ToIntPtr则每个句柄将返回不同的结果。它GCHandle.AddrOfPinnedObject返回句柄所指向的对象的地址,但是,您可以仅将该方法用于固定句柄。
安托沙

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.