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 (new Car ( "blue"))

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

오류가 발생합니다.

객체에 원시 데이터가 아니거나 블리트 할 수없는 데이터가 있습니다.

왜 더 이상 작동하지 않습니까?

이제 관리 대상 개체의 메모리 주소를 어떻게 얻을 수 있습니까?


17
개체의 주소를 얻으려고합니다. 내용이 실제로 복사되고 있는지 (즉, 값으로 전달되는지) 여부를 결정할 때 매우 유용하고 유익합니다.
lejon

1
나는 그가 자신의 코드가 객체를 복제하고 있는지 확인하려고 노력하고 있다고 가정합니다.
SLaks

211
"어떻게 작동하는지 알고 싶기 때문에"는 충분한 이유입니다.
Samuel Meacham 2011

14
"작동 방식을 알고 싶기 때문에"가 충분한 이유라는 데 동의합니다. 또한 두 개체가 동일한 인스턴스 (정확한 복사본과 비교)인지 확인하려는 경우이 object.ReferenceEquals방법을 사용할 수 있습니다 . 실제로 참조가 무엇인지 알려주지는 않지만 두 개체가 동일한 힙 위치를 가리키는 지 여부를 나타내는 부울을 반환합니다. (희망 누군가를하는 데 도움이.)
BrainSlugs83

4
GCHandle.ToIntPtr가리키는 객체의 주소가 아니라 핸들 자체의 내부 표현을 반환합니다. 동일한 개체에 대해 여러 개의 핸들을 만들면 GCHandle.ToIntPtr에서 각 핸들에 대해 다른 결과를 반환합니다. 그것은이다 GCHandle.AddrOfPinnedObject그 반환 핸들 점 객체의 주소를 입력합니다. 자세한 내용은 GCHandle.ToIntPtr 대 GCHandle.AddrOfPinnedObject 를 참조하십시오.
Antosha

답변:


58

Pinned 대신 GCHandleType.Weak을 사용할 수 있습니다. 반면에 객체에 대한 포인터를 얻는 또 다른 방법이 있습니다.

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 가 "generic" 이라는 점에는 한 가지 중요한 차이점이 있습니다 . 모든 유형의 변수에 대한 참조를 저장하는 데 사용할 수 있습니다. 이러한 참조에 액세스하려면 해당 유형 (예 :)을 지정해야하며 __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 은 개체 를 고정하기 위해 유형을 blittable로 설정해야하므로 개체를 고정하는 가장 쉬운 방법 입니다. 구현 세부 사항, 문서화되지 않은 키워드 및 메모리 해킹을 사용하지 않는 이점이 있습니다.


7
이 방법은 한 가지 시나리오에서만 유용합니다. 프로세스의 자체 덤프를 시작하려고 할 때와 그 전에 관리 개체에 메모리 주소를 인쇄하여 나중에 덤프 파일을 더 쉽게 분석 할 수 있도록합니다.
Andrew Arnott 2014

나는 끈 인턴의 개념을 검증하는 데 사용했는데 잘 작동했습니다!
Malik Khalil

"매우, 매우 위험하다". 객체의 실제 주소를 읽고 어딘가에 저장하기 위해서만 이렇게하면 잘못 될 수있는 것이 있습니까? (예 : 고유 식별자). 개체의 계층 구조를 걷고 싶고 한 번만 방문하도록하고 싶다고 가정 해 보겠습니다.
tigrou

@tigrou 가비지 수집기는 메모리에서 개체를 이동하여 주소를 변경할 수 있습니다. 사용에 대해 생각해야하는 객체의 첫 번째 고유 식별자는 참조, 즉 단순히 object. 숫자가 필요한 경우 ObjectIDGeneratorGCHandle 을 모두 사용하여 개체의 고유 번호를 생성 할 수 있습니다.
IllidanS4 모니카 지원

세 번째 줄은 안됩니다 : IntPtr ptr = *(IntPtr*)&tr??
momt99

21

이 코드 대신을 호출해야합니다 GetHashCode(). 그러면 각 인스턴스에 대해 고유 한 값이 반환됩니다.

고유 한 ObjectIDGenerator클래스를 사용할 수도 있습니다 .


33
GetHashCode는 고유하지 않습니다. 개체 ID 생성기 ID는 고유하지만 개체가 수집되지 않도록합니다.
Eric Lippert

2
@Eric : 1) 실질적인 목적으로 충분하다고 생각합니다 (하지만 확인하지 않았습니다). 2) 문서화되어 있습니까? 소스를 확인하면 사실입니다.
SLaks

4
Re (1) 글쎄요, 실제적인 것은 해결되는 문제에 달려 있습니다. 9300 회 해시 후에 충돌 가능성이 1 %를 초과합니다. Re (2) 명시 적으로 문서화되어 있지는 않지만 리플렉터를 사용하여 구현을 분리하면 객체를 해시 테이블에 고정하는 것뿐임을 알 수 있습니다. 해시 테이블이 살아있는 한 객체도 살아 있습니다. 문서는 직렬화 작업이 실행되는 동안에 만 id 생성기가 살아 있어야 함을 의미합니다.
Eric Lippert

2
개체 ID 생성기에 +1했지만 존재하는지 몰랐습니다. 해시는 캐시해야하는 많은 양의 개체에서 제대로 작동하지 않으며 중복 Hashkey 오류가 발생한 것은 이번이 처음이 아닙니다. @EricLippert 주석이 충돌이 ID 생성기와 함께 나타날 수 있음을 의미하는지 확실하지 않습니다.
Abel

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
또는 전체 간결함을 알고 있다면 확장 방법으로.
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가 다르게 작동한다고 생각하지 않습니다. 둘 다 pointer전에 계산 handle.Free()합니다.

@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에서 객체 주소가 아닌 스택 할당 주소를 반환합니다 . 객체 주소를 얻으려면이 답변 에서처럼 두 번 역 참조해야합니다 .
Antosha

0

할당 유형을 전환합니다.

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

GCHandleType을 전환하면 ToIntPtr 메서드를 사용할 수 없습니다.
Security Hound

이것이 내가 객체를 움직이지 않기를 바라는 GC에 대한 지시를 변경하지 않습니까? (하지만 코드를 변경해도 컴파일이되고 런타임 중에 오류가 발생하지 않습니다.)
lejon

1
고정되지 않은 개체에서 ToIntPtr ()을 계속 호출 할 수 있습니다. 그러나 반환되는 ptr은 .NET 런타임 내에서만 실제로 유효합니다. 더 이상 직접 쓸 수있는 실제 주소를 나타내지 않지만 CLR 내에서 개체에 대한 "참조"로 사용할 수 있습니다
Matt Warren

GCHandle고정 또는 고정되지 않은 모든 유형 GCHandle.ToIntPtr에 대해 핸들이 가리키는 개체의 주소가 아닌 핸들 자체의 내부 표현을 반환합니다. 동일한 개체에 대해 여러 개의 핸들을 만들면 GCHandle.ToIntPtr에서 각 핸들에 대해 다른 결과를 반환합니다. 그것은이다 GCHandle.AddrOfPinnedObject반환 핸들 점 객체의 주소가, 그러나, 당신은 고정 된 핸들 만이 방법을 사용할 수 있음.
Antosha

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.