Reimplement !dumpheap and !verifyheap in C# (#3738)
authorLee Culver <leculver@microsoft.com>
Tue, 14 Mar 2023 17:30:22 +0000 (10:30 -0700)
committerGitHub <noreply@github.com>
Tue, 14 Mar 2023 17:30:22 +0000 (10:30 -0700)
* 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 <gue@korporal.at>
* Fix Span issue

Co-authored-by: Günther Foidl <gue@korporal.at>
* 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 <gue@korporal.at>
18 files changed:
src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs
src/Microsoft.Diagnostics.DebugServices.Implementation/FileLoggingConsoleService.cs
src/Microsoft.Diagnostics.DebugServices/CommandAttributes.cs
src/Microsoft.Diagnostics.DebugServices/IConsoleService.cs
src/Microsoft.Diagnostics.ExtensionCommands/DumpHeapCommand.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.ExtensionCommands/EEHeapCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/HeapWithFilters.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.ExtensionCommands/LiveObjectService.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.ExtensionCommands/SimulateGCHeapCorruption.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.ExtensionCommands/TableOutput.cs
src/Microsoft.Diagnostics.ExtensionCommands/VerifyHeapCommand.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Repl/ConsoleService.cs
src/SOS/SOS.Extensions/ConsoleServiceFromDebuggerServices.cs
src/SOS/SOS.Hosting/Commands/SOSCommand.cs
src/SOS/SOS.UnitTests/SOSRunner.cs
src/SOS/SOS.UnitTests/Scripts/ConcurrentDictionaries.script
src/SOS/SOS.UnitTests/Scripts/GCPOH.script
src/SOS/Strike/strike.cpp

index 5fcdeaed5278371aa1ef57eaba5be72200f946be..01e4cfdb6156ce29ec13b14d19938e2f3b764e42 100644 (file)
@@ -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)
index d4e46c2625edd3cf8f8cc8a5fb4246516fc82b23..2c60c6d0cd7edcff68d2ec73c2201681b50d6f74 100644 (file)
@@ -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; }
index dbacc125a25a8056ea4a82894fdda3e81c3f7b25..4b1df4b601e4423d2ced63fdec80e8e391158fee 100644 (file)
@@ -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;
     }
 
+    /// <summary>
+    /// 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.
+    /// </summary>
+    [Conditional("DEBUG")]
+    public class DebugCommandAttribute : CommandAttribute
+    {
+    }
+
     /// <summary>
     /// Marks the property as a Option.
     /// </summary>
index a2de2488a7c2621a0fa90e847525933d1ca6af06..426e328c1b773dd2a08f1313f51eeaee654ca5f7 100644 (file)
@@ -31,6 +31,13 @@ namespace Microsoft.Diagnostics.DebugServices
         /// <summary>Writes Debugger Markup Language (DML) markup text.</summary>
         void WriteDml(string text);
 
+        /// <summary>
+        /// Writes an exec tag to the output stream.
+        /// </summary>
+        /// <param name="text">The display text.</param>
+        /// <param name="action">The action to perform.</param>
+        void WriteDmlExec(string text, string action);
+
         /// <summary>Gets whether <see cref="WriteDml"/> is supported.</summary>
         bool SupportsDml { get; }
 
diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpHeapCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpHeapCommand.cs
new file mode 100644 (file)
index 0000000..ce5334f
--- /dev/null
@@ -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<ulong, (int Count, ulong Size, string TypeName)> 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 ?? $"<unknown_type_{mt:x}>"));
+                    }
+                    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<char> buffer = stackalloc char[maxLen];
+                ReadOnlySpan<char> 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;
+        }
+    }
+}
index 1b596b67634680720dd2690b20cacf3bc9963638..4c9a695da94d40fe11ff70866f5b0ace98ff2959 100644 (file)
@@ -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 (file)
index 0000000..fb9cc06
--- /dev/null
@@ -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;
+
+        /// <summary>
+        /// Whether the heap will be filtered at all
+        /// </summary>
+        public bool HasFilters => _gcheap is not null || Segment is not null || MemoryRange is not null || MinimumObjectSize > 0 || MaximumObjectSize > 0;
+
+        /// <summary>
+        /// Only enumerate objects or segments within this range.
+        /// </summary>
+        public MemoryRange? MemoryRange { get; set; }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        public ulong? Segment { get; set; }
+
+        /// <summary>
+        /// The GC Heap number to filter on.
+        /// </summary>
+        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;
+            }
+        }
+
+        /// <summary>
+        /// The minimum size of an object to enumerate.
+        /// </summary>
+        public ulong MinimumObjectSize { get; set; }
+
+        /// <summary>
+        /// The maximum size of an object to enumerate
+        /// </summary>
+        public ulong MaximumObjectSize { get; set; }
+
+        /// <summary>
+        /// The order in which to enumerate segments.  This also applies to object enumeration.
+        /// </summary>
+        public Func<IEnumerable<ClrSegment>, IOrderedEnumerable<ClrSegment>> 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<ClrSegment> EnumerateFilteredSegments()
+        {
+            IEnumerable<ClrSegment> 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<ClrObject> EnumerateFilteredObjects(CancellationToken cancellation)
+        {
+            foreach (ClrSegment segment in EnumerateFilteredSegments())
+            {
+                IEnumerable<ClrObject> 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 (file)
index 0000000..8de4743
--- /dev/null
@@ -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<ulong> 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 (file)
index 0000000..2158d8e
--- /dev/null
@@ -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<Change> _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<ClrObject> 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<ClrObject> 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<ClrObject> 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<T>(ObjectCorruptionKind kind, ulong obj, ulong address, T value)
+            where T : unmanaged
+        {
+            byte[] old = new byte[sizeof(T)];
+
+            Span<byte> 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; }
+        }
+    }
+}
index a12420b24fa457776c8f8dfc7925f6d58e1edcd4..51806de1817147637f27891f247fe87e7a6c9fe5 100644 (file)
@@ -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<byte> 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 (file)
index 0000000..07e2bcf
--- /dev/null
@@ -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<ClrObject> EnumerateWithCount(IEnumerable<ClrObject> objs)
+        {
+            _totalObjects = 0;
+            foreach (ClrObject obj in objs)
+            {
+                _totalObjects++;
+                yield return obj;
+            }
+        }
+
+        private void VerifyHeap(IEnumerable<ClrObject> 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 "???";
+        }
+    }
+}
index 8df94ded525f1fb9c29c7b69217e3567de6b81d1..be30dc8671c0a3d701eb9e4e5ceab1f9409a3fa0 100644 (file)
@@ -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
index 16b9de8d70cdbfc735824fee4040646a4c60f50c..9fceefd68750a6f7a5f04e51125d1fa6bed8ef72 100644 (file)
@@ -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 = $"<exec cmd=\"{DmlEscape(cmd)}\">{DmlEscape(text)}</exec>";
+            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();
     }
 }
index ced6f909143db0b3d1a65f3ad2812f185418b318..db4955aadb992bbcbf939bdae59507cb3537e856 100644 (file)
@@ -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.")]
index bc38a80bde5b49ac417a4cc89bdb1f904031eca7..e1f993f3be41c0a20f6fde4bc85bc4e46055491f 100644 (file)
@@ -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; }
 
index a5f22338b1f922b78f1e1c8ed794818d03bd6872..3472a8445488a9759a9bcd8332469048638d6acf 100644 (file)
@@ -19,13 +19,8 @@ EXTCOMMAND: dcd 0000000000000001
 VERIFY: is not referencing an object
 
 # Checks on ConcurrentDictionary<int, string[]>
-SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary`2[[
-IFDEF:DESKTOP
-SOSCOMMAND: DumpHeap -mt <POUT>^(<HEXVAL>) .*System.Collections.Concurrent.ConcurrentDictionary`2\[\[System.Int32, mscorlib\],\[System.String\[\], mscorlib\]\]<POUT>
-ENDIF:DESKTOP
-!IFDEF:DESKTOP
-SOSCOMMAND: DumpHeap -mt <POUT>^(<HEXVAL>) .*System.Collections.Concurrent.ConcurrentDictionary`2\[\[System.Int32, System.Private.CoreLib\],\[System.String\[\], System.Private.CoreLib\]\]<POUT>
-ENDIF:DESKTOP
+SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary<
+SOSCOMMAND: DumpHeap -mt <POUT>^(<HEXVAL>) .*System.Collections.Concurrent.ConcurrentDictionary<System.Int32, System.String\[\]>[^+]<POUT>
 EXTCOMMAND: dcd <POUT>^(<HEXVAL>)\s+<HEXVAL>\s+\d+<POUT>
 VERIFY: 2 items
 VERIFY: Key: 2
@@ -34,13 +29,8 @@ VERIFY: Name:\s+System.String\[\]
 VERIFY: Number of elements 4
 
 # Checks on ConcurrentDictionary<int, int>
-SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary`2[[
-IFDEF:DESKTOP
-SOSCOMMAND: DumpHeap -mt <POUT>^(<HEXVAL>) .*System.Collections.Concurrent.ConcurrentDictionary`2\[\[System.Int32, mscorlib\],\[System.Int32, mscorlib\]\]<POUT>
-ENDIF:DESKTOP
-!IFDEF:DESKTOP
-SOSCOMMAND: DumpHeap -mt <POUT>^(<HEXVAL>) .*System.Collections.Concurrent.ConcurrentDictionary`2\[\[System.Int32, System.Private.CoreLib\],\[System.Int32, System.Private.CoreLib\]\]<POUT>
-ENDIF:DESKTOP
+SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary<
+SOSCOMMAND: DumpHeap -mt <POUT>^(<HEXVAL>) .*System.Collections.Concurrent.ConcurrentDictionary<System.Int32, System.Int32>[^+]<POUT>
 EXTCOMMAND: dcd <POUT>^(<HEXVAL>)\s+<HEXVAL>\s+\d+<POUT>
 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<string, bool>
-SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary`2[[
-IFDEF:DESKTOP
-SOSCOMMAND: DumpHeap -mt <POUT>^(<HEXVAL>) .*System.Collections.Concurrent.ConcurrentDictionary`2\[\[System.String, mscorlib\],\[System.Boolean, mscorlib\]\]<POUT>
-ENDIF:DESKTOP
-!IFDEF:DESKTOP
-SOSCOMMAND: DumpHeap -mt <POUT>^(<HEXVAL>) .*System.Collections.Concurrent.ConcurrentDictionary`2\[\[System.String, System.Private.CoreLib\],\[System.Boolean, System.Private.CoreLib\]\]<POUT>
-ENDIF:DESKTOP
+SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary<
+SOSCOMMAND: DumpHeap -mt <POUT>^(<HEXVAL>) .*System.Collections.Concurrent.ConcurrentDictionary<System.String, System.Boolean>[^+]<POUT>
 EXTCOMMAND: dcd <POUT>^(<HEXVAL>)\s+<HEXVAL>\s+\d+<POUT>
 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<DumpSampleStruct, DumpSampleClass>
-SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary`2[[
-SOSCOMMAND: DumpHeap -mt <POUT>^(<HEXVAL>) .*System.Collections.Concurrent.ConcurrentDictionary`2\[\[DotnetDumpCommands\.Program\+DumpSampleStruct, DotnetDumpCommands\],\[DotnetDumpCommands\.Program\+DumpSampleClass, DotnetDumpCommands\]\]<POUT>
+SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary<
+SOSCOMMAND: DumpHeap -mt <POUT>^(<HEXVAL>) .*System.Collections.Concurrent.ConcurrentDictionary<DotnetDumpCommands\.Program\+DumpSampleStruct, DotnetDumpCommands\.Program\+DumpSampleClass>[^+]<POUT>
 EXTCOMMAND: dcd <POUT>^(<HEXVAL>)\s+<HEXVAL>\s+\d+<POUT>
 VERIFY: 2 items
 VERIFY: Key: dumpvc <HEXVAL> <HEXVAL>\s+Value: null
@@ -79,13 +64,8 @@ VERIFY: instance\s+00000000\s+\<Value2\>
 ENDIF:32BIT
 
 # Checks on ConcurrentDictionary<int, DumpSampleStruct>
-SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary`2[[
-IFDEF:DESKTOP
-SOSCOMMAND: DumpHeap -mt <POUT>^(<HEXVAL>) .*System.Collections.Concurrent.ConcurrentDictionary`2\[\[System.Int32, mscorlib\],\[DotnetDumpCommands\.Program\+DumpSampleStruct, DotnetDumpCommands\]\]<POUT>
-ENDIF:DESKTOP
-!IFDEF:DESKTOP
-SOSCOMMAND: DumpHeap -mt <POUT>^(<HEXVAL>) .*System.Collections.Concurrent.ConcurrentDictionary`2\[\[System.Int32, System.Private.CoreLib\],\[DotnetDumpCommands\.Program\+DumpSampleStruct, DotnetDumpCommands\]\]<POUT>
-ENDIF:DESKTOP
+SOSCOMMAND: DumpHeap -stat -type System.Collections.Concurrent.ConcurrentDictionary<
+SOSCOMMAND: DumpHeap -mt <POUT>^(<HEXVAL>) .*System.Collections.Concurrent.ConcurrentDictionary<System.Int32, DotnetDumpCommands\.Program\+DumpSampleStruct>[^+]<POUT>
 EXTCOMMAND: dcd <POUT>^(<HEXVAL>)\s+<HEXVAL>\s+\d+<POUT>
 VERIFY: 1 items
 VERIFY: Key: 0\s+Value: dumpvc <HEXVAL> <HEXVAL>
@@ -95,4 +75,4 @@ VERIFY: instance\s+12\s+IntValue
 VERIFY: instance\s+<HEXVAL>\s+StringValue
 VERIFY: instance\s+<HEXVAL>\s+Date
 
-ENDIF:NETCORE_OR_DOTNETDUMP
\ No newline at end of file
+ENDIF:NETCORE_OR_DOTNETDUMP
index a4c890dfe9ff9be1d2f872565a3b19c09a807185..f384c6bfd96e8d4c9b03e3362b0f6dd1c8eaec10 100644 (file)
@@ -39,6 +39,7 @@ VERIFY:.*Thread <HEXVAL>:
 VERIFY:\s+<HEXVAL>\s+<HEXVAL>\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
index 701990c02681448a840d4ca77c9055d3c124712c..463e17393cdbb7b3ee3197e0ddf157c19d630a6c 100644 (file)
@@ -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<char> 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<sos::Exception>("Failed to parse command line arguments.");
-
-        if (mStart == 0)
-            mStart = minTemp;
-
-        if (mStop == 0)
-            mStop = sos::GCHeap::HeapEnd;
-
-        if (type && mMT)
-        {
-            sos::Throw<sos::Exception>("Cannot specify both -mt and -type");
-        }
-
-        if (mLive && mDead)
-        {
-            sos::Throw<sos::Exception>("Cannot specify both -live and -dead.");
-        }
-
-        if (mMinSize > mMaxSize)
-        {
-            sos::Throw<sos::Exception>("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<StringSetEntry> 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<StringSetEntry> 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<TADDR> mLiveness;
-    typedef std::list<sos::FragmentationBlock> 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