Адреса пам'яті об'єкта в C #


79

У мене є функція, написана деякий час тому (для .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 (новий автомобіль ("синій"))

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

Я отримую помилку:

Об'єкт містить непримітивні або непрозорі дані.

Чому це вже не працює?

Як я можу зараз отримати адресу пам'яті керованих об'єктів?


17
Я намагаюся отримати адресу об'єкта. Це дуже корисно (і повчально) при визначенні того, чи речі дійсно копіюються (тобто передаються за значенням) чи ні.
lejon

1
Я припускаю (сподіваюся?), Що він намагається зрозуміти, чи клонує його власний код об'єкта.
Слакс

211
"Тому що я хочу знати, як це працює" - досить вагома причина.
Семюель Міхам

14
Я погоджуюся, що "оскільки я хочу знати, як це працює", є достатньо вагомою причиною - крім того - якщо ви намагаєтесь перевірити, що два об'єкти є однаковим екземпляром (на відміну від точних копій), ви можете використовувати object.ReferenceEqualsметод. Це насправді не говорить вам, що таке посилання, але воно повертає логічне значення, яке вказує, вказують два об'єкти на одне і те ж місце кучі чи ні. (Сподіваємось, це комусь допомагає.)
BrainSlugs83

4
GCHandle.ToIntPtrповертає внутрішнє представлення самого дескриптора, а не адресу об'єкта, на який він вказує. Якщо ви створите кілька дескрипторів для одного і того ж об'єкта, GCHandle.ToIntPtrповерне різні результати для кожного дескриптора. Він GCHandle.AddrOfPinnedObjectповертає адресу об’єкта, на який вказує дескриптор. Детальніше див. У розділі GCHandle.ToIntPtr проти GCHandle.AddrOfPinnedObject .
Антоша

Відповіді:


58

Ви можете використовувати GCHandleType.Weak замість Pinned. З іншого боку, є інший спосіб отримати вказівник на об’єкт:

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

Потрібен небезпечний блок і є дуже, дуже небезпечним і не повинен використовуватися взагалі. ☺


У той час, коли місцеві провідники не були можливі в 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
Я вважаю це корисним лише за одним сценарієм: коли я збираюся ініціювати самозвантаження даного процесу, і перед тим, як це зробити, я хочу роздрукувати купу адрес пам'яті для керованих об’єктів, щоб пізніше було простіше проаналізувати файл дампа.
Ендрю Арнотт

Я використовував його для перевірки концепції інтернізації рядків, вона працювала добре!
Малік Халіл,

"дуже, дуже небезпечно". Чи може щось піти не так, якщо ви робите це лише для того, щоб прочитати фактичну адресу об’єкта і десь зберегти його? (наприклад: як унікальний ідентифікатор). Скажімо, ви хочете пройтись по ієрархії об’єктів і хочете, щоб ви відвідували їх лише один раз.
tigrou

@tigrou Колекціонер сміття цілком може втрутитися і перемістити об'єкт в пам'яті, змінивши його адресу. Першим унікальним ідентифікатором об’єкта, над яким слід подумати, є його посилання, тобто просто object. Якщо ви хочете щось числове, як ObjectIDGenerator , так і GCHandle можна використовувати для створення унікального номера для об’єкта.
IllidanS4 підтримує Моніку

Чи не повинен бути третій рядок: IntPtr ptr = *(IntPtr*)&tr??
momt99

21

Замість цього коду вам слід зателефонувати GetHashCode(), який поверне (сподіваємось) унікальне значення для кожного екземпляра.

Ви також можете використовувати ObjectIDGeneratorклас , який гарантовано буде унікальним.


33
GetHashCode не є унікальним. Ідентифікатор об'єкта ідентифікатора унікальний, але він запобігає збору об'єкта.
Eric Lippert

2
@Eric: 1) З практичних цілей я думаю (але ніколи не перевіряв), що це досить добре. 2) Чи це десь задокументовано? Перевірка джерела, це правда.
Слакс

4
Re (1) ну, що практичне, залежить від вирішуваної проблеми; Існує> 1% ймовірності зіткнення лише після 9300 хешей. Щодо (2) це явно не задокументовано, але, як ви зауважуєте, якщо ви використовуєте рефлектор, щоб розібрати реалізацію, ви побачите, що все, що він робить, це вставляє об'єкти в хеш-таблицю. Поки ця хеш-таблиця жива, існують і об’єкти. Документація передбачає, що генератор ідентифікаторів повинен залишатися в живих лише до тих пір, поки працює серіалізація.
Eric Lippert

2
+1 для генератора ідентифікаторів об’єкта, я не знав, що він існує. Хеші погано працюють із великою кількістю об’єктів, які потрібно кешувати, не вперше я зіткнувся б із помилкою подвійного хешкі. Не впевнений, чи коментарі @EricLippert означають, що зіткнення можуть з'являтися також із генератором ідентифікаторів.
Абель

2
Якщо ви перевизначаєте Equals, вам слід також замінити GetHashCode - іноді вам також потрібен хеш на основі посилання, а не хеш на основі рівності. (тобто дві копії одного і того ж об'єкта, у цьому випадку мають дати два різні хеші.)
BrainSlugs83,

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, мабуть, роблять кращу роботу з аналізу життя об'єкта.

Якщо ви дійсно хочете зробити це, ви можете використовувати a, 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 поводяться по-різному. Обидва обчислюють pointerраніше handle.Free().
Джо

@Joh: Здається, спочатку я неправильно прочитав його код. З того, що ми бачимо, схоже, обидва повинні діяти однаково. Цікаво, якщо вони це роблять. Якщо шахта діє по-іншому, мені доведеться задатися питанням, що робить компілятор JIT.
Jim Mischel

4
Це "GCHandle.Alloc (a, GCHandleType.Pinned)", який не вдається виконати.
лейон

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повертає адресу об’єкта, на який вказує дескриптор, однак ви можете використовувати цей метод лише для закріплених дескрипторів.
Антоша

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.