Speicheradresse eines Objekts in C #

78

Ich habe vor einiger Zeit eine Funktion geschrieben (für .NET 3.5) und jetzt, da ich auf 4.0 aktualisiert habe

Ich kann es nicht zum Laufen bringen.

Die Funktion ist:

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");
    }
}

Nun, wenn ich es nenne - MemoryAddress.Get (neues Auto ("blau"))

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

Ich bekomme den Fehler:

Das Objekt enthält nicht primitive oder nicht blittbare Daten.

Warum funktioniert es nicht mehr?

Wie kann ich jetzt die Speicheradresse von verwalteten Objekten erhalten?

Lejon
quelle
10
Warum ? Was versuchst du zu machen?
SLaks
17
Ich versuche, die Adresse des Objekts zu erhalten. Es ist sehr hilfreich (und lehrreich), um festzustellen, ob tatsächlich Material kopiert wird (dh als Wert übergeben wird) oder nicht.
Lejon
208
"Weil ich wissen will, wie es funktioniert" ist ein guter Grund.
Samuel Meacham
14
Ich bin damit einverstanden, dass "weil ich wissen möchte, wie es funktioniert" ein guter Grund ist - zusätzlich - wenn Sie versuchen zu überprüfen, ob zwei Objekte dieselbe Instanz sind (im Vergleich zu exakten Kopien), können Sie die object.ReferenceEqualsMethode verwenden. Es sagt Ihnen nicht wirklich, was die Referenz ist, aber es gibt einen Booleschen Wert zurück, der angibt, ob die beiden Objekte auf denselben Heap-Speicherort verweisen oder nicht. (Hoffentlich hilft das jemandem.)
BrainSlugs83
4
GCHandle.ToIntPtrGibt eine interne Darstellung des Handles selbst zurück, nicht die Adresse des Objekts, auf das es zeigt. Wenn Sie mehrere Handles für dasselbe Objekt erstellen, GCHandle.ToIntPtrwerden für jedes Handle unterschiedliche Ergebnisse zurückgegeben. Es GCHandle.AddrOfPinnedObjectgibt die Adresse des Objekts zurück, auf das das Handle zeigt. Weitere Informationen finden Sie unter GCHandle.ToIntPtr vs. GCHandle.AddrOfPinnedObject .
Antosha

Antworten:

58

Sie können GCHandleType.Weak anstelle von Pinned verwenden. Auf der anderen Seite gibt es eine andere Möglichkeit, einen Zeiger auf ein Objekt zu erhalten:

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

Benötigt einen unsicheren Block und ist sehr, sehr gefährlich und sollte überhaupt nicht verwendet werden. ☺


Damals, als in C # keine Einheimischen mit Ref-Funktion möglich waren, gab es einen undokumentierten Mechanismus, der etwas Ähnliches erreichen konnte - __makeref.

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

Es gibt einen wichtigen Unterschied darin, dass TypedReference "generisch" ist. Es kann verwendet werden, um einen Verweis auf eine Variable eines beliebigen Typs zu speichern. Für den Zugriff auf eine solche Referenz muss deren Typ angegeben werden, z. B. __refvalue(tr, object)wenn sie nicht übereinstimmt, wird eine Ausnahme ausgelöst.

Um die Typprüfung zu implementieren, muss TypedReference zwei Felder haben, eines mit der tatsächlichen Adresse der Variablen und eines mit einem Zeiger auf die Typendarstellung . Es ist einfach so, dass die Adresse das erste Feld ist.

Daher __makerefwird zuerst verwendet, um eine Referenz auf die Variable zu erhalten o. Die Umwandlung (IntPtr**)(&tr)behandelt die Struktur als ein Array (dargestellt durch einen Zeiger) von IntPtr*(Zeiger auf einen generischen Zeigertyp), auf das über einen Zeiger zugegriffen wird. Der Zeiger wird zuerst dereferenziert, um das erste Feld zu erhalten, und der Zeiger dort wird erneut dereferenziert, um den tatsächlich in der Variablen gespeicherten Wert zu erhalten o- den Zeiger auf das Objekt selbst.

Jedoch , seit 2012, ich habe eine bessere und sicherere Lösung zu kommen:

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

Dadurch wird eine dynamische Methode erstellt, die zuerst das Objekt fixiert (damit sich sein Speicher nicht im verwalteten Heap verschiebt) und dann einen Delegaten ausführt, der seine Adresse erhält. Während der Ausführung des Delegaten bleibt das Objekt fixiert und kann somit sicher über den Zeiger bearbeitet werden:

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

