{
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)
}
}
+ 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; }
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Diagnostics;
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>
/// <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; }
--- /dev/null
+// 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;
+ }
+ }
+}
[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()
--- /dev/null
+// 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;
+ }
+ }
+ }
+ }
+}
--- /dev/null
+// 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;
+ }
+ }
+}
--- /dev/null
+// 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; }
+ }
+ }
+}
// 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;
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;
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);
}
(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());
(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);
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)
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}" : "")
+ {
+
+ }
+ }
}
}
--- /dev/null
+// 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 "???";
+ }
+ }
+}
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
// 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;
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; }
int IConsoleService.WindowWidth => _debuggerServices.GetOutputWidth();
#endregion
+
+ private static string DmlEscape(string text) => new XText(text).ToString();
}
}
[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.")]
[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.")]
}
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; }
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
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
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
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
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>
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
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
};
}
-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: *
* *
\**********************************************************************/
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