Продолжаем тему интересного на .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; }
Далее посмотрим, как выглядит MathodTableInfo:
[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
- Можно ли получить объекты с чужого домена?
Конечно! Ведь мы смотрим на виртуальную память. Это раз… А второе… между доменами нет границ, объекты выделяются друг за другом даже при пересечении границы доменов. Разница — в коде. Потому можно, например, передать между доменами объект без сериализации.