C#中对象的内存地址

98

我之前写了一个函数(用于 .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;
    }
}

我遇到了这个错误:

对象包含非原始或非可固定数据。

为什么它不再起作用了?

现在我如何获取托管对象的内存地址?


17
我正试图获取对象的地址。这对于确定是否实际上正在进行复制(即按值传递)非常有帮助(并具有指导意义)。 - lejon
1
我假设(希望?)他是想看看自己的代码是否在克隆对象。 - SLaks
227
“因为我想知道它是如何工作的”是一个足够好的理由。 - Samuel Meacham
16
我同意"因为我想知道它是如何工作的"是一个足够好的理由。此外,如果你想验证两个对象是否是同一实例(而不是完全相同的副本),你可以使用object.ReferenceEquals方法。它并不会告诉你引用是什么,但它会返回一个布尔值,指示这两个对象是否指向同一堆位置。(希望这能帮助到某些人。) - BrainSlugs83
6
GCHandle.ToIntPtr返回的是句柄本身的内部表示,而不是它所指向对象的地址。如果你为同一个对象创建多个句柄,GCHandle.ToIntPtr将为每个句柄返回不同的结果。GCHandle.AddrOfPinnedObject返回的是句柄所指向对象的地址。有关更多详细信息,请参见GCHandle.ToIntPtr vs. GCHandle.AddrOfPinnedObject - Antosha
请注意:对于任何想查看地址以确保理解底层操作的人,请观看这两个视频,而不必费力思考。 (VIDEO 1: youtube.com/watch?v=h6aXzd1nTXQ)(VIDEO 2: youtube.com/watch?v=mvieNUe9Urs)。这为我提供了我想知道的一切。我来自C ++背景,当无法自由地使用监视窗口查看内存地址时变得非常沮丧。在讨论内存时,具有关于后台正在发生的事情的视觉理解非常有用。 - Fractal
9个回答

64

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

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

需要使用unsafe块,非常非常危险,不应该使用。☺


在C#中无法使用按引用传递的本地变量之前,有一种未记录的机制可以实现类似的功能 - __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
我用它来验证字符串内部化的概念,效果很好! - Malik Khalil
“非常,非常危险”。如果您只是读取对象的实际地址并将其存储在某个地方(例如:作为唯一标识符),是否有任何可能出错的事情?假设您想要遍历对象的层次结构,并确保您只访问它们一次。 - tigrou
@cdiggins 谢谢。我终于编辑了答案,包括更多细节和更好的获取地址的方法。 - IS4
下次有人告诉我C#比C或C++更好(经典的“我讨厌指针”),我会展示这个答案(它非常聪明,别误解,只是相对于那些简单的'&'和'*'来说,这是C#的方式...) - Olivier Pons
显示剩余7条评论

29

不要使用这段代码,你应该调用GetHashCode()方法,它将为每个实例返回一个(希望是)唯一的值。

你还可以使用ObjectIDGenerator,它保证返回唯一的值。


42
GetHashCode 不是唯一的。对象 ID 生成器是唯一的,但它会防止对象被回收。 - Eric Lippert
4
  1. 对于实际目的而言,我认为(但从未核实过)这已经足够好了。
  2. 这个记录在哪里了吗?查看源代码后,它是真实的。
- SLaks
7
Re (1) 好的,实际情况取决于所解决的问题;进行9300次哈希之后,发生碰撞的概率超过1%。Re (2) 尽管没有明确记录,但正如你所指出的,如果使用反射器来分解实现,你会看到它只是将对象放入哈希表中。只要哈希表还在,对象也将存在。文档暗示id生成器应该仅在序列化操作运行时保持活动状态。 - Eric Lippert
3
+1 用于对象 ID 生成器,我不知道它的存在。哈希在需要缓存大量对象时效果不佳,这不是我第一次遇到重复哈希键错误。不确定 @EricLippert 的评论是否意味着 ID 生成器也可能出现冲突。 - Abel
2
如果你重写了Equals方法,你也必须重写GetHashCode方法 -- 有时候你需要基于引用的哈希而不是基于相等的哈希。(例如:在这种情况下,两个相同对象的副本应该给出两个不同的哈希值。) - BrainSlugs83
显示剩余2条评论

20
如果您不需要内存地址而是需要一种唯一标识托管对象的方法,那么有一个更好的解决方案:
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
或者作为扩展方法,如果你喜欢简洁的风格。 - Ronnie Overby

8
当您释放该句柄时,垃圾回收器可以移动被固定的内存。如果您有一个指向应该被固定的内存的指针,并且取消固定该内存,则所有赌注都将失效。在3.5中这样做能够工作可能只是侥幸。JIT编译器和4.0运行时可能更好地执行对象生命周期分析。
如果您真的想这样做,您可以使用try/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的行为没有区别。两者都在handle.Free()之前计算pointer - Joh
@Joh:看起来我一开始误读了他的代码。从我们所看到的来看,它们两个看起来应该是一样的。我想知道它们是否真的一样。如果我的代码表现不同,我会想知道JIT编译器在做什么。 - Jim Mischel
5
执行失败的是"GCHandle.Alloc(a, GCHandleType.Pinned)"。 - lejon

8

我想分享一个简单方法,不需要使用不安全的代码或pin object。它也可以反过来使用(从地址获取object):

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;
            T obj = (T)AddressHelper.reinterpreter.AsObject.Object;
            AddressHelper.reinterpreter.AsObject.Object = null;
            return obj;
        }
    }

    // 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;
    }
}

1
那是个很棒的技巧。 - Shimmy Weitzhandler
GetInstance<T>中您存在内存泄漏。在返回实例之前,您应该使用类似于“reinterpreter.AsObject.Object = null”的语句将对象设置为null。否则,这个静态类将一直保留对您所检索的最后一个实例的引用。 - Sjoerd222888

3

这对我来说很有效...

#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 的地址,而不是对象地址。要获取对象地址,你需要进行两次解引用,就像这个答案中所示。 - Antosha

0

切换分配类型:

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

如果他切换GCHandleType,他将无法使用ToIntPtr方法。 - Security Hound
这难道不会改变对GC的指令,告诉它不要移动对象吗?(修改代码确实可以编译,并且在运行时没有出现错误。) - lejon
1
您仍然可以在未固定的对象上调用ToIntPtr()。但是它返回的指针只在.NET运行时内真正有效。它不再表示您可以直接写入的物理地址,但在CLR中,它可以用作对对象的“引用”。 - Matt Warren
对于所有的 GCHandle 类型,无论是固定的还是非固定的,GCHandle.ToIntPtr 都返回句柄本身的内部表示,而不是它所指向的对象的地址。如果您为同一对象创建多个句柄,则 GCHandle.ToIntPtr 将为每个句柄返回不同的结果。然而,GCHandle.AddrOfPinnedObject 返回句柄所指向的对象的地址,但是,您只能将该方法用于固定的句柄。 - Antosha

0
对于 .net core
public static string Get(object a)
{
    var ptr = Unsafe.As<object, IntPtr>(ref a);
    return $"0x{ptr:X}";
}

感谢您对Stack Overflow社区的贡献。这可能是一个正确的答案,但如果您能提供代码的额外解释,那将非常有帮助,这样开发人员就能理解您的思路。对于那些对语法不太熟悉或难以理解概念的新开发人员来说,这尤其有用。您是否可以编辑您的答案,以便为社区的利益提供更多细节? - undefined

-4

2
不是通常的“是”,也不是不可能的“否”……请参见上文。 - Jay

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接