Dies ist der einfachste Weg, ein Objekt zu pinnen , da GCHandle erfordert, dass der Typ blittbar ist, um es zu pinnen. Es hat den Vorteil, dass keine Implementierungsdetails, nicht dokumentierte Schlüsselwörter und Speicherhacking verwendet werden.

IllidanS4 unterstützt Monica
quelle
7
Ich finde dies in nur einem Szenario nützlich: Wenn ich einen Selbstspeicherauszug des Prozesses initiieren möchte und bevor ich dies tue, möchte ich eine Reihe von Speicheradressen an verwaltete Objekte ausdrucken, um die Analyse der Speicherauszugsdatei später zu vereinfachen.
Andrew Arnott
Ich habe es zur Validierung des Konzepts der String-Internierung verwendet, es hat gut funktioniert!
Malik Khalil
"ist sehr, sehr gefährlich". Gibt es etwas, das schief gehen könnte, wenn Sie dies nur tun, um die tatsächliche Adresse des Objekts zu lesen und irgendwo zu speichern? (zB: als eindeutige Kennung). Angenommen, Sie möchten eine Hierarchie von Objekten durchlaufen und sicherstellen, dass Sie sie nur einmal besuchen.
Tigrou
@tigrou Der Garbage Collector könnte möglicherweise eingreifen und das Objekt im Speicher verschieben und seine Adresse ändern. Die erste eindeutige Kennung eines Objekts, über die Sie nachdenken sollten, ist seine Referenz, dh einfach object. Wenn Sie etwas Numerisches möchten, können sowohl ObjectIDGenerator als auch GCHandle verwendet werden, um eine eindeutige Nummer für ein Objekt zu erstellen.
IllidanS4 unterstützt Monica
Sollte die dritte Zeile nicht lauten: IntPtr ptr = *(IntPtr*)&tr??
Mohammad Omidvar
20

Anstelle dieses Codes sollten Sie aufrufen GetHashCode(), der für jede Instanz einen (hoffentlich) eindeutigen Wert zurückgibt.

Sie können auch die ObjectIDGeneratorKlasse verwenden , die garantiert eindeutig ist.

SLaks
quelle
33
GetHashCode ist nicht eindeutig. Die Objekt-ID-Generator-ID ist eindeutig, verhindert jedoch, dass das Objekt erfasst wird.
Eric Lippert
2
@Eric: 1) Aus praktischen Gründen denke ich (aber nie überprüft), dass es gut genug ist. 2) Ist das irgendwo dokumentiert? Überprüfen Sie die Quelle, es ist wahr.
SLaks
4
Zu (1) Nun, was praktisch ist, hängt von dem zu lösenden Problem ab. Nach nur 9300 Hashes besteht eine Kollisionswahrscheinlichkeit von> 1%. Zu (2) ist es nicht explizit dokumentiert, aber wie Sie bemerken, wenn Sie Reflektor verwenden, um die Implementierung zu zerlegen, werden Sie sehen, dass alles, was es tut, die Objekte in eine Hash-Tabelle steckt. Solange diese Hash-Tabelle aktiv ist, sind es auch die Objekte. Die Dokumentation impliziert, dass der ID-Generator nur so lange am Leben bleiben sollte, wie der Serialisierungsvorgang ausgeführt wird.
Eric Lippert
2
+1 für den Objekt-ID-Generator, ich wusste nicht, dass er existiert. Hashes funktionieren nicht gut mit großen Mengen von Objekten, die zwischengespeichert werden müssen. Es wäre nicht das erste Mal, dass ich auf einen doppelten Hashkey-Fehler stoße. Nicht sicher, ob @ EricLippert-Kommentare bedeuten, dass Kollisionen auch mit dem ID-Generator auftreten können.
Abel
2
Wenn Sie Equals überschreiben, müssen Sie auch GetHashCode überschreiben. Manchmal benötigen Sie auch einen referenzbasierten Hash und keinen gleichheitsbasierten Hash. (dh zwei Kopien desselben Objekts sollten in diesem Fall zwei verschiedene Hashes ergeben.)
BrainSlugs83
15

Es gibt eine bessere Lösung, wenn Sie die Speicheradresse nicht wirklich benötigen, sondern ein verwaltetes Objekt eindeutig identifizieren können:

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();
    }
}

Dies ist threadsicher und verwendet intern schwache Referenzen, sodass keine Speicherlecks auftreten.

Sie können jedes beliebige Schlüsselgenerierungsmittel verwenden. Ich benutze Guid.NewGuid()hier, weil es einfach und threadsicher ist.

Aktualisieren

