From f9c2d8047dc6268697ef108afa022363e257f577 Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Mon, 3 Oct 2022 02:13:05 +0100 Subject: [PATCH 01/19] Clean up csproj files --- UMS.Analysis/UMS.Analysis.csproj | 2 +- UMS.LowLevel/UMS.LowLevel.csproj | 1 + UnityMemorySnapshotThing/UnityMemorySnapshotThing.csproj | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/UMS.Analysis/UMS.Analysis.csproj b/UMS.Analysis/UMS.Analysis.csproj index c620556..902c052 100644 --- a/UMS.Analysis/UMS.Analysis.csproj +++ b/UMS.Analysis/UMS.Analysis.csproj @@ -4,8 +4,8 @@ net6.0 enable enable - true preview + embedded diff --git a/UMS.LowLevel/UMS.LowLevel.csproj b/UMS.LowLevel/UMS.LowLevel.csproj index 4e8188e..8b2dd29 100644 --- a/UMS.LowLevel/UMS.LowLevel.csproj +++ b/UMS.LowLevel/UMS.LowLevel.csproj @@ -5,6 +5,7 @@ enable enable true + embedded diff --git a/UnityMemorySnapshotThing/UnityMemorySnapshotThing.csproj b/UnityMemorySnapshotThing/UnityMemorySnapshotThing.csproj index 03d50b2..7c720c5 100644 --- a/UnityMemorySnapshotThing/UnityMemorySnapshotThing.csproj +++ b/UnityMemorySnapshotThing/UnityMemorySnapshotThing.csproj @@ -5,8 +5,8 @@ net6.0 enable enable - true true + embedded From d0b3d6c39aa272841b011042026e02a60e7f2879 Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Mon, 3 Oct 2022 12:21:53 +0100 Subject: [PATCH 02/19] Output retention paths --- UMS.Analysis/SnapshotFile.cs | 30 +++++++- UMS.Analysis/Structures/LoadedReason.cs | 9 +++ .../Structures/Objects/ComplexFieldValue.cs | 6 +- .../Objects/ManagedClassInstance.cs | 75 +++++++++++++++++-- UnityMemorySnapshotThing/Program.cs | 5 +- 5 files changed, 109 insertions(+), 16 deletions(-) create mode 100644 UMS.Analysis/Structures/LoadedReason.cs diff --git a/UMS.Analysis/SnapshotFile.cs b/UMS.Analysis/SnapshotFile.cs index 7e562fe..4e8c9c0 100644 --- a/UMS.Analysis/SnapshotFile.cs +++ b/UMS.Analysis/SnapshotFile.cs @@ -22,10 +22,14 @@ public class SnapshotFile : LowLevelSnapshotFile private readonly Dictionary _typeInfoCacheByTypeIndex = new(); - private Dictionary _managedObjectInfoCache = new(); + private readonly Dictionary _managedObjectInfoCache = new(); - private Dictionary _managedClassInstanceCache = new(); + private readonly Dictionary _managedClassInstanceCache = new(); + private readonly Dictionary _typeNamesByTypeIndex = new(); + + private readonly Dictionary _fieldNamesByFieldIndex = new(); + public IEnumerable AllManagedClassInstances => _managedClassInstanceCache.Values; public SnapshotFile(string path) : base(path) @@ -75,7 +79,7 @@ public RawManagedObjectInfo ParseManagedObjectInfo(ulong address) return info; } - public ManagedClassInstance? GetManagedClassInstance(ulong address, ManagedClassInstance? parent = null, int depth = 0) + public ManagedClassInstance? GetManagedClassInstance(ulong address, ManagedClassInstance? parent = null, int depth = 0, LoadedReason reason = LoadedReason.GcRoot, int fieldOrArrayIdx = int.MinValue) { if (_managedClassInstanceCache.TryGetValue(address, out var ret)) return ret; @@ -84,7 +88,7 @@ public RawManagedObjectInfo ParseManagedObjectInfo(ulong address) if (!info.IsKnownType) return null; - var instance = new ManagedClassInstance(this, info, parent, depth); + var instance = new ManagedClassInstance(this, info, parent, depth, reason, fieldOrArrayIdx); _managedClassInstanceCache[address] = instance; return instance; } @@ -284,4 +288,22 @@ public IEnumerable WalkFieldInfoForTypeIndex(int typeIndex) }; } } + + public string GetTypeName(int typeIndex) + { + if (_typeNamesByTypeIndex.TryGetValue(typeIndex, out var ret)) + return ret; + + _typeNamesByTypeIndex[typeIndex] = ret = ReadSingleStringFromChapter(EntryType.TypeDescriptions_Name, typeIndex); + return ret; + } + + public string GetFieldName(int fieldIndex) + { + if (_fieldNamesByFieldIndex.TryGetValue(fieldIndex, out var ret)) + return ret; + + _fieldNamesByFieldIndex[fieldIndex] = ret = ReadSingleStringFromChapter(EntryType.FieldDescriptions_Name, fieldIndex); + return ret; + } } \ No newline at end of file diff --git a/UMS.Analysis/Structures/LoadedReason.cs b/UMS.Analysis/Structures/LoadedReason.cs new file mode 100644 index 0000000..afb38d4 --- /dev/null +++ b/UMS.Analysis/Structures/LoadedReason.cs @@ -0,0 +1,9 @@ +namespace UMS.Analysis.Structures; + +public enum LoadedReason : byte +{ + GcRoot, + StaticField, + InstanceField, + ArrayElement +} \ No newline at end of file diff --git a/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs b/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs index aa2141e..1865d09 100644 --- a/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs +++ b/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs @@ -22,8 +22,8 @@ public ComplexFieldValue(SnapshotFile file, BasicFieldInfoCache info, ManagedCla var size = info.FieldTypeSize; if (size > 0) //Have observed a negative size (-8), MIGHT be for pointers to value types, so we'll fall back to the below logic. { - var valueTypeData = data[..size]; - Value = new(file, info.TypeDescriptionIndex, info.Flags, size, data, parent, depth); + var vtInst = new ManagedClassInstance(file, info.TypeDescriptionIndex, info.Flags, size, data, parent, depth, LoadedReason.InstanceField, info.FieldIndex); + Value = vtInst; return; } } @@ -38,7 +38,7 @@ public ComplexFieldValue(SnapshotFile file, BasicFieldInfoCache info, ManagedCla return; } - var mci = file.GetManagedClassInstance(ptr, parent, depth); + var mci = file.GetManagedClassInstance(ptr, parent, depth, LoadedReason.InstanceField, info.FieldIndex); if (mci == null) { diff --git a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs index 010d0ca..ff07a22 100644 --- a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs +++ b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using System.Text; using UMS.LowLevel.Structures; namespace UMS.Analysis.Structures.Objects; @@ -6,17 +7,20 @@ namespace UMS.Analysis.Structures.Objects; public readonly struct ManagedClassInstance { private readonly object? _parent; - public readonly ulong ObjectAddress; + public readonly BasicTypeInfoCache TypeInfo; public readonly IFieldValue[] Fields; + public readonly TypeFlags TypeDescriptionFlags; + public int FieldIndexOrArrayOffset { get; init; } private readonly bool IsInitialized; + public readonly LoadedReason LoadedReason; private ManagedClassInstance TypedParent => _parent == null ? default : Unsafe.Unbox(_parent!); - public ManagedClassInstance(SnapshotFile file, int typeDescriptionIndex, TypeFlags flags, int size, Span data, ManagedClassInstance parent, int depth) + public ManagedClassInstance(SnapshotFile file, int typeDescriptionIndex, TypeFlags flags, int size, Span data, ManagedClassInstance parent, int depth, LoadedReason loadedReason, int fieldIndexOrArrayOffset = int.MinValue) { if ((flags & TypeFlags.ValueType) != TypeFlags.ValueType) throw new("This constructor can only be used for value types"); @@ -26,6 +30,11 @@ public ManagedClassInstance(SnapshotFile file, int typeDescriptionIndex, TypeFla TypeInfo = file.GetTypeInfo(typeDescriptionIndex); TypeDescriptionFlags = flags; IsInitialized = true; + LoadedReason = loadedReason; + FieldIndexOrArrayOffset = fieldIndexOrArrayOffset; + + if(LoadedReason != LoadedReason.GcRoot && FieldIndexOrArrayOffset == int.MinValue) + throw new("FieldIndexOrArrayOffset must be set for non-GcRoot instances"); if (IsEnumType(file)) { @@ -47,13 +56,18 @@ public ManagedClassInstance(SnapshotFile file, int typeDescriptionIndex, TypeFla Fields = ReadFields(file, data, depth); } - public ManagedClassInstance(SnapshotFile file, RawManagedObjectInfo info, ManagedClassInstance? parent = null, int depth = 0) + public ManagedClassInstance(SnapshotFile file, RawManagedObjectInfo info, ManagedClassInstance? parent = null, int depth = 0, LoadedReason loadedReason = LoadedReason.GcRoot, int fieldIndexOrArrayOffset = int.MinValue) { _parent = parent; ObjectAddress = info.SelfAddress; TypeInfo = file.GetTypeInfo(info.TypeDescriptionIndex); TypeDescriptionFlags = info.Flags; IsInitialized = true; + LoadedReason = loadedReason; + FieldIndexOrArrayOffset = fieldIndexOrArrayOffset; + + if(LoadedReason != LoadedReason.GcRoot && FieldIndexOrArrayOffset == int.MinValue) + throw new("FieldIndexOrArrayOffset must be set for non-GcRoot instances"); var data = info.Data; @@ -138,15 +152,62 @@ public bool InheritsFromUnityEngineObject(SnapshotFile file) if((TypeDescriptionFlags & TypeFlags.Array) == TypeFlags.Array) return false; - var parent = TypeInfo.BaseTypeIndex; - while (parent != -1) + var baseClass = TypeInfo.BaseTypeIndex; + while (baseClass != -1) { - if (parent == file.WellKnownTypes.UnityEngineObject) + if (baseClass == file.WellKnownTypes.UnityEngineObject) return true; - parent = file.GetTypeInfo(parent).BaseTypeIndex; + baseClass = file.GetTypeInfo(baseClass).BaseTypeIndex; } return false; } + + public string GetFirstObservedRetentionPath(SnapshotFile file) + { + var name = file.GetTypeName(TypeInfo.TypeIndex); + + var sb = new StringBuilder(name); + sb.Append(" at 0x").Append(ObjectAddress.ToString("X")); + sb.Append(" (target) <- "); + + var parent = TypedParent; + AppendRetentionReason(sb, file, this, parent); + + while (parent.IsInitialized) + { + var child = parent; + parent = child.TypedParent; + AppendRetentionReason(sb, file, child, parent); + } + + return sb.ToString(); + } + + private void AppendRetentionReason(StringBuilder sb, SnapshotFile file, ManagedClassInstance child, ManagedClassInstance parent) + { + switch (child.LoadedReason) + { + case LoadedReason.GcRoot: + sb.Append("GC Root"); + return; + case LoadedReason.StaticField: + //TODO + break; + case LoadedReason.InstanceField: + var fieldList = file.GetFieldInfoForTypeIndex(parent.TypeInfo.TypeIndex); + var parentName = file.GetTypeName(parent.TypeInfo.TypeIndex); + var field = fieldList.First(f => f.FieldIndex == child.FieldIndexOrArrayOffset); + sb.Append("Field ").Append(file.GetFieldName(field.FieldIndex)).Append(" of "); + sb.Append(parentName).Append(" at 0x").Append(parent.ObjectAddress.ToString("X")); + sb.Append(" <- "); + break; + case LoadedReason.ArrayElement: + //TODO + break; + default: + throw new ArgumentOutOfRangeException(nameof(child), "Invalid LoadedReason"); + } + } } \ No newline at end of file diff --git a/UnityMemorySnapshotThing/Program.cs b/UnityMemorySnapshotThing/Program.cs index 444b3da..dc3142d 100644 --- a/UnityMemorySnapshotThing/Program.cs +++ b/UnityMemorySnapshotThing/Program.cs @@ -118,7 +118,7 @@ private static void FindLeakedUnityObjects(SnapshotFile file) for (var fieldNumber = 0; fieldNumber < fields.Length; fieldNumber++) { var basicFieldInfoCache = fields[fieldNumber]; - var name = file.ReadSingleStringFromChapter(EntryType.FieldDescriptions_Name, basicFieldInfoCache.FieldIndex); + var name = file.GetFieldName(basicFieldInfoCache.FieldIndex); if (name == "m_CachedPtr") { @@ -129,8 +129,9 @@ private static void FindLeakedUnityObjects(SnapshotFile file) if (integerFieldValue.Value == 0) { - var typeName = file.ReadSingleStringFromChapter(EntryType.TypeDescriptions_Name, managedClassInstance.TypeInfo.TypeIndex); + var typeName = file.GetTypeName(managedClassInstance.TypeInfo.TypeIndex); Console.WriteLine($"Found leaked managed object of type: {typeName} at memory address 0x{managedClassInstance.ObjectAddress:X}"); + Console.WriteLine($" Retention Path: {managedClassInstance.GetFirstObservedRetentionPath(file)}"); numLeaked++; } } From e12080e3b0ff27547277c9562f1d6b8d9fd5b6dd Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Mon, 3 Oct 2022 13:57:49 +0100 Subject: [PATCH 03/19] Support static fields as roots --- UMS.Analysis/SnapshotFile.cs | 114 ++++++++++++++---- .../Structures/BasicFieldInfoCache.cs | 4 + .../Structures/Objects/ComplexFieldValue.cs | 2 +- .../Objects/ManagedClassInstance.cs | 35 ++++-- UnityMemorySnapshotThing/Program.cs | 36 +----- 5 files changed, 126 insertions(+), 65 deletions(-) diff --git a/UMS.Analysis/SnapshotFile.cs b/UMS.Analysis/SnapshotFile.cs index 4e8c9c0..61e5f35 100644 --- a/UMS.Analysis/SnapshotFile.cs +++ b/UMS.Analysis/SnapshotFile.cs @@ -16,9 +16,12 @@ public class SnapshotFile : LowLevelSnapshotFile public readonly Dictionary TypeIndexIndices = new(); // TypeIndex -> Index in the TypeIndex array. What. + public readonly Dictionary StaticFieldsToOwningTypes = new(); + public readonly WellKnownTypeHelper WellKnownTypes; private readonly Dictionary _nonStaticFieldIndicesByTypeIndex = new(); + private readonly Dictionary _staticFieldIndicesByTypeIndex = new(); private readonly Dictionary _typeInfoCacheByTypeIndex = new(); @@ -45,6 +48,74 @@ public SnapshotFile(string path) : base(path) TypeIndexIndices.Add(indices[i], i); } + public void LoadManagedObjectsFromGcRoots() + { + var gcRoots = GcHandles; + var start = DateTime.Now; + + Console.WriteLine($"Processing {gcRoots.Length} GC roots..."); + foreach (var gcHandle in gcRoots) + GetOrCreateManagedClassInstance(gcHandle); + + Console.WriteLine($"Found {_managedClassInstanceCache.Count} managed objects in {(DateTime.Now - start).TotalMilliseconds}ms"); + } + + public void LoadManagedObjectsFromStaticFields() + { + var allStaticFields = ReadValueTypeArrayChapter(EntryType.TypeDescriptions_StaticFieldBytes, 0, -1); + + var start = DateTime.Now; + var initialCount = _managedClassInstanceCache.Count; + + Console.WriteLine($"Processing static field info for {allStaticFields.Length} types..."); + for (var typeIndex = 0; typeIndex < allStaticFields.Length; typeIndex++) + { + var typeFieldBytes = allStaticFields[typeIndex].AsSpan(); + if (typeFieldBytes.Length == 0) + continue; + + var typeInfo = GetTypeInfo(typeIndex); + var staticFields = GetStaticFieldInfoForTypeIndex(typeIndex); + + foreach (var field in staticFields) + { + StaticFieldsToOwningTypes[field.FieldIndex] = typeIndex; + if(field.IsValueType) + continue; //TODO + + if(field.IsArray) + continue; + + var fieldOffset = field.FieldOffset; + + if(fieldOffset < 0) + continue; //Generics, mainly + + var fieldPointer = MemoryMarshal.Read(typeFieldBytes[fieldOffset..]); + if (fieldPointer == 0) + continue; + + GetOrCreateManagedClassInstance(fieldPointer, reason: LoadedReason.StaticField, fieldOrArrayIdx: field.FieldIndex); + } + } + + Console.WriteLine($"Found {_managedClassInstanceCache.Count - initialCount} additional managed objects from static fields in {(DateTime.Now - start).TotalMilliseconds}ms"); + } + + public ManagedClassInstance? GetOrCreateManagedClassInstance(ulong address, ManagedClassInstance? parent = null, int depth = 0, LoadedReason reason = LoadedReason.GcRoot, int fieldOrArrayIdx = int.MinValue) + { + if (_managedClassInstanceCache.TryGetValue(address, out var ret)) + return ret; + + var info = ParseManagedObjectInfo(address); + if (!info.IsKnownType) + return null; + + var instance = new ManagedClassInstance(this, info, parent, depth, reason, fieldOrArrayIdx); + _managedClassInstanceCache[address] = instance; + return instance; + } + public RawManagedObjectInfo ParseManagedObjectInfo(ulong address) { if (_managedObjectInfoCache.TryGetValue(address, out var ret)) @@ -78,20 +149,6 @@ public RawManagedObjectInfo ParseManagedObjectInfo(ulong address) return info; } - - public ManagedClassInstance? GetManagedClassInstance(ulong address, ManagedClassInstance? parent = null, int depth = 0, LoadedReason reason = LoadedReason.GcRoot, int fieldOrArrayIdx = int.MinValue) - { - if (_managedClassInstanceCache.TryGetValue(address, out var ret)) - return ret; - - var info = ParseManagedObjectInfo(address); - if (!info.IsKnownType) - return null; - - var instance = new ManagedClassInstance(this, info, parent, depth, reason, fieldOrArrayIdx); - _managedClassInstanceCache[address] = instance; - return instance; - } public int SizeOfObjectInBytes(RawManagedObjectInfo info, Span heap) { @@ -247,30 +304,43 @@ public BasicTypeInfoCache GetTypeInfo(int typeIndex) return info; } - public BasicFieldInfoCache[] GetFieldInfoForTypeIndex(int typeIndex) + public BasicFieldInfoCache[] GetInstanceFieldInfoForTypeIndex(int typeIndex) { if(_nonStaticFieldIndicesByTypeIndex.TryGetValue(typeIndex, out var indices)) return indices; - _nonStaticFieldIndicesByTypeIndex[typeIndex] = indices = WalkFieldInfoForTypeIndex(typeIndex).ToArray(); + _nonStaticFieldIndicesByTypeIndex[typeIndex] = indices = WalkFieldInfoForTypeIndex(typeIndex, false).ToArray(); + + return indices; + } + + public BasicFieldInfoCache[] GetStaticFieldInfoForTypeIndex(int typeIndex) + { + if(_staticFieldIndicesByTypeIndex.TryGetValue(typeIndex, out var indices)) + return indices; + + _staticFieldIndicesByTypeIndex[typeIndex] = indices = WalkFieldInfoForTypeIndex(typeIndex, true).ToArray(); return indices; } - public IEnumerable WalkFieldInfoForTypeIndex(int typeIndex) + public IEnumerable WalkFieldInfoForTypeIndex(int typeIndex, bool wantStatic) { - var baseTypeIndex = ReadSingleValueType(EntryType.TypeDescriptions_BaseOrElementTypeIndex, typeIndex); - if (baseTypeIndex != -1) + if (!wantStatic) { - foreach (var i in GetFieldInfoForTypeIndex(baseTypeIndex)) - yield return i; + var baseTypeIndex = ReadSingleValueType(EntryType.TypeDescriptions_BaseOrElementTypeIndex, typeIndex); + if (baseTypeIndex != -1) + { + foreach (var i in GetInstanceFieldInfoForTypeIndex(baseTypeIndex)) + yield return i; + } } var fieldIndices = ReadSingleValueTypeArrayChapterElement(EntryType.TypeDescriptions_FieldIndices, typeIndex).ToArray(); foreach (var fieldIndex in fieldIndices) { var isStatic = ReadSingleValueType(EntryType.FieldDescriptions_IsStatic, fieldIndex); - if (isStatic) + if (isStatic != wantStatic) continue; var fieldOffset = ReadSingleValueType(EntryType.FieldDescriptions_Offset, fieldIndex); diff --git a/UMS.Analysis/Structures/BasicFieldInfoCache.cs b/UMS.Analysis/Structures/BasicFieldInfoCache.cs index c48906f..d81fbb5 100644 --- a/UMS.Analysis/Structures/BasicFieldInfoCache.cs +++ b/UMS.Analysis/Structures/BasicFieldInfoCache.cs @@ -9,4 +9,8 @@ public struct BasicFieldInfoCache public TypeFlags Flags; public int FieldOffset; public int FieldTypeSize; + + public bool IsValueType => (Flags & TypeFlags.ValueType) == TypeFlags.ValueType; + + public bool IsArray => (Flags & TypeFlags.Array) == TypeFlags.Array; } \ No newline at end of file diff --git a/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs b/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs index 1865d09..fddff47 100644 --- a/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs +++ b/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs @@ -38,7 +38,7 @@ public ComplexFieldValue(SnapshotFile file, BasicFieldInfoCache info, ManagedCla return; } - var mci = file.GetManagedClassInstance(ptr, parent, depth, LoadedReason.InstanceField, info.FieldIndex); + var mci = file.GetOrCreateManagedClassInstance(ptr, parent, depth, LoadedReason.InstanceField, info.FieldIndex); if (mci == null) { diff --git a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs index ff07a22..ee389f9 100644 --- a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs +++ b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs @@ -20,15 +20,20 @@ public readonly struct ManagedClassInstance private ManagedClassInstance TypedParent => _parent == null ? default : Unsafe.Unbox(_parent!); + public bool IsArray => (TypeDescriptionFlags & TypeFlags.Array) == TypeFlags.Array; + + public bool IsValueType => (TypeDescriptionFlags & TypeFlags.ValueType) == TypeFlags.ValueType; + public ManagedClassInstance(SnapshotFile file, int typeDescriptionIndex, TypeFlags flags, int size, Span data, ManagedClassInstance parent, int depth, LoadedReason loadedReason, int fieldIndexOrArrayOffset = int.MinValue) { - if ((flags & TypeFlags.ValueType) != TypeFlags.ValueType) + TypeDescriptionFlags = flags; + + if (!IsValueType) throw new("This constructor can only be used for value types"); _parent = parent; ObjectAddress = 0; TypeInfo = file.GetTypeInfo(typeDescriptionIndex); - TypeDescriptionFlags = flags; IsInitialized = true; LoadedReason = loadedReason; FieldIndexOrArrayOffset = fieldIndexOrArrayOffset; @@ -71,7 +76,7 @@ public ManagedClassInstance(SnapshotFile file, RawManagedObjectInfo info, Manage var data = info.Data; - if ((TypeDescriptionFlags & TypeFlags.Array) == TypeFlags.Array) + if (IsArray) { //TODO array items Fields = Array.Empty(); @@ -92,11 +97,11 @@ private IFieldValue[] ReadFields(SnapshotFile file, Span data, int depth) return Array.Empty(); } - var fieldInfo = file.GetFieldInfoForTypeIndex(TypeInfo.TypeIndex); + var fieldInfo = file.GetInstanceFieldInfoForTypeIndex(TypeInfo.TypeIndex); var fields = new IFieldValue[fieldInfo.Length]; - var isValueType = (TypeDescriptionFlags & TypeFlags.ValueType) == TypeFlags.ValueType; + var isValueType = IsValueType; for (var index = 0; index < fieldInfo.Length; index++) { @@ -147,7 +152,10 @@ private bool CheckIfRecursiveReference() return false; } - public bool InheritsFromUnityEngineObject(SnapshotFile file) + public bool InheritsFromUnityEngineObject(SnapshotFile file) + => InheritsFrom(file, file.WellKnownTypes.UnityEngineObject); + + public bool InheritsFrom(SnapshotFile file, int baseTypeIndex) { if((TypeDescriptionFlags & TypeFlags.Array) == TypeFlags.Array) return false; @@ -155,7 +163,7 @@ public bool InheritsFromUnityEngineObject(SnapshotFile file) var baseClass = TypeInfo.BaseTypeIndex; while (baseClass != -1) { - if (baseClass == file.WellKnownTypes.UnityEngineObject) + if (baseClass == baseTypeIndex) return true; baseClass = file.GetTypeInfo(baseClass).BaseTypeIndex; @@ -193,16 +201,25 @@ private void AppendRetentionReason(StringBuilder sb, SnapshotFile file, ManagedC sb.Append("GC Root"); return; case LoadedReason.StaticField: - //TODO + { + var staticFieldDeclaringType = file.StaticFieldsToOwningTypes[child.FieldIndexOrArrayOffset]; + var fieldList = file.GetStaticFieldInfoForTypeIndex(staticFieldDeclaringType); + var parentName = file.GetTypeName(staticFieldDeclaringType); + var field = fieldList.First(f => f.FieldIndex == child.FieldIndexOrArrayOffset); + sb.Append("Static Field ").Append(file.GetFieldName(field.FieldIndex)).Append(" of "); + sb.Append(parentName); break; + } case LoadedReason.InstanceField: - var fieldList = file.GetFieldInfoForTypeIndex(parent.TypeInfo.TypeIndex); + { + var fieldList = file.GetInstanceFieldInfoForTypeIndex(parent.TypeInfo.TypeIndex); var parentName = file.GetTypeName(parent.TypeInfo.TypeIndex); var field = fieldList.First(f => f.FieldIndex == child.FieldIndexOrArrayOffset); sb.Append("Field ").Append(file.GetFieldName(field.FieldIndex)).Append(" of "); sb.Append(parentName).Append(" at 0x").Append(parent.ObjectAddress.ToString("X")); sb.Append(" <- "); break; + } case LoadedReason.ArrayElement: //TODO break; diff --git a/UnityMemorySnapshotThing/Program.cs b/UnityMemorySnapshotThing/Program.cs index dc3142d..760c52c 100644 --- a/UnityMemorySnapshotThing/Program.cs +++ b/UnityMemorySnapshotThing/Program.cs @@ -57,41 +57,11 @@ public static void Main(string[] args) // // Console.WriteLine($"Querying large dynamic arrays took {(DateTime.Now - start).TotalMilliseconds} ms\n"); - CrawlManagedObjects(file); + file.LoadManagedObjectsFromGcRoots(); + file.LoadManagedObjectsFromStaticFields(); FindLeakedUnityObjects(file); } - - private static void CrawlManagedObjects(SnapshotFile file) - { - //Start with GC Handles - var gcHandles = file.GcHandles; - - //Each of those is a pointer into a managed heap section which can be mapped to the data in that file - //At that address there will be an object header which allows us to find the type index or type description - //It also then contains the instance fields (which we can parse with type data), and we can potentially crawl to other objects using connection data - //We want to find all the objects so we can then iterate on them easily - - var validCount = 0; - var start = DateTime.Now; - var rootObjects = new List(gcHandles.Length); - - Console.WriteLine($"Processing {gcHandles.Length} GC roots..."); - // GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency; - foreach (var gcHandle in gcHandles) - { - rootObjects.Add(file.GetManagedClassInstance(gcHandle)!.Value); - - validCount++; - - // if(validCount % 1000 == 0) - // Console.WriteLine($"Processed {validCount} GC roots in {(DateTime.Now - start).TotalMilliseconds} ms"); - } - - GCSettings.LatencyMode = GCLatencyMode.Interactive; - - Console.WriteLine($"Found {validCount} valid GC roots out of {gcHandles.Length} total in {(DateTime.Now - start).TotalMilliseconds} ms"); - } private static void FindLeakedUnityObjects(SnapshotFile file) { @@ -114,7 +84,7 @@ private static void FindLeakedUnityObjects(SnapshotFile file) int numLeaked = 0; foreach (var managedClassInstance in unityEngineObjects) { - var fields = file.GetFieldInfoForTypeIndex(managedClassInstance.TypeInfo.TypeIndex); + var fields = file.GetInstanceFieldInfoForTypeIndex(managedClassInstance.TypeInfo.TypeIndex); for (var fieldNumber = 0; fieldNumber < fields.Length; fieldNumber++) { var basicFieldInfoCache = fields[fieldNumber]; From 09a9068f2d9addab52053bf90ea3943392b544ea Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Mon, 3 Oct 2022 20:47:22 +0100 Subject: [PATCH 04/19] Write leaked types to file + write summary of types by count too --- UnityMemorySnapshotThing/Program.cs | 41 ++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/UnityMemorySnapshotThing/Program.cs b/UnityMemorySnapshotThing/Program.cs index 760c52c..ddf4bb2 100644 --- a/UnityMemorySnapshotThing/Program.cs +++ b/UnityMemorySnapshotThing/Program.cs @@ -1,9 +1,6 @@ -using System.Runtime; +using System.Text; using UMS.Analysis; -using UMS.Analysis.Structures; using UMS.Analysis.Structures.Objects; -using UMS.LowLevel; -using UMS.LowLevel.Structures; namespace UnityMemorySnapshotThing; @@ -71,17 +68,23 @@ private static void FindLeakedUnityObjects(SnapshotFile file) //Find all the managed objects, filter to those which have a m_CachedObjectPtr field //Then filter to those for which that field is 0 (i.e. not pointing to a native object) //That gives the leaked managed shells. - Console.WriteLine($"Snapshot contains {file.AllManagedClassInstances.Count()} managed objects"); + var ret = new StringBuilder(); + var str = $"Snapshot contains {file.AllManagedClassInstances.Count()} managed objects"; + Console.WriteLine(str); + ret.AppendLine(str); var filterStart = DateTime.Now; var unityEngineObjects = file.AllManagedClassInstances.Where(i => i.InheritsFromUnityEngineObject(file)).ToArray(); - - Console.WriteLine($"Of those, {unityEngineObjects.Length} inherit from UnityEngine.Object (filtered in {(DateTime.Now - filterStart).TotalMilliseconds} ms)"); + + str = $"Of those, {unityEngineObjects.Length} inherit from UnityEngine.Object (filtered in {(DateTime.Now - filterStart).TotalMilliseconds} ms)"; + Console.WriteLine(str); + ret.AppendLine(str); var detectStart = DateTime.Now; int numLeaked = 0; + var leakedTypes = new Dictionary(); foreach (var managedClassInstance in unityEngineObjects) { var fields = file.GetInstanceFieldInfoForTypeIndex(managedClassInstance.TypeInfo.TypeIndex); @@ -100,14 +103,32 @@ private static void FindLeakedUnityObjects(SnapshotFile file) if (integerFieldValue.Value == 0) { var typeName = file.GetTypeName(managedClassInstance.TypeInfo.TypeIndex); - Console.WriteLine($"Found leaked managed object of type: {typeName} at memory address 0x{managedClassInstance.ObjectAddress:X}"); - Console.WriteLine($" Retention Path: {managedClassInstance.GetFirstObservedRetentionPath(file)}"); + + str = $"Found leaked managed object of type: {typeName} at memory address 0x{managedClassInstance.ObjectAddress:X}"; + Console.WriteLine(str); + ret.AppendLine(str); + + str = $" Retention Path: {managedClassInstance.GetFirstObservedRetentionPath(file)}"; + Console.WriteLine(str); + ret.AppendLine(str); + + leakedTypes[typeName] = leakedTypes.GetValueOrDefault(typeName) + 1; + numLeaked++; } } } } + + str = $"Finished detection in {(DateTime.Now - detectStart).TotalMilliseconds} ms. {numLeaked} of those are leaked managed shells"; + Console.WriteLine(str); + ret.AppendLine(str); + + var leakedTypesSorted = leakedTypes.OrderByDescending(kvp => kvp.Value).ToArray(); + + str = $"Leaked types by count: \n{string.Join("\n", leakedTypesSorted.Select(kvp => $"{kvp.Value} x {kvp.Key}"))}"; + ret.AppendLine(str); - Console.WriteLine($"Finished detection in {(DateTime.Now - detectStart).TotalMilliseconds} ms. {numLeaked} of those are leaked managed shells"); + File.WriteAllText("leaked_objects.txt", ret.ToString()); } } \ No newline at end of file From b4c9f6e9b1e0cabb2234b2272a41568d00fe1dc7 Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Tue, 18 Apr 2023 15:25:11 -0700 Subject: [PATCH 05/19] Performance optimizations, fixes, support net7 --- UMS.Analysis/SnapshotFile.cs | 6 +++--- UMS.Analysis/Structures/Objects/ManagedClassInstance.cs | 4 ++-- UMS.Analysis/UMS.Analysis.csproj | 2 +- UMS.LowLevel/UMS.LowLevel.csproj | 2 +- UnityMemorySnapshotThing/UnityMemorySnapshotThing.csproj | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/UMS.Analysis/SnapshotFile.cs b/UMS.Analysis/SnapshotFile.cs index 61e5f35..59fde7d 100644 --- a/UMS.Analysis/SnapshotFile.cs +++ b/UMS.Analysis/SnapshotFile.cs @@ -25,9 +25,9 @@ public class SnapshotFile : LowLevelSnapshotFile private readonly Dictionary _typeInfoCacheByTypeIndex = new(); - private readonly Dictionary _managedObjectInfoCache = new(); - - private readonly Dictionary _managedClassInstanceCache = new(); + private readonly Dictionary _managedObjectInfoCache = new(1024*1024 * 4); + + private readonly Dictionary _managedClassInstanceCache = new(1024 * 1024 * 4); private readonly Dictionary _typeNamesByTypeIndex = new(); diff --git a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs index ee389f9..7e8e5ba 100644 --- a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs +++ b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs @@ -91,9 +91,9 @@ private IFieldValue[] ReadFields(SnapshotFile file, Span data, int depth) if (CheckIfRecursiveReference()) return Array.Empty(); - if (depth > 250) + if (depth > 350) { - Console.WriteLine($"Stopped reading fields due to too-deeply nested object at depth {depth}"); + Console.WriteLine($"Stopped reading fields due to too-deeply nested object at depth {depth} (this object is of type {file.GetTypeName(TypeInfo.TypeIndex)}, parent is of type {file.GetTypeName(TypedParent.TypeInfo.TypeIndex)})"); return Array.Empty(); } diff --git a/UMS.Analysis/UMS.Analysis.csproj b/UMS.Analysis/UMS.Analysis.csproj index 902c052..a269358 100644 --- a/UMS.Analysis/UMS.Analysis.csproj +++ b/UMS.Analysis/UMS.Analysis.csproj @@ -1,7 +1,7 @@ - net6.0 + net6.0;net7.0 enable enable preview diff --git a/UMS.LowLevel/UMS.LowLevel.csproj b/UMS.LowLevel/UMS.LowLevel.csproj index 8b2dd29..964cb59 100644 --- a/UMS.LowLevel/UMS.LowLevel.csproj +++ b/UMS.LowLevel/UMS.LowLevel.csproj @@ -1,7 +1,7 @@ - net6.0 + net6.0;net7.0 enable enable true diff --git a/UnityMemorySnapshotThing/UnityMemorySnapshotThing.csproj b/UnityMemorySnapshotThing/UnityMemorySnapshotThing.csproj index 7c720c5..896d9b3 100644 --- a/UnityMemorySnapshotThing/UnityMemorySnapshotThing.csproj +++ b/UnityMemorySnapshotThing/UnityMemorySnapshotThing.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net6.0;net7.0 enable enable true From c1551bacd9e65faa581060c701714a38b0c023f1 Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Tue, 18 Apr 2023 16:23:02 -0700 Subject: [PATCH 06/19] Support reading arrays --- UMS.Analysis/SnapshotFile.cs | 19 +++- .../Structures/Objects/ComplexFieldValue.cs | 6 +- .../Objects/ManagedClassInstance.cs | 94 ++++++++++++++----- 3 files changed, 89 insertions(+), 30 deletions(-) diff --git a/UMS.Analysis/SnapshotFile.cs b/UMS.Analysis/SnapshotFile.cs index 59fde7d..ab367f1 100644 --- a/UMS.Analysis/SnapshotFile.cs +++ b/UMS.Analysis/SnapshotFile.cs @@ -140,7 +140,8 @@ public RawManagedObjectInfo ParseManagedObjectInfo(ulong address) throw new($"Failed to resolve type for object at {address:X}"); } - info.Flags = ReadSingleValueType(EntryType.TypeDescriptions_Flags, info.TypeDescriptionIndex); + var typeIndex = info.TypeDescriptionIndex; + info.Flags = GetTypeFlagsByIndex(typeIndex); info.Size = SizeOfObjectInBytes(info, heap); info.Data = heap[..info.Size].ToArray(); info.SelfAddress = address; @@ -150,6 +151,14 @@ public RawManagedObjectInfo ParseManagedObjectInfo(ulong address) return info; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TypeFlags GetTypeFlagsByIndex(int typeIndex) + => ReadSingleValueType(EntryType.TypeDescriptions_Flags, typeIndex); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetTypeDescriptionSizeBytes(int index) + => ReadSingleValueType(EntryType.TypeDescriptions_Size, index); + public int SizeOfObjectInBytes(RawManagedObjectInfo info, Span heap) { if (info.Flags.HasFlag(TypeFlags.Array)) @@ -158,7 +167,7 @@ public int SizeOfObjectInBytes(RawManagedObjectInfo info, Span heap) if (info.TypeDescriptionIndex == WellKnownTypes.String) return GetObjectSizeFromStringInBytes(info, heap); - return ReadSingleValueType(EntryType.TypeDescriptions_Size, info.TypeDescriptionIndex); + return GetTypeDescriptionSizeBytes(info.TypeDescriptionIndex); } private int GetObjectSizeFromStringInBytes(RawManagedObjectInfo info, Span heap) @@ -177,7 +186,7 @@ private int GetObjectSizeFromStringInBytes(RawManagedObjectInfo info, Span private int GetObjectSizeFromArrayInBytes(RawManagedObjectInfo info, Span heap) { - var arrayLength = ReadArrayLength(info, heap); + var arrayLength = ReadArrayLength(info.Flags, heap); if (arrayLength > heap.Length) { @@ -200,7 +209,7 @@ private int GetObjectSizeFromArrayInBytes(RawManagedObjectInfo info, Span return VirtualMachineInformation.ArrayHeaderSize + elementSize * arrayLength; } - private int ReadArrayLength(RawManagedObjectInfo info, Span heap) + public int ReadArrayLength(TypeFlags flags, Span heap) { var heapTemp = heap[VirtualMachineInformation.ArrayBoundsOffsetInHeader..]; //Seek to the bounds offset var bounds = ReadPointer(heapTemp); @@ -213,7 +222,7 @@ private int ReadArrayLength(RawManagedObjectInfo info, Span heap) return 0; var length = 1; - var rank = (int)(info.Flags & TypeFlags.ArrayRankMask) >> 16; + var rank = (int)(flags & TypeFlags.ArrayRankMask) >> 16; for (var i = 0; i < rank; i++) { length *= MemoryMarshal.Read(boundsHeap); diff --git a/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs b/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs index fddff47..694c456 100644 --- a/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs +++ b/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs @@ -10,7 +10,7 @@ public struct ComplexFieldValue : IFieldValue public ManagedClassInstance? Value { get; } - public ComplexFieldValue(SnapshotFile file, BasicFieldInfoCache info, ManagedClassInstance parent, Span data, int depth) + public ComplexFieldValue(SnapshotFile file, BasicFieldInfoCache info, ManagedClassInstance parent, Span data, int depth, bool array) { IsNull = false; FailedToParse = false; @@ -22,7 +22,7 @@ public ComplexFieldValue(SnapshotFile file, BasicFieldInfoCache info, ManagedCla var size = info.FieldTypeSize; if (size > 0) //Have observed a negative size (-8), MIGHT be for pointers to value types, so we'll fall back to the below logic. { - var vtInst = new ManagedClassInstance(file, info.TypeDescriptionIndex, info.Flags, size, data, parent, depth, LoadedReason.InstanceField, info.FieldIndex); + var vtInst = new ManagedClassInstance(file, info.TypeDescriptionIndex, info.Flags, size, data, parent, depth, array ? LoadedReason.ArrayElement : LoadedReason.InstanceField, info.FieldIndex); Value = vtInst; return; } @@ -38,7 +38,7 @@ public ComplexFieldValue(SnapshotFile file, BasicFieldInfoCache info, ManagedCla return; } - var mci = file.GetOrCreateManagedClassInstance(ptr, parent, depth, LoadedReason.InstanceField, info.FieldIndex); + var mci = file.GetOrCreateManagedClassInstance(ptr, parent, depth, array ? LoadedReason.ArrayElement : LoadedReason.InstanceField, info.FieldIndex); if (mci == null) { diff --git a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs index 7e8e5ba..35b3631 100644 --- a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs +++ b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs @@ -78,8 +78,35 @@ public ManagedClassInstance(SnapshotFile file, RawManagedObjectInfo info, Manage if (IsArray) { - //TODO array items - Fields = Array.Empty(); + var arrayElementCount = file.ReadArrayLength(TypeDescriptionFlags, data); + + if (arrayElementCount == 0) + { + Fields = Array.Empty(); + return; + } + + var typeInfo = file.GetTypeInfo(info.TypeDescriptionIndex); + + if (typeInfo.BaseTypeIndex < 0) + { + Console.WriteLine("WARNING: Skipping uninitialized array type"); + Fields = Array.Empty(); + return; + } + + var elementType = file.GetTypeInfo(typeInfo.BaseTypeIndex); + var elementFlags = file.GetTypeFlagsByIndex(elementType.TypeIndex); + var elementTypeSize = (elementFlags & TypeFlags.ValueType) != 0 ? file.GetTypeDescriptionSizeBytes(elementType.TypeIndex) : 8; + var arrayData = info.Data.AsSpan(file.VirtualMachineInformation.ArrayHeaderSize..); + + Fields = new IFieldValue[arrayElementCount]; + for (var i = 0; i < arrayElementCount; i++) + { + var elementData = arrayData[(i * elementTypeSize)..]; + Fields[i] = ReadFieldValue(file, elementData, depth, elementFlags, elementTypeSize, elementType.TypeIndex, i); + } + return; } @@ -111,30 +138,48 @@ private IFieldValue[] ReadFields(SnapshotFile file, Span data, int depth) if (isValueType) fieldOffset -= file.VirtualMachineInformation.ObjectHeaderSize; - var fieldPtr = data[fieldOffset..]; - - //For all integer types, we just handle unsigned as signed - if (info.TypeDescriptionIndex == file.WellKnownTypes.String) - fields[index] = new StringFieldValue(file, fieldPtr); - else if (info.TypeDescriptionIndex == file.WellKnownTypes.Boolean || info.TypeDescriptionIndex == file.WellKnownTypes.Byte) - fields[index] = new IntegerFieldValue(fieldPtr[0]); - else if (info.TypeDescriptionIndex == file.WellKnownTypes.Int16 || info.TypeDescriptionIndex == file.WellKnownTypes.UInt16 || info.TypeDescriptionIndex == file.WellKnownTypes.Char) - fields[index] = new IntegerFieldValue(BitConverter.ToInt16(fieldPtr)); - else if (info.TypeDescriptionIndex == file.WellKnownTypes.Int32 || info.TypeDescriptionIndex == file.WellKnownTypes.UInt32) - fields[index] = new IntegerFieldValue(BitConverter.ToInt32(fieldPtr)); - else if (info.TypeDescriptionIndex == file.WellKnownTypes.Int64 || info.TypeDescriptionIndex == file.WellKnownTypes.UInt64 || info.TypeDescriptionIndex == file.WellKnownTypes.IntPtr) - fields[index] = new IntegerFieldValue(BitConverter.ToInt64(fieldPtr)); - else if (info.TypeDescriptionIndex == file.WellKnownTypes.Single) - fields[index] = new FloatingPointFieldValue(BitConverter.ToSingle(fieldPtr)); - else if (info.TypeDescriptionIndex == file.WellKnownTypes.Double) - fields[index] = new FloatingPointFieldValue(BitConverter.ToDouble(fieldPtr)); - else - fields[index] = new ComplexFieldValue(file, info, this, fieldPtr, depth + 1); + var fieldData = data[fieldOffset..]; + + fields[index] = ReadFieldValue(file, info, fieldData, depth, false); } return fields; } + private IFieldValue ReadFieldValue(SnapshotFile file, BasicFieldInfoCache info, Span fieldData, int depth, bool array) + { + //For all integer types, we just handle unsigned as signed + if (info.TypeDescriptionIndex == file.WellKnownTypes.String) + return new StringFieldValue(file, fieldData); + if (info.TypeDescriptionIndex == file.WellKnownTypes.Boolean || info.TypeDescriptionIndex == file.WellKnownTypes.Byte) + return new IntegerFieldValue(fieldData[0]); + if (info.TypeDescriptionIndex == file.WellKnownTypes.Int16 || info.TypeDescriptionIndex == file.WellKnownTypes.UInt16 || info.TypeDescriptionIndex == file.WellKnownTypes.Char) + return new IntegerFieldValue(BitConverter.ToInt16(fieldData)); + if (info.TypeDescriptionIndex == file.WellKnownTypes.Int32 || info.TypeDescriptionIndex == file.WellKnownTypes.UInt32) + return new IntegerFieldValue(BitConverter.ToInt32(fieldData)); + if (info.TypeDescriptionIndex == file.WellKnownTypes.Int64 || info.TypeDescriptionIndex == file.WellKnownTypes.UInt64 || info.TypeDescriptionIndex == file.WellKnownTypes.IntPtr) + return new IntegerFieldValue(BitConverter.ToInt64(fieldData)); + if (info.TypeDescriptionIndex == file.WellKnownTypes.Single) + return new FloatingPointFieldValue(BitConverter.ToSingle(fieldData)); + if (info.TypeDescriptionIndex == file.WellKnownTypes.Double) + return new FloatingPointFieldValue(BitConverter.ToDouble(fieldData)); + + return new ComplexFieldValue(file, info, this, fieldData, depth + 1, array); + } + + private IFieldValue ReadFieldValue(SnapshotFile file, Span fieldData, int depth, TypeFlags fieldTypeFlags, int fieldTypeSize, int fieldTypeIndex, int arrayOffset) + { + BasicFieldInfoCache info = new() + { + Flags = fieldTypeFlags, + FieldIndex = arrayOffset, + TypeDescriptionIndex = fieldTypeIndex, + FieldTypeSize = fieldTypeSize, + }; + + return ReadFieldValue(file, info, fieldData, depth, true); + } + private bool IsEnumType(SnapshotFile file) => TypeInfo.BaseTypeIndex == file.WellKnownTypes.Enum; @@ -221,8 +266,13 @@ private void AppendRetentionReason(StringBuilder sb, SnapshotFile file, ManagedC break; } case LoadedReason.ArrayElement: - //TODO + { + var parentName = file.GetTypeName(parent.TypeInfo.TypeIndex); + sb.Append("Array Element ").Append(child.FieldIndexOrArrayOffset).Append(" of "); + sb.Append(parentName).Append(" at 0x").Append(parent.ObjectAddress.ToString("X")); + sb.Append(" <- "); break; + } default: throw new ArgumentOutOfRangeException(nameof(child), "Invalid LoadedReason"); } From b717d7677cfd6750741d291f1cfed7faf4e429fb Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Thu, 17 Aug 2023 15:32:06 +0900 Subject: [PATCH 07/19] Small refactor --- UMS.Analysis/Structures/Objects/ManagedClassInstance.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs index 35b3631..b2496dc 100644 --- a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs +++ b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs @@ -104,7 +104,7 @@ public ManagedClassInstance(SnapshotFile file, RawManagedObjectInfo info, Manage for (var i = 0; i < arrayElementCount; i++) { var elementData = arrayData[(i * elementTypeSize)..]; - Fields[i] = ReadFieldValue(file, elementData, depth, elementFlags, elementTypeSize, elementType.TypeIndex, i); + Fields[i] = ReadArrayEntry(file, elementData, depth, elementFlags, elementTypeSize, elementType.TypeIndex, i); } return; @@ -167,7 +167,7 @@ private IFieldValue ReadFieldValue(SnapshotFile file, BasicFieldInfoCache info, return new ComplexFieldValue(file, info, this, fieldData, depth + 1, array); } - private IFieldValue ReadFieldValue(SnapshotFile file, Span fieldData, int depth, TypeFlags fieldTypeFlags, int fieldTypeSize, int fieldTypeIndex, int arrayOffset) + private IFieldValue ReadArrayEntry(SnapshotFile file, Span fieldData, int depth, TypeFlags fieldTypeFlags, int fieldTypeSize, int fieldTypeIndex, int arrayOffset) { BasicFieldInfoCache info = new() { From 31e1d8493b77ec2c8c6179e6fb450b96d0f753af Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Thu, 17 Aug 2023 15:32:19 +0900 Subject: [PATCH 08/19] Log if an object is itself a LMS in the retention path --- .../Objects/ManagedClassInstance.cs | 41 +++++++++++++++++++ UnityMemorySnapshotThing/Program.cs | 37 ++++++----------- 2 files changed, 53 insertions(+), 25 deletions(-) diff --git a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs index b2496dc..44c03d3 100644 --- a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs +++ b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs @@ -262,6 +262,17 @@ private void AppendRetentionReason(StringBuilder sb, SnapshotFile file, ManagedC var field = fieldList.First(f => f.FieldIndex == child.FieldIndexOrArrayOffset); sb.Append("Field ").Append(file.GetFieldName(field.FieldIndex)).Append(" of "); sb.Append(parentName).Append(" at 0x").Append(parent.ObjectAddress.ToString("X")); + + if (parent.InheritsFromUnityEngineObject(file)) + { + var parentInst = file.GetOrCreateManagedClassInstance(parent.ObjectAddress); + + if (parentInst.HasValue && parentInst.Value.IsLeakedManagedShell(file)) + sb.Append(" (leaked managed shell)"); + else + sb.Append(" (unity object, non-leaked)"); + } + sb.Append(" <- "); break; } @@ -277,4 +288,34 @@ private void AppendRetentionReason(StringBuilder sb, SnapshotFile file, ManagedC throw new ArgumentOutOfRangeException(nameof(child), "Invalid LoadedReason"); } } + + public bool IsLeakedManagedShell(SnapshotFile file) + { + if (!InheritsFromUnityEngineObject(file)) + //Can't be a leaked managed shell if it's not a managed shell at all + return false; + + // if (Fields == null) + // return false; //Can't check + + var fields = file.GetInstanceFieldInfoForTypeIndex(TypeInfo.TypeIndex); + for (var fieldNumber = 0; fieldNumber < fields.Length; fieldNumber++) + { + var basicFieldInfoCache = fields[fieldNumber]; + var name = file.GetFieldName(basicFieldInfoCache.FieldIndex); + + if (name == "m_CachedPtr") + { + var value = Fields[fieldNumber]; + + if (value is not IntegerFieldValue integerFieldValue) + throw new Exception("Expected integer field value"); + + return integerFieldValue.Value == 0; + } + } + + //Couldn't find the m_CachedPtr field. Weird, but return false. + return false; + } } \ No newline at end of file diff --git a/UnityMemorySnapshotThing/Program.cs b/UnityMemorySnapshotThing/Program.cs index ddf4bb2..baddef0 100644 --- a/UnityMemorySnapshotThing/Program.cs +++ b/UnityMemorySnapshotThing/Program.cs @@ -87,36 +87,21 @@ private static void FindLeakedUnityObjects(SnapshotFile file) var leakedTypes = new Dictionary(); foreach (var managedClassInstance in unityEngineObjects) { - var fields = file.GetInstanceFieldInfoForTypeIndex(managedClassInstance.TypeInfo.TypeIndex); - for (var fieldNumber = 0; fieldNumber < fields.Length; fieldNumber++) + if (managedClassInstance.IsLeakedManagedShell(file)) { - var basicFieldInfoCache = fields[fieldNumber]; - var name = file.GetFieldName(basicFieldInfoCache.FieldIndex); + var typeName = file.GetTypeName(managedClassInstance.TypeInfo.TypeIndex); - if (name == "m_CachedPtr") - { - var value = managedClassInstance.Fields[fieldNumber]; - - if(value is not IntegerFieldValue integerFieldValue) - throw new Exception("Expected integer field value"); - - if (integerFieldValue.Value == 0) - { - var typeName = file.GetTypeName(managedClassInstance.TypeInfo.TypeIndex); + str = $"Found leaked managed object of type: {typeName} at memory address 0x{managedClassInstance.ObjectAddress:X}"; + Console.WriteLine(str); + ret.AppendLine(str); - str = $"Found leaked managed object of type: {typeName} at memory address 0x{managedClassInstance.ObjectAddress:X}"; - Console.WriteLine(str); - ret.AppendLine(str); - - str = $" Retention Path: {managedClassInstance.GetFirstObservedRetentionPath(file)}"; - Console.WriteLine(str); - ret.AppendLine(str); + str = $" Retention Path: {managedClassInstance.GetFirstObservedRetentionPath(file)}"; + Console.WriteLine(str); + ret.AppendLine(str); - leakedTypes[typeName] = leakedTypes.GetValueOrDefault(typeName) + 1; + leakedTypes[typeName] = leakedTypes.GetValueOrDefault(typeName) + 1; - numLeaked++; - } - } + numLeaked++; } } @@ -131,4 +116,6 @@ private static void FindLeakedUnityObjects(SnapshotFile file) File.WriteAllText("leaked_objects.txt", ret.ToString()); } + + } \ No newline at end of file From e88ab71d8b0798205646701ec4ce5cf1ed760c46 Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Thu, 17 Aug 2023 16:36:24 +0900 Subject: [PATCH 09/19] Update README.md to be a little more informative --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index beeb21f..dd33f27 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,18 @@ Tool to analyze unity memory snapshot files (`*.snapshot`) outside of unity, and find leaked managed shells. Also includes a library for you to work with these files yourself. + +The primary use case of the executables in the published releases is to be run with a path to a unity memory snapshot. It will parse the snapshot and output all objects it can find that are what Unity calls "Leaked Managed Shells". These are Unity objects that have been destroyed but are still referenced from c# code, and so act as a memory leak. + +This tool is - in my opinion - a better workflow than the official Memory Profiler for a couple of reasons: +- It shows the direct retention path that's causing an object to be loaded. +- It's a *lot* faster than the in-editor profiler. In my tests, a dump that takes the editor ~90 seconds to load, and a further 30 seconds to filter to the leaked objects, takes less than 10 seconds to be processed by this tool. +- It filters out the noise and only shows you leaked objects. +- It shows you which specific fields are causing an object to be referenced, instead of just showing which objects reference it. +- It uses a lot less memory than the editor. Dumps that cause my editor to use in excess of 14gb of memory (while themselves being ~800mb) only take a couple GB to process. + +But there are also a couple reasons you might need to use the in-editor one over this tool. Primarily: +- This tool doesn't show non-leaked shells, so it's useless for e.g. showing usage by category. +- This tool doesn't calculate how *much* memory is leaked, just how many objects. +- This tool doesn't allow comparing two snapshots. +- This tool doesn't support snapshots greater than 2GiB in size. From 39bae18e4bff0d733629d04014bf8d427087e5f9 Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Thu, 25 Jan 2024 22:47:27 +0000 Subject: [PATCH 10/19] Fixup support for newer (> 2022) versions. --- .../Structures/Objects/StringFieldValue.cs | 8 ++++++++ UMS.LowLevel/LowLevelSnapshotFile.cs | 5 ++++- UMS.LowLevel/Structures/EntryType.cs | 18 ++++++++++++++++++ UMS.LowLevel/Structures/FormatVersion.cs | 7 ++++++- UMS.LowLevel/Structures/ManagedHeapSection.cs | 18 ++++++++++++++++-- 5 files changed, 52 insertions(+), 4 deletions(-) diff --git a/UMS.Analysis/Structures/Objects/StringFieldValue.cs b/UMS.Analysis/Structures/Objects/StringFieldValue.cs index 9abc7c8..a8ee43c 100644 --- a/UMS.Analysis/Structures/Objects/StringFieldValue.cs +++ b/UMS.Analysis/Structures/Objects/StringFieldValue.cs @@ -38,6 +38,14 @@ public StringFieldValue(SnapshotFile file, Span data) data = managedObjectInfo.Data; var offset = file.VirtualMachineInformation.ObjectHeaderSize + 4; + + if(offset > data.Length) + { + FailedToParse = true; + FailedParseFromPtr = ptr; + return; + } + var stringPtr = data[offset..]; var end = stringPtr.LastIndexOf(new byte[] { 0, 0 }); diff --git a/UMS.LowLevel/LowLevelSnapshotFile.cs b/UMS.LowLevel/LowLevelSnapshotFile.cs index ad2b644..304582a 100644 --- a/UMS.LowLevel/LowLevelSnapshotFile.cs +++ b/UMS.LowLevel/LowLevelSnapshotFile.cs @@ -86,8 +86,11 @@ public unsafe LowLevelSnapshotFile(string path) ReadMetadataForAllChapters(entryTypeToChapterOffset); VirtualMachineInformation = ReadChapterAsStruct(EntryType.Metadata_VirtualMachineInformation); + + var areHeapAddressesEncoded = SnapshotFormatVersion >= FormatVersion.MemLabelSizeAndHeapIdVersion; + ManagedHeapSectionStartAddresses = ReadValueTypeChapter(EntryType.ManagedHeapSections_StartAddress, 0, -1).ToArray() - .Select((a, i) => new ManagedHeapSection(a, ReadChapterBody(EntryType.ManagedHeapSections_Bytes, i, 1))) + .Select((a, i) => new ManagedHeapSection(a, areHeapAddressesEncoded, ReadChapterBody(EntryType.ManagedHeapSections_Bytes, i, 1))) .ToArray(); Array.Sort(ManagedHeapSectionStartAddresses); diff --git a/UMS.LowLevel/Structures/EntryType.cs b/UMS.LowLevel/Structures/EntryType.cs index 0584cb9..10ebb77 100644 --- a/UMS.LowLevel/Structures/EntryType.cs +++ b/UMS.LowLevel/Structures/EntryType.cs @@ -86,5 +86,23 @@ public enum EntryType : ushort NativeAllocatorInfo_PeakUsedSize, NativeAllocatorInfo_AllocationCount, NativeAllocatorInfo_Flags, //80 + // NativeObjectMetaDataVersion = 15 + // Adds meta data + ObjectMetaData_MetaDataBufferIndicies, + ObjectMetaData_MetaDataBuffer, + // SystemMemoryRegionsVersion = 16 + // Adds system memory regions + SystemMemoryRegions_Address, + SystemMemoryRegions_Size, + SystemMemoryRegions_Resident, + SystemMemoryRegions_Type, + SystemMemoryRegions_Name, + // SystemMemoryResidentPagesVersion = 17 + // Adds system memory resident pages + SystemMemoryResidentPages_Address, + SystemMemoryResidentPages_FirstPageIndex, + SystemMemoryResidentPages_LastPageIndex, + SystemMemoryResidentPages_PagesState, + SystemMemoryResidentPages_PageSize, Count, //used to keep track of entry count, only add c++ matching entries above this one } \ No newline at end of file diff --git a/UMS.LowLevel/Structures/FormatVersion.cs b/UMS.LowLevel/Structures/FormatVersion.cs index e311de0..1c4ff92 100644 --- a/UMS.LowLevel/Structures/FormatVersion.cs +++ b/UMS.LowLevel/Structures/FormatVersion.cs @@ -6,6 +6,11 @@ public enum FormatVersion : uint NativeConnectionsAsInstanceIdsVersion = 10, //native object collection reworked, added new gchandleIndex array to native objects for fast managed object access (2019.3 or newer?) ProfileTargetInfoAndMemStatsVersion = 11, //added profile target info and memory summary struct (shortly before 2021.2.0a12 on 2021.2, backported together with v.12) MemLabelSizeAndHeapIdVersion = 12, //added gc heap / vm heap identification encoded within each heap address and memory label size reporting (2021.2.0a12, 2021.1.9, 2020.3.12f1, 2019.4.29f1 or newer) + // below are present from 2022.2+ SceneRootsAndAssetBundlesVersion = 13, //added scene roots and asset bundle relations (not yet landed) - GfxResourceReferencesAndAllocatorsVersion = 14 //added gfx resource to root mapping and allocators information (including extra allocator information to memory labels) + GfxResourceReferencesAndAllocatorsVersion = 14, //added gfx resource to root mapping and allocators information (including extra allocator information to memory labels) + NativeObjectMetaDataVersion = 15, // adds the ability to get meta data for native objects + SystemMemoryRegionsVersion = 16, // adds system memory regions information + // below are present from 2023.1+ + SystemMemoryResidentPagesVersion = 17, // adds system memory resident pages information } \ No newline at end of file diff --git a/UMS.LowLevel/Structures/ManagedHeapSection.cs b/UMS.LowLevel/Structures/ManagedHeapSection.cs index 595c398..b58501e 100644 --- a/UMS.LowLevel/Structures/ManagedHeapSection.cs +++ b/UMS.LowLevel/Structures/ManagedHeapSection.cs @@ -2,13 +2,27 @@ public class ManagedHeapSection : IComparable { + private const ulong referenceBit = 1UL << 63; + public ulong VirtualAddress; public ulong VirtualAddressEnd; public byte[] Heap; - public ManagedHeapSection(ulong virtualAddress, byte[] heap) + public bool IsVmSection; + + public ManagedHeapSection(ulong virtualAddress, bool areAddressesEncoded, byte[] heap) { - VirtualAddress = virtualAddress; + if (areAddressesEncoded) + { + VirtualAddress = virtualAddress & ~referenceBit; + IsVmSection = (virtualAddress & referenceBit) != 0; + } + else + { + VirtualAddress = virtualAddress; + IsVmSection = false; + } + Heap = heap; VirtualAddressEnd = VirtualAddress + (ulong) Heap.Length; } From 9a24ec013ddf8c4a4e6a8d321eb843d0f0993333 Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Tue, 12 Mar 2024 20:09:34 -0700 Subject: [PATCH 11/19] Don't choke as much on malformed data --- UMS.Analysis/SnapshotFile.cs | 18 ++++++++++++------ .../Structures/Objects/ManagedClassInstance.cs | 4 +++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/UMS.Analysis/SnapshotFile.cs b/UMS.Analysis/SnapshotFile.cs index ab367f1..3df2901 100644 --- a/UMS.Analysis/SnapshotFile.cs +++ b/UMS.Analysis/SnapshotFile.cs @@ -134,15 +134,21 @@ public RawManagedObjectInfo ParseManagedObjectInfo(ulong address) return new(); info.TypeInfoAddress = ReadPointer(typeInfoSpan); - TypeIndicesByPointer.TryGetValue(info.TypeInfoAddress, out info.TypeDescriptionIndex); - - if (!info.IsKnownType) - throw new($"Failed to resolve type for object at {address:X}"); + if (!TypeIndicesByPointer.TryGetValue(info.TypeInfoAddress, out info.TypeDescriptionIndex) || !info.IsKnownType) + { + Console.WriteLine($"WARNING: Failed to resolve type for object at {address:X}"); + + //Cache the failure - let's not waste time. + ret = _managedObjectInfoCache[address] = new(); + return ret; + } } var typeIndex = info.TypeDescriptionIndex; info.Flags = GetTypeFlagsByIndex(typeIndex); info.Size = SizeOfObjectInBytes(info, heap); + if (info.Size == 0) + throw new("Size 0?"); info.Data = heap[..info.Size].ToArray(); info.SelfAddress = address; @@ -190,8 +196,8 @@ private int GetObjectSizeFromArrayInBytes(RawManagedObjectInfo info, Span if (arrayLength > heap.Length) { - Console.WriteLine($"Warning: Skipping unreasonable array length {arrayLength}"); - return VirtualMachineInformation.ArrayHeaderSize; //Sanity bailout + Console.WriteLine($"Warning: Reducing array length {arrayLength} to {heap.Length} because the heap doesn't contain all the data."); + return heap.Length; //Sanity bailout } //Need to check if the array element type is a value type diff --git a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs index 44c03d3..46f07ab 100644 --- a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs +++ b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs @@ -100,6 +100,8 @@ public ManagedClassInstance(SnapshotFile file, RawManagedObjectInfo info, Manage var elementTypeSize = (elementFlags & TypeFlags.ValueType) != 0 ? file.GetTypeDescriptionSizeBytes(elementType.TypeIndex) : 8; var arrayData = info.Data.AsSpan(file.VirtualMachineInformation.ArrayHeaderSize..); + arrayElementCount = Math.Min(arrayElementCount, arrayData.Length / elementTypeSize); //Just in case the array length is wrong + Fields = new IFieldValue[arrayElementCount]; for (var i = 0; i < arrayElementCount; i++) { @@ -304,7 +306,7 @@ public bool IsLeakedManagedShell(SnapshotFile file) var basicFieldInfoCache = fields[fieldNumber]; var name = file.GetFieldName(basicFieldInfoCache.FieldIndex); - if (name == "m_CachedPtr") + if (name == "m_CachedPtr" && Fields.Length > fieldNumber) { var value = Fields[fieldNumber]; From d209d4a935c7613651059546e046898897619eed Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Fri, 24 May 2024 15:11:28 +0100 Subject: [PATCH 12/19] Ensure we really gather all managed objects we can --- UMS.Analysis/SnapshotFile.cs | 21 ++++++++++--- .../Structures/Objects/ComplexFieldValue.cs | 7 +++-- .../Structures/Objects/IFieldValue.cs | 21 +++++++++++++ .../Objects/ManagedClassInstance.cs | 31 +++++-------------- 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/UMS.Analysis/SnapshotFile.cs b/UMS.Analysis/SnapshotFile.cs index 3df2901..56c12d6 100644 --- a/UMS.Analysis/SnapshotFile.cs +++ b/UMS.Analysis/SnapshotFile.cs @@ -32,8 +32,11 @@ public class SnapshotFile : LowLevelSnapshotFile private readonly Dictionary _typeNamesByTypeIndex = new(); private readonly Dictionary _fieldNamesByFieldIndex = new(); + + //Stores any additional ManagedClassInstances that we create from reading fields that are value types, because those don't have a pointer to them so we can't cache them + private readonly List _additionalManagedValueTypeInstances = new(1024 * 1024 * 4); - public IEnumerable AllManagedClassInstances => _managedClassInstanceCache.Values; + public IEnumerable AllManagedClassInstances => _managedClassInstanceCache.Values.Concat(_additionalManagedValueTypeInstances); public SnapshotFile(string path) : base(path) { @@ -80,11 +83,14 @@ public void LoadManagedObjectsFromStaticFields() foreach (var field in staticFields) { StaticFieldsToOwningTypes[field.FieldIndex] = typeIndex; - if(field.IsValueType) - continue; //TODO - - if(field.IsArray) + if (field.IsValueType) + { + //Simply read this, if it has any managed objects in it, they'll be added to the cache + IFieldValue.Read(this, field, typeFieldBytes[field.FieldOffset..], 0, LoadedReason.StaticField, null); continue; + } + + //If array, then we have a pointer to it and can use the exact same logic as for static non-array fields var fieldOffset = field.FieldOffset; @@ -391,4 +397,9 @@ public string GetFieldName(int fieldIndex) _fieldNamesByFieldIndex[fieldIndex] = ret = ReadSingleStringFromChapter(EntryType.FieldDescriptions_Name, fieldIndex); return ret; } + + internal void RegisterAdditionalManagedValueTypeInstance(ManagedClassInstance instance) + { + _additionalManagedValueTypeInstances.Add(instance); + } } \ No newline at end of file diff --git a/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs b/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs index 694c456..6e64480 100644 --- a/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs +++ b/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs @@ -10,7 +10,7 @@ public struct ComplexFieldValue : IFieldValue public ManagedClassInstance? Value { get; } - public ComplexFieldValue(SnapshotFile file, BasicFieldInfoCache info, ManagedClassInstance parent, Span data, int depth, bool array) + public ComplexFieldValue(SnapshotFile file, BasicFieldInfoCache info, ManagedClassInstance? parent, Span data, int depth, LoadedReason loadedReason) { IsNull = false; FailedToParse = false; @@ -22,8 +22,9 @@ public ComplexFieldValue(SnapshotFile file, BasicFieldInfoCache info, ManagedCla var size = info.FieldTypeSize; if (size > 0) //Have observed a negative size (-8), MIGHT be for pointers to value types, so we'll fall back to the below logic. { - var vtInst = new ManagedClassInstance(file, info.TypeDescriptionIndex, info.Flags, size, data, parent, depth, array ? LoadedReason.ArrayElement : LoadedReason.InstanceField, info.FieldIndex); + var vtInst = new ManagedClassInstance(file, info.TypeDescriptionIndex, info.Flags, size, data, parent, depth, loadedReason, info.FieldIndex); Value = vtInst; + file.RegisterAdditionalManagedValueTypeInstance(vtInst); return; } } @@ -38,7 +39,7 @@ public ComplexFieldValue(SnapshotFile file, BasicFieldInfoCache info, ManagedCla return; } - var mci = file.GetOrCreateManagedClassInstance(ptr, parent, depth, array ? LoadedReason.ArrayElement : LoadedReason.InstanceField, info.FieldIndex); + var mci = file.GetOrCreateManagedClassInstance(ptr, parent, depth, loadedReason, info.FieldIndex); if (mci == null) { diff --git a/UMS.Analysis/Structures/Objects/IFieldValue.cs b/UMS.Analysis/Structures/Objects/IFieldValue.cs index c1a9ab6..24eeee7 100644 --- a/UMS.Analysis/Structures/Objects/IFieldValue.cs +++ b/UMS.Analysis/Structures/Objects/IFieldValue.cs @@ -5,4 +5,25 @@ public interface IFieldValue public bool IsNull { get; } public bool FailedToParse { get; } public ulong FailedParseFromPtr { get; } + + public static IFieldValue Read(SnapshotFile file, BasicFieldInfoCache info, Span fieldData, int depth, LoadedReason loadedReason, ManagedClassInstance? parent = null) + { + //For all integer types, we just handle unsigned as signed + if (info.TypeDescriptionIndex == file.WellKnownTypes.String) + return new StringFieldValue(file, fieldData); + if (info.TypeDescriptionIndex == file.WellKnownTypes.Boolean || info.TypeDescriptionIndex == file.WellKnownTypes.Byte) + return new IntegerFieldValue(fieldData[0]); + if (info.TypeDescriptionIndex == file.WellKnownTypes.Int16 || info.TypeDescriptionIndex == file.WellKnownTypes.UInt16 || info.TypeDescriptionIndex == file.WellKnownTypes.Char) + return new IntegerFieldValue(BitConverter.ToInt16(fieldData)); + if (info.TypeDescriptionIndex == file.WellKnownTypes.Int32 || info.TypeDescriptionIndex == file.WellKnownTypes.UInt32) + return new IntegerFieldValue(BitConverter.ToInt32(fieldData)); + if (info.TypeDescriptionIndex == file.WellKnownTypes.Int64 || info.TypeDescriptionIndex == file.WellKnownTypes.UInt64 || info.TypeDescriptionIndex == file.WellKnownTypes.IntPtr) + return new IntegerFieldValue(BitConverter.ToInt64(fieldData)); + if (info.TypeDescriptionIndex == file.WellKnownTypes.Single) + return new FloatingPointFieldValue(BitConverter.ToSingle(fieldData)); + if (info.TypeDescriptionIndex == file.WellKnownTypes.Double) + return new FloatingPointFieldValue(BitConverter.ToDouble(fieldData)); + + return new ComplexFieldValue(file, info, parent, fieldData, depth + 1, loadedReason); + } } \ No newline at end of file diff --git a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs index 46f07ab..e8da1c8 100644 --- a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs +++ b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs @@ -24,7 +24,7 @@ public readonly struct ManagedClassInstance public bool IsValueType => (TypeDescriptionFlags & TypeFlags.ValueType) == TypeFlags.ValueType; - public ManagedClassInstance(SnapshotFile file, int typeDescriptionIndex, TypeFlags flags, int size, Span data, ManagedClassInstance parent, int depth, LoadedReason loadedReason, int fieldIndexOrArrayOffset = int.MinValue) + public ManagedClassInstance(SnapshotFile file, int typeDescriptionIndex, TypeFlags flags, int size, Span data, ManagedClassInstance? parent, int depth, LoadedReason loadedReason, int fieldIndexOrArrayOffset = int.MinValue) { TypeDescriptionFlags = flags; @@ -106,7 +106,8 @@ public ManagedClassInstance(SnapshotFile file, RawManagedObjectInfo info, Manage for (var i = 0; i < arrayElementCount; i++) { var elementData = arrayData[(i * elementTypeSize)..]; - Fields[i] = ReadArrayEntry(file, elementData, depth, elementFlags, elementTypeSize, elementType.TypeIndex, i); + if(elementData.Length > 0) + Fields[i] = ReadArrayEntry(file, elementData, depth, elementFlags, elementTypeSize, elementType.TypeIndex, i); } return; @@ -142,32 +143,14 @@ private IFieldValue[] ReadFields(SnapshotFile file, Span data, int depth) var fieldData = data[fieldOffset..]; - fields[index] = ReadFieldValue(file, info, fieldData, depth, false); + fields[index] = ReadFieldValue(file, info, fieldData, depth, LoadedReason.InstanceField); } return fields; } - private IFieldValue ReadFieldValue(SnapshotFile file, BasicFieldInfoCache info, Span fieldData, int depth, bool array) - { - //For all integer types, we just handle unsigned as signed - if (info.TypeDescriptionIndex == file.WellKnownTypes.String) - return new StringFieldValue(file, fieldData); - if (info.TypeDescriptionIndex == file.WellKnownTypes.Boolean || info.TypeDescriptionIndex == file.WellKnownTypes.Byte) - return new IntegerFieldValue(fieldData[0]); - if (info.TypeDescriptionIndex == file.WellKnownTypes.Int16 || info.TypeDescriptionIndex == file.WellKnownTypes.UInt16 || info.TypeDescriptionIndex == file.WellKnownTypes.Char) - return new IntegerFieldValue(BitConverter.ToInt16(fieldData)); - if (info.TypeDescriptionIndex == file.WellKnownTypes.Int32 || info.TypeDescriptionIndex == file.WellKnownTypes.UInt32) - return new IntegerFieldValue(BitConverter.ToInt32(fieldData)); - if (info.TypeDescriptionIndex == file.WellKnownTypes.Int64 || info.TypeDescriptionIndex == file.WellKnownTypes.UInt64 || info.TypeDescriptionIndex == file.WellKnownTypes.IntPtr) - return new IntegerFieldValue(BitConverter.ToInt64(fieldData)); - if (info.TypeDescriptionIndex == file.WellKnownTypes.Single) - return new FloatingPointFieldValue(BitConverter.ToSingle(fieldData)); - if (info.TypeDescriptionIndex == file.WellKnownTypes.Double) - return new FloatingPointFieldValue(BitConverter.ToDouble(fieldData)); - - return new ComplexFieldValue(file, info, this, fieldData, depth + 1, array); - } + private IFieldValue ReadFieldValue(SnapshotFile file, BasicFieldInfoCache info, Span fieldData, int depth, LoadedReason loadedReason) + => IFieldValue.Read(file, info, fieldData, depth, loadedReason, this); private IFieldValue ReadArrayEntry(SnapshotFile file, Span fieldData, int depth, TypeFlags fieldTypeFlags, int fieldTypeSize, int fieldTypeIndex, int arrayOffset) { @@ -179,7 +162,7 @@ private IFieldValue ReadArrayEntry(SnapshotFile file, Span fieldData, int FieldTypeSize = fieldTypeSize, }; - return ReadFieldValue(file, info, fieldData, depth, true); + return ReadFieldValue(file, info, fieldData, depth, LoadedReason.ArrayElement); } private bool IsEnumType(SnapshotFile file) From 746f5922e200ad259da8ca2a3e2b000a0bfdbbae Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Thu, 21 Nov 2024 18:36:34 +0000 Subject: [PATCH 13/19] Fix edge case with thread-static value type fields --- UMS.Analysis/SnapshotFile.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/UMS.Analysis/SnapshotFile.cs b/UMS.Analysis/SnapshotFile.cs index 56c12d6..5b944ba 100644 --- a/UMS.Analysis/SnapshotFile.cs +++ b/UMS.Analysis/SnapshotFile.cs @@ -83,19 +83,22 @@ public void LoadManagedObjectsFromStaticFields() foreach (var field in staticFields) { StaticFieldsToOwningTypes[field.FieldIndex] = typeIndex; + + var fieldOffset = field.FieldOffset; + if(fieldOffset < 0) + continue; //Thread-static fields for example + if (field.IsValueType) { - //Simply read this, if it has any managed objects in it, they'll be added to the cache - IFieldValue.Read(this, field, typeFieldBytes[field.FieldOffset..], 0, LoadedReason.StaticField, null); + if (fieldOffset < typeFieldBytes.Length - 1) + { + //Simply read this, if it has any managed objects in it, they'll be added to the cache + IFieldValue.Read(this, field, typeFieldBytes[fieldOffset..], 0, LoadedReason.StaticField, null); + } continue; } //If array, then we have a pointer to it and can use the exact same logic as for static non-array fields - - var fieldOffset = field.FieldOffset; - - if(fieldOffset < 0) - continue; //Generics, mainly var fieldPointer = MemoryMarshal.Read(typeFieldBytes[fieldOffset..]); if (fieldPointer == 0) From c2c758ecc926e195584def73e32fd3bc89ecf0e6 Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Fri, 22 Nov 2024 16:27:00 +0000 Subject: [PATCH 14/19] Basic support for > 2GiB snapshots --- README.md | 2 +- UMS.LowLevel/LowLevelSnapshotFile.cs | 26 ++++---- .../Structures/FileStructure/Block.cs | 48 +++++++------- .../Structures/FileStructure/BlockHeader.cs | 4 +- .../Structures/FileStructure/Chapter.cs | 6 +- .../Utils/MemoryMappedFileSpanHelper.cs | 65 ++++++++++++++++--- UnityMemorySnapshotThing.sln | 1 + 7 files changed, 100 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index dd33f27..f9a3297 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,4 @@ But there are also a couple reasons you might need to use the in-editor one over - This tool doesn't show non-leaked shells, so it's useless for e.g. showing usage by category. - This tool doesn't calculate how *much* memory is leaked, just how many objects. - This tool doesn't allow comparing two snapshots. -- This tool doesn't support snapshots greater than 2GiB in size. +- This tool may not have full support for snapshots greater than 2GiB in size. It works in theory but may not catch everything. diff --git a/UMS.LowLevel/LowLevelSnapshotFile.cs b/UMS.LowLevel/LowLevelSnapshotFile.cs index 304582a..7b1b861 100644 --- a/UMS.LowLevel/LowLevelSnapshotFile.cs +++ b/UMS.LowLevel/LowLevelSnapshotFile.cs @@ -46,8 +46,8 @@ public unsafe LowLevelSnapshotFile(string path) if(magic != MagicNumbers.HeaderMagic || endMagic != MagicNumbers.FooterMagic) throw new($"Magic number mismatch. Expected {MagicNumbers.HeaderMagic} and {MagicNumbers.FooterMagic} but got {magic} and {endMagic}"); - var directoryMetadataOffset = (int) _file.As(^12..); //8 bytes before end magic - var directoryMetadata = _file.As(directoryMetadataOffset..); + var directoryMetadataOffset = _file.As(^12..); //8 bytes before end magic + var directoryMetadata = _file.As(directoryMetadataOffset, sizeof(DirectoryMetadata)); if(directoryMetadata.Magic != MagicNumbers.DirectoryMagic) throw new($"Directory magic number mismatch. Expected {MagicNumbers.DirectoryMagic} but got {directoryMetadata.Magic}"); @@ -55,7 +55,7 @@ public unsafe LowLevelSnapshotFile(string path) if (directoryMetadata.Version != MagicNumbers.SupportedDirectoryVersion) throw new($"Directory version mismatch. Expected {MagicNumbers.SupportedDirectoryVersion} but got {directoryMetadata.Version}"); - var blockSection = _file.As(directoryMetadata.BlocksOffset); + var blockSection = _file.As((long) directoryMetadata.BlocksOffset); if (blockSection.Version != MagicNumbers.SupportedBlockSectionVersion) throw new($"Block section version mismatch. Expected {MagicNumbers.SupportedBlockSectionVersion} but got {blockSection.Version}"); @@ -73,11 +73,11 @@ public unsafe LowLevelSnapshotFile(string path) var startOfEntryOffsets = directoryMetadataOffset + sizeof(DirectoryMetadata); //Start of entry offsets is right after directory metadata var endOfEntryOffsets = startOfEntryOffsets + entryOffsetCount * sizeof(long); - var entryTypeToChapterOffset = _file.AsSpan(startOfEntryOffsets..endOfEntryOffsets); + var entryTypeToChapterOffset = _file.AsSpan(startOfEntryOffsets, (int)(endOfEntryOffsets - startOfEntryOffsets)); - var startOfDataBlockOffsets = (int) directoryMetadata.BlocksOffset + sizeof(BlockSection); + var startOfDataBlockOffsets = (long) directoryMetadata.BlocksOffset + sizeof(BlockSection); var endOfDataBlockOffsets = startOfDataBlockOffsets + blockSection.Count * sizeof(long); - var dataBlockOffsets = _file.AsSpan(startOfDataBlockOffsets..endOfDataBlockOffsets); + var dataBlockOffsets = _file.AsSpan(startOfDataBlockOffsets, (int) (endOfDataBlockOffsets - startOfDataBlockOffsets)); _blocks = new Block[dataBlockOffsets.Length]; ReadAllBlocks(dataBlockOffsets); @@ -101,7 +101,7 @@ private unsafe void ReadAllBlocks(Span dataBlockOffsets) for (var i = 0; i < dataBlockOffsets.Length; i++) { var header = _file.As(dataBlockOffsets[i]); - _blocks[i] = new(header, _file, (int)(dataBlockOffsets[i] + sizeof(BlockHeader))); + _blocks[i] = new(header, _file, dataBlockOffsets[i] + sizeof(BlockHeader)); } } @@ -112,7 +112,7 @@ private void ReadMetadataForAllChapters(Span entryTypeToChapterOffset) if (entryTypeToChapterOffset[i] == 0) continue; - _chaptersByEntryType[(EntryType)i] = ReadChapterMetadata((int)entryTypeToChapterOffset[i]); + _chaptersByEntryType[(EntryType)i] = ReadChapterMetadata(entryTypeToChapterOffset[i]); if (_chaptersByEntryType[(EntryType)i].AdditionalEntryStorage != null) { @@ -121,7 +121,7 @@ private void ReadMetadataForAllChapters(Span entryTypeToChapterOffset) } } - private unsafe Chapter ReadChapterMetadata(int chapterOffset) + private unsafe Chapter ReadChapterMetadata(long chapterOffset) { var header = _file.As(chapterOffset); var chapter = new Chapter(header) @@ -132,9 +132,9 @@ private unsafe Chapter ReadChapterMetadata(int chapterOffset) if (header.Format == EntryFormat.DynamicSizeElementArray) { var dataStart = chapterOffset + sizeof(ChapterHeader); - var dataEnd = dataStart + (int)header.Count * sizeof(long); + var dataEnd = dataStart + header.Count * sizeof(long); - chapter.AdditionalEntryStorage = _file.AsSpan(dataStart..dataEnd).ToArray(); + chapter.AdditionalEntryStorage = _file.AsSpan(dataStart, (int)(dataEnd - dataStart)).ToArray(); } return chapter; @@ -172,8 +172,8 @@ public byte[] ReadChapterBody(EntryType entryType, int startOffset, int count) var chapter = _chaptersByEntryType[entryType]; var block = chapter.Block; - var start = (uint) chapter.GetOffsetIntoBlock((uint)startOffset); - var size = (int) chapter.ComputeByteSizeForEntryRange(startOffset, count, false); + var start = chapter.GetOffsetIntoBlock(startOffset); + var size = chapter.ComputeByteSizeForEntryRange(startOffset, count, false); return block.Read(start, size); } diff --git a/UMS.LowLevel/Structures/FileStructure/Block.cs b/UMS.LowLevel/Structures/FileStructure/Block.cs index b122110..3c9a9de 100644 --- a/UMS.LowLevel/Structures/FileStructure/Block.cs +++ b/UMS.LowLevel/Structures/FileStructure/Block.cs @@ -13,7 +13,7 @@ public struct Block public long StartOffset => Offsets[0]; - public Block(BlockHeader header, MemoryMappedFileSpanHelper file, int start) + public Block(BlockHeader header, MemoryMappedFileSpanHelper file, long start) { _file = file; Header = header; @@ -38,7 +38,7 @@ private List MergeRanges() var ranges = new List(); var currOffset = RangeFromOffset(0, Offsets[0]); var index = 1; - var blockOffset = (int) Header.ChunkSize; + var blockOffset = (long) Header.ChunkSize; while (index < Offsets.Length) { @@ -63,38 +63,38 @@ private List MergeRanges() return ranges; } - private MergedRange RangeFromOffset(int blockOffset, long fileOffset) + private MergedRange RangeFromOffset(long blockOffset, long fileOffset) { - var start = (int)fileOffset; + var start = fileOffset; return new(blockOffset, start, (int)Header.ChunkSize); } - public bool TryReadAsSpan(uint index, int length, out Span result) + public bool TryReadAsSpan(long index, int length, out Span result) { result = default; - var relevantBlock = BinarySearchForRelevantChunk(MergedRanges, (int)index); + var relevantBlock = BinarySearchForRelevantChunk(MergedRanges, index); if (relevantBlock == -1) //No block found containing index return false; var range = MergedRanges[relevantBlock]; - var offset = (int)(index - range.BlockStart); + var offset = index - range.BlockStart; var remaining = range.Length - offset; if (remaining < length) return false; - result = _file.Span.Slice(range.FileStart + offset, length); + result = _file.AsSpan(range.FileStart + offset, length); return true; } - public byte[] Read(uint index, int length) + public byte[] Read(long index, long length) { - var startChunk = (uint) (index / Header.ChunkSize); - var offsetIntoFirstChunk = (uint) (index % Header.ChunkSize); - var bytesLeftInChunk = (uint) Header.ChunkSize - offsetIntoFirstChunk; + var startChunk = index / (long) Header.ChunkSize; + var offsetIntoFirstChunk = index % (long) Header.ChunkSize; + var bytesLeftInChunk = (long) Header.ChunkSize - offsetIntoFirstChunk; if (length > bytesLeftInChunk) { @@ -103,12 +103,12 @@ public byte[] Read(uint index, int length) } var chunk = Offsets[startChunk]; - var offset = (int) (chunk + offsetIntoFirstChunk); + var offset = chunk + offsetIntoFirstChunk; - return _file.Span[offset..(offset + length)].ToArray(); + return _file.AsSpan(offset, (int)length).ToArray(); } - private byte[] ReadMultipleChunks(uint startChunk, uint offsetIntoFirstChunk, int length) + private byte[] ReadMultipleChunks(long startChunk, long offsetIntoFirstChunk, long length) { var ret = new byte[length]; var bytesLeft = length; @@ -120,10 +120,10 @@ private byte[] ReadMultipleChunks(uint startChunk, uint offsetIntoFirstChunk, in while (bytesLeft > 0) { var chunkStart = Offsets[currChunk]; - var bytesToRead = Math.Min((int) Header.ChunkSize - (int) chunkOffset, bytesLeft); - var chunk = _file.Span[(int) (chunkStart + chunkOffset)..(int) (chunkStart + chunkOffset + bytesToRead)]; + var bytesToRead = Math.Min((long) Header.ChunkSize - chunkOffset, bytesLeft); + var chunk = _file.AsSpan(chunkStart + chunkOffset, (int)bytesToRead); chunk.CopyTo(ret.AsSpan(offset)); - offset += bytesToRead; + offset += (int) bytesToRead; bytesLeft -= bytesToRead; chunkOffset = 0; currChunk++; @@ -132,7 +132,7 @@ private byte[] ReadMultipleChunks(uint startChunk, uint offsetIntoFirstChunk, in return ret; } - private static int BinarySearchForRelevantChunk(MergedRange[] ranges, int offset) + private static int BinarySearchForRelevantChunk(MergedRange[] ranges, long offset) { var first = 0; var last = ranges.Length - 1; @@ -154,13 +154,13 @@ private static int BinarySearchForRelevantChunk(MergedRange[] ranges, int offset public struct MergedRange { - public int BlockStart; - public int FileStart; + public long BlockStart; + public long FileStart; public int Length; - public int BlockEnd; - public int FileEnd; + public long BlockEnd; + public long FileEnd; - public MergedRange(int blockStart, int fileStart, int length) + public MergedRange(long blockStart, long fileStart, int length) { BlockStart = blockStart; FileStart = fileStart; diff --git a/UMS.LowLevel/Structures/FileStructure/BlockHeader.cs b/UMS.LowLevel/Structures/FileStructure/BlockHeader.cs index 241163f..0a79c58 100644 --- a/UMS.LowLevel/Structures/FileStructure/BlockHeader.cs +++ b/UMS.LowLevel/Structures/FileStructure/BlockHeader.cs @@ -10,6 +10,6 @@ public struct BlockHeader public uint OffsetCount => (uint)(TotalBytes / ChunkSize) + (TotalBytes % ChunkSize == 0 ? 0u : 1u); - public Span GetOffsets(MemoryMappedFileSpanHelper file, int start) - => file.AsSpan(start..(start + (int) OffsetCount * sizeof(long))); + public Span GetOffsets(MemoryMappedFileSpanHelper file, long start) + => file.AsSpan(start, (int) OffsetCount * sizeof(long)); } \ No newline at end of file diff --git a/UMS.LowLevel/Structures/FileStructure/Chapter.cs b/UMS.LowLevel/Structures/FileStructure/Chapter.cs index fc98b72..9264399 100644 --- a/UMS.LowLevel/Structures/FileStructure/Chapter.cs +++ b/UMS.LowLevel/Structures/FileStructure/Chapter.cs @@ -14,12 +14,12 @@ public Chapter(ChapterHeader header) public uint Count => Header.Count; - public ulong GetOffsetIntoBlock(uint startOffset) => + public long GetOffsetIntoBlock(long startOffset) => Header.Format switch { - EntryFormat.SingleElement => Header.HeaderMeta, + EntryFormat.SingleElement => (long)Header.HeaderMeta, EntryFormat.ConstantSizeElementArray => Header.EntriesMeta * startOffset, - EntryFormat.DynamicSizeElementArray => startOffset == 0 ? 0u : (ulong)AdditionalEntryStorage![startOffset - 1], + EntryFormat.DynamicSizeElementArray => (long)(startOffset == 0 ? 0u : (ulong)AdditionalEntryStorage![startOffset - 1]), _ => throw new("Invalid format") }; diff --git a/UMS.LowLevel/Utils/MemoryMappedFileSpanHelper.cs b/UMS.LowLevel/Utils/MemoryMappedFileSpanHelper.cs index 82b81bf..00156e5 100644 --- a/UMS.LowLevel/Utils/MemoryMappedFileSpanHelper.cs +++ b/UMS.LowLevel/Utils/MemoryMappedFileSpanHelper.cs @@ -13,11 +13,11 @@ public unsafe class MemoryMappedFileSpanHelper : IDisposable where T : struc private readonly MemoryMappedFile _file; private readonly MemoryMappedViewAccessor _accessor; private byte* _ptr; - private int _length; + private long _length; public MemoryMappedFileSpanHelper(string filePath) { - _length = (int)new FileInfo(filePath).Length; + _length = new FileInfo(filePath).Length; _file = MemoryMappedFile.CreateFromFile(filePath, FileMode.Open, null, 0, MemoryMappedFileAccess.Read); _accessor = _file.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read); @@ -25,22 +25,69 @@ public MemoryMappedFileSpanHelper(string filePath) _accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref _ptr); } - public Span Span => MemoryMarshal.Cast(new(_ptr, _length)); + public Span Span => MemoryMarshal.Cast(new(_ptr, (int)_length)); - public Span this[Range range] => Span[range]; + public Span this[Range range] => InternalGetSpan(range); [Pure] - public TDest As(Range range) where TDest : struct => MemoryMarshal.Read(MemoryMarshal.Cast(Span[range])); + public TDest As(Range range) where TDest : struct => MemoryMarshal.Read(MemoryMarshal.Cast(InternalGetSpan(range))); [Pure] - public TDest As(ulong start) where TDest : struct => MemoryMarshal.Read(MemoryMarshal.Cast(Span[(int) start..])); + public TDest As(long start) where TDest : struct => MemoryMarshal.Read(MemoryMarshal.Cast(InternalGetSpan(start))); [Pure] - public TDest As(long start) where TDest : struct => MemoryMarshal.Read(MemoryMarshal.Cast(Span[(int) start..])); + public TDest As(long start, int length) where TDest : struct => MemoryMarshal.Read(MemoryMarshal.Cast(InternalGetSpan(start, length))); [Pure] - public TDest As(int start) where TDest : struct => MemoryMarshal.Read(MemoryMarshal.Cast(Span[start..])); + public TDest As(int start) where TDest : struct => MemoryMarshal.Read(MemoryMarshal.Cast(InternalGetSpan(start..))); [Pure] - public Span AsSpan(Range range) where TDest : struct => MemoryMarshal.Cast(Span[range]); + public Span AsSpan(Range range) where TDest : struct => MemoryMarshal.Cast(InternalGetSpan(range)); + + [Pure] + public Span AsSpan(long start, int length) where TDest : struct => MemoryMarshal.Cast(InternalGetSpan(start, length)); + + private Span InternalGetSpan(Range range) + { + var (start, length) = GetOffsetAndLength(range); + + return InternalGetSpan(start, length); + } + + private Span InternalGetSpan(long start, int length) + { + if(start < 0 || length < 0 || start + length > _length) + throw new ArgumentOutOfRangeException(nameof(start), $"Start {start} and length {length} out of bounds, file length is {_length}"); + + return new(_ptr + start, length); + } + + private Span InternalGetSpan(long start) + { + var remainder = _length - start; + + return InternalGetSpan(start, (int)Math.Min(remainder, int.MaxValue)); + } + + private (long start, int length) GetOffsetAndLength(Range range) + { + long start; + var startIndex = range.Start; + if (startIndex.IsFromEnd) + start = _length - startIndex.Value; + else + start = startIndex.Value; + + long end; + var endIndex = range.End; + if (endIndex.IsFromEnd) + end = _length - endIndex.Value; + else + end = endIndex.Value; + + if((end - start) > int.MaxValue) + throw new ArgumentOutOfRangeException(nameof(range), $"Range {range} is too large - got start {start} and end {end}, file length is {_length}"); + + return (start, (int) (end - start)); + } public void Dispose() { diff --git a/UnityMemorySnapshotThing.sln b/UnityMemorySnapshotThing.sln index 44382d9..76de56c 100644 --- a/UnityMemorySnapshotThing.sln +++ b/UnityMemorySnapshotThing.sln @@ -7,6 +7,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F11A27FD-B12F-4BEF-8A6F-38268685A979}" ProjectSection(SolutionItems) = preProject .gitignore = .gitignore + README.md = README.md EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UMS.Analysis", "UMS.Analysis\UMS.Analysis.csproj", "{E09A6526-8F53-4D37-9AC4-6F333DFB0AEC}" From fbabf43aca975ed6be6dbd4765120e91757e5f26 Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Fri, 22 Nov 2024 19:24:22 +0000 Subject: [PATCH 15/19] Perf/memory usage optimizations --- UMS.Analysis/SnapshotFile.cs | 36 ++++++++++++++++--- .../Objects/ManagedClassInstance.cs | 6 +++- .../Structures/Objects/StringFieldValue.cs | 2 +- .../Structures/RawManagedObjectInfo.cs | 13 +++++-- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/UMS.Analysis/SnapshotFile.cs b/UMS.Analysis/SnapshotFile.cs index 5b944ba..f838d29 100644 --- a/UMS.Analysis/SnapshotFile.cs +++ b/UMS.Analysis/SnapshotFile.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using System.Buffers; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using UMS.Analysis.Structures; using UMS.Analysis.Structures.Objects; @@ -116,7 +117,7 @@ public void LoadManagedObjectsFromStaticFields() if (_managedClassInstanceCache.TryGetValue(address, out var ret)) return ret; - var info = ParseManagedObjectInfo(address); + using var info = ParseManagedObjectInfo(address); if (!info.IsKnownType) return null; @@ -145,7 +146,7 @@ public RawManagedObjectInfo ParseManagedObjectInfo(ulong address) info.TypeInfoAddress = ReadPointer(typeInfoSpan); if (!TypeIndicesByPointer.TryGetValue(info.TypeInfoAddress, out info.TypeDescriptionIndex) || !info.IsKnownType) { - Console.WriteLine($"WARNING: Failed to resolve type for object at {address:X}"); + Console.WriteLine($"WARNING: Failed to resolve type for object at {address:X} - type info address {info.TypeInfoAddress:X} resulted in type index {info.TypeDescriptionIndex}"); //Cache the failure - let's not waste time. ret = _managedObjectInfoCache[address] = new(); @@ -158,7 +159,34 @@ public RawManagedObjectInfo ParseManagedObjectInfo(ulong address) info.Size = SizeOfObjectInBytes(info, heap); if (info.Size == 0) throw new("Size 0?"); - info.Data = heap[..info.Size].ToArray(); + + if (info.Size > heap.Length) + { + //Non-contiguous heap, let's copy piecemeal + var destData = ArrayPool.Shared.Rent(info.Size); + var pos = 0; + var readingFromMemAddress = address; + + do + { + var toCopy = Math.Min(heap.Length, info.Size - pos); + heap[..toCopy].CopyTo(destData.AsSpan(pos..)); + pos += toCopy; + readingFromMemAddress += (ulong)toCopy; + + if (!TryGetSpanForHeapAddress(readingFromMemAddress, out heap)) + break; + } while (pos < info.Size); + + info.Data = destData; + } + else + { + info.Data = ArrayPool.Shared.Rent(info.Size); + var heapSpan = heap[..info.Size]; + heapSpan.CopyTo(info.Data); + } + info.SelfAddress = address; _managedObjectInfoCache.Add(address, info); diff --git a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs index e8da1c8..f96a5cf 100644 --- a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs +++ b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs @@ -100,7 +100,11 @@ public ManagedClassInstance(SnapshotFile file, RawManagedObjectInfo info, Manage var elementTypeSize = (elementFlags & TypeFlags.ValueType) != 0 ? file.GetTypeDescriptionSizeBytes(elementType.TypeIndex) : 8; var arrayData = info.Data.AsSpan(file.VirtualMachineInformation.ArrayHeaderSize..); + var oldArrayElementCount = arrayElementCount; arrayElementCount = Math.Min(arrayElementCount, arrayData.Length / elementTypeSize); //Just in case the array length is wrong + + if (oldArrayElementCount != arrayElementCount) + Console.WriteLine($"WARNING: Array length mismatch for {file.GetTypeName(info.TypeDescriptionIndex)} at 0x{info.SelfAddress:X} (expected {oldArrayElementCount}, got {arrayElementCount})"); Fields = new IFieldValue[arrayElementCount]; for (var i = 0; i < arrayElementCount; i++) @@ -121,7 +125,7 @@ private IFieldValue[] ReadFields(SnapshotFile file, Span data, int depth) if (CheckIfRecursiveReference()) return Array.Empty(); - if (depth > 350) + if (depth > 380) { Console.WriteLine($"Stopped reading fields due to too-deeply nested object at depth {depth} (this object is of type {file.GetTypeName(TypeInfo.TypeIndex)}, parent is of type {file.GetTypeName(TypedParent.TypeInfo.TypeIndex)})"); return Array.Empty(); diff --git a/UMS.Analysis/Structures/Objects/StringFieldValue.cs b/UMS.Analysis/Structures/Objects/StringFieldValue.cs index a8ee43c..2c2ba6f 100644 --- a/UMS.Analysis/Structures/Objects/StringFieldValue.cs +++ b/UMS.Analysis/Structures/Objects/StringFieldValue.cs @@ -27,7 +27,7 @@ public StringFieldValue(SnapshotFile file, Span data) return; } - var managedObjectInfo = file.ParseManagedObjectInfo(ptr); + using var managedObjectInfo = file.ParseManagedObjectInfo(ptr); if (!managedObjectInfo.IsKnownType) { FailedToParse = true; diff --git a/UMS.Analysis/Structures/RawManagedObjectInfo.cs b/UMS.Analysis/Structures/RawManagedObjectInfo.cs index f1eb044..b249c0a 100644 --- a/UMS.Analysis/Structures/RawManagedObjectInfo.cs +++ b/UMS.Analysis/Structures/RawManagedObjectInfo.cs @@ -1,8 +1,9 @@ -using UMS.LowLevel.Structures; +using System.Buffers; +using UMS.LowLevel.Structures; namespace UMS.Analysis.Structures; -public struct RawManagedObjectInfo +public struct RawManagedObjectInfo : IDisposable { public ulong SelfAddress; public ulong TypeInfoAddress; @@ -33,5 +34,11 @@ public RawManagedObjectInfo() public bool IsKnownType => TypeDescriptionIndex >= 0; - + + public void Dispose() + { + Array.Clear(Data); + ArrayPool.Shared.Return(Data); + Data = []; + } } \ No newline at end of file From e6fa40e4d470b7ab3d908666ef2cde56d9b95f55 Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Mon, 2 Dec 2024 18:11:42 +0000 Subject: [PATCH 16/19] Proper enum support, add new mode to dump info for a specific object --- UMS.Analysis/SnapshotFile.cs | 8 ++ .../Structures/Objects/ComplexFieldValue.cs | 8 ++ .../Structures/Objects/EnumFieldValue.cs | 48 ++++++++ .../Objects/FloatingPointFieldValue.cs | 7 ++ .../Structures/Objects/IFieldValue.cs | 7 +- .../Structures/Objects/IntegerFieldValue.cs | 5 + .../Objects/ManagedClassInstance.cs | 2 +- .../Structures/Objects/StringFieldValue.cs | 5 + .../Structures/WellKnownTypeHelper.cs | 3 + UMS.LowLevel/LowLevelSnapshotFile.cs | 8 +- UnityMemorySnapshotThing/Program.cs | 110 ++++++++++++------ 11 files changed, 171 insertions(+), 40 deletions(-) create mode 100644 UMS.Analysis/Structures/Objects/EnumFieldValue.cs diff --git a/UMS.Analysis/SnapshotFile.cs b/UMS.Analysis/SnapshotFile.cs index f838d29..2f77944 100644 --- a/UMS.Analysis/SnapshotFile.cs +++ b/UMS.Analysis/SnapshotFile.cs @@ -112,6 +112,14 @@ public void LoadManagedObjectsFromStaticFields() Console.WriteLine($"Found {_managedClassInstanceCache.Count - initialCount} additional managed objects from static fields in {(DateTime.Now - start).TotalMilliseconds}ms"); } + public ManagedClassInstance? TryFindManagedClassInstanceByAddress(ulong address) + { + if (_managedClassInstanceCache.TryGetValue(address, out var ret)) + return ret; + + return _additionalManagedValueTypeInstances.AsParallel().FirstOrDefault(i => i.ObjectAddress == address); + } + public ManagedClassInstance? GetOrCreateManagedClassInstance(ulong address, ManagedClassInstance? parent = null, int depth = 0, LoadedReason reason = LoadedReason.GcRoot, int fieldOrArrayIdx = int.MinValue) { if (_managedClassInstanceCache.TryGetValue(address, out var ret)) diff --git a/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs b/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs index 6e64480..e883083 100644 --- a/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs +++ b/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs @@ -50,4 +50,12 @@ public ComplexFieldValue(SnapshotFile file, BasicFieldInfoCache info, ManagedCla Value = mci; } + + public override string ToString() + { + if (Value == null) + return "null"; + + return $"{{Managed Class, Address=0x{Value.Value.ObjectAddress:X8}}}"; + } } \ No newline at end of file diff --git a/UMS.Analysis/Structures/Objects/EnumFieldValue.cs b/UMS.Analysis/Structures/Objects/EnumFieldValue.cs new file mode 100644 index 0000000..23dfb78 --- /dev/null +++ b/UMS.Analysis/Structures/Objects/EnumFieldValue.cs @@ -0,0 +1,48 @@ +namespace UMS.Analysis.Structures.Objects; + +public class EnumFieldValue : IFieldValue +{ + public bool IsNull { get; } + public bool FailedToParse { get; } + public ulong FailedParseFromPtr { get; } + + private SnapshotFile _file; + + private int _enumTypeIndex; + + private long _value; + + public EnumFieldValue(SnapshotFile file, BasicTypeInfoCache typeInfo, Span fieldData) + { + var allFields = file.GetInstanceFieldInfoForTypeIndex(typeInfo.TypeIndex); + var valueField = allFields.Single(); + _enumTypeIndex = typeInfo.TypeIndex; + _file = file; + + var backingTypeIndex = valueField.TypeDescriptionIndex; + + if (backingTypeIndex == file.WellKnownTypes.Boolean || backingTypeIndex == file.WellKnownTypes.Byte || backingTypeIndex == file.WellKnownTypes.SByte) + _value = fieldData[0]; + else if (backingTypeIndex == file.WellKnownTypes.Int16 || backingTypeIndex == file.WellKnownTypes.UInt16 || backingTypeIndex == file.WellKnownTypes.Char) + _value = BitConverter.ToInt16(fieldData); + else if (backingTypeIndex == file.WellKnownTypes.Int32 || backingTypeIndex == file.WellKnownTypes.UInt32) + _value = BitConverter.ToInt32(fieldData); + else if (backingTypeIndex == file.WellKnownTypes.Int64 || backingTypeIndex == file.WellKnownTypes.UInt64 || backingTypeIndex == file.WellKnownTypes.IntPtr) + _value = BitConverter.ToInt64(fieldData); + else + throw new NotImplementedException($"Enum backing type {file.GetTypeName(backingTypeIndex)} not implemented"); + + //TODO Maybe consider getting enum names here + + IsNull = false; + FailedToParse = false; + FailedParseFromPtr = 0; + } + + public override string ToString() + { + var enumName = _file.GetTypeName(_enumTypeIndex); + + return $"({enumName}) {_value}"; + } +} \ No newline at end of file diff --git a/UMS.Analysis/Structures/Objects/FloatingPointFieldValue.cs b/UMS.Analysis/Structures/Objects/FloatingPointFieldValue.cs index 309d3bb..fc58df0 100644 --- a/UMS.Analysis/Structures/Objects/FloatingPointFieldValue.cs +++ b/UMS.Analysis/Structures/Objects/FloatingPointFieldValue.cs @@ -1,3 +1,5 @@ +using System.Globalization; + namespace UMS.Analysis.Structures.Objects; public struct FloatingPointFieldValue : IFieldValue @@ -12,4 +14,9 @@ public FloatingPointFieldValue(double value) { Value = value; } + + public override string ToString() + { + return Value.ToString(CultureInfo.InvariantCulture); + } } \ No newline at end of file diff --git a/UMS.Analysis/Structures/Objects/IFieldValue.cs b/UMS.Analysis/Structures/Objects/IFieldValue.cs index 24eeee7..8a361ea 100644 --- a/UMS.Analysis/Structures/Objects/IFieldValue.cs +++ b/UMS.Analysis/Structures/Objects/IFieldValue.cs @@ -11,7 +11,7 @@ public static IFieldValue Read(SnapshotFile file, BasicFieldInfoCache info, Span //For all integer types, we just handle unsigned as signed if (info.TypeDescriptionIndex == file.WellKnownTypes.String) return new StringFieldValue(file, fieldData); - if (info.TypeDescriptionIndex == file.WellKnownTypes.Boolean || info.TypeDescriptionIndex == file.WellKnownTypes.Byte) + if (info.TypeDescriptionIndex == file.WellKnownTypes.Boolean || info.TypeDescriptionIndex == file.WellKnownTypes.Byte || info.TypeDescriptionIndex == file.WellKnownTypes.SByte) return new IntegerFieldValue(fieldData[0]); if (info.TypeDescriptionIndex == file.WellKnownTypes.Int16 || info.TypeDescriptionIndex == file.WellKnownTypes.UInt16 || info.TypeDescriptionIndex == file.WellKnownTypes.Char) return new IntegerFieldValue(BitConverter.ToInt16(fieldData)); @@ -24,6 +24,11 @@ public static IFieldValue Read(SnapshotFile file, BasicFieldInfoCache info, Span if (info.TypeDescriptionIndex == file.WellKnownTypes.Double) return new FloatingPointFieldValue(BitConverter.ToDouble(fieldData)); + //Check for enums + var typeInfo = file.GetTypeInfo(info.TypeDescriptionIndex); + if(typeInfo.BaseTypeIndex == file.WellKnownTypes.Enum) + return new EnumFieldValue(file, typeInfo, fieldData); + return new ComplexFieldValue(file, info, parent, fieldData, depth + 1, loadedReason); } } \ No newline at end of file diff --git a/UMS.Analysis/Structures/Objects/IntegerFieldValue.cs b/UMS.Analysis/Structures/Objects/IntegerFieldValue.cs index 971a62b..f4cff0e 100644 --- a/UMS.Analysis/Structures/Objects/IntegerFieldValue.cs +++ b/UMS.Analysis/Structures/Objects/IntegerFieldValue.cs @@ -12,4 +12,9 @@ public IntegerFieldValue(long value) { Value = value; } + + public override string ToString() + { + return Value.ToString(); + } } \ No newline at end of file diff --git a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs index f96a5cf..475013c 100644 --- a/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs +++ b/UMS.Analysis/Structures/Objects/ManagedClassInstance.cs @@ -125,7 +125,7 @@ private IFieldValue[] ReadFields(SnapshotFile file, Span data, int depth) if (CheckIfRecursiveReference()) return Array.Empty(); - if (depth > 380) + if (depth > 370) { Console.WriteLine($"Stopped reading fields due to too-deeply nested object at depth {depth} (this object is of type {file.GetTypeName(TypeInfo.TypeIndex)}, parent is of type {file.GetTypeName(TypedParent.TypeInfo.TypeIndex)})"); return Array.Empty(); diff --git a/UMS.Analysis/Structures/Objects/StringFieldValue.cs b/UMS.Analysis/Structures/Objects/StringFieldValue.cs index 2c2ba6f..e60737b 100644 --- a/UMS.Analysis/Structures/Objects/StringFieldValue.cs +++ b/UMS.Analysis/Structures/Objects/StringFieldValue.cs @@ -53,4 +53,9 @@ public StringFieldValue(SnapshotFile file, Span data) Value = Encoding.Unicode.GetString(stringData); } + + public override string ToString() + { + return $"\"{Value}\""; + } } \ No newline at end of file diff --git a/UMS.Analysis/Structures/WellKnownTypeHelper.cs b/UMS.Analysis/Structures/WellKnownTypeHelper.cs index aff0916..a6f3907 100644 --- a/UMS.Analysis/Structures/WellKnownTypeHelper.cs +++ b/UMS.Analysis/Structures/WellKnownTypeHelper.cs @@ -14,6 +14,7 @@ public class WellKnownTypeHelper { "System.Int64", -1 }, { "System.UInt64", -1 }, { "System.Byte", -1 }, + { "System.SByte", -1 }, { "System.Object", -1 }, { "System.ValueType", -1 }, { "System.Enum", -1 }, @@ -37,6 +38,7 @@ public class WellKnownTypeHelper public int Int64 {get;} public int UInt64 {get;} public int Byte {get;} + public int SByte {get;} public int Object {get;} public int ValueType {get;} public int Enum {get;} @@ -77,6 +79,7 @@ public WellKnownTypeHelper(SnapshotFile file) Int64 = this["System.Int64"]; UInt64 = this["System.UInt64"]; Byte = this["System.Byte"]; + SByte = this["System.SByte"]; Object = this["System.Object"]; ValueType = this["System.ValueType"]; Enum = this["System.Enum"]; diff --git a/UMS.LowLevel/LowLevelSnapshotFile.cs b/UMS.LowLevel/LowLevelSnapshotFile.cs index 7b1b861..af1cf0c 100644 --- a/UMS.LowLevel/LowLevelSnapshotFile.cs +++ b/UMS.LowLevel/LowLevelSnapshotFile.cs @@ -89,10 +89,13 @@ public unsafe LowLevelSnapshotFile(string path) var areHeapAddressesEncoded = SnapshotFormatVersion >= FormatVersion.MemLabelSizeAndHeapIdVersion; + Console.WriteLine("Loading managed heap sections..."); + ManagedHeapSectionStartAddresses = ReadValueTypeChapter(EntryType.ManagedHeapSections_StartAddress, 0, -1).ToArray() .Select((a, i) => new ManagedHeapSection(a, areHeapAddressesEncoded, ReadChapterBody(EntryType.ManagedHeapSections_Bytes, i, 1))) .ToArray(); + Console.WriteLine("Sorting managed heap sections..."); Array.Sort(ManagedHeapSectionStartAddresses); } @@ -113,11 +116,6 @@ private void ReadMetadataForAllChapters(Span entryTypeToChapterOffset) continue; _chaptersByEntryType[(EntryType)i] = ReadChapterMetadata(entryTypeToChapterOffset[i]); - - if (_chaptersByEntryType[(EntryType)i].AdditionalEntryStorage != null) - { - Console.WriteLine($"Read additional entry storage for chapter {(EntryType) i}"); - } } } diff --git a/UnityMemorySnapshotThing/Program.cs b/UnityMemorySnapshotThing/Program.cs index baddef0..0277331 100644 --- a/UnityMemorySnapshotThing/Program.cs +++ b/UnityMemorySnapshotThing/Program.cs @@ -1,6 +1,8 @@ -using System.Text; +using System.Globalization; +using System.Text; using UMS.Analysis; using UMS.Analysis.Structures.Objects; +using UMS.LowLevel.Structures; namespace UnityMemorySnapshotThing; @@ -22,42 +24,34 @@ public static void Main(string[] args) using var file = new SnapshotFile(filePath); Console.WriteLine($"Read snapshot file in {(DateTime.Now - start).TotalMilliseconds} ms\n"); - Console.WriteLine($"Snapshot file version: {file.SnapshotFormatVersion}\n"); - Console.WriteLine($"Snapshot taken on {file.CaptureDateTime}\n"); - Console.WriteLine($"Target platform: {file.ProfileTargetInfo}\n"); - Console.WriteLine($"Memory stats: {file.ProfileTargetMemoryStats}\n"); - Console.WriteLine($"VM info: {file.VirtualMachineInformation}\n"); + Console.WriteLine($"Snapshot file version: {file.SnapshotFormatVersion} ({(int) file.SnapshotFormatVersion})"); + Console.WriteLine($"Snapshot taken on {file.CaptureDateTime}"); + Console.WriteLine($"Target platform: {file.ProfileTargetInfo}"); + Console.WriteLine($"Memory info at time of snapshot: {file.ProfileTargetMemoryStats}"); - // Console.WriteLine("Querying large dynamic arrays..."); - // start = DateTime.Now; - // Console.WriteLine($"Snapshot contains {file.NativeObjectNames.Length} native objects and {file.TypeDescriptionNames.Length} managed objects"); - // - // var heapSections = file.ManagedHeapSectionBytes; - // var heapSectionStartAddresses = file.ManagedHeapSectionStartAddresses; - // Console.WriteLine($"Snapshot contains {heapSections.Length} managed heap sections (starting at {heapSectionStartAddresses.Length} start addresses) totalling {heapSections.Sum(b => b.Length)} bytes"); - // - // var fieldIndices = file.TypeDescriptionFieldIndices; - // var fieldBytes = file.TypeDescriptionStaticFieldBytes; - // Console.WriteLine($"Snapshot contains {fieldIndices.Length} type description-field index mappings, totalling {fieldIndices.Sum(i => i.Length)} field indices, and {fieldBytes.Length} type description-static field bytes"); - // - // var fieldNames = file.FieldDescriptionNames; - // Console.WriteLine($"Snapshot contains {fieldNames.Length} field names"); - // - // var fieldOffsets = file.FieldDescriptionOffsets; - // Console.WriteLine($"Snapshot contains {fieldOffsets.Length} field offsets"); - // - // var fieldTypes = file.FieldDescriptionTypeIndices; - // Console.WriteLine($"Snapshot contains {fieldTypes.Length} field-type mappings"); - // - // //Field indices map type description names to field names - // //e.g. field indices element 2 has some values, so those values are the indices into the field name array for type description name 2 - // - // Console.WriteLine($"Querying large dynamic arrays took {(DateTime.Now - start).TotalMilliseconds} ms\n"); + Console.WriteLine(); + Console.WriteLine("Finding objects in snapshot..."); file.LoadManagedObjectsFromGcRoots(); file.LoadManagedObjectsFromStaticFields(); - - FindLeakedUnityObjects(file); + + Console.WriteLine($"Found {file.AllManagedClassInstances.Count()} managed objects."); + + while (true) + { + Console.Write("\n\nWhat would you like to do now?\n1: Find leaked managed shells.\n2: Dump information on a specific object (by address).\n0: Exit\nChoice: "); + + var choice = Console.ReadLine(); + + if (choice == "1") + FindLeakedUnityObjects(file); + else if (choice == "2") + DumpObjectInfo(file); + else if(choice == "0") + break; + else + Console.WriteLine("Invalid choice."); + } } private static void FindLeakedUnityObjects(SnapshotFile file) @@ -116,6 +110,56 @@ private static void FindLeakedUnityObjects(SnapshotFile file) File.WriteAllText("leaked_objects.txt", ret.ToString()); } + + private static void DumpObjectInfo(SnapshotFile file) + { + Console.WriteLine("Enter the memory address of the object you want to dump:"); + var addressString = Console.ReadLine(); + + if (!ulong.TryParse(addressString, NumberStyles.HexNumber, null, out var address)) + { + Console.WriteLine("Unable to parse address."); + return; + } + + var nullableObj = file.TryFindManagedClassInstanceByAddress(address); + + if (nullableObj == null) + { + Console.WriteLine($"No object at address 0x{address:X8} was found in the snapshot"); + return; + } + + var obj = nullableObj.Value; + + if ((obj.TypeDescriptionFlags & TypeFlags.Array) != 0) + { + Console.WriteLine("Dumping arrays is not supported, yet."); + return; + } + + Console.WriteLine($"Found object at address 0x{address:X8}"); + Console.WriteLine($"Type: {file.GetTypeName(obj.TypeInfo.TypeIndex)}"); + Console.WriteLine($"Flags: {obj.TypeDescriptionFlags}"); + Console.WriteLine("Fields:"); + + for (var i = 0; i < obj.Fields.Length; i++) + { + WriteField(file, obj, i); + } + } + + private static void WriteField(SnapshotFile file, ManagedClassInstance parent, int index) + { + var fields = file.GetInstanceFieldInfoForTypeIndex(parent.TypeInfo.TypeIndex); + var fieldInfo = fields[index]; + var fieldValue = parent.Fields[index]; + + var fieldName = file.GetFieldName(fieldInfo.FieldIndex); + var fieldType = file.GetTypeName(fieldInfo.TypeDescriptionIndex); + + Console.WriteLine($" {fieldType} {fieldName} = {fieldValue}"); + } } \ No newline at end of file From 80f1430e16a78f67d3f892fc591d8da9972754fc Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Fri, 10 Jan 2025 10:57:08 +0000 Subject: [PATCH 17/19] Also log retention path in object dump mode --- UnityMemorySnapshotThing/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/UnityMemorySnapshotThing/Program.cs b/UnityMemorySnapshotThing/Program.cs index 0277331..5e1182d 100644 --- a/UnityMemorySnapshotThing/Program.cs +++ b/UnityMemorySnapshotThing/Program.cs @@ -141,6 +141,7 @@ private static void DumpObjectInfo(SnapshotFile file) Console.WriteLine($"Found object at address 0x{address:X8}"); Console.WriteLine($"Type: {file.GetTypeName(obj.TypeInfo.TypeIndex)}"); Console.WriteLine($"Flags: {obj.TypeDescriptionFlags}"); + Console.WriteLine($"Retention path: {obj.GetFirstObservedRetentionPath(file)}"); Console.WriteLine("Fields:"); for (var i = 0; i < obj.Fields.Length; i++) From 474fa5126bcf989fb5376165b3ee6b39619c9e9e Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Fri, 10 Jan 2025 20:53:59 +0000 Subject: [PATCH 18/19] Fix ArgumentOutOfRange in ComplexFieldValue --- UMS.Analysis/Structures/Objects/ComplexFieldValue.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs b/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs index e883083..9d69c3c 100644 --- a/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs +++ b/UMS.Analysis/Structures/Objects/ComplexFieldValue.cs @@ -28,6 +28,12 @@ public ComplexFieldValue(SnapshotFile file, BasicFieldInfoCache info, ManagedCla return; } } + + if(data.Length < 8) + { + FailedToParse = true; + return; + } //General object handling var ptr = BitConverter.ToUInt64(data); From 509275ffba677f73dc80a1193c05d4527e120802 Mon Sep 17 00:00:00 2001 From: Sam Byass Date: Fri, 10 Jan 2025 20:58:11 +0000 Subject: [PATCH 19/19] ...and another in SnapshotFile --- UMS.Analysis/SnapshotFile.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/UMS.Analysis/SnapshotFile.cs b/UMS.Analysis/SnapshotFile.cs index 2f77944..04e38ae 100644 --- a/UMS.Analysis/SnapshotFile.cs +++ b/UMS.Analysis/SnapshotFile.cs @@ -101,6 +101,9 @@ public void LoadManagedObjectsFromStaticFields() //If array, then we have a pointer to it and can use the exact same logic as for static non-array fields + if(typeFieldBytes.Length - fieldOffset < VirtualMachineInformation.PointerSize) + continue; //Not enough data to read a pointer + var fieldPointer = MemoryMarshal.Read(typeFieldBytes[fieldOffset..]); if (fieldPointer == 0) continue;