From: Lee Culver Date: Tue, 14 Mar 2023 17:30:22 +0000 (-0700) Subject: Reimplement !dumpheap and !verifyheap in C# (#3738) X-Git-Tag: accepted/tizen/unified/riscv/20231226.055542~43^2~2^2~32 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=c5102f0cee857ea026f4ed7855bd1f802d236039;p=platform%2Fcore%2Fdotnet%2Fdiagnostics.git Reimplement !dumpheap and !verifyheap in C# (#3738) * C# !verifyheap implementation * Bump ClrMD version * Add DML support to TableOutput * Add Segment filter, clean up parsing * Cleanups * Allow cancellation * Remove test code, cleanup * Add SimulateGCHeapCorruption * Output improvements * Update VerifyHeapCommand.cs * Add class to filter segments and objects * Initial DumpHeap command * Initial !dumpheap implementation Still needs -thinlocks and -string. * Cleanups and timer output * Add -strings support * Update C++ commands to use C# verify/dump heap * CodeFormatting fixes * Better Sanitize method Co-authored-by: Günther Foidl * Fix Span issue Co-authored-by: Günther Foidl * Fix a few warnings * Use pattern matching in FileLoggingConsoleService * Update ClrMD version * Remove accidental include * Implement !dumpheap -thinlock * Fix CommandLine parsing * Handle invalid objects * Test fixes * Add back MINIDUMP_NOT_SUPPORTED * Fix concurrent dictionary tests * Fix DECVAL * Fix DECVAL again * Fix !verifyheap failures * Fix coding style * Code review feedback * Fix !dumpobj format * Fix DML issues --------- Co-authored-by: Günther Foidl --- diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs index 5fcdeaed5..01e4cfdb6 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs @@ -182,7 +182,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation { break; } - CommandAttribute[] commandAttributes = (CommandAttribute[])baseType.GetCustomAttributes(typeof(CommandAttribute), inherit: false); + CommandAttribute[] commandAttributes = (CommandAttribute[])baseType.GetCustomAttributes(typeof(CommandAttribute), inherit: true); foreach (CommandAttribute commandAttribute in commandAttributes) { if ((commandAttribute.Flags & CommandFlags.Manual) == 0 || factory != null) diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/FileLoggingConsoleService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/FileLoggingConsoleService.cs index d4e46c262..2c60c6d0c 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/FileLoggingConsoleService.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/FileLoggingConsoleService.cs @@ -158,6 +158,22 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation } } + public void WriteDmlExec(string text, string action) + { + _consoleService.WriteDmlExec(text, action); + + foreach (StreamWriter writer in _writers) + { + try + { + writer.Write(text); + } + catch (Exception ex) when (ex is IOException or ObjectDisposedException or NotSupportedException) + { + } + } + } + public CancellationToken CancellationToken { get { return _consoleService.CancellationToken; } diff --git a/src/Microsoft.Diagnostics.DebugServices/CommandAttributes.cs b/src/Microsoft.Diagnostics.DebugServices/CommandAttributes.cs index dbacc125a..4b1df4b60 100644 --- a/src/Microsoft.Diagnostics.DebugServices/CommandAttributes.cs +++ b/src/Microsoft.Diagnostics.DebugServices/CommandAttributes.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics; namespace Microsoft.Diagnostics.DebugServices { @@ -63,6 +64,15 @@ namespace Microsoft.Diagnostics.DebugServices public string DefaultOptions; } + /// + /// Marks a class as a Debug-Only command. These commands are only available for debug versions + /// of SOS, but does not appear in shipping builds. + /// + [Conditional("DEBUG")] + public class DebugCommandAttribute : CommandAttribute + { + } + /// /// Marks the property as a Option. /// diff --git a/src/Microsoft.Diagnostics.DebugServices/IConsoleService.cs b/src/Microsoft.Diagnostics.DebugServices/IConsoleService.cs index a2de2488a..426e328c1 100644 --- a/src/Microsoft.Diagnostics.DebugServices/IConsoleService.cs +++ b/src/Microsoft.Diagnostics.DebugServices/IConsoleService.cs @@ -31,6 +31,13 @@ namespace Microsoft.Diagnostics.DebugServices /// Writes Debugger Markup Language (DML) markup text. void WriteDml(string text); + /// + /// Writes an exec tag to the output stream. + /// + /// The display text. + /// The action to perform. + void WriteDmlExec(string text, string action); + /// Gets whether is supported. bool SupportsDml { get; } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpHeapCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpHeapCommand.cs new file mode 100644 index 000000000..ce5334f7f --- /dev/null +++ b/src/Microsoft.Diagnostics.ExtensionCommands/DumpHeapCommand.cs @@ -0,0 +1,381 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using Microsoft.Diagnostics.DebugServices; +using Microsoft.Diagnostics.Runtime; +using static Microsoft.Diagnostics.ExtensionCommands.TableOutput; + +namespace Microsoft.Diagnostics.ExtensionCommands +{ + [Command(Name = "dumpheap", Help = "Displays a list of all managed objects.")] + public class DumpHeapCommand : CommandBase + { + private const char StringReplacementCharacter = '.'; + + [ServiceImport] + public IMemoryService MemoryService { get; set; } + + [ServiceImport] + public ClrRuntime Runtime { get; set; } + + [ServiceImport] + public LiveObjectService LiveObjects { get; set; } + + [Option(Name = "-mt")] + public string MethodTableString { get; set; } + + private ulong? MethodTable { get; set; } + + [Option(Name = "-type")] + public string Type { get; set; } + + [Option(Name = "-stat")] + public bool StatOnly { get; set; } + + [Option(Name = "-strings")] + public bool Strings { get; set; } + + [Option(Name = "-verify")] + public bool Verify { get; set; } + + [Option(Name = "-short")] + public bool Short { get; set; } + + [Option(Name = "-min")] + public ulong Min { get; set; } + + [Option(Name = "-max")] + public ulong Max { get; set; } + + [Option(Name = "-live")] + public bool Live { get; set; } + + [Option(Name = "-dead")] + public bool Dead{ get; set; } + + [Option(Name = "-heap")] + public int GCHeap { get; set; } = -1; + + [Option(Name = "-segment")] + public string Segment { get; set; } + + [Option(Name = "-thinlock")] + public bool ThinLock { get; set; } + + [Argument(Help = "Optional memory ranges in the form of: [Start [End]]")] + public string[] MemoryRange { get; set; } + + private HeapWithFilters FilteredHeap { get; set; } + + public override void Invoke() + { + ParseArguments(); + + TableOutput thinLockOutput = null; + TableOutput objectTable = new(Console, (12, "x12"), (12, "x12"), (12, ""), (0, "")); + if (!StatOnly && !Short && !ThinLock) + { + objectTable.WriteRow("Address", "MT", "Size"); + } + + bool checkTypeName = !string.IsNullOrWhiteSpace(Type); + Dictionary stats = new(); + Dictionary<(string String, ulong Size), uint> stringTable = null; + + foreach (ClrObject obj in FilteredHeap.EnumerateFilteredObjects(Console.CancellationToken)) + { + ulong mt = obj.Type?.MethodTable ?? 0; + if (mt == 0) + { + MemoryService.ReadPointer(obj, out mt); + } + + // Filter by MT, if the user specified -strings then MethodTable has been pre-set + // to the string MethodTable + if (MethodTable.HasValue && mt != MethodTable.Value) + { + continue; + } + + // Filter by liveness + if (Live && !LiveObjects.IsLive(obj)) + { + continue; + } + + if (Dead && LiveObjects.IsLive(obj)) + { + continue; + } + + // Filter by type name + if (checkTypeName && obj.Type?.Name is not null && !obj.Type.Name.Contains(Type)) + { + continue; + } + + if (ThinLock) + { + ClrThinLock thinLock = obj.GetThinLock(); + if (thinLock != null) + { + if (thinLockOutput is null) + { + thinLockOutput = new(Console, (12, "x"), (16, "x"), (16, "x"), (10, "n0")); + thinLockOutput.WriteRow("Object", "Thread", "OSId", "Recursion"); + } + + thinLockOutput.WriteRow(new DmlDumpObj(obj), thinLock.Thread?.Address ?? 0, thinLock.Thread?.OSThreadId ?? 0, thinLock.Recursion); + } + + continue; + } + + if (Short) + { + Console.WriteLine(obj.Address.ToString("x12")); + continue; + } + + ulong size = obj.IsValid ? obj.Size : 0; + if (!StatOnly) + { + objectTable.WriteRow(new DmlDumpObj(obj), new DmlDumpHeapMT(obj.Type?.MethodTable ?? 0), size, obj.IsFree ? "Free" : ""); + } + + if (Strings) + { + // We only read a maximum of 1024 characters for each string. This may lead to some collisions if strings are unique + // only after their 1024th character while being the exact same size as another string. However, this will be correct + // the VAST majority of the time, and it will also keep us from hitting OOM or other weirdness if the heap is corrupt. + + string value = obj.AsString(1024); + + stringTable ??= new(); + (string value, ulong size) key = (value, size); + stringTable.TryGetValue(key, out uint stringCount); + stringTable[key] = stringCount + 1; + } + else + { + if (!stats.TryGetValue(mt, out (int Count, ulong Size, string TypeName) typeStats)) + { + stats.Add(mt, (1, size, obj.Type?.Name ?? $"")); + } + else + { + stats[mt] = (typeStats.Count + 1, typeStats.Size + size, typeStats.TypeName); + } + } + } + + // Print statistics, but not for -short or -thinlock + if (!Short && !ThinLock) + { + if (Strings && stringTable is not null) + { + // For -strings, we print the strings themselves with their stats + if (!StatOnly) + { + Console.WriteLine(); + } + + int countLen = stringTable.Max(ts => ts.Value).ToString("n0").Length; + countLen = Math.Max(countLen, "Count".Length); + + int sizeLen = stringTable.Max(ts => ts.Key.Size * ts.Value).ToString("n0").Length; + sizeLen = Math.Max(countLen, "TotalSize".Length); + + int stringLen = 128; + int possibleWidth = Console.WindowWidth - countLen - sizeLen - 2; + if (possibleWidth > 16) + { + stringLen = Math.Min(possibleWidth, stringLen); + } + + Console.WriteLine("Statistics:"); + TableOutput statsTable = new(Console, (countLen, "n0"), (sizeLen, "n0"), (0, "")); + + var stringsSorted = from item in stringTable + let Count = item.Value + let Size = item.Key.Size + let String = Sanitize(item.Key.String, stringLen) + let TotalSize = Count * Size + orderby TotalSize + select new + { + Count, + TotalSize, + String + }; + + foreach (var item in stringsSorted) + { + statsTable.WriteRow(item.Count, item.TotalSize, item.String); + } + } + else if (stats.Count != 0) + { + // Print statistics table + if (!StatOnly) + { + Console.WriteLine(); + } + + int countLen = stats.Values.Max(ts => ts.Count).ToString("n0").Length; + countLen = Math.Max(countLen, "Count".Length); + + int sizeLen = stats.Values.Max(ts => ts.Size).ToString("n0").Length; + sizeLen = Math.Max(countLen, "TotalSize".Length); + + TableOutput statsTable = new(Console, (12, "x12"), (countLen, "n0"), (sizeLen, "n0"), (0, "")); + + Console.WriteLine("Statistics:"); + statsTable.WriteRow("MT", "Count", "TotalSize", "Class Name"); + + var statsSorted = from item in stats + let MethodTable = item.Key + let Size = item.Value.Size + orderby Size + select new + { + MethodTable = item.Key, + item.Value.Count, + Size, + item.Value.TypeName + }; + + foreach (var item in statsSorted) + { + statsTable.WriteRow(new DmlDumpHeapMT(item.MethodTable), item.Count, item.Size, item.TypeName); + } + + Console.WriteLine($"Total {stats.Values.Sum(r => r.Count):n0} objects"); + } + } + } + + private string Sanitize(string str, int maxLen) + { + foreach (char ch in str) + { + if (!char.IsLetterOrDigit(ch)) + { + return FilterString(str, maxLen); + } + } + + return str; + + static string FilterString(string str, int maxLen) + { + maxLen = Math.Min(str.Length, maxLen); + Debug.Assert(maxLen <= 128); + + Span buffer = stackalloc char[maxLen]; + ReadOnlySpan value = str.AsSpan(0, buffer.Length); + + for (int i = 0; i < value.Length; ++i) + { + char ch = value[i]; + buffer[i] = char.IsLetterOrDigit(ch) || char.IsPunctuation(ch) || ch == ' ' ? ch : StringReplacementCharacter; + } + + return buffer.ToString(); + } + } + + private void ParseArguments() + { + if (Live && Dead) + { + Live = false; + Dead = false; + } + + if (!string.IsNullOrWhiteSpace(MethodTableString)) + { + if (ParseHexString(MethodTableString, out ulong mt)) + { + MethodTable = mt; + } + else + { + throw new ArgumentException($"Invalid MethodTable: {MethodTableString}"); + } + } + + FilteredHeap = new(Runtime.Heap); + if (GCHeap >= 0) + { + FilteredHeap.GCHeap = GCHeap; + } + + if (!string.IsNullOrWhiteSpace(Segment)) + { + FilteredHeap.FilterBySegmentHex(Segment); + } + + if (MemoryRange is not null && MemoryRange.Length > 0) + { + if (MemoryRange.Length > 2) + { + string badArgument = MemoryRange.FirstOrDefault(f => f.StartsWith("-") || f.StartsWith("/")); + if (badArgument != null) + { + throw new ArgumentException($"Unknown argument: {badArgument}"); + } + + throw new ArgumentException("Too many arguments to !dumpheap"); + } + + string start = MemoryRange[0]; + string end = MemoryRange.Length > 1 ? MemoryRange[1] : null; + FilteredHeap.FilterByHexMemoryRange(start, end); + } + + if (Min > 0) + { + FilteredHeap.MinimumObjectSize = Min; + } + + if (Max > 0) + { + FilteredHeap.MaximumObjectSize = Max; + } + + if (Strings) + { + MethodTable = Runtime.Heap.StringType.MethodTable; + } + + FilteredHeap.SortSegments = (seg) => seg.OrderBy(seg => seg.Start); + } + + private static bool ParseHexString(string str, out ulong value) + { + value = 0; + if (string.IsNullOrWhiteSpace(str)) + { + return false; + } + + if (!ulong.TryParse(str, NumberStyles.HexNumber, null, out value)) + { + if (str.StartsWith("/") || str.StartsWith("-")) + { + throw new ArgumentException($"Unknown argument: {str}"); + } + + throw new ArgumentException($"Unknown format: {str}, expected hex number"); + } + + return true; + } + } +} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/EEHeapCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/EEHeapCommand.cs index 1b596b676..4c9a695da 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/EEHeapCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/EEHeapCommand.cs @@ -20,10 +20,10 @@ namespace Microsoft.Diagnostics.ExtensionCommands [ServiceImport] public IMemoryService MemoryService { get; set; } - [Option(Name = "--gc", Aliases = new string[] { "-gc" }, Help = "Only display the GC.")] + [Option(Name = "-gc", Help = "Only display the GC.")] public bool ShowGC { get; set; } - [Option(Name = "--loader", Aliases = new string[] { "-loader" }, Help = "Only display the Loader.")] + [Option(Name = "-loader", Help = "Only display the Loader.")] public bool ShowLoader { get; set; } public override void Invoke() diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/HeapWithFilters.cs b/src/Microsoft.Diagnostics.ExtensionCommands/HeapWithFilters.cs new file mode 100644 index 000000000..fb9cc06e5 --- /dev/null +++ b/src/Microsoft.Diagnostics.ExtensionCommands/HeapWithFilters.cs @@ -0,0 +1,194 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using Microsoft.Diagnostics.Runtime; + +namespace Microsoft.Diagnostics.ExtensionCommands +{ + internal sealed class HeapWithFilters + { + private int? _gcheap; + private readonly ClrHeap _heap; + + /// + /// Whether the heap will be filtered at all + /// + public bool HasFilters => _gcheap is not null || Segment is not null || MemoryRange is not null || MinimumObjectSize > 0 || MaximumObjectSize > 0; + + /// + /// Only enumerate objects or segments within this range. + /// + public MemoryRange? MemoryRange { get; set; } + + /// + /// Only enumerate the segment or objects on the segment which matches the given address. + /// This address may be anywhere within a Segment's committed memory. + /// + public ulong? Segment { get; set; } + + /// + /// The GC Heap number to filter on. + /// + public int? GCHeap + { + get => _gcheap; + set + { + if (!_heap.SubHeaps.Any(sh => sh.Index == value)) + { + throw new ArgumentException($"No GC heap with index of {value}"); + } + + _gcheap = value; + } + } + + /// + /// The minimum size of an object to enumerate. + /// + public ulong MinimumObjectSize { get; set; } + + /// + /// The maximum size of an object to enumerate + /// + public ulong MaximumObjectSize { get; set; } + + /// + /// The order in which to enumerate segments. This also applies to object enumeration. + /// + public Func, IOrderedEnumerable> SortSegments { get; set; } + + public HeapWithFilters(ClrHeap heap) + { + _heap = heap; + SortSegments = (seg) => seg.OrderBy(s => s.SubHeap.Index).ThenBy(s => s.Address); + } + + public void FilterBySegmentHex(string segmentStr) + { + if (!ulong.TryParse(segmentStr, NumberStyles.HexNumber, null, out ulong segment)) + { + throw new ArgumentException($"Invalid segment address: {segmentStr}"); + } + + if (!_heap.Segments.Any(seg => seg.Address == segment || seg.CommittedMemory.Contains(segment))) + { + throw new ArgumentException($"No segments match address: {segment:x}"); + } + + Segment = segment; + } + + public void FilterByHexMemoryRange(string startStr, string endStr) + { + if (!ulong.TryParse(startStr, NumberStyles.HexNumber, null, out ulong start)) + { + throw new ArgumentException($"Invalid start address: {startStr}"); + } + + if (string.IsNullOrWhiteSpace(endStr)) + { + MemoryRange = new(start, ulong.MaxValue); + } + else + { + bool length = false; + if (endStr.StartsWith("L")) + { + length = true; + endStr = endStr.Substring(1); + } + + if (!ulong.TryParse(endStr, NumberStyles.HexNumber, null, out ulong end)) + { + throw new ArgumentException($"Invalid end address: {endStr}"); + } + + if (length) + { + end += start; + } + + if (end <= start) + { + throw new ArgumentException($"Start address must be before end address: '{startStr}' < '{endStr}'"); + } + + MemoryRange = new(start, end); + } + + if (!_heap.Segments.Any(seg => seg.CommittedMemory.Overlaps(MemoryRange.Value))) + { + throw new ArgumentException($"No segments or objects in range {MemoryRange.Value}"); + } + } + + public IEnumerable EnumerateFilteredSegments() + { + IEnumerable segments = _heap.Segments; + if (GCHeap is int gcheap) + { + segments = segments.Where(seg => seg.SubHeap.Index == gcheap); + } + + if (Segment is ulong segment) + { + segments = segments.Where(seg => seg.Address == segment || seg.CommittedMemory.Contains(segment)); + } + + if (MemoryRange is MemoryRange range) + { + segments = segments.Where(seg => seg.CommittedMemory.Overlaps(range)); + } + + if (SortSegments is not null) + { + segments = SortSegments(segments); + } + + return segments; + } + + public IEnumerable EnumerateFilteredObjects(CancellationToken cancellation) + { + foreach (ClrSegment segment in EnumerateFilteredSegments()) + { + IEnumerable objs; + if (MemoryRange is MemoryRange range) + { + objs = segment.EnumerateObjects(range, carefully: true); + } + else + { + objs = segment.EnumerateObjects(carefully: true); + } + + foreach (ClrObject obj in objs) + { + cancellation.ThrowIfCancellationRequested(); + + if (obj.IsValid) + { + ulong size = obj.Size; + if (MinimumObjectSize != 0 && size < MinimumObjectSize) + { + continue; + } + + if (MaximumObjectSize != 0 && size > MaximumObjectSize) + { + continue; + } + } + + yield return obj; + } + } + } + } +} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/LiveObjectService.cs b/src/Microsoft.Diagnostics.ExtensionCommands/LiveObjectService.cs new file mode 100644 index 000000000..8de47430f --- /dev/null +++ b/src/Microsoft.Diagnostics.ExtensionCommands/LiveObjectService.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Diagnostics.DebugServices; +using Microsoft.Diagnostics.Runtime; + +namespace Microsoft.Diagnostics.ExtensionCommands +{ + [ServiceExport(Scope = ServiceScope.Runtime)] + public class LiveObjectService + { + private ObjectSet _liveObjs; + + public int UpdateSeconds { get; set; } = 15; + + public bool PrintWarning { get; set; } = true; + + [ServiceImport] + public ClrRuntime Runtime { get; set; } + + [ServiceImport] + public IConsoleService Console { get; set; } + + public bool IsLive(ulong obj) + { + _liveObjs ??= CreateObjectSet(); + return _liveObjs.Contains(obj); + } + + private ObjectSet CreateObjectSet() + { + ClrHeap heap = Runtime.Heap; + ObjectSet live = new(heap); + + Stopwatch sw = Stopwatch.StartNew(); + int updateSeconds = Math.Max(UpdateSeconds, 10); + bool printWarning = PrintWarning; + + if (printWarning) + { + Console.WriteLine("Calculating live objects, this may take a while..."); + } + + int roots = 0; + Queue todo = new(); + foreach (ClrRoot root in heap.EnumerateRoots()) + { + roots++; + if (printWarning && sw.Elapsed.TotalSeconds > updateSeconds && live.Count > 0) + { + Console.WriteLine($"Calculating live objects: {live.Count:n0} found"); + sw.Restart(); + } + + if (live.Add(root.Object)) + { + todo.Enqueue(root.Object); + } + } + + // We calculate the % complete based on how many are left in our todo queue. + // This means that % complete can go down if we end up seeing an unexpectedly + // high number of references compared to earlier objects. + int maxCount = todo.Count; + while (todo.Count > 0) + { + if (printWarning && sw.Elapsed.TotalSeconds > updateSeconds) + { + if (todo.Count > maxCount) + { + Console.WriteLine($"Calculating live objects: {live.Count:n0} found"); + } + else + { + Console.WriteLine($"Calculating live objects: {live.Count:n0} found - {(maxCount - todo.Count) * 100 / (float)maxCount:0.0}% complete"); + } + + maxCount = Math.Max(maxCount, todo.Count); + sw.Restart(); + } + + Console.CancellationToken.ThrowIfCancellationRequested(); + + ulong currAddress = todo.Dequeue(); + ClrObject obj = heap.GetObject(currAddress); + + foreach (ulong address in obj.EnumerateReferenceAddresses(carefully: false, considerDependantHandles: true)) + { + if (live.Add(address)) + { + todo.Enqueue(address); + } + } + } + + if (printWarning) + { + Console.WriteLine($"Calculating live objects complete: {live.Count:n0} objects from {roots:n0} roots"); + } + + return live; + } + } +} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/SimulateGCHeapCorruption.cs b/src/Microsoft.Diagnostics.ExtensionCommands/SimulateGCHeapCorruption.cs new file mode 100644 index 000000000..2158d8ead --- /dev/null +++ b/src/Microsoft.Diagnostics.ExtensionCommands/SimulateGCHeapCorruption.cs @@ -0,0 +1,256 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Diagnostics.DebugServices; +using Microsoft.Diagnostics.Runtime; + +using static Microsoft.Diagnostics.ExtensionCommands.TableOutput; + +namespace Microsoft.Diagnostics.ExtensionCommands +{ + [DebugCommand(Name=nameof(SimulateGCHeapCorruption), Help = "Writes values to the GC heap in strategic places to simulate heap corruption.")] + public class SimulateGCHeapCorruption : CommandBase + { + private static readonly List _changes = new(); + + [ServiceImport] + public IMemoryService MemoryService { get; set; } + + [ServiceImport] + public ClrRuntime Runtime { get; set; } + + [Argument] + public string Command { get; set; } + + public override void Invoke() + { + switch (Command?.ToLowerInvariant()) + { + case "revert": + case "rollback": + case "undo": + Rollback(); + break; + + case "corrupt": + Corrupt(); + break; + + default: + List(); + break; + } + } + + private void Usage() + { + Console.WriteLine($"To simulate heap corruption, use: !sos {nameof(SimulateGCHeapCorruption)} corrupt"); + Console.WriteLine($"To revert heap corruption, use: !sos {nameof(SimulateGCHeapCorruption)} revert"); + } + + private void List() + { + if (_changes.Count == 0) + { + Console.WriteLine("No changes written to the heap."); + } + else + { + TableOutput output = new(Console, (12, "x12"), (12, "x12"), (16, "x"), (16, "x"), (0, "")); + output.WriteRow("Object", "ModifiedAddr", "Old Value", "New Value", "Expected Failure"); + + foreach (Change change in _changes) + { + output.WriteRow(new DmlDumpObj(change.Object), change.AddressModified, change.OriginalValue.Reverse(), change.NewValue.Reverse(), change.ExpectedFailure); + } + } + + Console.WriteLine(); + Usage(); + } + + private void Rollback() + { + if (_changes is null) + { + Console.WriteLine("No changes written to the heap."); + Usage(); + return; + } + + foreach (Change change in _changes) + { + if (!MemoryService.WriteMemory(change.AddressModified, change.OriginalValue, out int written)) + { + Console.WriteLine($"Failed to restore memory at address: {change.AddressModified:x}, heap is still corrupted!"); + } + else if (written != change.OriginalValue.Length) + { + Console.WriteLine($"Failed to restore memory at address: {change.AddressModified:x}, only wrote {written} bytes out of {change.OriginalValue.Length}!"); + } + } + + _changes.Clear(); + } + + + private void Corrupt() + { + if (_changes.Count > 0) + { + Console.WriteLine("Heap is already corrupted!"); + Usage(); + return; + } + + ClrObject[] syncBlocks = FindObjectsWithSyncBlock().Take(2).ToArray(); + if (syncBlocks.Length >= 1) + { + WriteValue(ObjectCorruptionKind.SyncBlockMismatch, syncBlocks[0], syncBlocks[0] - 4, (byte)0xcc); + } + + if (syncBlocks.Length >= 2) + { + WriteValue(ObjectCorruptionKind.SyncBlockZero, syncBlocks[1], syncBlocks[1] - 4, 0x08000000); + } + + ClrObject[] withRefs = FindObjectsWithReferences().Take(3).ToArray(); + if (withRefs.Length >= 1) + { + (ulong Object, ulong FirstReference) entry = GetFirstReference(withRefs[0]); + WriteValue(ObjectCorruptionKind.InvalidObjectReference, entry.Object, entry.FirstReference, 0xcccccccc); + } + if (withRefs.Length >= 2) + { + ulong free = Runtime.Heap.EnumerateObjects().FirstOrDefault(f => f.IsFree); + if (free != 0) + { + (ulong Object, ulong FirstReference) entry = GetFirstReference(withRefs[1]); + WriteValue(ObjectCorruptionKind.FreeObjectReference, entry.Object, entry.FirstReference, free); + } + } + if (withRefs.Length >= 3) + { + (ulong Object, ulong FirstReference) entry = GetFirstReference(withRefs[2]); + WriteValue(ObjectCorruptionKind.ObjectReferenceNotPointerAligned, entry.Object, entry.FirstReference, (byte)1); + } + + ClrObject[] arrays = FindArrayObjects().Take(2).ToArray(); + if (arrays.Length >= 1) + { + WriteValue(ObjectCorruptionKind.InvalidMethodTable, arrays[0], arrays[0], 0xcccccccc); + } + + if (arrays.Length >= 2) + { + WriteValue(ObjectCorruptionKind.ObjectTooLarge, arrays[1], arrays[1] + (uint)MemoryService.PointerSize, 0xcccccccc); + } + + List(); + } + + private static (ulong Object, ulong FirstReference) GetFirstReference(ClrObject obj) + { + return (obj, obj.EnumerateReferenceAddresses().First()); + } + + private IEnumerable FindObjectsWithSyncBlock() + { + foreach (SyncBlock sync in Runtime.Heap.EnumerateSyncBlocks()) + { + ClrObject obj = Runtime.Heap.GetObject(sync.Object); + + if (_changes.Any(ch => ch.Object == obj)) + { + continue; + } + + if (obj.IsValid && !obj.IsFree) + { + yield return obj; + } + } + } + + private IEnumerable FindObjectsWithReferences() + { + foreach (ClrObject obj in Runtime.Heap.EnumerateObjects()) + { + if (obj.IsFree || !obj.IsValid) + { + continue; + } + + if (_changes.Any(ch => ch.Object == obj)) + { + continue; + } + + if (obj.EnumerateReferenceAddresses().Any()) + { + yield return obj; + } + } + } + + private IEnumerable FindArrayObjects() + { + foreach (ClrObject obj in Runtime.Heap.EnumerateObjects()) + { + if (obj.IsFree || !obj.IsValid) + { + continue; + } + + if (_changes.Any(ch => ch.Object == obj)) + { + continue; + } + + if (obj.IsArray) + { + yield return obj; + } + } + } + + private unsafe void WriteValue(ObjectCorruptionKind kind, ulong obj, ulong address, T value) + where T : unmanaged + { + byte[] old = new byte[sizeof(T)]; + + Span newBuffer = new(&value, sizeof(T)); + + if (!MemoryService.ReadMemory(address, old, old.Length, out int read) || read != old.Length) + { + throw new Exception("Failed to read memory."); + } + + if (!MemoryService.WriteMemory(address, newBuffer, out int written) || written != newBuffer.Length) + { + throw new Exception($"Failed to write to {address:x}"); + } + + _changes.Add(new() + { + Object = obj, + AddressModified = address, + OriginalValue = old, + NewValue = newBuffer.ToArray(), + ExpectedFailure = kind + }); + } + + private sealed class Change + { + public ulong Object { get; set; } + public ulong AddressModified { get; set; } + public byte[] OriginalValue { get; set; } + public byte[] NewValue { get; set; } + public ObjectCorruptionKind ExpectedFailure { get; set; } + } + } +} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/TableOutput.cs b/src/Microsoft.Diagnostics.ExtensionCommands/TableOutput.cs index a12420b24..51806de18 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/TableOutput.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/TableOutput.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Diagnostics.DebugServices; @@ -11,10 +12,15 @@ namespace Microsoft.Diagnostics.ExtensionCommands internal sealed class TableOutput { private readonly char _spacing = ' '; + public string Divider { get; set; } = " "; + public bool AlignLeft { get; set; } + public int ColumnCount => _formats.Length; + public IConsoleService Console { get; } + public int TotalWidth => 1 * (_formats.Length - 1) + _formats.Sum(c => Math.Abs(c.width)); private readonly (int width, string format)[] _formats; @@ -25,6 +31,11 @@ namespace Microsoft.Diagnostics.ExtensionCommands Console = console; } + public void WriteSpacer(char spacer) + { + Console.WriteLine(new string(spacer, Divider.Length * (_formats.Length - 1) + _formats.Sum(c => Math.Abs(c.width)))); + } + public void WriteRow(params object[] columns) { StringBuilder sb = new(Divider.Length * columns.Length + _formats.Sum(c => Math.Abs(c.width)) + 32); @@ -37,18 +48,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands } (int width, string format) = i < _formats.Length ? _formats[i] : default; - - string value; - if (string.IsNullOrWhiteSpace(format)) - { - value = columns[i]?.ToString(); - } - else - { - value = Format(columns[i], format); - } - - AddValue(_spacing, sb, width, value ?? ""); + FormatColumn(_spacing, columns[i], sb, width, format); } Console.WriteLine(sb.ToString()); @@ -67,39 +67,61 @@ namespace Microsoft.Diagnostics.ExtensionCommands (int width, string format) = i < _formats.Length ? _formats[i] : default; - string value; - if (string.IsNullOrWhiteSpace(format)) - { - value = columns[i]?.ToString(); - } - else - { - value = Format(columns[i], format); - } - - AddValue(spacing, sb, width, value ?? ""); + FormatColumn(spacing, columns[i], sb, width, format); } Console.WriteLine(sb.ToString()); } - - public void WriteSpacer(char spacer) + private void FormatColumn(char spacing, object value, StringBuilder sb, int width, string format) { - Console.WriteLine(new string(spacer, Divider.Length * (_formats.Length - 1) + _formats.Sum(c => Math.Abs(c.width)))); + string action = null; + string text; + if (value is DmlExec dml) + { + value = dml.Text; + if (Console.SupportsDml) + { + action = dml.Action; + } + } + + if (string.IsNullOrWhiteSpace(format)) + { + text = value?.ToString(); + } + else + { + text = Format(value, format); + } + + AddValue(spacing, sb, width, text ?? "", action); } - private void AddValue(char spacing, StringBuilder sb, int width, string value) + private void AddValue(char spacing, StringBuilder sb, int width, string value, string action) { bool leftAlign = AlignLeft ? width > 0 : width < 0; width = Math.Abs(width); if (width == 0) { - sb.Append(value); + if (string.IsNullOrWhiteSpace(action)) + { + sb.Append(value); + } + else + { + WriteAndClear(sb); + Console.WriteDmlExec(value, action); + } } else if (value.Length > width) { + if (!string.IsNullOrWhiteSpace(action)) + { + WriteAndClear(sb); + } + if (width <= 3) { sb.Append(value, 0, width); @@ -117,17 +139,61 @@ namespace Microsoft.Diagnostics.ExtensionCommands sb.Append(value); } + if (!string.IsNullOrWhiteSpace(action)) + { + WriteDmlExecAndClear(sb, action); + } } else if (leftAlign) { - sb.Append(value.PadRight(width, spacing)); + if (!string.IsNullOrWhiteSpace(action)) + { + WriteAndClear(sb); + Console.WriteDmlExec(value, action); + } + else + { + sb.Append(value); + } + + int remaining = width - value.Length; + if (remaining > 0) + { + sb.Append(spacing, remaining); + } } else { - sb.Append(value.PadLeft(width, spacing)); + int remaining = width - value.Length; + if (remaining > 0) + { + sb.Append(spacing, remaining); + } + + if (!string.IsNullOrWhiteSpace(action)) + { + WriteAndClear(sb); + Console.WriteDmlExec(value, action); + } + else + { + sb.Append(value); + } } } + private void WriteDmlExecAndClear(StringBuilder sb, string action) + { + Console.WriteDmlExec(sb.ToString(), action); + sb.Clear(); + } + + private void WriteAndClear(StringBuilder sb) + { + Console.Write(sb.ToString()); + sb.Clear(); + } + private static string Format(object obj, string format) { if (obj is null) @@ -148,9 +214,39 @@ namespace Microsoft.Diagnostics.ExtensionCommands uint ui => ui.ToString(format), int i => i.ToString(format), StringBuilder sb => sb.ToString(), + IEnumerable bytes => string.Join("", bytes.Select(b => b.ToString("x2"))), string s => s, _ => throw new NotImplementedException(obj.GetType().ToString()), }; } + + public class DmlExec + { + public object Text { get; } + public string Action { get; } + + public DmlExec(object text, string action) + { + Text = text; + Action = action; + } + } + + public sealed class DmlDumpObj : DmlExec + { + public DmlDumpObj(ulong address) + : base(address, address != 0 ? $"!dumpobj /d {address:x}" : "") + { + } + } + + public sealed class DmlDumpHeapMT : DmlExec + { + public DmlDumpHeapMT(ulong methodTable) + : base (methodTable, methodTable != 0 ? $"!dumpheap -mt {methodTable:x}" : "") + { + + } + } } } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/VerifyHeapCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/VerifyHeapCommand.cs new file mode 100644 index 000000000..07e2bcffd --- /dev/null +++ b/src/Microsoft.Diagnostics.ExtensionCommands/VerifyHeapCommand.cs @@ -0,0 +1,283 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.Diagnostics.DebugServices; +using Microsoft.Diagnostics.Runtime; +using static Microsoft.Diagnostics.ExtensionCommands.TableOutput; + +namespace Microsoft.Diagnostics.ExtensionCommands +{ + [Command(Name = "verifyheap", Help = "Searches the managed heap for memory corruption..")] + public class VerifyHeapCommand : CommandBase + { + private int _totalObjects; + + [ServiceImport] + public ClrRuntime Runtime { get; set; } + + [ServiceImport] + public IMemoryService MemoryService { get; set; } + + [Option(Name = "-heap")] + public int GCHeap { get; set; } = -1; + + [Option(Name = "-segment")] + public string Segment { get; set; } + + [Argument(Help ="Optional memory ranges in the form of: [Start [End]]")] + public string[] MemoryRange { get; set; } + + public override void Invoke() + { + HeapWithFilters filteredHeap = new(Runtime.Heap); + if (GCHeap >= 0) + { + filteredHeap.GCHeap = GCHeap; + } + + if (!string.IsNullOrWhiteSpace(Segment)) + { + filteredHeap.FilterBySegmentHex(Segment); + } + + if (MemoryRange is not null && MemoryRange.Length > 0) + { + if (MemoryRange.Length > 2) + { + string badArgument = MemoryRange.FirstOrDefault(f => f.StartsWith("-") || f.StartsWith("/")); + if (badArgument != null) + { + throw new ArgumentException($"Unknown argument: {badArgument}"); + } + + throw new ArgumentException("Too many arguments to !verifyheap"); + } + + string start = MemoryRange[0]; + string end = MemoryRange.Length > 1 ? MemoryRange[1] : null; + filteredHeap.FilterByHexMemoryRange(start, end); + } + + VerifyHeap(filteredHeap.EnumerateFilteredObjects(Console.CancellationToken), verifySyncTable: filteredHeap.HasFilters); + } + + private IEnumerable EnumerateWithCount(IEnumerable objs) + { + _totalObjects = 0; + foreach (ClrObject obj in objs) + { + _totalObjects++; + yield return obj; + } + } + + private void VerifyHeap(IEnumerable objects, bool verifySyncTable) + { + // Count _totalObjects + objects = EnumerateWithCount(objects); + + int errors = 0; + TableOutput output = null; + ClrHeap heap = Runtime.Heap; + + // Verify heap + foreach (ObjectCorruption corruption in heap.VerifyHeap(objects)) + { + errors++; + WriteError(ref output, heap, corruption); + } + + // Verify SyncBlock table unless the user asked us to verify only a small range: + int syncBlockErrors = 0; + if (verifySyncTable) + { + int totalSyncBlocks = 0; + foreach (SyncBlock syncBlk in heap.EnumerateSyncBlocks()) + { + totalSyncBlocks++; + + if (syncBlk.Object != 0 && heap.IsObjectCorrupted(syncBlk.Object, out ObjectCorruption corruption)) + { + // If we already printed some errors, create a break in the previous table and + // write the table header again + if (syncBlockErrors++ == 0) + { + if (output is not null) + { + output = null; + Console.WriteLine(); + } + + Console.WriteLine("SyncBlock Table:"); + } + + WriteError(ref output, heap, corruption); + } + } + + if (syncBlockErrors > 0) + { + Console.WriteLine(); + } + + Console.WriteLine($"{totalSyncBlocks:n0} SyncBlocks verified, {syncBlockErrors:n0} error{(syncBlockErrors == 1 ? "" :"s")}."); + } + + if (errors + syncBlockErrors > 0) + { + Console.WriteLine(); + } + + Console.WriteLine($"{_totalObjects:n0} objects verified, {errors:n0} error{(errors == 1 ? "" : "s")}."); + + if (errors == 0 && syncBlockErrors == 0) + { + Console.WriteLine("No heap corruption detected."); + } + } + + private void WriteError(ref TableOutput output, ClrHeap heap, ObjectCorruption corruption) + { + ClrObject obj = corruption.Object; + + string message = corruption.Kind switch + { + // odd failures + ObjectCorruptionKind.ObjectNotOnTheHeap => $"Tried to validate {obj.Address:x} but its address was not on any segment.", + ObjectCorruptionKind.ObjectNotPointerAligned => $"Object {obj.Address:x} is not pointer aligned", + + // Object failures + ObjectCorruptionKind.ObjectTooLarge => $"Object {obj.Address:x} is too large, size={obj.Size:x}, segmentEnd: {ValueWithError(heap.GetSegmentByAddress(obj)?.End)}", + ObjectCorruptionKind.InvalidMethodTable => $"Object {obj.Address:x} has an invalid method table {ReadPointerWithError(obj):x}", + ObjectCorruptionKind.InvalidThinlock => $"Object {obj.Address:x} has an invalid thin lock", + ObjectCorruptionKind.SyncBlockMismatch => GetSyncBlockFailureMessage(corruption), + ObjectCorruptionKind.SyncBlockZero => GetSyncBlockFailureMessage(corruption), + + // Object reference failures + ObjectCorruptionKind.ObjectReferenceNotPointerAligned => $"Object {obj.Address:x} has an unaligned member at {corruption.Offset:x}: is not pointer aligned", + ObjectCorruptionKind.InvalidObjectReference => $"Object {obj.Address:x} has a bad member at offset {corruption.Offset:x}: {ReadPointerWithError(obj + (uint)corruption.Offset)}", + ObjectCorruptionKind.FreeObjectReference => $"Object {obj.Address:x} contains free object at offset {corruption.Offset:x}: {ReadPointerWithError(obj + (uint)corruption.Offset)}", + + // Memory read failures + ObjectCorruptionKind.CouldNotReadObject => $"Could not read object {obj.Address:x} at offset {corruption.Offset:x}: {ReadPointerWithError(obj + (uint)corruption.Offset)}", + ObjectCorruptionKind.CouldNotReadMethodTable => $"Could not read method table for Object {obj.Address:x}", + ObjectCorruptionKind.CouldNotReadCardTable => $"Could not verify object {obj.Address:x}: could not read card table", + ObjectCorruptionKind.CouldNotReadGCDesc => $"Could not verify object {obj.Address:x}: could not read GCDesc", + + _ => "" + }; + + WriteRow(ref output, heap, corruption, message); + } + + private void WriteRow(ref TableOutput output, ClrHeap heap, ObjectCorruption corruption, string message) + { + if (output is null) + { + if (heap.IsServer) + { + output = new(Console, (-4, ""), (-12, "x12"), (-12, "x12"), (32, ""), (0, "")) + { + AlignLeft = true, + }; + + output.WriteRow("Heap", "Segment", "Object", "Failure", ""); + } + else + { + output = new(Console, (-12, "x12"), (-12, "x12"), (22, ""), (0, "")) + { + AlignLeft = true, + }; + + output.WriteRow("Segment", "Object", "Failure", ""); + } + } + + + ClrSegment segment = heap.GetSegmentByAddress(corruption.Object); + + object[] columns = new object[output.ColumnCount]; + int i = 0; + if (heap.IsServer) + { + columns[i++] = ValueWithError(segment?.SubHeap.Index, format: "", error: ""); + } + + columns[i++] = ValueWithError(segment?.Address, format: "x12", error: ""); + columns[i++] = new DmlExec(corruption.Object.Address, $"!ListNearObj {corruption.Object.Address:x}"); + columns[i++] = corruption.Kind; + columns[i++] = message; + + output.WriteRow(columns); + } + + private static string GetSyncBlockFailureMessage(ObjectCorruption corruption) + { + Debug.Assert(corruption.Kind == ObjectCorruptionKind.SyncBlockZero || corruption.Kind == ObjectCorruptionKind.SyncBlockMismatch); + + // due to how we store syncblock indexes, we can't have a negative index + // negative index here means the object or CLR didn't have a syncblock + string result; + if (corruption.ClrSyncBlockIndex >= 0) + { + result = $"Object {corruption.Object:x} should have a SyncBlock index of {corruption.ClrSyncBlockIndex} "; + if (corruption.SyncBlockIndex >= 0) + { + result += $"but instead had an index of {corruption.SyncBlockIndex}"; + } + else + { + result += $"but instead had no SyncBlock"; + } + } + else + { + // We shouldn't have a case where ClrSyncBlockIndex < 0 && SyncBLockIndex < 0, but we'll handle that case anyway + if (corruption.SyncBlockIndex >= 0) + { + result = $"Object {corruption.Object:x} had a SyncBlock index of {corruption.SyncBlockIndex} but the runtime has no matching SyncBlock"; + } + else + { + result = $"Object {corruption.Object:x} had no SyncBlock when it was expected to"; + } + } + + return result; + } + + private static string ValueWithError(int? value, string format = "x", string error = "???") + { + if (value.HasValue) + { + return value.Value.ToString(format); + } + + return error; + } + + private static string ValueWithError(ulong? value, string format = "x", string error = "???") + { + if (value.HasValue) + { + return value.Value.ToString(format); + } + + return error; + } + + private string ReadPointerWithError(ulong address) + { + if (MemoryService.ReadPointer(address, out ulong value)) + { + return value.ToString("x"); + } + + return "???"; + } + } +} diff --git a/src/Microsoft.Diagnostics.Repl/ConsoleService.cs b/src/Microsoft.Diagnostics.Repl/ConsoleService.cs index 8df94ded5..be30dc867 100644 --- a/src/Microsoft.Diagnostics.Repl/ConsoleService.cs +++ b/src/Microsoft.Diagnostics.Repl/ConsoleService.cs @@ -591,6 +591,8 @@ namespace Microsoft.Diagnostics.Repl void IConsoleService.WriteDml(string text) => throw new NotSupportedException(); + void IConsoleService.WriteDmlExec(string text, string _) => throw new NotSupportedException(); + CancellationToken IConsoleService.CancellationToken { get; set; } int IConsoleService.WindowWidth diff --git a/src/SOS/SOS.Extensions/ConsoleServiceFromDebuggerServices.cs b/src/SOS/SOS.Extensions/ConsoleServiceFromDebuggerServices.cs index 16b9de8d7..9fceefd68 100644 --- a/src/SOS/SOS.Extensions/ConsoleServiceFromDebuggerServices.cs +++ b/src/SOS/SOS.Extensions/ConsoleServiceFromDebuggerServices.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Threading; +using System.Xml.Linq; using Microsoft.Diagnostics.DebugServices; using SOS.Hosting.DbgEng.Interop; @@ -27,6 +28,12 @@ namespace SOS.Extensions public void WriteDml(string text) => _debuggerServices.OutputDmlString(DEBUG_OUTPUT.NORMAL, text); + public void WriteDmlExec(string text, string cmd) + { + string dml = $"{DmlEscape(text)}"; + WriteDml(dml); + } + public bool SupportsDml => _supportsDml ??= _debuggerServices.SupportsDml; public CancellationToken CancellationToken { get; set; } @@ -34,5 +41,7 @@ namespace SOS.Extensions int IConsoleService.WindowWidth => _debuggerServices.GetOutputWidth(); #endregion + + private static string DmlEscape(string text) => new XText(text).ToString(); } } diff --git a/src/SOS/SOS.Hosting/Commands/SOSCommand.cs b/src/SOS/SOS.Hosting/Commands/SOSCommand.cs index ced6f9091..db4955aad 100644 --- a/src/SOS/SOS.Hosting/Commands/SOSCommand.cs +++ b/src/SOS/SOS.Hosting/Commands/SOSCommand.cs @@ -20,7 +20,6 @@ namespace SOS.Hosting [Command(Name = "dumpdelegate", DefaultOptions = "DumpDelegate", Help = "Displays information about a delegate.")] [Command(Name = "dumpdomain", DefaultOptions = "DumpDomain", Help = "Displays the Microsoft intermediate language (MSIL) that's associated with a managed method.")] [Command(Name = "dumpgcdata", DefaultOptions = "DumpGCData", Help = "Displays information about the GC data.")] - [Command(Name = "dumpheap", DefaultOptions = "DumpHeap", Help = "Displays info about the garbage-collected heap and collection statistics about objects.")] [Command(Name = "dumpil", DefaultOptions = "DumpIL", Help = "Displays the Microsoft intermediate language (MSIL) that is associated with a managed method.")] [Command(Name = "dumplog", DefaultOptions = "DumpLog", Help = "Writes the contents of an in-memory stress log to the specified file.")] [Command(Name = "dumpmd", DefaultOptions = "DumpMD", Help = "Displays information about a MethodDesc structure at the specified address.")] @@ -59,7 +58,6 @@ namespace SOS.Hosting [Command(Name = "threadpool", DefaultOptions = "ThreadPool", Help = "Lists basic information about the thread pool.")] [Command(Name = "threadstate", DefaultOptions = "ThreadState", Help = "Pretty prints the meaning of a threads state.")] [Command(Name = "traverseheap", DefaultOptions = "TraverseHeap", Help = "Writes out heap information to a file in a format understood by the CLR Profiler.")] - [Command(Name = "verifyheap", DefaultOptions = "VerifyHeap", Help = "Checks the GC heap for signs of corruption.")] [Command(Name = "verifyobj", DefaultOptions = "VerifyObj", Help = "Checks the object for signs of corruption.")] [Command(Name = "comstate", DefaultOptions = "COMState", Flags = CommandFlags.Windows, Help = "Lists the COM apartment model for each thread.")] [Command(Name = "dumprcw", DefaultOptions = "DumpRCW", Flags = CommandFlags.Windows, Help = "Displays information about a Runtime Callable Wrapper.")] diff --git a/src/SOS/SOS.UnitTests/SOSRunner.cs b/src/SOS/SOS.UnitTests/SOSRunner.cs index bc38a80bd..e1f993f3b 100644 --- a/src/SOS/SOS.UnitTests/SOSRunner.cs +++ b/src/SOS/SOS.UnitTests/SOSRunner.cs @@ -170,7 +170,7 @@ public class SOSRunner : IDisposable } public const string HexValueRegEx = "[A-Fa-f0-9]+(`[A-Fa-f0-9]+)?"; - public const string DecValueRegEx = "[0-9]+(`[0-9]+)?"; + public const string DecValueRegEx = "[,0-9]+(`[,0-9]+)?"; public NativeDebugger Debugger { get; private set; } diff --git a/src/SOS/SOS.UnitTests/Scripts/ConcurrentDictionaries.script b/src/SOS/SOS.UnitTests/Scripts/ConcurrentDictionaries.script index a5f22338b..3472a8445 100644 --- a/src/SOS/SOS.UnitTests/Scripts/ConcurrentDictionaries.script +++ b/src/SOS/SOS.UnitTests/Scripts/ConcurrentDictionaries.script @@ -19,13 +19,8 @@ EXTCOMMAND: dcd 0000000000000001 VERIFY: is not referencing an object # Checks on ConcurrentDictionary -SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary`2[[ -IFDEF:DESKTOP -SOSCOMMAND: DumpHeap -mt ^() .*System.Collections.Concurrent.ConcurrentDictionary`2\[\[System.Int32, mscorlib\],\[System.String\[\], mscorlib\]\] -ENDIF:DESKTOP -!IFDEF:DESKTOP -SOSCOMMAND: DumpHeap -mt ^() .*System.Collections.Concurrent.ConcurrentDictionary`2\[\[System.Int32, System.Private.CoreLib\],\[System.String\[\], System.Private.CoreLib\]\] -ENDIF:DESKTOP +SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary< +SOSCOMMAND: DumpHeap -mt ^() .*System.Collections.Concurrent.ConcurrentDictionary[^+] EXTCOMMAND: dcd ^()\s+\s+\d+ VERIFY: 2 items VERIFY: Key: 2 @@ -34,13 +29,8 @@ VERIFY: Name:\s+System.String\[\] VERIFY: Number of elements 4 # Checks on ConcurrentDictionary -SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary`2[[ -IFDEF:DESKTOP -SOSCOMMAND: DumpHeap -mt ^() .*System.Collections.Concurrent.ConcurrentDictionary`2\[\[System.Int32, mscorlib\],\[System.Int32, mscorlib\]\] -ENDIF:DESKTOP -!IFDEF:DESKTOP -SOSCOMMAND: DumpHeap -mt ^() .*System.Collections.Concurrent.ConcurrentDictionary`2\[\[System.Int32, System.Private.CoreLib\],\[System.Int32, System.Private.CoreLib\]\] -ENDIF:DESKTOP +SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary< +SOSCOMMAND: DumpHeap -mt ^() .*System.Collections.Concurrent.ConcurrentDictionary[^+] EXTCOMMAND: dcd ^()\s+\s+\d+ VERIFY: 3 items VERIFY: Key: 0\s+Value: 1 @@ -48,13 +38,8 @@ VERIFY: Key: 31\s+Value: 17 VERIFY: Key: 1521482\s+Value: 512487 # Checks on ConcurrentDictionary -SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary`2[[ -IFDEF:DESKTOP -SOSCOMMAND: DumpHeap -mt ^() .*System.Collections.Concurrent.ConcurrentDictionary`2\[\[System.String, mscorlib\],\[System.Boolean, mscorlib\]\] -ENDIF:DESKTOP -!IFDEF:DESKTOP -SOSCOMMAND: DumpHeap -mt ^() .*System.Collections.Concurrent.ConcurrentDictionary`2\[\[System.String, System.Private.CoreLib\],\[System.Boolean, System.Private.CoreLib\]\] -ENDIF:DESKTOP +SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary< +SOSCOMMAND: DumpHeap -mt ^() .*System.Collections.Concurrent.ConcurrentDictionary[^+] EXTCOMMAND: dcd ^()\s+\s+\d+ VERIFY: 3 items VERIFY: Key: "String true"\s+Value: True @@ -62,8 +47,8 @@ VERIFY: Key: "String false"\s+Value: False VERIFY: Key: "SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS\.\.\.\s+Value: False # Checks on ConcurrentDictionary -SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary`2[[ -SOSCOMMAND: DumpHeap -mt ^() .*System.Collections.Concurrent.ConcurrentDictionary`2\[\[DotnetDumpCommands\.Program\+DumpSampleStruct, DotnetDumpCommands\],\[DotnetDumpCommands\.Program\+DumpSampleClass, DotnetDumpCommands\]\] +SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary< +SOSCOMMAND: DumpHeap -mt ^() .*System.Collections.Concurrent.ConcurrentDictionary[^+] EXTCOMMAND: dcd ^()\s+\s+\d+ VERIFY: 2 items VERIFY: Key: dumpvc \s+Value: null @@ -79,13 +64,8 @@ VERIFY: instance\s+00000000\s+\ ENDIF:32BIT # Checks on ConcurrentDictionary -SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary`2[[ -IFDEF:DESKTOP -SOSCOMMAND: DumpHeap -mt ^() .*System.Collections.Concurrent.ConcurrentDictionary`2\[\[System.Int32, mscorlib\],\[DotnetDumpCommands\.Program\+DumpSampleStruct, DotnetDumpCommands\]\] -ENDIF:DESKTOP -!IFDEF:DESKTOP -SOSCOMMAND: DumpHeap -mt ^() .*System.Collections.Concurrent.ConcurrentDictionary`2\[\[System.Int32, System.Private.CoreLib\],\[DotnetDumpCommands\.Program\+DumpSampleStruct, DotnetDumpCommands\]\] -ENDIF:DESKTOP +SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary< +SOSCOMMAND: DumpHeap -mt ^() .*System.Collections.Concurrent.ConcurrentDictionary[^+] EXTCOMMAND: dcd ^()\s+\s+\d+ VERIFY: 1 items VERIFY: Key: 0\s+Value: dumpvc @@ -95,4 +75,4 @@ VERIFY: instance\s+12\s+IntValue VERIFY: instance\s+\s+StringValue VERIFY: instance\s+\s+Date -ENDIF:NETCORE_OR_DOTNETDUMP \ No newline at end of file +ENDIF:NETCORE_OR_DOTNETDUMP diff --git a/src/SOS/SOS.UnitTests/Scripts/GCPOH.script b/src/SOS/SOS.UnitTests/Scripts/GCPOH.script index a4c890dfe..f384c6bfd 100644 --- a/src/SOS/SOS.UnitTests/Scripts/GCPOH.script +++ b/src/SOS/SOS.UnitTests/Scripts/GCPOH.script @@ -39,6 +39,7 @@ VERIFY:.*Thread : VERIFY:\s+\s+\s+GCPOH\.Main\(\)\s+\[.*[Gg][Cc][Pp][Oo][Hh]\.cs\s+@\s+22\]\s+ SOSCOMMAND:VerifyHeap +VERIFY:.* 0 errors.* VERIFY:\s*No heap corruption detected.\s* SOSCOMMAND:GCHeapStat diff --git a/src/SOS/Strike/strike.cpp b/src/SOS/Strike/strike.cpp index 701990c02..463e17393 100644 --- a/src/SOS/Strike/strike.cpp +++ b/src/SOS/Strike/strike.cpp @@ -3868,461 +3868,6 @@ namespace sos }; } -class DumpHeapImpl -{ -public: - DumpHeapImpl(PCSTR args) - : mStart(0), mStop(0), mMT(0), mMinSize(0), mMaxSize(~0), - mStat(FALSE), mStrings(FALSE), mVerify(FALSE), - mThinlock(FALSE), mShort(FALSE), mDML(FALSE), - mLive(FALSE), mDead(FALSE), mType(NULL) - { - ArrayHolder type = NULL; - - TADDR minTemp = 0; - CMDOption option[] = - { // name, vptr, type, hasValue - {"-mt", &mMT, COHEX, TRUE}, // dump objects with a given MethodTable - {"-type", &type, COSTRING, TRUE}, // list objects of specified type - {"-stat", &mStat, COBOOL, FALSE}, // dump a summary of types and the number of instances of each - {"-strings", &mStrings, COBOOL, FALSE}, // dump a summary of string objects - {"-verify", &mVerify, COBOOL, FALSE}, // verify heap objects (heapverify) - {"-thinlock", &mThinlock, COBOOL, FALSE},// list only thinlocks - {"-short", &mShort, COBOOL, FALSE}, // list only addresses - {"-min", &mMinSize, COHEX, TRUE}, // min size of objects to display (hex) - {"-max", &mMaxSize, COHEX, TRUE}, // max size of objects to display (hex) - {"-live", &mLive, COHEX, FALSE}, // only print live objects - {"-dead", &mDead, COHEX, FALSE}, // only print dead objects - {"/d", &mDML, COBOOL, FALSE}, // Debugger Markup Language - }; - - CMDValue arg[] = - { // vptr, type - {&mStart, COHEX}, - {&mStop, COHEX} - }; - - size_t nArgs = 0; - if (!GetCMDOption(args, option, ARRAY_SIZE(option), arg, ARRAY_SIZE(arg), &nArgs)) - sos::Throw("Failed to parse command line arguments."); - - if (mStart == 0) - mStart = minTemp; - - if (mStop == 0) - mStop = sos::GCHeap::HeapEnd; - - if (type && mMT) - { - sos::Throw("Cannot specify both -mt and -type"); - } - - if (mLive && mDead) - { - sos::Throw("Cannot specify both -live and -dead."); - } - - if (mMinSize > mMaxSize) - { - sos::Throw("wrong argument"); - } - - // If the user gave us a type, convert it to unicode and clean up "type". - if (type && !mStrings) - { - size_t iLen = strlen(type) + 1; - mType = new WCHAR[iLen]; - MultiByteToWideChar(CP_ACP, 0, type, -1, mType, (int)iLen); - } - } - - ~DumpHeapImpl() - { - if (mType) - delete [] mType; - } - - void Run() - { - // enable Debugger Markup Language - EnableDMLHolder dmlholder(mDML); - sos::GCHeap gcheap; - - if (!gcheap.AreGCStructuresValid()) - DisplayInvalidStructuresMessage(); - - if (IsMiniDumpFile()) - { - ExtOut("In a minidump without full memory, most gc heap structures will not be valid.\n"); - ExtOut("If you need this functionality, get a full memory dump with \".dump /ma mydump.dmp\"\n"); - } - - if (mLive || mDead) - { - GCRootImpl gcroot; - mLiveness = gcroot.GetLiveObjects(); - } - - // Some of the "specialty" versions of DumpHeap have slightly - // different implementations than the standard version of DumpHeap. - // We seperate them out to not clutter the standard DumpHeap function. - if (mShort) - DumpHeapShort(gcheap); - else if (mThinlock) - DumpHeapThinlock(gcheap); - else if (mStrings) - DumpHeapStrings(gcheap); - else - DumpHeap(gcheap); - - if (mVerify) - ValidateSyncTable(gcheap); - } - - static bool ValidateSyncTable(sos::GCHeap &gcheap) - { - bool succeeded = true; - for (sos::SyncBlkIterator itr; itr; ++itr) - { - sos::CheckInterrupt(); - - if (!itr->IsFree()) - { - if (!sos::IsObject(itr->GetObject(), true)) - { - ExtOut("SyncBlock %d corrupted, points to invalid object %p\n", - itr->GetIndex(), SOS_PTR(itr->GetObject())); - succeeded = false; - } - else - { - // Does the object header point to this syncblock index? - sos::Object obj = itr->GetObject(); - ULONG header = 0; - - if (!obj.TryGetHeader(header)) - { - ExtOut("Failed to get object header for object %p while inspecting syncblock at index %d.\n", - SOS_PTR(itr->GetObject()), itr->GetIndex()); - succeeded = false; - } - else - { - bool valid = false; - if ((header & BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX) != 0 && (header & BIT_SBLK_IS_HASHCODE) == 0) - { - ULONG index = header & MASK_SYNCBLOCKINDEX; - valid = (ULONG)itr->GetIndex() == index; - } - - if (!valid) - { - ExtOut("Object header for %p should have a SyncBlock index of %d.\n", - SOS_PTR(itr->GetObject()), itr->GetIndex()); - succeeded = false; - } - } - } - } - } - - return succeeded; - } -private: - DumpHeapImpl(const DumpHeapImpl &); - - bool Verify(const sos::ObjectIterator &itr) - { - if (mVerify) - { - char buffer[1024]; - if (!itr.Verify(buffer, ARRAY_SIZE(buffer))) - { - ExtOut(buffer); - return false; - } - } - - return true; - } - - bool IsCorrectType(const sos::Object &obj) - { - if (mMT != NULL) - return mMT == obj.GetMT(); - - if (mType != NULL) - { - WString name = obj.GetTypeName(); - return _wcsstr(name.c_str(), mType) != NULL; - } - - return true; - } - - bool IsCorrectSize(const sos::Object &obj) - { - size_t size = obj.GetSize(); - return size >= mMinSize && size <= mMaxSize; - } - - bool IsCorrectLiveness(const sos::Object &obj) - { - if (mLive && mLiveness.find(obj.GetAddress()) == mLiveness.end()) - return false; - - if (mDead && (mLiveness.find(obj.GetAddress()) != mLiveness.end() || obj.IsFree())) - return false; - - return true; - } - - inline void PrintHeader() - { - ExtOut("%" POINTERSIZE "s %" POINTERSIZE "s %8s\n", "Address", "MT", "Size"); - } - - void DumpHeap(sos::GCHeap &gcheap) - { - HeapStat stats; - - // For heap fragmentation tracking. - TADDR lastFreeObj = NULL; - size_t lastFreeSize = 0; - - if (!mStat) - PrintHeader(); - - for (sos::ObjectIterator itr = gcheap.WalkHeap(mStart, mStop); itr; ++itr) - { - if (!Verify(itr)) - return; - - bool onLOH = itr.IsCurrObjectOnLOH(); - - // Check for free objects to report fragmentation - if (lastFreeObj != NULL) - ReportFreeObject(lastFreeObj, lastFreeSize, itr->GetAddress(), itr->GetMT()); - - if (!onLOH && itr->IsFree()) - { - lastFreeObj = *itr; - lastFreeSize = itr->GetSize(); - } - else - { - lastFreeObj = NULL; - } - - if (IsCorrectType(*itr) && IsCorrectSize(*itr) && IsCorrectLiveness(*itr)) - { - stats.Add((DWORD_PTR)itr->GetMT(), (DWORD)itr->GetSize()); - if (!mStat) - DMLOut("%s %s %8d%s\n", DMLObject(itr->GetAddress()), DMLDumpHeapMT(itr->GetMT()), itr->GetSize(), - itr->IsFree() ? " Free":" "); - } - } - - if (!mStat) - ExtOut("\n"); - - stats.Sort(); - stats.Print(); - - PrintFragmentationReport(); - } - - struct StringSetEntry - { - StringSetEntry() : count(0), size(0) - { - str[0] = 0; - } - - StringSetEntry(__in_ecount(64) WCHAR tmp[64], size_t _size) - : count(1), size(_size) - { - memcpy(str, tmp, sizeof(str)); - } - - void Add(size_t _size) const - { - count++; - size += _size; - } - - mutable size_t count; - mutable size_t size; - WCHAR str[64]; - - bool operator<(const StringSetEntry &rhs) const - { - return _wcscmp(str, rhs.str) < 0; - } - }; - - - static bool StringSetCompare(const StringSetEntry &a1, const StringSetEntry &a2) - { - return a1.size < a2.size; - } - - void DumpHeapStrings(sos::GCHeap &gcheap) - { - const int offset = sos::Object::GetStringDataOffset(); - typedef std::set Set; - Set set; // A set keyed off of the string's text - - StringSetEntry tmp; // Temp string used to keep track of the set - ULONG fetched = 0; - - TableOutput out(3, POINTERSIZE_HEX, AlignRight); - for (sos::ObjectIterator itr = gcheap.WalkHeap(mStart, mStop); itr; ++itr) - { - if (IsInterrupt()) - break; - - if (itr->IsString() && IsCorrectSize(*itr) && IsCorrectLiveness(*itr)) - { - CLRDATA_ADDRESS addr = itr->GetAddress(); - size_t size = itr->GetSize(); - - if (!mStat) - out.WriteRow(ObjectPtr(addr), Pointer(itr->GetMT()), Decimal(size)); - - // Don't bother calculating the size of the string, just read the full 64 characters of the buffer. The null - // terminator we read will terminate the string. - HRESULT hr = g_ExtData->ReadVirtual(TO_CDADDR(addr+offset), tmp.str, sizeof(WCHAR)*(ARRAY_SIZE(tmp.str)-1), &fetched); - if (SUCCEEDED(hr)) - { - // Ensure we null terminate the string. Note that this will not overrun the buffer as we only - // wrote a max of 63 characters into the 64 character buffer. - tmp.str[fetched/sizeof(WCHAR)] = 0; - Set::iterator sitr = set.find(tmp); - if (sitr == set.end()) - { - tmp.size = size; - tmp.count = 1; - set.insert(tmp); - } - else - { - sitr->Add(size); - } - } - } - } - - ExtOut("\n"); - - // Now flatten the set into a vector. This is much faster than keeping two sets, or using a multimap. - typedef std::vector Vect; - Vect v(set.begin(), set.end()); - std::sort(v.begin(), v.end(), &DumpHeapImpl::StringSetCompare); - - // Now print out the data. The call to Flatten ensures that we don't print newlines to break up the - // output in strange ways. - for (Vect::iterator vitr = v.begin(); vitr != v.end(); ++vitr) - { - if (IsInterrupt()) - break; - - Flatten(vitr->str, (unsigned int)_wcslen(vitr->str)); - out.WriteRow(Decimal(vitr->size), Decimal(vitr->count), vitr->str); - } - } - - void DumpHeapShort(sos::GCHeap &gcheap) - { - for (sos::ObjectIterator itr = gcheap.WalkHeap(mStart, mStop); itr; ++itr) - { - if (!Verify(itr)) - return; - - if (IsCorrectType(*itr) && IsCorrectSize(*itr) && IsCorrectLiveness(*itr)) - DMLOut("%s\n", DMLObject(itr->GetAddress())); - } - } - - void DumpHeapThinlock(sos::GCHeap &gcheap) - { - int count = 0; - - PrintHeader(); - for (sos::ObjectIterator itr = gcheap.WalkHeap(mStart, mStop); itr; ++itr) - { - if (!Verify(itr)) - return; - - sos::ThinLockInfo lockInfo; - if (IsCorrectType(*itr) && itr->GetThinLock(lockInfo)) - { - DMLOut("%s %s %8d", DMLObject(itr->GetAddress()), DMLDumpHeapMT(itr->GetMT()), itr->GetSize()); - ExtOut(" ThinLock owner %x (%p) Recursive %x\n", lockInfo.ThreadId, - SOS_PTR(lockInfo.ThreadPtr), lockInfo.Recursion); - - count++; - } - } - - ExtOut("Found %d objects.\n", count); - } - -private: - TADDR mStart, - mStop, - mMT, - mMinSize, - mMaxSize; - - BOOL mStat, - mStrings, - mVerify, - mThinlock, - mShort, - mDML, - mLive, - mDead; - - - WCHAR *mType; - -private: - std::unordered_set mLiveness; - typedef std::list FragmentationList; - FragmentationList mFrag; - - void InitFragmentationList() - { - mFrag.clear(); - } - - void ReportFreeObject(TADDR addr, size_t size, TADDR next, TADDR mt) - { - if (size >= MIN_FRAGMENTATIONBLOCK_BYTES) - mFrag.push_back(sos::FragmentationBlock(addr, size, next, mt)); - } - - void PrintFragmentationReport() - { - if (mFrag.size() > 0) - { - ExtOut("Fragmented blocks larger than 0.5 MB:\n"); - ExtOut("%" POINTERSIZE "s %8s %16s\n", "Addr", "Size", "Followed by"); - - for (FragmentationList::const_iterator itr = mFrag.begin(); itr != mFrag.end(); ++itr) - { - sos::MethodTable mt = itr->GetNextMT(); - ExtOut("%p %6.1fMB " WIN64_8SPACES "%p %S\n", - SOS_PTR(itr->GetAddress()), - ((double)itr->GetSize()) / 1024.0 / 1024.0, - SOS_PTR(itr->GetNextObject()), - mt.GetName()); - } - } - } -}; - /**********************************************************************\ * Routine Description: * * * @@ -4333,74 +3878,16 @@ private: \**********************************************************************/ DECLARE_API(DumpHeap) { - INIT_API(); + INIT_API_EXT(); MINIDUMP_NOT_SUPPORTED(); - - if (!g_snapshot.Build()) - { - ExtOut("Unable to build snapshot of the garbage collector state\n"); - return E_FAIL; - } - - try - { - DumpHeapImpl dumpHeap(args); - dumpHeap.Run(); - - return S_OK; - } - catch(const sos::Exception &e) - { - ExtOut("%s\n", e.what()); - return E_FAIL; - } + return ExecuteCommand("dumpheap", args); } DECLARE_API(VerifyHeap) { - INIT_API(); + INIT_API_EXT(); MINIDUMP_NOT_SUPPORTED(); - - if (!g_snapshot.Build()) - { - ExtOut("Unable to build snapshot of the garbage collector state\n"); - return E_FAIL; - } - - try - { - bool succeeded = true; - char buffer[1024]; - sos::GCHeap gcheap; - sos::ObjectIterator itr = gcheap.WalkHeap(); - - while (itr) - { - if (itr.Verify(buffer, ARRAY_SIZE(buffer))) - { - ++itr; - } - else - { - succeeded = false; - ExtOut(buffer); - itr.MoveToNextObjectCarefully(); - } - } - - if (!DumpHeapImpl::ValidateSyncTable(gcheap)) - succeeded = false; - - if (succeeded) - ExtOut("No heap corruption detected.\n"); - - return S_OK; - } - catch(const sos::Exception &e) - { - ExtOut("%s\n", e.what()); - return E_FAIL; - } + return ExecuteCommand("verifyheap", args); } enum failure_get_memory