Ich habe ein Nuget-Paket Overby.Extensions.Attachments erstellt , das einige Erweiterungsmethoden zum Anhängen von Objekten an andere Objekte enthält. Es gibt eine Erweiterung namens GetReferenceId(), die effektiv das tut, was der Code in dieser Antwort zeigt.

Ronnie Overby
quelle
Man würde es so benutzen:System.Guid guid1 = Overby.Extensions.Attachments.AttachmentExtensions.GetReferenceId(myObject);
Al Lelopath
1
Oder als Erweiterungsmethode, wenn Sie sich für die ganze Kürze interessieren.
Ronnie Overby
8

Wenn Sie dieses Handle freigeben, kann der Garbage Collector den angehefteten Speicher verschieben. Wenn Sie einen Zeiger auf den Speicher haben, der fixiert werden soll, und diesen Speicher entfernen, sind alle Wetten deaktiviert. Dass dies in 3.5 überhaupt funktionierte, war wahrscheinlich nur ein Glücksfall. Der JIT-Compiler und die Laufzeit für 4.0 machen die Analyse der Objektlebensdauer wahrscheinlich besser.

Wenn Sie dies wirklich tun möchten, können Sie a verwenden try/finally, um zu verhindern, dass das Objekt erst nach der Verwendung entfernt wird:

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();
    }
}
Jim Mischel
quelle
Ich stimme Ihrem Kommentar zu der Möglichkeit zu, dass ein Objekt averschoben werden kann, nachdem es nicht fixiert wurde, aber ich denke nicht, dass sich Ihr Codebeispiel und die OPs unterschiedlich verhalten. Beide rechnen pointervorher handle.Free().
Joh
@Joh: Es scheint, dass ich seinen Code zuerst falsch gelesen habe. Nach allem, was wir sehen können, sollten beide gleich handeln. Ich frage mich, ob sie es tun. Wenn sich mein Verhalten anders verhält, muss ich mich fragen, was der JIT-Compiler tut.
Jim Mischel
4
Es ist das "GCHandle.Alloc (a, GCHandleType.Pinned)", das nicht ausgeführt werden kann.
Lejon
3

Hier ist ein einfacher Weg, den ich mir ausgedacht habe, bei dem es nicht um unsicheren Code oder das Fixieren des Objekts geht. Funktioniert auch umgekehrt (Objekt von Adresse):

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;
    }
}
MulleDK19
quelle
Das ist ein ordentlicher Hack.
Shimmy Weitzhandler
2

Das funktioniert bei mir ...

#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
Jay
quelle
2
AddressOf(object obj)In Ihrem Code wird die Adresse des zugewiesenen Stapels zurückgegeben TypedReference, nicht die Objektadresse. Um die Objektadresse zu erhalten, müssen Sie wie in dieser Antwort zweimal dereferenzieren .
Antosha
0

Wechseln Sie den Zuordnungstyp:

GCHandle handle = GCHandle.Alloc(a, GCHandleType.Normal);
Will Charczuk
quelle
Wenn er den GCHandleType wechselt, kann er die ToIntPtr-Methode nicht verwenden.
Sicherheitshund
Ändert dies nicht die Anweisung an den GC, dass das Objekt nicht verschoben werden soll? (Das Ändern des Codes wird jedoch kompiliert und gibt zur Laufzeit keine Fehler.)
Lejon
1
Sie können ToIntPtr () weiterhin für ein nicht fixiertes Objekt aufrufen. Der zurückgegebene ptr ist jedoch nur innerhalb der .NET-Laufzeit wirklich gültig. Es stellt keine pyhsische Adresse mehr dar, an die Sie direkt schreiben können, aber innerhalb der CLR kann es als "Referenz" auf ein Objekt verwendet werden
Matt Warren
GCHandleGibt für alle angehefteten oder nicht angehefteten Typen GCHandle.ToIntPtreine interne Darstellung des Handles selbst zurück, nicht die Adresse des Objekts, auf das es zeigt. Wenn Sie mehrere Handles für dasselbe Objekt erstellen, GCHandle.ToIntPtrwerden für jedes Handle unterschiedliche Ergebnisse zurückgegeben. Es GCHandle.AddrOfPinnedObjectgibt die Adresse des Objekts zurück, auf das das Handle verweist. Sie können diese Methode jedoch nur für angeheftete Handles verwenden.
Antosha
-4

Das Abrufen der Adresse eines beliebigen Objekts in .NET ist nicht möglich, kann jedoch erfolgen, wenn Sie den Quellcode ändern und Mono verwenden. Siehe Anweisungen hier: Speicheradresse des .NET-Objekts abrufen (C #)

entwickelte Mikrorobe
quelle
2
Nicht üblich ja, nicht möglich ... nein ... siehe oben
Jay