habrahabr

Снимаем дамп объектов с памяти .Net приложения

  • пятница, 9 января 2015 г. в 02:10:55
http://habrahabr.ru/company/luxoft/blog/247447/

Продолжаем тему интересного на .Net, от чего мир Java будет посмеиваться (хотя у них это также возможно сделать), а приверженцы С++ говорить: «чего они только не сделают чтобы не учить C++».

В данной заметке мы напишем по сути – простенькое ядрышко профилировщика памяти для платформы .Net, который будет снимать дамп с SOH кучи (а в перспективе и с LOH).

Для написания статьи нам понадобится код из статьи Получение указателя на объект .Net и Ручное клонирование потока (измерение размера объектов).

Наши цели на сегодня:
  • Научиться итерировать кучу .Net
  • Научиться находить начало кучи .Net
  • Попробовать сытерировать все объекты чужого домена.


Ссылка на проект в GitHub: DotNetEx


Как и в прошлый раз, будем решать проблемы по мере их поступления:
  • Найдем начало кучи в .Net

    Как мы, наверное, знаем, в .Net существует два вида куч. Это куча для маленьких объектов и куча для больших объектов (> 85K). Они отличаются прежде всего организацией объектов внутри себя. Если в SOH объекты выделяются друг за другом, то в LOH все основано на связанных списках и таблице свободных промежутков между занятыми участками. Это то, что мы знаем. Но правда заключается в другом. Во-первых SOH не может быть непрерывной ввиду возможности наткнуться на занятый участок виртуальной памяти при ее расширении. Второе – на самом деле то, что объекты выделяются непрерывно друг за другом – красивое допущение, поскольку есть pinned объекты, которые нельзя двигать, а значит при сжатии кучи возникнут пустые промежутки. А это значит что SOH также содержит таблицу пустот. И по итогу это должно означать конкретно для нас что: (а) в памяти может быть несколько зон виртуальной памяти, которые выделены под кучу, (б) вероятнее всего они выделены через VirtualAlloc, (в) объекты находятся не непрерывно, а значит наш алгоритм не может на это полагаться.

    Для упрощения примера, давайте будем снимать дамп только с объектов своей кучи. Для того чтобы это сделать, мы возмем указатель на любой объект и при помощи WinApi функции VirtualQuery попробуем получить регион виртуальной памяти, которая была выделена под эту кучу
           public static void GetManagedHeap(out IntPtr heapsOffset, out IntPtr lastHeapByte)
              {
                // получаем указатель на любой объект. Для этого просто выделим его 
                var offset = EntityPtr.ToPointer(new object());
    
                var memoryBasicInformation = new WinApi.MEMORY_BASIC_INFORMATION();
    
                unsafe
                {
                    WinApi.VirtualQuery(offset, ref memoryBasicInformation, 
                                                (IntPtr)Marshal.SizeOf(memoryBasicInformation));
                    heapsOffset = (IntPtr)memoryBasicInformation.AllocationBase;
                    lastHeapByte = (IntPtr)((long)offset + (long)memoryBasicInformation.RegionSize);
                }
              }
    

    Прекрасно! Теперь мы знаем участок памяти, на котором искать объекты.
  • Сытерируем все объекты чужого домена
    Для того чтобы это сделать, нам необходимо понять как их распознать. Так вот если мы посидим с отладчиком и поизучаем память, то можно в итоге прийти к выводу, что ссылка на объект указывает не на первое поле объекта, а на указатель на таблицу виртуальных методов. Которая на самом деле не только таблица виртуальных методов, а методов вообще и описания типа. Ведь мы не в C++ мире, а в мире .Net, где по каждому объекту можно понять, «чей будешь?». А как же SyncBlockIndex, просите вы? А он, как выяснилось, находится «перед» объектом, за вычетом размера слова (4 байта на 32-х и 8 — на 64-х разрядной системе). Потому структура заголовка любого объекта выглядит следующим образом:
    	[StructLayout(LayoutKind.Explicit)]
    	public unsafe struct EntityInfo
    	{
    		[FieldOffset(0)]
    		public int SyncBlockIndex;
    
    		[FieldOffset(4)]
    		public MethodTableInfo *MethodTable;				
    	}
    	
    	[StructLayout(LayoutKind.Explicit)]
    	public struct RefTypeInfo
    	{
    		[FieldOffset(0)]
    		public EntityInfo BasicInfo;
    		
    		[FieldOffset(8)]
    		public byte fieldsStart;
    	}
    

    Далее посмотрим, как выглядит MethodTableInfo:
    	[StructLayout(LayoutKind.Explicit)]
    	public unsafe struct MethodTableInfo
    	{
    		#region Basic Type Info
    		
    		[FieldOffset(0)]
    		public MethodTableFlags Flags;
    
    		[FieldOffset(4)]
    		public int Size;
    		
    		[FieldOffset(8)]
    		public short AdditionalFlags;
    		
    		[FieldOffset(10)]
    		public short MethodsCount;
    		
    		[FieldOffset(12)]
    		public short VirtMethodsCount;
    		
    		[FieldOffset(14)]
    		public short InterfacesCount;
    		
    		[FieldOffset(16)]
    		public MethodTableInfo *ParentTable;
    		
    		#endregion
    		
    		[FieldOffset(20)]
    		public ObjectTypeInfo *ModuleInfo;		
    		
    		[FieldOffset(24)]
    		public ObjectTypeInfo *EEClass;
    	}
    

    Здесь — только часть всей информации с этой структуры. На самом деле она более обширная. Нам тут важно поле EEClass, которое ведет на структуру описания типа. Его я практически не изучал. Выглядит содержимое примерно так:
        [StructLayout(LayoutKind.Explicit)]
    	public unsafe struct ObjectTypeInfo
    	{		
            [FieldOffset(0)]
    	    public ObjectTypeInfo *ParentClass;
            
            [FieldOffset(16)]
    	    public MethodTableInfo *MethodsTable;
    	}
    
    

    Поскольку для нас важно только поле MethodsTable, я только его и нашел. Остальное — пропустил. Зачем оно нам? Это — обратная ссылка на MethodsTable, который своим полем EEClass ссылается сюда.

    Таким вот не хитрым образом мы нашли несколько не точный, однако прекрасно работающий метод детектирования .Net объекта. Выглядит он так:
            private static unsafe bool IsCorrectMethodsTable(IntPtr mt)
            {
                if (mt == IntPtr.Zero) return false;
    
                if (PointsToAllocated(mt))
                    if (PointsToAllocated((IntPtr) ((MethodTableInfo*) mt)->EEClass))
                        if (PointsToAllocated((IntPtr) ((MethodTableInfo*) mt)->EEClass->MethodsTable))
                            return ((IntPtr) ((MethodTableInfo*) mt)->EEClass->MethodsTable == mt) ||
                                   ((IntPtr) ((MethodTableInfo*) mt)->ModuleInfo == MscorlibModule);
    
                return false;
            }
    
            private static bool PointsToAllocated(IntPtr ptr)
            {
                if (ptr == IntPtr.Zero) return false;
                return !WinApi.IsBadReadPtr(ptr, 32);
            }
    

    Для каждого указателя проверяется, указывает ли он на выделенный участок памяти. Это первый барьер. Второй барьер — если у теоретического объекта первое поле указывает на нечто, что мы изначально интерпретируем как MethodsTable. Проверяем что поле EEClass указывает на выделенный участок памяти и интерпретируем этот указатель как указатель на структуру ObjectTypeInfo, после чего проверяем, равен ли указатель на MethodsTable нашему найденному указателю (есть ли обратная ссылка?) Если все норм, объект найден.

    Осталось только пройтись по всем участкам памяти и попробовать распознать там объекты. Я не буду выкладывать эту простыню, поскольку там также есть код для регистрации найденного и вывода на экран. Скажу только что задача решена, ссылка вывода из программы — ниже.

    Ссылка на полный код примера: GitHub/DotNetEx/AdvSample
    Вывод нашей программы, дамп памяти
    00606 : System.String
    00583 : System.Object
    00277 : System.RuntimeType
    00072 : System.Array+SZArrayEnumerator
    00046 : System.Char[]
    00041 : System.Int32[]
    00033 : System.String[]
    00032 : System.Object[]
    00030 : System.Version
    00029 : System.Byte[]
    00024 : System.Text.StringBuilder
    00023 : System.Collections.Hashtable+bucket[]
    00020 : System.Security.PermissionSet
    00020 : System.Collections.Hashtable
    00020 : System.Reflection.AssemblyName
    00014 : System.Reflection.RuntimeAssembly
    00014 : System.Security.Permissions.EnvironmentPermission
    00013 : System.Collections.Hashtable+SyncHashtable
    00012 : System.Globalization.CompareInfo
    00012 : System.Collections.Generic.Dictionary`2+Entry[[System.Type, mscorlib, Ve
    rsion=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Securit
    y.Policy.EvidenceTypeDescriptor, mscorlib, Version=4.0.0.0, Culture=neutral, Pub
    licKeyToken=b77a5c561934e089]][]
    00011 : System.Globalization.CultureInfo
    00010 : System.Collections.ArrayList
    00010 : System.Int32
    00010 : System.Threading.ThreadStart
    00010 : System.Internal.HandleCollector+HandleType
    00010 : System.Internal.HandleCollector+HandleType
    00009 : System.Security.Permissions.SecurityPermission
    00009 : System.EventHandler
    00009 : Microsoft.Win32.SafeHandles.SafeRegistryHandle
    00009 : Microsoft.Win32.RegistryKey
    00008 : System.Threading.Thread
    00008 : Microsoft.Win32.SafeHandles.SafeWaitHandle
    00008 : System.Security.Policy.EvidenceTypeDescriptor
    00008 : System.Runtime.InteropServices.HandleRef
    00008 : System.UInt16
    00006 : System.Runtime.Remoting.Metadata.SoapTypeAttribute[]
    00005 : System.Type[]
    00005 : System.Threading.ReaderWriterLock
    00005 : System.Reflection.CustomAttributeRecord[]
    00005 : System.Globalization.TextInfo
    00005 : System.Security.Permissions.EnvironmentStringExpressionSet
    00005 : System.WeakReference
    00005 : System.Threading.ThreadHelper
    00004 : System.Security.FrameSecurityDescriptor
    00004 : System.Reflection.RuntimeModule
    00004 : System.Reflection.RuntimeConstructorInfo
    00004 : System.Guid
    00004 : Microsoft.Win32.SafeHandles.SafePEFileHandle
    00004 : System.Security.Policy.Evidence
    00004 : System.Collections.Generic.Dictionary`2[[System.Type, mscorlib, Version=
    4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Security.Poli
    cy.EvidenceTypeDescriptor, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKey
    Token=b77a5c561934e089]]
    00004 : System.Security.Policy.AssemblyEvidenceFactory
    00004 : System.Security.Policy.Evidence+EvidenceUpgradeLockHolder
    00003 : System.Security.Util.TokenBasedSet
    00003 : System.Globalization.CultureData
    00003 : System.Reflection.MemberFilter
    00003 : System.Reflection.MethodBase[]
    00003 : System.RuntimeType[]
    00003 : System.Runtime.Remoting.Lifetime.LeaseLifeTimeServiceProperty
    00003 : System.Attribute[]
    00003 : System.Threading.ManualResetEvent
    00003 : System.IO.PathHelper
    00003 : System.Security.Permissions.UIPermission
    00002 : System.AppDomainSetup
    00002 : System.Security.PermissionToken
    00002 : System.Runtime.Remoting.Contexts.IContextProperty[]
    00002 : System.Reflection.TypeFilter
    00002 : System.Collections.Queue
    00002 : System.WeakReference[]
    00002 : System.Char
    00002 : System.Security.Policy.StrongName[]
    00002 : System.Reflection.RuntimeMethodInfo
    00002 : System.Threading.SynchronizationContext
    00002 : System.Internal.HandleCollector+HandleType[]
    00002 : System.Globalization.NumberFormatInfo
    00002 : Microsoft.Win32.SystemEvents+SystemEventInvokeInfo[]
    00002 : System.Text.EncoderReplacementFallback
    00002 : System.IO.UnmanagedMemoryStream
    00002 : System.Security.Permissions.FileIOAccess
    00002 : System.Security.Util.StringExpressionSet
    00001 : System.Exception
    00001 : System.OutOfMemoryException
    00001 : System.StackOverflowException
    00001 : System.ExecutionEngineException
    00001 : System.AppDomain
    00001 : System.Security.PermissionTokenFactory
    00001 : System.Security.PermissionToken[]
    00001 : System.Globalization.CalendarData[]
    00001 : System.Globalization.CalendarData
    00001 : System.__Filters
    00001 : System.DefaultBinder
    00001 : System.RuntimeType+RuntimeTypeCache+MemberInfoCache`1[[System.Reflection
    .RuntimeConstructorInfo, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyTo
    ken=b77a5c561934e089]]
    00001 : System.Reflection.RuntimeConstructorInfo[]
    00001 : System.Reflection.ConstructorInfo[]
    00001 : System.Collections.Generic.List`1[[System.Reflection.MethodBase, mscorli
    b, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
    00001 : System.Signature
    00001 : System.Reflection.ParameterInfo[]
    00001 : System.Int32[][]
    00001 : System.Runtime.Remoting.Proxies.ProxyAttribute
    00001 : System.Runtime.Remoting.DomainSpecificRemotingData
    00001 : System.Runtime.Remoting.Channels.ChannelServicesData
    00001 : System.Runtime.Remoting.Activation.LocalActivator
    00001 : System.Runtime.Remoting.Activation.ActivationListener
    00001 : System.Runtime.Remoting.Contexts.ContextAttribute[]
    00001 : System.Runtime.Remoting.Contexts.Context
    00001 : System.Runtime.Remoting.Messaging.ConstructorCallMessage
    00001 : System.Runtime.Remoting.Metadata.RemotingTypeCachedData
    00001 : System.Collections.Generic.Dictionary`2[[System.RuntimeType, mscorlib, V
    ersion=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Runtim
    eType, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e0
    89]]
    00001 : System.Collections.Generic.Dictionary`2+Entry[[System.RuntimeType, mscor
    lib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.
    RuntimeType, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c56
    1934e089]][]
    00001 : System.AttributeUsageAttribute
    00001 : System.Runtime.Remoting.Metadata.SoapTypeAttribute
    00001 : System.Runtime.Remoting.Activation.ConstructionLevelActivator
    00001 : System.Runtime.Remoting.RemotingConfigHandler+RemotingConfigInfo
    00001 : System.Collections.Generic.Dictionary`2[[System.String, mscorlib, Versio
    n=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Globalizati
    on.CultureData, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5
    c561934e089]]
    00001 : System.Runtime.Remoting.ObjectHandle
    00001 : System.Diagnostics.TraceSwitch
    00001 : System.Collections.Generic.Dictionary`2[[System.Int16, mscorlib, Version
    =4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.IntPtr, msco
    rlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
    00001 : System.Collections.Generic.GenericEqualityComparer`1[[System.Int16, msco
    rlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
    00001 : System.Collections.Generic.ObjectEqualityComparer`1[[System.IntPtr, msco
    rlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
    00001 : System.Threading.Mutex
    00001 : System.Runtime.CompilerServices.RuntimeHelpers+CleanupCode
    00001 : System.Threading.Mutex+MutexCleanupInfo
    00001 : System.Threading.Mutex+MutexTryCodeHelper
    00001 : System.Threading.EventWaitHandle
    00001 : System.Threading.HostExecutionContextManager
    00001 : System.Collections.Generic.ObjectEqualityComparer`1[[System.Type, mscorl
    ib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
    00001 : System.Security.Policy.ApplicationTrust
    00001 : System.Security.Policy.PolicyStatement
    00001 : System.Collections.Generic.List`1[[System.Security.Policy.StrongName, ms
    corlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
    00001 : System.SZArrayHelper+SZGenericArrayEnumerator`1[[System.Security.Policy.
    StrongName, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561
    934e089]]
    00001 : System.Collections.ObjectModel.ReadOnlyCollection`1[[System.Security.Pol
    icy.StrongName, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5
    c561934e089]]
    00001 : Microsoft.Win32.Win32Native+OSVERSIONINFO
    00001 : Microsoft.Win32.Win32Native+OSVERSIONINFOEX
    00001 : System.OperatingSystem
    00001 : System.__ComObject
    00001 : System.Collections.Queue+SynchronizedQueue
    00001 : System.Threading.AutoResetEvent
    00001 : System.Threading.ContextCallback
    00001 : System.RuntimeMethodInfoStub
    00001 : System.RuntimeType+RuntimeTypeCache+MemberInfoCache`1[[System.Reflection
    .RuntimeMethodInfo, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b
    77a5c561934e089]]
    00001 : System.Reflection.RuntimeMethodInfo[]
    00001 : System.Collections.Generic.List`1[[System.Attribute, mscorlib, Version=4
    .0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
    00001 : System.Drawing.SizeF
    00001 : System.Drawing.Point
    00001 : System.Windows.Forms.Application+ThreadContext
    00001 : System.Windows.Forms.WindowsFormsSynchronizationContext
    00001 : System.EventArgs
    00001 : System.Windows.Forms.NativeMethods+WNDCLASS_D
    00001 : Microsoft.Win32.UserPreferenceChangedEventHandler
    00001 : System.Random
    00001 : System.Collections.Generic.ObjectEqualityComparer`1[[System.Object, msco
    rlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
    00001 : System.Collections.Generic.Dictionary`2[[System.String, mscorlib, Versio
    n=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Object[], m
    scorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
    00001 : Microsoft.Win32.SystemEvents
    00001 : System.Runtime.Remoting.Messaging.LogicalCallContext
    00001 : System.Text.UTF8Encoding
    00001 : Microsoft.Win32.NativeMethods+WNDCLASS
    00001 : System.Internal.HandleCollector+HandleType[]
    00001 : System.IntPtr[]
    00001 : System.Security.Util.URLString
    00001 : System.Security.Permissions.FileIOPermission
    00001 : System.Security.Util.LocalSiteString
    00001 : System.Security.Util.DirectoryString
    00001 : Microsoft.Win32.SafeHandles.SafeFileHandle
    00001 : System.Text.SBCSCodePageEncoding
    00001 : System.Text.InternalEncoderBestFitFallback
    00001 : System.Text.InternalDecoderBestFitFallback
    00001 : Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
    00001 : Microsoft.Win32.SafeHandles.SafeFileMappingHandle
    00001 : System.IO.__ConsoleStream
    00001 : System.Text.EncoderNLS
    00001 : System.IO.StreamWriter+MdaHelper
    00001 : System.IO.TextWriter+SyncTextWriter
    00001 : System.Diagnostics.Stopwatch
    00001 : System.Predicate`1[[System.Int64, mscorlib, Version=4.0.0.0, Culture=neu
    tral, PublicKeyToken=b77a5c561934e089]]
    00001 : System.DBNull
    Objects total: 2294. Time taken: 437
    

  • Можно ли получить объекты с чужого домена?

    Конечно! Ведь мы смотрим на виртуальную память. Это раз… А второе… между доменами нет границ, объекты выделяются друг за другом даже при пересечении границы доменов. Разница — в коде. Потому можно, например, передать между доменами объект без сериализации.