--- /dev/null
+namespace Microsoft.Diagnostics.DebugServices
+{
+ public enum MemoryRegionType
+ {
+ MEM_UNKNOWN = 0,
+ MEM_IMAGE = 0x1000000,
+ MEM_MAPPED = 0x40000,
+ MEM_PRIVATE = 0x20000
+ }
+
+ public enum MemoryRegionState
+ {
+ MEM_UNKNOWN = 0,
+ MEM_COMMIT = 0x1000,
+ MEM_FREE = 0x10000,
+ MEM_RESERVE = 0x2000
+ }
+
+ public enum MemoryRegionProtection
+ {
+ PAGE_UNKNOWN = 0,
+ PAGE_EXECUTE = 0x00000010,
+ PAGE_EXECUTE_READ = 0x00000020,
+ PAGE_EXECUTE_READWRITE = 0x00000040,
+ PAGE_EXECUTE_WRITECOPY = 0x00000080,
+ PAGE_NOACCESS = 0x00000001,
+ PAGE_READONLY = 0x00000002,
+ PAGE_READWRITE = 0x00000004,
+ PAGE_WRITECOPY = 0x00000008,
+ PAGE_GUARD = 0x00000100,
+ PAGE_NOCACHE = 0x00000200,
+ PAGE_WRITECOMBINE = 0x00000400
+ }
+
+ public enum MemoryRegionUsage
+ {
+ Unknown,
+ Free,
+ Image,
+ Peb,
+ Teb,
+ Stack,
+ Heap,
+ PageHeap,
+ FileMapping,
+ CLR,
+ Other,
+ }
+
+ /// <summary>
+ /// Represents a single virtual address region in the target process.
+ /// </summary>
+ public interface IMemoryRegion
+ {
+ /// <summary>
+ /// The start address of the region.
+ /// </summary>
+ ulong Start { get; }
+
+ /// <summary>
+ /// The end address of the region.
+ /// </summary>
+ ulong End { get; }
+
+ /// <summary>
+ /// The size of the region.
+ /// </summary>
+ ulong Size { get; }
+
+ /// <summary>
+ /// The type of the region. (Image/Private/Mapped)
+ /// </summary>
+ MemoryRegionType Type { get; }
+
+ /// <summary>
+ /// The state of the region. (Commit/Free/Reserve)
+ /// </summary>
+ MemoryRegionState State { get; }
+
+ /// <summary>
+ /// The protection of the region.
+ /// </summary>
+ MemoryRegionProtection Protection { get; }
+
+ /// <summary>
+ /// What this memory is being used for.
+ /// This field is a best attempt at determining what the memory is being used for,
+ /// and may be marked as Unknown if certain debugging symbols are not available.
+ /// </summary>
+ MemoryRegionUsage Usage { get; }
+
+ /// <summary>
+ /// If this file is an image or mapped file, this property may be non-null and
+ /// contain its path.
+ /// </summary>
+ public string Image { get; }
+ }
+}
--- /dev/null
+using System.Collections.Generic;
+
+namespace Microsoft.Diagnostics.DebugServices
+{
+ /// <summary>
+ /// Enumerate virtual address regions, their protections, and usage.
+ /// </summary>
+ public interface IMemoryRegionService
+ {
+ IEnumerable<IMemoryRegion> EnumerateRegions();
+ }
+}
namespace Microsoft.Diagnostics.DebugServices
{
+ /// <summary>
+ /// The status of the symbols for a module.
+ /// </summary>
+ public enum SymbolStatus
+ {
+ /// <summary>
+ /// The status of the symbols is unknown. The symbol may be
+ /// loaded or unloaded.
+ /// </summary>
+ Unknown,
+
+ /// <summary>
+ /// The debugger has successfully loaded symbols for this module.
+ /// </summary>
+ Loaded,
+
+ /// <summary>
+ /// The debugger does not have symbols loaded for this module.
+ /// </summary>
+ NotLoaded,
+
+ /// <summary>
+ /// The debugger does not have symbols loaded for this module, but
+ /// it is able to report addresses of exported functions.
+ /// </summary>
+ ExportOnly,
+ }
+
/// <summary>
/// Module symbol lookup
/// </summary>
/// <param name="type">returned type if found</param>
/// <returns>true if type found</returns>
bool TryGetType(string typeName, out IType type);
+
+ /// <summary>
+ /// Returns the status of the symbols for this module. This function may cause
+ /// the debugger to load symbols for this module, which may take a long time.
+ /// </summary>
+ /// <returns>The status of symbols for this module.</returns>
+ SymbolStatus GetSymbolStatus();
}
}
--- /dev/null
+using Microsoft.Diagnostics.Runtime;
+using Microsoft.Diagnostics.Runtime.DacInterface;
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.Diagnostics.ExtensionCommands
+{
+ internal sealed class ClrMemoryPointer
+ {
+ public ulong Address { get; }
+
+ public ClrMemoryKind Kind { get; }
+
+ public ClrMemoryPointer(ulong address, ClrMemoryKind kind)
+ {
+ Address = address;
+ Kind = kind;
+ }
+
+ /// <summary>
+ /// Enumerates pointers to various CLR heaps in memory.
+ /// </summary>
+ public static IEnumerable<ClrMemoryPointer> EnumerateClrMemoryAddresses(ClrRuntime runtime)
+ {
+ SOSDac sos = runtime.DacLibrary.SOSDacInterface;
+ foreach (JitManagerInfo jitMgr in sos.GetJitManagers())
+ {
+ foreach (var handle in runtime.EnumerateHandles())
+ yield return new ClrMemoryPointer(handle.Address, ClrMemoryKind.HandleTable);
+
+ foreach (var mem in sos.GetCodeHeapList(jitMgr.Address))
+ yield return new ClrMemoryPointer(mem.Address, mem.Type switch
+ {
+ CodeHeapType.Loader => ClrMemoryKind.LoaderHeap,
+ CodeHeapType.Host => ClrMemoryKind.Host,
+ _ => ClrMemoryKind.UnknownCodeHeap
+ });
+
+ Console.WriteLine("GC Segments:");
+ foreach (var seg in runtime.Heap.Segments)
+ {
+ if (seg.CommittedMemory.Length > 0)
+ yield return new ClrMemoryPointer(seg.CommittedMemory.Start, ClrMemoryKind.GCHeapSegment);
+
+ if (seg.ReservedMemory.Length > 0)
+ yield return new ClrMemoryPointer(seg.ReservedMemory.Start, ClrMemoryKind.GCHeapReserve);
+ }
+
+ HashSet<ulong> seen = new();
+
+ List<ClrMemoryPointer> heaps = new();
+ if (runtime.SystemDomain is not null)
+ AddAppDomainHeaps(sos, runtime.SystemDomain.Address, heaps);
+
+ if (runtime.SharedDomain is not null)
+ AddAppDomainHeaps(sos, runtime.SharedDomain.Address, heaps);
+
+ foreach (var heap in heaps)
+ if (seen.Add(heap.Address))
+ yield return heap;
+
+ foreach (ClrDataAddress address in sos.GetAppDomainList())
+ {
+ heaps.Clear();
+ AddAppDomainHeaps(sos, address, heaps);
+
+ foreach (var heap in heaps)
+ if (seen.Add(heap.Address))
+ yield return heap;
+ }
+ }
+ }
+
+ private enum VCSHeapType
+ {
+ IndcellHeap,
+ LookupHeap,
+ ResolveHeap,
+ DispatchHeap,
+ CacheEntryHeap
+ }
+
+ private static void AddAppDomainHeaps(SOSDac sos, ClrDataAddress address, List<ClrMemoryPointer> heaps)
+ {
+ if (sos.GetAppDomainData(address, out AppDomainData domain))
+ {
+ sos.TraverseLoaderHeap(domain.StubHeap, (address, size, isCurrent) => heaps.Add(new ClrMemoryPointer(address, ClrMemoryKind.StubHeap)));
+ sos.TraverseLoaderHeap(domain.HighFrequencyHeap, (address, size, isCurrent) => heaps.Add(new ClrMemoryPointer(address, ClrMemoryKind.HighFrequencyHeap)));
+ sos.TraverseLoaderHeap(domain.LowFrequencyHeap, (address, size, isCurrent) => heaps.Add(new ClrMemoryPointer(address, ClrMemoryKind.LowFrequencyHeap)));
+ sos.TraverseStubHeap(address, (int)VCSHeapType.IndcellHeap, (address, size, isCurrent) => heaps.Add(new ClrMemoryPointer(address, ClrMemoryKind.IndcellHeap)));
+ sos.TraverseStubHeap(address, (int)VCSHeapType.LookupHeap, (address, size, isCurrent) => heaps.Add(new ClrMemoryPointer(address, ClrMemoryKind.LookupHeap)));
+ sos.TraverseStubHeap(address, (int)VCSHeapType.ResolveHeap, (address, size, isCurrent) => heaps.Add(new ClrMemoryPointer(address, ClrMemoryKind.ResolveHeap)));
+ sos.TraverseStubHeap(address, (int)VCSHeapType.DispatchHeap, (address, size, isCurrent) => heaps.Add(new ClrMemoryPointer(address, ClrMemoryKind.DispatchHeap)));
+ sos.TraverseStubHeap(address, (int)VCSHeapType.CacheEntryHeap, (address, size, isCurrent) => heaps.Add(new ClrMemoryPointer(address, ClrMemoryKind.CacheEntryHeap)));
+ }
+ }
+ }
+
+ internal enum ClrMemoryKind
+ {
+ None,
+ LoaderHeap,
+ Host,
+ UnknownCodeHeap,
+ GCHeapSegment,
+ GCHeapReserve,
+ StubHeap,
+ HighFrequencyHeap,
+ LowFrequencyHeap,
+ IndcellHeap,
+ LookupHeap,
+ ResolveHeap,
+ DispatchHeap,
+ CacheEntryHeap,
+ HandleTable,
+ }
+}
--- /dev/null
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.Diagnostics.ExtensionCommands
+{
+ internal static class ExtensionMethodHelpers
+ {
+ public static string ConvertToHumanReadable(this ulong totalBytes) => ConvertToHumanReadable((double)totalBytes);
+ public static string ConvertToHumanReadable(this long totalBytes) => ConvertToHumanReadable((double)totalBytes);
+
+ public static string ConvertToHumanReadable(this double totalBytes)
+ {
+ double updated = totalBytes;
+
+ updated /= 1024;
+ if (updated < 1024)
+ return $"{updated:0.00}kb";
+
+ updated /= 1024;
+ if (updated < 1024)
+ return $"{updated:0.00}mb";
+
+ updated /= 1024;
+ return $"{updated:0.00}gb";
+ }
+
+ internal static ulong FindMostCommonPointer(this IEnumerable<ulong> enumerable)
+ => (from ptr in enumerable
+ group ptr by ptr into g
+ orderby g.Count() descending
+ select g.First()).First();
+ }
+}
--- /dev/null
+using Microsoft.Diagnostics.DebugServices;
+using Microsoft.Diagnostics.Runtime;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using static Microsoft.Diagnostics.ExtensionCommands.NativeAddressHelper;
+
+namespace Microsoft.Diagnostics.ExtensionCommands
+{
+ [Command(Name = "findpointersin", Help="Finds pointers to the GC heap within the given memory regions.")]
+ public sealed class FindPointersInCommand : CommandBase
+ {
+ [ServiceImport]
+ public IModuleService ModuleService { get; set; }
+
+ [ServiceImport]
+ public ClrRuntime Runtime { get; set; }
+
+ [ServiceImport]
+ public NativeAddressHelper AddressHelper { get; set; }
+
+ [Option(Name = "--showAllObjects", Aliases = new string[] { "-a", "--all" }, Help = "Show all objects instead of only objects Pinned on the heap.")]
+ public bool ShowAllObjects { get; set; }
+
+ [Argument(Help = "The types of memory to search for pointers to the GC heap.")]
+ public string[] Regions { get; set; }
+
+ private const string VtableConst = "vtable for ";
+
+ private int Width
+ {
+ get
+ {
+ int width = Console.WindowWidth;
+ if (width == 0)
+ width = 120;
+ if (width > 256)
+ width = 256;
+
+ return width;
+ }
+ }
+
+ public override void Invoke()
+ {
+ if (Regions is null || Regions.Length == 0)
+ throw new DiagnosticsException("Must specify at least one memory region type to search for.");
+
+ PrintPointers(!ShowAllObjects, Regions);
+ }
+
+ private void PrintPointers(bool pinnedOnly, params string[] memTypes)
+ {
+ DescribedRegion[] allRegions = AddressHelper.EnumerateAddressSpace(tagClrMemoryRanges: true, includeReserveMemory: false, tagReserveMemoryHeuristically: false).ToArray();
+
+ WriteLine("Scanning for pinned objects...");
+ var ctx = CreateMemoryWalkContext();
+
+ foreach (string type in memTypes)
+ {
+ DescribedRegion[] matchingRanges = allRegions.Where(r => r.Name == type).ToArray();
+ if (matchingRanges.Length == 0)
+ {
+ WriteLine($"Found not memory regions matching '{type}'.");
+ continue;
+ }
+
+ RegionPointers totals = new();
+
+ foreach (DescribedRegion mem in matchingRanges.OrderBy(r => r.Start))
+ {
+ var pointersFound = AddressHelper.EnumerateRegionPointers(mem.Start, mem.End, allRegions).Select(r => (r.Pointer, r.MemoryRange));
+ RegionPointers result = ProcessOneRegion(pinnedOnly, pointersFound, ctx);
+
+ WriteMemoryHeaderLine(mem);
+
+ WriteLine($"Type: {mem.Name}");
+ if (mem.Image is not null)
+ WriteLine($"Image: {mem.Image}");
+
+ WriteTables(ctx, result, false);
+
+ WriteLine("");
+ WriteLine("END REGION".PadLeft((Width - 10) / 2, '=').PadRight(Width, '='));
+ WriteLine("");
+
+ result.AddTo(totals);
+ }
+
+ if (matchingRanges.Length > 1)
+ {
+ WriteLine(" TOTALS ".PadLeft(Width / 2).PadRight(Width));
+
+ WriteTables(ctx, totals, truncate: true);
+
+ WriteLine(new string('=', Width));
+ }
+ }
+ }
+
+ private void WriteTables(MemoryWalkContext ctx, RegionPointers result, bool truncate)
+ {
+ if (result.PointersToGC > 0)
+ {
+ WriteLine("");
+ WriteLine("Pointers to GC heap:");
+
+ PrintGCPointerTable(result);
+ }
+
+ if (result.ResolvablePointers.Count > 0)
+ {
+ WriteLine("");
+ WriteLine("Pointers to images with symbols:");
+
+ WriteResolvablePointerTable(ctx, result, truncate);
+ }
+
+ if (result.UnresolvablePointers.Count > 0)
+ {
+ WriteLine("");
+ WriteLine("Other pointers:");
+
+ WriteUnresolvablePointerTable(result, truncate);
+ }
+ }
+
+ private void WriteMemoryHeaderLine(DescribedRegion mem)
+ {
+ string header = $"REGION [{mem.Start:x}-{mem.End:x}] {mem.Type} {mem.State} {mem.Protection}";
+ int lpad = (Width - header.Length) / 2;
+ if (lpad > 0)
+ header = header.PadLeft(Width - lpad, '=');
+ WriteLine(header.PadRight(Width, '='));
+ }
+
+ private void PrintGCPointerTable(RegionPointers result)
+ {
+ if (result.PinnedPointers.Count == 0)
+ {
+ WriteLine($"{result.PointersToGC:n0} pointers to the GC heap, but none pointed to a pinned object.");
+ }
+ else
+ {
+ var gcResult = from obj in result.PinnedPointers
+ let name = obj.Type?.Name ?? "<unknown_object_types>"
+ group obj.Address by name into g
+ let Count = g.Count()
+ orderby Count descending
+ select
+ (
+ g.Key,
+ Count,
+ new HashSet<ulong>(g).Count,
+ g.AsEnumerable()
+ );
+
+ if (result.NonPinnedGCPointers.Count > 0)
+ {
+ var v = new (string, int, int, IEnumerable<ulong>)[] { ("[Pointers to non-pinned objects]", result.NonPinnedGCPointers.Count, new HashSet<ulong>(result.NonPinnedGCPointers).Count, result.NonPinnedGCPointers) };
+ gcResult = v.Concat(gcResult);
+ }
+
+ PrintPointerTable("Type", "[Other Pinned Object Pointers]", forceTruncate: false, gcResult);
+ }
+ }
+
+ private void WriteUnresolvablePointerTable(RegionPointers result, bool forceTruncate)
+ {
+ var unresolvedQuery = from item in result.UnresolvablePointers
+ let Name = item.Key.Image ?? item.Key.Name
+ group item.Value by Name into g
+ let All = g.SelectMany(r => r).ToArray()
+ let Count = All.Length
+ orderby Count descending
+ select
+ (
+ g.Key,
+ Count,
+ new HashSet<ulong>(All).Count,
+ All.AsEnumerable()
+ );
+
+
+ PrintPointerTable("Region", "[Unique Pointers to Unique Regions]", forceTruncate, unresolvedQuery);
+ }
+
+ private void WriteResolvablePointerTable(MemoryWalkContext ctx, RegionPointers result, bool forceTruncate)
+ {
+ var resolvedQuery = from ptr in result.ResolvablePointers.SelectMany(r => r.Value)
+ let r = ctx.ResolveSymbol(ModuleService, ptr)
+ let name = r.Symbol ?? "<unknown_function>"
+ group (ptr, r.Offset) by name into g
+ let Count = g.Count()
+ let UniqueOffsets = new HashSet<int>(g.Select(g => g.Offset))
+ orderby Count descending
+ select
+ (
+ FixTypeName(g.Key, UniqueOffsets),
+ Count,
+ UniqueOffsets.Count,
+ g.Select(r => r.ptr)
+ );
+
+ PrintPointerTable("Symbol", "[Unique Pointers]", forceTruncate, resolvedQuery);
+ }
+
+ private void PrintPointerTable(string nameColumn, string truncatedName, bool forceTruncate, IEnumerable<(string Name, int Count, int Unique, IEnumerable<ulong> Pointers)> query)
+ {
+ var resolved = query.ToArray();
+ if (resolved.Length == 0)
+ return;
+
+ int single = resolved.Count(r => r.Count == 1);
+ int multi = resolved.Length - single;
+ bool truncate = forceTruncate || (single + multi > 75 && single > multi);
+ truncate = false;
+
+ int maxNameLen = multi > 0 ? resolved.Where(r => !truncate || r.Count > 1).Max(r => r.Name.Length) : resolved.Max(r => r.Name.Length);
+ int nameLen = Math.Min(80, maxNameLen);
+ nameLen = Math.Max(nameLen, truncatedName.Length);
+
+ TableOutput table = new(Console, (nameLen, ""), (12, "n0"), (12, "n0"), (12, "x"));
+ table.Divider = " ";
+ table.WriteRowWithSpacing('-', nameColumn, "Unique", "Count", "RndPtr");
+
+ var items = truncate ? resolved.Take(multi) : resolved;
+ foreach (var (Name, Count, Unique, Pointers) in items)
+ table.WriteRow(Name, Unique, Count, Pointers.FindMostCommonPointer());
+
+ if (truncate)
+ table.WriteRow(truncatedName, single, single);
+
+ table.WriteRowWithSpacing('-', " [ TOTALS ] ", resolved.Sum(r => r.Unique), resolved.Sum(r => r.Count), "");
+ }
+
+ private static string FixTypeName(string typeName, HashSet<int> offsets)
+ {
+ if (typeName.EndsWith("!") && typeName.Count(r => r == '!') == 1)
+ typeName = typeName.Substring(0, typeName.Length - 1);
+
+ int vtableIdx = typeName.IndexOf(VtableConst);
+ if (vtableIdx > 0)
+ typeName = typeName.Replace(VtableConst, "") + "::vtable";
+
+ if (offsets.Count == 1 && offsets.Single() > 0)
+ typeName = $"{typeName}+{offsets.Single():x}";
+ else if (offsets.Count > 1)
+ typeName = $"{typeName}+...";
+
+ return typeName;
+ }
+
+ private RegionPointers ProcessOneRegion(bool pinnedOnly, IEnumerable<(ulong Pointer, DescribedRegion Range)> pointersFound, MemoryWalkContext ctx)
+ {
+ RegionPointers result = new();
+
+ foreach ((ulong Pointer, DescribedRegion Range) found in pointersFound)
+ {
+ if (found.Range.ClrMemoryKind == ClrMemoryKind.GCHeapSegment)
+ {
+ if (pinnedOnly)
+ {
+ if (ctx.IsPinnedObject(found.Pointer, out ClrObject obj))
+
+ result.AddGCPointer(obj);
+ else
+ result.AddGCPointer(found.Pointer);
+ }
+ else
+ {
+ ClrObject obj = Runtime.Heap.GetObject(found.Pointer);
+ if (obj.IsValid)
+ result.AddGCPointer(obj);
+ }
+ }
+ else if (found.Range.Type == MemoryRegionType.MEM_IMAGE)
+ {
+ bool hasSymbols = false;
+ IModuleSymbols symbols = found.Range.Module?.Services.GetService<IModuleSymbols>();
+ if (symbols is not null)
+ hasSymbols = symbols.GetSymbolStatus() == SymbolStatus.Loaded;
+
+ result.AddRegionPointer(found.Range, found.Pointer, hasSymbols);
+ }
+ else
+ {
+ result.AddRegionPointer(found.Range, found.Pointer, hasSymbols: false);
+ }
+ }
+
+ return result;
+ }
+
+ private MemoryWalkContext CreateMemoryWalkContext()
+ {
+ HashSet<ulong> seen = new();
+ List<ClrObject> pinned = new();
+
+ foreach (var root in Runtime.Heap.EnumerateRoots().Where(r => r.IsPinned))
+ {
+ if (root.Object.IsValid && !root.Object.IsFree)
+ if (seen.Add(root.Object))
+ pinned.Add(root.Object);
+ }
+
+ foreach (ClrSegment seg in Runtime.Heap.Segments.Where(s => s.IsPinnedObjectSegment || s.IsLargeObjectSegment))
+ {
+ foreach (ClrObject obj in seg.EnumerateObjects().Where(o => seen.Add(o)))
+ {
+ if (!obj.IsFree && obj.IsValid)
+ pinned.Add(obj);
+ }
+ }
+
+ return new MemoryWalkContext(pinned);
+ }
+
+ private class RegionPointers
+ {
+ public Dictionary<DescribedRegion, List<ulong>> ResolvablePointers { get; } = new();
+ public Dictionary<DescribedRegion, List<ulong>> UnresolvablePointers { get; } = new();
+ public List<ClrObject> PinnedPointers { get; } = new();
+ public List<ulong> NonPinnedGCPointers { get; } = new();
+ public long PointersToGC => PinnedPointers.Count + NonPinnedGCPointers.Count;
+
+ public RegionPointers()
+ {
+ }
+
+ public void AddGCPointer(ulong address)
+ {
+ NonPinnedGCPointers.Add(address);
+ }
+
+ public void AddGCPointer(ClrObject obj)
+ {
+ PinnedPointers.Add(obj);
+ }
+
+ internal void AddRegionPointer(DescribedRegion range, ulong pointer, bool hasSymbols)
+ {
+ var pointerMap = hasSymbols ? ResolvablePointers : UnresolvablePointers;
+
+ if (!pointerMap.TryGetValue(range, out List<ulong> pointers))
+ pointers = pointerMap[range] = new();
+
+ pointers.Add(pointer);
+ }
+
+ public void AddTo(RegionPointers destination)
+ {
+ AddTo(ResolvablePointers, destination.ResolvablePointers);
+ AddTo(UnresolvablePointers, destination.UnresolvablePointers);
+ destination.PinnedPointers.AddRange(PinnedPointers);
+ destination.NonPinnedGCPointers.AddRange(NonPinnedGCPointers);
+ }
+
+ private static void AddTo(Dictionary<DescribedRegion, List<ulong>> sourceDict, Dictionary<DescribedRegion, List<ulong>> destDict)
+ {
+ foreach (var item in sourceDict)
+ {
+ if (destDict.TryGetValue(item.Key, out List<ulong> values))
+ values.AddRange(item.Value);
+ else
+ destDict[item.Key] = new(item.Value);
+ }
+ }
+ }
+
+ private class MemoryWalkContext
+ {
+ private readonly Dictionary<ulong, (string, int)> _resolved = new();
+ private readonly ClrObject[] _pinned;
+
+ public MemoryWalkContext(IEnumerable<ClrObject> pinnedObjects)
+ {
+ _pinned = pinnedObjects.Where(o => o.IsValid && !o.IsFree).OrderBy(o => o.Address).ToArray();
+ }
+
+ public bool IsPinnedObject(ulong address, out ClrObject found)
+ {
+ ClrObject last = _pinned.LastOrDefault();
+ if (_pinned.Length == 0 || address < _pinned[0].Address || address >= last.Address + last.Size)
+ {
+ found = default;
+ return false;
+ }
+
+ int low = 0;
+ int high = _pinned.Length - 1;
+ while (low <= high)
+ {
+ int mid = (low + high) >> 1;
+ if (_pinned[mid].Address + _pinned[mid].Size <= address)
+ {
+ low = mid + 1;
+ }
+ else if (address < _pinned[mid].Address)
+ {
+ high = mid - 1;
+ }
+ else
+ {
+ found = _pinned[mid];
+ return true;
+ }
+ }
+
+ found = default;
+ return false;
+ }
+
+ public (string Symbol, int Offset) ResolveSymbol(IModuleService modules, ulong ptr)
+ {
+ if (_resolved.TryGetValue(ptr, out (string, int) result))
+ return result;
+
+ // _resolved is just a cache. Don't let it get so big we eat all of the memory.
+ if (_resolved.Count > 16 * 1024)
+ _resolved.Clear();
+
+ IModule module = modules.GetModuleFromAddress(ptr);
+ IModuleSymbols symbols = module?.Services.GetService<IModuleSymbols>();
+
+ if (symbols is not null && symbols.TryGetSymbolName(ptr, out string symbolName, out ulong displacement))
+ {
+ string moduleName = module.FileName;
+ if (!string.IsNullOrWhiteSpace(moduleName))
+ symbolName = Path.GetFileName(moduleName) + "!" + symbolName;
+
+ return _resolved[ptr] = (symbolName, displacement > int.MaxValue ? int.MaxValue : (int)displacement);
+ }
+
+ return (null, -1);
+ }
+ }
+
+ [HelpInvoke]
+ public void HelpInvoke()
+ {
+ WriteLine(
+@"-------------------------------------------------------------------------------
+The findpointersin command will search the regions of memory given by MADDRESS_TYPE_LIST
+to find all pointers to other memory regions and display them. By default, pointers
+to the GC heap are only displayed if the object it points to is pinned. (As any
+random pointer to the GC heap to a non-pinned object is either an old/leftover
+pointer, or a stray pointer that should be ignored.) If --all is set,
+then this command print out ALL objects that are pointed to instead of collapsing
+them into one entry.
+
+usage: !findpointersin [--all] MADDRESS_TYPE_LIST
+
+Note: The MADDRESS_TYPE_LIST must be a memory type as printed by !maddress.
+
+Example: ""!findpointersin PAGE_READWRITE"" will only search for regions of memory that
+!maddress marks as ""PAGE_READWRITE"" and not every page of memory that's
+marked with PAGE_READWRITE protection.
+
+Example: Running the command ""!findpointersin Stack PAGE_READWRITE"" will find all pointers
+on any ""Stack"" and ""PAGE_READWRITE"" memory segments and summarize those contents into
+three tables: One table for pointers to the GC heap, one table for pointers where
+symbols could be resolved, and one table of pointers where we couldn't resolve symbols.
+
+
+Sample Output:
+
+ Pointers to GC heap:
+ -------------------------------Type---------Unique----------Count---------RndPtr
+ [Pointers to non-pinned objects] 3,168 16,765 7f05b80d5b60
+ System.Object[] 3 58 7f07380f3120
+ System.Byte[] 7 22 7f07380f00d8
+ Microsoft.Caching.ClrMD.RawResult[] 2 14 7f063822ae58
+ ----------------------- [ TOTALS ] ----------3,180---------16,859---------------
+
+ Pointers to images:
+ --------------------------------------------------------------------------Symbol---------Unique----------Count---------RndPtr
+ libcoreclr+... 34 457 7f08c66ff776
+ libcoreclr!JIT_GetSharedGCThreadStaticBase+33 1 260 7f08c637b453
+ libcoreclr!COMInterlocked::ExchangeObject+17 1 258 7f08c6336597
+
+ ...
+ -------------------------------------------------------------------- [ TOTALS ] ------------740---------10,361---------------
+
+ Other pointers:
+ ---------------------------------------------------------------Region---------Unique----------Count---------RndPtr
+ Stack 25,229 37,656 7f05297f4738
+ PAGE_READWRITE 1,696 7,882 7f0500000000
+ LowFrequencyHeap 2,618 7,347 7f084d1868e0
+
+ ...
+ --------------------------------------------------------- [ TOTALS ] ---------33,360---------72,029---------------
+");
+ }
+ }
+}
--- /dev/null
+using Microsoft.Diagnostics.DebugServices;
+using Microsoft.Diagnostics.Runtime;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using static Microsoft.Diagnostics.ExtensionCommands.NativeAddressHelper;
+
+namespace Microsoft.Diagnostics.ExtensionCommands
+{
+ [Command(Name = "gctonative", Help = "Finds GC objects which point to the given native memory ranges.")]
+ public sealed class GCToNativeCommand : CommandBase
+ {
+ [Argument(Help ="The types of memory to search the GC heap for.")]
+ public string[] MemoryTypes { get; set; }
+
+ [Option(Name = "--all", Aliases = new string[] { "-a" }, Help = "Show the complete list of objects and not just a summary.")]
+ public bool ShowAll { get; set; }
+
+ [ServiceImport]
+ public ClrRuntime Runtime { get; set; }
+
+ [ServiceImport]
+ public NativeAddressHelper AddressHelper { get; set; }
+
+ private int Width
+ {
+ get
+ {
+ int width = Console.WindowWidth;
+ if (width == 0)
+ width = 120;
+ if (width > 256)
+ width = 256;
+
+ return width;
+ }
+ }
+
+ public override void Invoke()
+ {
+ if (MemoryTypes is null || MemoryTypes.Length == 0)
+ throw new DiagnosticsException("Must specify at least one memory region type to search for.");
+
+ PrintGCPointersToMemory(ShowAll, MemoryTypes);
+ }
+
+ public void PrintGCPointersToMemory(bool showAll, params string[] memoryTypes)
+ {
+ // Strategy:
+ // 1. Use ClrMD to get the bounds of the GC heap where objects are allocated.
+ // 2. Manually read that memory and check every pointer-aligned address for pointers to the heap regions requested
+ // while recording pointers in a list as we go (along with which ClrSegment they came from).
+ // 3. Walk each GC segment which has pointers to the target regions to find objects:
+ // a. Annotate each target pointer so we know what object points to the region (and throw away any pointers
+ // that aren't in an object...those are pointers from dead or relocated objects).
+ // b. We have some special knowledge of "well known types" here that contain pointers. These types point to
+ // native memory and contain a size of the region they point to. Record that information as we go.
+ // 4. Use information from "well known types" about regions of memory to annotate other pointers that do not have
+ // size information.
+ // 5. Display all of this to the user.
+
+ if (memoryTypes.Length == 0)
+ return;
+
+ IEnumerable<DescribedRegion> rangeEnum = AddressHelper.EnumerateAddressSpace(tagClrMemoryRanges: true, includeReserveMemory: false, tagReserveMemoryHeuristically: false);
+ rangeEnum = rangeEnum.Where(r => memoryTypes.Any(memType => r.Name.Equals(memType, StringComparison.OrdinalIgnoreCase)));
+ rangeEnum = rangeEnum.OrderBy(r => r.Start);
+
+ DescribedRegion[] ranges = rangeEnum.ToArray();
+
+ if (ranges.Length == 0)
+ {
+ Console.WriteLine($"No matching memory ranges.");
+ Console.WriteLine("");
+ return;
+ }
+
+ Console.WriteLine("Walking GC heap to find pointers...");
+ Dictionary<ClrSegment, List<GCObjectToRange>> segmentLists = new();
+
+ var items = Runtime.Heap.Segments
+ .SelectMany(Segment => AddressHelper
+ .EnumerateRegionPointers(Segment.ObjectRange.Start, Segment.ObjectRange.End, ranges)
+ .Select(regionPointer => (Segment, regionPointer.Address, regionPointer.Pointer, regionPointer.MemoryRange)));
+
+ foreach (var item in items)
+ {
+ if (!segmentLists.TryGetValue(item.Segment, out List<GCObjectToRange> list))
+ list = segmentLists[item.Segment] = new();
+
+ list.Add(new GCObjectToRange(item.Address, item.Pointer, item.MemoryRange));
+ }
+
+ Console.WriteLine("Resolving object names...");
+ foreach (string type in memoryTypes)
+ {
+ WriteHeader($" {type} Regions ");
+
+ List<ulong> addressesNotInObjects = new();
+ List<(ulong Pointer, ClrObject Object)> unknownObjPointers = new();
+ Dictionary<ulong, KnownClrMemoryPointer> knownMemory = new();
+ Dictionary<ulong, int> sizeHints = new();
+
+ foreach (KeyValuePair<ClrSegment, List<GCObjectToRange>> segEntry in segmentLists)
+ {
+ ClrSegment seg = segEntry.Key;
+ List<GCObjectToRange> pointers = segEntry.Value;
+ pointers.Sort((x, y) => x.GCPointer.CompareTo(y.GCPointer));
+
+ int index = 0;
+ foreach (ClrObject obj in seg.EnumerateObjects())
+ {
+ if (index >= pointers.Count)
+ break;
+
+ while (index < pointers.Count && pointers[index].GCPointer < obj.Address)
+ {
+ // If we "missed" the pointer then it's outside of an object range.
+ addressesNotInObjects.Add(pointers[index].GCPointer);
+
+ Trace.WriteLine($"Skipping {pointers[index].GCPointer:x} lastObj={obj.Address:x}-{obj.Address + obj.Size:x} {obj.Type?.Name}");
+
+ index++;
+ }
+
+ if (index == pointers.Count)
+ break;
+
+ while (index < pointers.Count && obj.Address <= pointers[index].GCPointer && pointers[index].GCPointer < obj.Address + obj.Size)
+ {
+ string typeName = obj.Type?.Name ?? $"<unknown_type>";
+
+ if (obj.IsFree)
+ {
+ // This is free space, if we found a pointer here then it was likely just relocated and we'll mark it elsewhere
+ }
+ else if (pointers[index].NativeMemoryRange.Name != type)
+ {
+ // This entry is for a different memory type, we'll get it on another pass
+ }
+ else if (knownMemory.ContainsKey(obj))
+ {
+ // do nothing, we already marked this memory
+ }
+ else if (KnownClrMemoryPointer.ContainsKnownClrMemoryPointers(obj))
+ {
+ foreach (KnownClrMemoryPointer knownMem in KnownClrMemoryPointer.EnumerateKnownClrMemoryPointers(obj, sizeHints))
+ knownMemory.Add(obj, knownMem);
+ }
+ else
+ {
+ if (typeName.Contains('>'))
+ typeName = CollapseGenerics(typeName);
+
+ unknownObjPointers.Add((pointers[index].TargetSegmentPointer, obj));
+ }
+
+ index++;
+ }
+ }
+ }
+
+ Console.WriteLine("");
+ if (knownMemory.Count == 0 && unknownObjPointers.Count == 0)
+ {
+ Console.WriteLine($"No GC heap pointers to '{type}' regions.");
+ }
+ else
+ {
+ if (showAll)
+ {
+ Console.WriteLine($"All memory pointers:");
+
+ IEnumerable<(ulong Pointer, ulong Size, ulong Object, string Type)> allPointers = unknownObjPointers.Select(unknown => (unknown.Pointer, 0ul, unknown.Object.Address, unknown.Object.Type?.Name ?? "<unknown_type>"));
+ allPointers = allPointers.Concat(knownMemory.Values.Select(k => (k.Pointer, GetSize(sizeHints, k), k.Object.Address, k.Name)));
+
+ TableOutput allOut = new(Console, (16, "x"), (16, "x"), (16, "x"))
+ {
+ Divider = " | "
+ };
+
+ allOut.WriteRowWithSpacing('-', "Pointer", "Size", "Object", "Type");
+ foreach (var entry in allPointers)
+ if (entry.Size == 0)
+ allOut.WriteRow(entry.Pointer, "", entry.Object, entry.Type);
+ else
+ allOut.WriteRow(entry.Pointer, entry.Size, entry.Object, entry.Type);
+
+ Console.WriteLine("");
+ }
+
+ if (knownMemory.Count > 0)
+ {
+ Console.WriteLine($"Well-known memory pointer summary:");
+
+ // totals
+ var knownMemorySummary = from known in knownMemory.Values
+ group known by known.Name into g
+ let Name = g.Key
+ let Count = g.Count()
+ let TotalSize = g.Sum(k => (long)GetSize(sizeHints, k))
+ orderby TotalSize descending, Name ascending
+ select new
+ {
+ Name,
+ Count,
+ TotalSize,
+ Pointer = g.Select(p => p.Pointer).FindMostCommonPointer()
+ };
+
+ int maxNameLen = Math.Min(80, knownMemory.Values.Max(r => r.Name.Length));
+
+ TableOutput summary = new(Console, (-maxNameLen, ""), (8, "n0"), (12, "n0"), (12, "n0"), (12, "x"))
+ {
+ Divider = " | "
+ };
+
+ summary.WriteRowWithSpacing('-', "Type", "Count", "Size", "Size (bytes)", "RndPointer");
+
+ foreach (var item in knownMemorySummary)
+ summary.WriteRow(item.Name, item.Count, item.TotalSize.ConvertToHumanReadable(), item.TotalSize, item.Pointer);
+
+ (int totalRegions, ulong totalBytes) = GetSizes(knownMemory, sizeHints);
+
+ summary.WriteSpacer('-');
+ summary.WriteRow("[TOTAL]", totalRegions, totalBytes.ConvertToHumanReadable(), totalBytes);
+
+ Console.WriteLine("");
+ }
+
+
+ if (unknownObjPointers.Count > 0)
+ {
+ Console.WriteLine($"Other memory pointer summary:");
+
+ var unknownMemQuery = from known in unknownObjPointers
+ let name = CollapseGenerics(known.Object.Type?.Name ?? "<unknown_type>")
+ group known by name into g
+ let Name = g.Key
+ let Count = g.Count()
+ orderby Count descending
+ select new
+ {
+ Name,
+ Count,
+ Pointer = g.Select(p => p.Pointer).FindMostCommonPointer()
+ };
+
+ var unknownMem = unknownMemQuery.ToArray();
+ int maxNameLen = Math.Min(80, unknownMem.Max(r => r.Name.Length));
+
+ TableOutput summary = new(Console, (-maxNameLen, ""), (8, "n0"), (12, "x"))
+ {
+ Divider = " | "
+ };
+
+ summary.WriteRowWithSpacing('-', "Type", "Count", "RndPointer");
+
+ foreach (var item in unknownMem)
+ summary.WriteRow(item.Name, item.Count, item.Pointer);
+ }
+ }
+ }
+ }
+
+ private static (int Regions, ulong Bytes) GetSizes(Dictionary<ulong, KnownClrMemoryPointer> knownMemory, Dictionary<ulong, int> sizeHints)
+ {
+ var ordered = from item in knownMemory.Values
+ orderby item.Pointer ascending, item.Size descending
+ select item;
+
+ int totalRegions = 0;
+ ulong totalBytes = 0;
+ ulong prevEnd = 0;
+
+ foreach (var item in ordered)
+ {
+ ulong size = GetSize(sizeHints, item);
+
+ // overlapped pointer
+ if (item.Pointer < prevEnd)
+ {
+ if (item.Pointer + size <= prevEnd)
+ continue;
+
+ ulong diff = prevEnd - item.Pointer;
+ if (diff >= size)
+ continue;
+
+ size -= diff;
+ prevEnd += size;
+ }
+ else
+ {
+ totalRegions++;
+ prevEnd = item.Pointer + size;
+ }
+
+ totalBytes += size;
+ }
+
+ return (totalRegions, totalBytes);
+ }
+
+ private void WriteHeader(string header)
+ {
+ int lpad = (Width - header.Length) / 2;
+ if (lpad > 0)
+ header = header.PadLeft(Width - lpad, '=');
+ Console.WriteLine(header.PadRight(Width, '='));
+ }
+
+ private static string CollapseGenerics(string typeName)
+ {
+ StringBuilder result = new(typeName.Length + 16);
+ int nest = 0;
+ for (int i = 0; i < typeName.Length; i++)
+ {
+ if (typeName[i] == '<')
+ {
+ if (nest++ == 0)
+ {
+ if (i < typeName.Length - 1 && typeName[i + 1] == '>')
+ result.Append("<>");
+ else
+ result.Append("<...>");
+ }
+ }
+ else if (typeName[i] == '>')
+ {
+ nest--;
+ }
+ else if (nest == 0)
+ {
+ result.Append(typeName[i]);
+ }
+ }
+
+ return result.ToString();
+ }
+
+ private static ulong GetSize(Dictionary<ulong, int> sizeHints, KnownClrMemoryPointer k)
+ {
+ if (sizeHints.TryGetValue(k.Pointer, out int hint))
+ if ((ulong)hint > k.Size)
+ return (ulong)hint;
+
+ return k.Size;
+ }
+
+ private class GCObjectToRange
+ {
+ public ulong GCPointer { get; }
+ public ulong TargetSegmentPointer { get; }
+ public ClrObject Object { get; set; }
+ public DescribedRegion NativeMemoryRange { get; }
+
+ public GCObjectToRange(ulong gcaddr, ulong pointer, DescribedRegion nativeMemory)
+ {
+ GCPointer = gcaddr;
+ TargetSegmentPointer = pointer;
+ NativeMemoryRange = nativeMemory;
+ }
+ }
+
+ private class KnownClrMemoryPointer
+ {
+ private const string NativeHeapMemoryBlock = "System.Reflection.Internal.NativeHeapMemoryBlock";
+ private const string MetadataReader = "System.Reflection.Metadata.MetadataReader";
+ private const string NativeHeapMemoryBlockDisposableData = "System.Reflection.Internal.NativeHeapMemoryBlock+DisposableData";
+ private const string ExternalMemoryBlockProvider = "System.Reflection.Internal.ExternalMemoryBlockProvider";
+ private const string ExternalMemoryBlock = "System.Reflection.Internal.ExternalMemoryBlock";
+ private const string RuntimeParameterInfo = "System.Reflection.RuntimeParameterInfo";
+
+ public string Name => Object.Type?.Name ?? "<unknown_type>";
+ public ClrObject Object { get; }
+ public ulong Pointer { get; }
+ public ulong Size { get; }
+
+ public KnownClrMemoryPointer(ClrObject obj, nint pointer, int size)
+ {
+ Object = obj;
+ Pointer = (ulong)pointer;
+ Size = (ulong)size;
+ }
+
+ public static bool ContainsKnownClrMemoryPointers(ClrObject obj)
+ {
+ string typeName = obj.Type?.Name;
+ return typeName == NativeHeapMemoryBlock
+ || typeName == MetadataReader
+ || typeName == NativeHeapMemoryBlockDisposableData
+ || typeName == ExternalMemoryBlockProvider
+ || typeName == ExternalMemoryBlock
+ || typeName == RuntimeParameterInfo
+ ;
+ }
+
+ public static IEnumerable<KnownClrMemoryPointer> EnumerateKnownClrMemoryPointers(ClrObject obj, Dictionary<ulong, int> sizeHints)
+ {
+ switch (obj.Type?.Name)
+ {
+ case RuntimeParameterInfo:
+ {
+ const int MDInternalROSize = 0x5f8; // Doesn't have to be exact
+ nint pointer = obj.ReadValueTypeField("m_scope").ReadField<nint>("m_metadataImport2");
+ AddSizeHint(sizeHints, pointer, MDInternalROSize);
+
+ yield return new KnownClrMemoryPointer(obj, pointer, MDInternalROSize);
+ }
+ break;
+ case ExternalMemoryBlock:
+ {
+ nint pointer = obj.ReadField<nint>("_buffer");
+ int size = obj.ReadField<int>("_size");
+
+ if (pointer != 0 && size > 0)
+ AddSizeHint(sizeHints, pointer, size);
+
+ yield return new KnownClrMemoryPointer(obj, pointer, size);
+ }
+ break;
+
+ case ExternalMemoryBlockProvider:
+ {
+ nint pointer = obj.ReadField<nint>("_memory");
+ int size = obj.ReadField<int>("_size");
+
+ if (pointer != 0 && size > 0)
+ AddSizeHint(sizeHints, pointer, size);
+
+ yield return new KnownClrMemoryPointer(obj, pointer, size);
+ }
+ break;
+
+ case NativeHeapMemoryBlockDisposableData:
+ {
+ nint pointer = obj.ReadField<nint>("_pointer");
+ sizeHints.TryGetValue((ulong)pointer, out int size);
+ yield return new KnownClrMemoryPointer(obj, pointer, size);
+ }
+ break;
+
+ case NativeHeapMemoryBlock:
+ {
+ // Just here for size hints
+
+ ClrObject pointerObject = obj.ReadObjectField("_data");
+ nint pointer = pointerObject.ReadField<nint>("_pointer");
+ int size = obj.ReadField<int>("_size");
+
+ if (pointer != 0 && size > 0)
+ AddSizeHint(sizeHints, pointer, size);
+ }
+
+ break;
+
+ case MetadataReader:
+ {
+ MemoryBlockImpl block = obj.ReadField<MemoryBlockImpl>("Block");
+ if (block.Pointer != 0 && block.Size > 0)
+ yield return new KnownClrMemoryPointer(obj, block.Pointer, block.Size);
+ }
+ break;
+ }
+ }
+
+ private static void AddSizeHint(Dictionary<ulong, int> sizeHints, nint pointer, int size)
+ {
+ if (pointer != 0 && size != 0)
+ {
+ ulong ptr = (ulong)pointer;
+
+ if (sizeHints.TryGetValue(ptr, out int hint))
+ {
+ if (hint < size)
+ sizeHints[ptr] = size;
+ }
+ else
+ {
+ sizeHints[ptr] = size;
+ }
+ }
+ }
+
+ private readonly struct MemoryBlockImpl
+ {
+ public readonly nint Pointer { get; }
+ public readonly int Size { get; }
+ }
+ }
+
+ [HelpInvoke]
+ public void HelpInvoke()
+ {
+ WriteLine(
+@"-------------------------------------------------------------------------------
+!gctonative searches the GC heap for pointers to native memory. This is used
+to help locate regions of native memory that are referenced (or possibly held
+alive) by objects on the GC heap.
+
+usage: !gctonative [--all] MADDRESS_TYPE_LIST
+
+Note: The MADDRESS_TYPE_LIST must be a memory type as printed by !maddress.
+
+If --all is set, a full list of every pointer from the GC heap to the
+specified memory will be displayed instead of just a summary table.
+
+Sample Output:
+
+ 0:000> !gctonative PAGE_READWRITE
+ Walking GC heap to find pointers...
+ Resolving object names...
+ ================================================ PAGE_READWRITE Regions ================================================
+
+ Well-known memory pointer summary:
+ Type-----------------------------------------------------------------Count-----------Size---Size (bytes)-----RndPointer
+ System.Reflection.Internal.ExternalMemoryBlockProvider | 1,956 | 571.39mb | 599,145,088 | 7f0478747cf0
+ System.Reflection.Internal.NativeHeapMemoryBlock+DisposableData | 1,956 | 571.39mb | 599,145,088 | 7f0478747cf0
+ System.Reflection.Internal.ExternalMemoryBlock | 1,956 | 161.63mb | 169,483,352 | 7f04898e06a0
+ System.Reflection.Metadata.MetadataReader | 1,956 | 161.63mb | 169,483,352 | 7f04898e06a0
+ System.Reflection.RuntimeParameterInfo | 176 | 262.63kb | 268,928 | 7f058000c220
+ -----------------------------------------------------------------------------------------------------------------------
+ [TOTAL] | 1,963 | 571.40mb | 599,155,784
+
+ Other memory pointer summary:
+ Type----------------------------------------------------------------------------------Count-----RndPointer
+ System.SByte[] | 1,511 | 7f0500000000
+ System.Byte[] | 539 | 7f0500000000
+ System.Reflection.RuntimeAssembly | 135 | 7f05a0000ce0
+ System.Char[] | 121 | 7f0500000000
+ System.Threading.UnmanagedThreadPoolWorkItem | 113 | 7f05800120e0
+ System.Diagnostics.Tracing.EventSource+EventMetadata[] | 75 | 7f0564001a20
+ Microsoft.Win32.SafeHandles.SafeEvpMdCtxHandle | 56 | 7f044013c170
+ System.Threading.Thread | 40 | 7f05741d7bd0
+ System.Security.Cryptography.SafeEvpPKeyHandle | 40 | 7f051400cca0
+ Microsoft.Win32.SafeHandles.SafeBioHandle | 38 | 7f05400078d0
+ Microsoft.Win32.SafeHandles.SafeX509Handle | 37 | 7f04beab9c30
+ Microsoft.Win32.SafeHandles.SafeSslHandle | 20 | 7f0540007af0
+ System.Text.RegularExpressions.RegexCache+Node | 19 | 7f0500000001
+ System.Collections.Concurrent.ConcurrentDictionary<...>+Node | 19 | 7f0500000001
+ System.Diagnostics.Tracing.EventPipeEventProvider | 15 | 7f0580002240
+ System.IntPtr[] | 15 | 7f0578000d00
+ Microsoft.Extensions.Logging.LoggerMessage+LogValues<...> | 12 | 7f0500000002
+ Microsoft.Extensions.Logging.LoggerMessage+LogValues<...>+<...>d__9 | 12 | 7f0500000002
+ Microsoft.Win32.SafeHandles.SafeX509StackHandle | 10 | 7f0524179e50
+ Microsoft.CodeAnalysis.CSharp.Symbols.MethodSymbol+<...>d__32 | 10 | 7f0500000002
+ Microsoft.CodeAnalysis.ModuleMetadata[] | 5 | 7f052a7f7050
+ System.Threading.TimerQueue+AppDomainTimerSafeHandle | 2 | 7f05a0006d30
+ System.Net.Security.SafeFreeCertContext | 2 | 7f04beab9c30
+ System.Threading.LowLevelLock | 1 | 7f05a0003420
+ Microsoft.CodeAnalysis.CSharp.CSharpCompilation+ReferenceManager+AssemblyData... | 1 | 7f05800120e0
+ System.Net.Sockets.SocketAsyncEngine | 1 | 7f059800edd0
+ Microsoft.Extensions.Caching.Memory.CacheEntry | 1 | 7f05241e0000
+ System.Runtime.CompilerServices.AsyncTaskMethodBuilder<...>+AsyncStateMachine... | 1 | 7f0500000004
+");
+ }
+ }
+}
--- /dev/null
+using Microsoft.Diagnostics.DebugServices;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using static Microsoft.Diagnostics.ExtensionCommands.NativeAddressHelper;
+
+namespace Microsoft.Diagnostics.ExtensionCommands
+{
+ [Command(Name = "maddress", Help = "Displays a breakdown of the virtual address space.")]
+ public sealed class MAddressCommand : CommandBase
+ {
+ [Option(Name = "--list", Aliases = new string[] { "-l", "--all", }, Help = "Prints the full list of annotated memory regions.")]
+ public bool ListAll { get; set; }
+
+ [Option(Name = "--images", Aliases = new string[] { "-i", "-image", "-img", "--img" }, Help = "Prints the table of image memory usage.")]
+ public bool ShowImageTable { get; set; }
+
+ [Option(Name = "--includeReserve", Help = "Include MEM_RESERVE regions in the output.")]
+ public bool IncludeReserveMemory { get; set; }
+
+ [Option(Name = "--tagReserve", Help = "Heuristically tag MEM_RESERVE regions based on adjacent memory regions.")]
+ public bool TagReserveMemoryHeuristically { get; set; }
+
+ [ServiceImport]
+ public NativeAddressHelper AddressHelper { get; set; }
+
+ public override void Invoke()
+ {
+ if (TagReserveMemoryHeuristically && !IncludeReserveMemory)
+ throw new DiagnosticsException("Cannot use --tagReserve without --includeReserve");
+
+ PrintMemorySummary(ListAll, ShowImageTable, IncludeReserveMemory, TagReserveMemoryHeuristically);
+ }
+
+ public void PrintMemorySummary(bool printAllMemory, bool showImageTable, bool includeReserveMemory, bool tagReserveMemoryHeuristically)
+ {
+ IEnumerable<DescribedRegion> memoryRanges = AddressHelper.EnumerateAddressSpace(tagClrMemoryRanges: true, includeReserveMemory, tagReserveMemoryHeuristically);
+ if (!includeReserveMemory)
+ memoryRanges = memoryRanges.Where(m => m.State != MemoryRegionState.MEM_RESERVE);
+
+ DescribedRegion[] ranges = memoryRanges.ToArray();
+
+ int nameSizeMax = ranges.Max(r => r.Name.Length);
+
+ // Tag reserved memory based on what's adjacent.
+ if (tagReserveMemoryHeuristically)
+ CollapseReserveRegions(ranges);
+
+ if (printAllMemory)
+ {
+ int kindSize = ranges.Max(r => r.Type.ToString().Length);
+ int stateSize = ranges.Max(r => r.State.ToString().Length);
+ int protectSize = ranges.Max(r => r.Protection.ToString().Length);
+
+ TableOutput output = new(Console, (nameSizeMax, ""), (12, "x"), (12, "x"), (12, ""), (kindSize, ""), (stateSize, ""), (protectSize, ""))
+ {
+ AlignLeft = true,
+ Divider = " | "
+ };
+
+ output.WriteRowWithSpacing('-', "Memory Kind", "StartAddr", "EndAddr-1", "Size", "Type", "State", "Protect", "Image");
+ foreach (DescribedRegion mem in ranges)
+ output.WriteRow(mem.Name, mem.Start, mem.End, mem.Length.ConvertToHumanReadable(), mem.Type, mem.State, mem.Protection, mem.Image);
+
+ output.WriteSpacer('-');
+ }
+
+ if (showImageTable)
+ {
+ var imageGroups = from mem in ranges.Where(r => r.State != MemoryRegionState.MEM_RESERVE && r.Image != null)
+ group mem by mem.Image into g
+ let Size = g.Sum(k => (long)(k.End - k.Start))
+ orderby Size descending
+ select new
+ {
+ Image = g.Key,
+ Count = g.Count(),
+ Size
+ };
+
+ int moduleLen = Math.Max(80, ranges.Max(r => r.Image?.Length ?? 0));
+
+ TableOutput output = new(Console, (moduleLen, ""), (8, "n0"), (12, ""), (24, "n0"))
+ {
+ Divider = " | "
+ };
+
+ output.WriteRowWithSpacing('-', "Image", "Regions", "Size", "Size (bytes)");
+
+ int count = 0;
+ long size = 0;
+ foreach (var item in imageGroups)
+ {
+ output.WriteRow(item.Image, item.Count, item.Size.ConvertToHumanReadable(), item.Size);
+ count += item.Count;
+ size += item.Size;
+ }
+
+ output.WriteSpacer('-');
+ output.WriteRow("[TOTAL]", count, size.ConvertToHumanReadable(), size);
+ WriteLine("");
+ }
+
+
+ // Print summary table unconditionally
+ {
+ var grouped = from mem in ranges
+ let name = mem.Name
+ group mem by name into g
+ let Count = g.Count()
+ let Size = g.Sum(f => (long)(f.End - f.Start))
+ orderby Size descending
+ select new
+ {
+ Name = g.Key,
+ Count,
+ Size
+ };
+
+ TableOutput output = new(Console, (-nameSizeMax, ""), (8, "n0"), (12, ""), (24, "n0"))
+ {
+ Divider = " | "
+ };
+
+ output.WriteRowWithSpacing('-', "Region Type", "Count", "Size", "Size (bytes)");
+
+ int count = 0;
+ long size = 0;
+ foreach (var item in grouped)
+ {
+ output.WriteRow(item.Name, item.Count, item.Size.ConvertToHumanReadable(), item.Size);
+ count += item.Count;
+ size += item.Size;
+ }
+
+ output.WriteSpacer('-');
+ output.WriteRow("[TOTAL]", count, size.ConvertToHumanReadable(), size);
+ }
+ }
+
+
+ [HelpInvoke]
+ public void HelpInvoke()
+ {
+ WriteLine(
+@"-------------------------------------------------------------------------------
+!maddress is a managed version of !address, which attempts to annotate all memory
+with information about CLR's heaps.
+
+usage: !maddress [--list] [--images] [--includeReserve [--tagReserve]]
+
+Flags:
+ --list
+ Shows the full list of annotated memory regions and not just the statistics
+ table.
+
+ --images
+ Summarizes the memory ranges consumed by images in the process.
+
+ --includeReserve
+ Include reserved memory (MEM_RESERVE) in the output. This is usually only
+ useful if there is virtual address exhaustion.
+
+ --tagReserve
+ If this flag is set, then !maddress will attempt to ""blame"" reserve segments
+ on the region that immediately proceeded it. For example, if a ""Heap""
+ memory segment is immediately followed by a MEM_RESERVE region, we will call
+ that reserve region HeapReserve. Note that this is a heuristic and NOT
+ intended to be completely accurate. This can be useful to try to figure out
+ what is creating large amount of MEM_RESERVE regions.
+");
+ }
+ }
+}
--- /dev/null
+using Microsoft.Diagnostics.DebugServices;
+using Microsoft.Diagnostics.Runtime;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System;
+using System.Buffers;
+
+namespace Microsoft.Diagnostics.ExtensionCommands
+{
+ [ServiceExport(Scope = ServiceScope.Target)]
+ public sealed class NativeAddressHelper
+ {
+ [ServiceImport]
+ public ITarget Target { get; set; }
+
+ [ServiceImport]
+ public IMemoryService MemoryService { get; set; }
+
+ [ServiceImport]
+ public IThreadService ThreadService { get; set; }
+
+ [ServiceImport]
+ public IRuntimeService RuntimeService { get; set; }
+
+ [ServiceImport]
+ public IModuleService ModuleService { get; set; }
+
+ [ServiceImport]
+ public IMemoryRegionService MemoryRegionService { get; set; }
+
+ /// <summary>
+ /// Enumerates the entire address space, optionally tagging special CLR heaps, and optionally "collapsing"
+ /// MEM_RESERVE regions with a heuristic to blame them on the MEM_COMMIT region that came before it.
+ /// See <see cref="CollapseReserveRegions"/> for more info.
+ /// </summary>
+ /// <param name="tagClrMemoryRanges">Whether to "tag" regions with CLR memory for more details.</param>
+ /// <param name="includeReserveMemory">Whether to include MEM_RESERVE memory or not in the enumeration.</param>
+ /// <param name="tagReserveMemoryHeuristically">Whether to heuristically "blame" MEM_RESERVE regions on what
+ /// lives before it in the address space. For example, if there is a MEM_COMMIT region followed by a MEM_RESERVE
+ /// region in the address space, this function will "blame" the MEM_RESERVE region on whatever type of memory
+ /// the MEM_COMMIT region happens to be. Usually this will be correct (e.g. the native heap will reserve a
+ /// large chunk of memory and commit the beginning of it as it allocates more and more memory...the RESERVE
+ /// region was actually "caused" by the Heap space before it). Sometimes this will simply be wrong when
+ /// a MEM_COMMIT region is next to an unrelated MEM_RESERVE region.
+ ///
+ /// This is a heuristic, so use it accordingly.</param>
+ /// <exception cref="InvalidOperationException">If !address fails we will throw InvalidOperationException. This is usually
+ /// because symbols for ntdll couldn't be found.</exception>
+ /// <returns>An enumerable of memory ranges.</returns>
+ internal IEnumerable<DescribedRegion> EnumerateAddressSpace(bool tagClrMemoryRanges, bool includeReserveMemory, bool tagReserveMemoryHeuristically)
+ {
+ var addressResult = from region in MemoryRegionService.EnumerateRegions()
+ where region.State != MemoryRegionState.MEM_FREE
+ select new DescribedRegion(region, ModuleService.GetModuleFromAddress(region.Start));
+
+ if (!includeReserveMemory)
+ addressResult = addressResult.Where(m => m.State != MemoryRegionState.MEM_RESERVE);
+
+ DescribedRegion[] ranges = addressResult.OrderBy(r => r.Start).ToArray();
+ if (tagClrMemoryRanges)
+ {
+ foreach (IRuntime runtime in RuntimeService.EnumerateRuntimes())
+ {
+ ClrRuntime clrRuntime = runtime.Services.GetService<ClrRuntime>();
+ if (clrRuntime is not null)
+ {
+ foreach (ClrMemoryPointer mem in ClrMemoryPointer.EnumerateClrMemoryAddresses(clrRuntime))
+ {
+ var found = ranges.Where(m => m.Start <= mem.Address && mem.Address < m.End).ToArray();
+
+ if (found.Length == 0)
+ Trace.WriteLine($"Warning: Could not find a memory range for {mem.Address:x} - {mem.Kind}.");
+ else if (found.Length > 1)
+ Trace.WriteLine($"Warning: Found multiple memory ranges for entry {mem.Address:x} - {mem.Kind}.");
+
+ foreach (var entry in found)
+ {
+ if (entry.ClrMemoryKind != ClrMemoryKind.None && entry.ClrMemoryKind != mem.Kind)
+ Trace.WriteLine($"Warning: Overwriting range {entry.Start:x} {entry.ClrMemoryKind} -> {mem.Kind}.");
+
+ entry.ClrMemoryKind = mem.Kind;
+ }
+ }
+ }
+ }
+ }
+
+ if (tagReserveMemoryHeuristically)
+ {
+ foreach (DescribedRegion mem in ranges)
+ {
+ string memName = mem.Name;
+ if (memName == "RESERVED")
+ TagMemoryRecursive(mem, ranges);
+ }
+ }
+
+ // On Linux, !address doesn't mark stack space. Go do that.
+ if (Target.OperatingSystem == OSPlatform.Linux)
+ MarkStackSpace(ranges);
+
+ return ranges;
+ }
+
+ private void MarkStackSpace(DescribedRegion[] ranges)
+ {
+ foreach (IThread thread in ThreadService.EnumerateThreads())
+ {
+ if (thread.TryGetRegisterValue(ThreadService.StackPointerIndex, out ulong sp) && sp != 0)
+ {
+ DescribedRegion range = FindMemory(ranges, sp);
+ if (range is not null)
+ range.Description = "Stack";
+ }
+ }
+ }
+
+ private static DescribedRegion FindMemory(DescribedRegion[] ranges, ulong ptr)
+ {
+ if (ptr < ranges[0].Start || ptr >= ranges.Last().End)
+ return null;
+
+ int low = 0;
+ int high = ranges.Length - 1;
+ while (low <= high)
+ {
+ int mid = (low + high) >> 1;
+ if (ranges[mid].End <= ptr)
+ {
+ low = mid + 1;
+ }
+ else if (ptr < ranges[mid].Start)
+ {
+ high = mid - 1;
+ }
+ else
+ {
+ return ranges[mid];
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// This method heuristically tries to "blame" MEM_RESERVE regions on what lives before it on the heap.
+ /// For example, if there is a MEM_COMMIT region followed by a MEM_RESERVE region in the address space,
+ /// this function will "blame" the MEM_RESERVE region on whatever type of memory the MEM_COMMIT region
+ /// happens to be. Usually this will be correct (e.g. the native heap will reserve a large chunk of
+ /// memory and commit the beginning of it as it allocates more and more memory...the RESERVE region
+ /// was actually "caused" by the Heap space before it). Sometimes this will simply be wrong when
+ /// a MEM_COMMIT region is next to an unrelated MEM_RESERVE region.
+ ///
+ /// This is a heuristic, so use it accordingly.
+ /// </summary>
+ internal static void CollapseReserveRegions(DescribedRegion[] ranges)
+ {
+ foreach (DescribedRegion mem in ranges)
+ {
+ string memName = mem.Name;
+ if (memName == "RESERVED")
+ TagMemoryRecursive(mem, ranges);
+ }
+ }
+
+ private static DescribedRegion TagMemoryRecursive(DescribedRegion mem, DescribedRegion[] ranges)
+ {
+ if (mem.Name != "RESERVED")
+ return mem;
+
+ DescribedRegion found = ranges.SingleOrDefault(r => r.End == mem.Start);
+ if (found is null)
+ return null;
+
+ DescribedRegion nonReserved = TagMemoryRecursive(found, ranges);
+ if (nonReserved is null)
+ return null;
+
+ mem.Description = nonReserved.Name;
+ return nonReserved;
+ }
+
+ internal IEnumerable<(ulong Address, ulong Pointer, DescribedRegion MemoryRange)> EnumerateRegionPointers(ulong start, ulong end, DescribedRegion[] ranges)
+ {
+ ulong[] array = ArrayPool<ulong>.Shared.Rent(4096);
+ int arrayBytes = array.Length * sizeof(ulong);
+ try
+ {
+ ulong curr = start;
+ ulong remaining = end - start;
+
+ while (remaining > 0)
+ {
+ int size = Math.Min(remaining > int.MaxValue ? int.MaxValue : (int)remaining, arrayBytes);
+ bool res = ReadMemory(curr, array, size, out int bytesRead);
+ if (!res || bytesRead <= 0)
+ break;
+
+ for (int i = 0; i < bytesRead / sizeof(ulong); i++)
+ {
+ ulong ptr = array[i];
+
+ DescribedRegion found = FindMemory(ranges, ptr);
+ if (found is not null)
+ yield return (curr + (uint)i * sizeof(ulong), ptr, found);
+ }
+
+ curr += (uint)bytesRead;
+ remaining -= (uint)bytesRead; ;
+ }
+
+ }
+ finally
+ {
+ ArrayPool<ulong>.Shared.Return(array);
+ }
+ }
+
+ private unsafe bool ReadMemory(ulong start, ulong[] array, int size, out int bytesRead)
+ {
+ fixed (ulong* ptr = array)
+ {
+ Span<byte> buffer = new(ptr, size);
+ MemoryService.ReadMemory(start, buffer, out bytesRead);
+ return bytesRead == size;
+ }
+ }
+
+ internal class DescribedRegion : IMemoryRegion
+ {
+ private readonly IMemoryRegion _region;
+
+ public IModule Module { get; }
+
+ public DescribedRegion(IMemoryRegion region, IModule module)
+ {
+ _region = region;
+ Module = module;
+ }
+
+ public ulong Start => _region.Start;
+
+ public ulong End => _region.End;
+
+ public ulong Size => _region.Size;
+
+ public MemoryRegionType Type => _region.Type;
+
+ public MemoryRegionState State => _region.State;
+
+ public MemoryRegionProtection Protection => _region.Protection;
+
+ public MemoryRegionUsage Usage => _region.Usage;
+
+ public string Image => _region.Image;
+
+ public string Description { get; internal set; }
+
+ public ClrMemoryKind ClrMemoryKind { get; internal set; }
+ public ulong Length => End <= Start ? 0 : End - Start;
+
+ public string Name
+ {
+ get
+ {
+ if (ClrMemoryKind != ClrMemoryKind.None)
+ return ClrMemoryKind.ToString();
+
+ if (!string.IsNullOrWhiteSpace(Description))
+ return Description;
+
+ if (State == MemoryRegionState.MEM_RESERVE)
+ return "RESERVED";
+ else if (State == MemoryRegionState.MEM_FREE)
+ return "FREE";
+
+ if (Type == MemoryRegionType.MEM_IMAGE || !string.IsNullOrWhiteSpace(Image))
+ return "IMAGE";
+
+ string result = Protection.ToString();
+ if (Type == MemoryRegionType.MEM_MAPPED)
+ {
+ if (string.IsNullOrWhiteSpace(result))
+ result = Type.ToString();
+ else
+ result = result.Replace("PAGE", "MAPPED");
+ }
+
+ return result;
+ }
+ }
+ }
+ }
+}
--- /dev/null
+using Microsoft.Diagnostics.DebugServices;
+using System;
+using System.Linq;
+using System.Text;
+
+namespace Microsoft.Diagnostics.ExtensionCommands
+{
+ internal sealed class TableOutput
+ {
+ private readonly char _spacing = ' ';
+ public string Divider { get; set; } = " ";
+ public bool AlignLeft { get; set; } = false;
+
+ public IConsoleService Console { get; }
+
+ private readonly (int width, string format)[] _formats;
+
+ public TableOutput(IConsoleService console, params (int width, string format)[] columns)
+ {
+ _formats = columns.ToArray();
+ Console = console;
+ }
+
+ public void WriteRow(params object[] columns)
+ {
+ StringBuilder sb = new(Divider.Length * columns.Length + _formats.Sum(c => Math.Abs(c.width)) + 32);
+
+ for (int i = 0; i < columns.Length; i++)
+ {
+ if (i != 0)
+ sb.Append(Divider);
+
+ (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 ?? "");
+ }
+
+ Console.WriteLine(sb.ToString());
+ }
+
+ public void WriteRowWithSpacing(char spacing, params object[] columns)
+ {
+ StringBuilder sb = new(columns.Length + _formats.Sum(c => Math.Abs(c.width)));
+
+ for (int i = 0; i < columns.Length; i++)
+ {
+ if (i != 0)
+ sb.Append(spacing, Divider.Length);
+
+ (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 ?? "");
+ }
+
+ Console.WriteLine(sb.ToString());
+ }
+
+
+ public void WriteSpacer(char spacer)
+ {
+ Console.WriteLine(new string(spacer, Divider.Length * (_formats.Length - 1) + _formats.Sum(c => Math.Abs(c.width))));
+ }
+
+ private void AddValue(char spacing, StringBuilder sb, int width, string value)
+ {
+ bool leftAlign = AlignLeft ? width > 0 : width < 0;
+ width = Math.Abs(width);
+
+ if (width == 0)
+ {
+ sb.Append(value);
+ }
+ else if (value.Length > width)
+ {
+ if (width <= 3)
+ {
+ sb.Append(value, 0, width);
+ }
+ else if (leftAlign)
+ {
+ value = value.Substring(0, width - 3);
+ sb.Append(value);
+ sb.Append("...");
+ }
+ else
+ {
+ value = value.Substring(value.Length - (width - 3));
+ sb.Append("...");
+ sb.Append(value);
+ }
+
+ }
+ else if (leftAlign)
+ {
+ sb.Append(value.PadRight(width, spacing));
+ }
+ else
+ {
+ sb.Append(value.PadLeft(width, spacing));
+ }
+ }
+
+ private static string Format(object obj, string format)
+ {
+ if (obj is null)
+ return null;
+
+ return obj switch
+ {
+ nint ni => ni.ToString(format),
+ ulong ul => ul.ToString(format),
+ long l => l.ToString(format),
+ uint ui => ui.ToString(format),
+ int i => i.ToString(format),
+ string s => s,
+ _ => throw new NotImplementedException(obj.GetType().ToString()),
+ };
+ }
+ }
+}
--- /dev/null
+using Microsoft.Diagnostics.Runtime.Utilities;
+using SOS.Hosting.DbgEng.Interop;
+using System;
+using System.Runtime.InteropServices;
+
+namespace SOS.Extensions
+{
+ /// <summary>
+ /// A helper class to capture output of DbgEng commands that will restore the previous output callbacks
+ /// when disposed.
+ /// </summary>
+ internal class DbgEngOutputHolder : IDebugOutputCallbacksWide, IDisposable
+ {
+ private readonly IDebugClient5 _client;
+ private readonly IDebugOutputCallbacksWide _previous;
+
+ public DEBUG_OUTPUT InterestMask { get; set; }
+
+ /// <summary>
+ /// Event fired when we receive output from the debugger.
+ /// </summary>
+ public Action<DEBUG_OUTPUT, string> OutputReceived;
+
+ public DbgEngOutputHolder(IDebugClient5 client, DEBUG_OUTPUT interestMask = DEBUG_OUTPUT.NORMAL)
+ {
+ _client = client;
+ InterestMask = interestMask;
+
+ _client.GetOutputCallbacksWide(out _previous);
+ HResult hr = _client.SetOutputCallbacksWide(this);
+ if (!hr)
+ throw Marshal.GetExceptionForHR(hr);
+ }
+
+ public void Dispose()
+ {
+ if (_previous is not null)
+ _client.SetOutputCallbacksWide(_previous);
+ }
+
+ public int Output(DEBUG_OUTPUT Mask, [In, MarshalAs(UnmanagedType.LPStr)] string Text)
+ {
+ if ((InterestMask & Mask) != 0 && Text is not null)
+ OutputReceived?.Invoke(Mask, Text);
+
+ return 0;
+ }
+ }
+}
private readonly HostType _hostType;
+ /// <summary>
+ /// A pointer to the underlying IDebugClient interface if the host is DbgEng.
+ /// </summary>
+ public IDebugClient5 DebugClient { get; }
+
internal DebuggerServices(IntPtr punk, HostType hostType)
: base(new RefCountedFreeLibrary(IntPtr.Zero), IID_IDebuggerServices, punk)
{
_hostType = hostType;
+
+ // This uses COM marshalling code, so we also check that the OSPlatform is Windows.
+ if (hostType == HostType.DbgEng && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ object obj = Marshal.GetObjectForIUnknown(punk);
+ if (obj is IDebugClient5 client)
+ DebugClient = client;
+ }
}
public HResult GetOperatingSystem(out DebuggerServices.OperatingSystem operatingSystem)
using Microsoft.Diagnostics.Runtime;
using Microsoft.Diagnostics.Runtime.Utilities;
using SOS.Hosting;
+using SOS.Hosting.DbgEng.Interop;
using System;
using System.Collections.Generic;
using System.Diagnostics;
Trace.TraceWarning($"Cannot add extension command {hr:X8} {name} - {help}");
}
}
+
+ if (DebuggerServices.DebugClient is IDebugControl5 control)
+ {
+ MemoryRegionServiceFromDebuggerServices memRegions = new(DebuggerServices.DebugClient, control);
+ _serviceContainer.AddService<IMemoryRegionService>(memRegions);
+ }
}
catch (Exception ex)
{
--- /dev/null
+using Microsoft.Diagnostics.DebugServices;
+using SOS.Hosting.DbgEng.Interop;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace SOS.Extensions
+{
+ internal class MemoryRegionServiceFromDebuggerServices : IMemoryRegionService
+ {
+ private readonly IDebugClient5 _client;
+ private readonly IDebugControl5 _control;
+
+ public MemoryRegionServiceFromDebuggerServices(IDebugClient5 client, IDebugControl5 control)
+ {
+ _client = client;
+ _control = control;
+ }
+
+ public IEnumerable<IMemoryRegion> EnumerateRegions()
+ {
+ bool foundHeader = false;
+ bool skipped = false;
+
+ (int hr, string text) = RunCommandWithOutput("!address");
+ if (hr < 0)
+ throw new InvalidOperationException($"!address failed with hresult={hr:x}");
+
+ foreach (string line in text.Split('\n'))
+ {
+ if (line.Length == 0)
+ continue;
+
+ if (!foundHeader)
+ {
+ // find the !address header
+ string[] split = line.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ if (split.Length > 0)
+ foundHeader = split[0] == "BaseAddress" && split.Last() == "Usage";
+ }
+ else if (!skipped)
+ {
+ // skip the ---------- line
+ skipped = true;
+ }
+ else
+ {
+ string[] parts = ((line[0] == '+') ? line.Substring(1) : line).Split(new char[] { ' ' }, 6, StringSplitOptions.RemoveEmptyEntries);
+ ulong start = ulong.Parse(parts[0].Replace("`", ""), System.Globalization.NumberStyles.HexNumber);
+ ulong end = ulong.Parse(parts[1].Replace("`", ""), System.Globalization.NumberStyles.HexNumber);
+
+ int index = 3;
+ if (Enum.TryParse(parts[index], ignoreCase: true, out MemoryRegionType type))
+ index++;
+
+ if (Enum.TryParse(parts[index], ignoreCase: true, out MemoryRegionState state))
+ index++;
+
+ StringBuilder sbRemainder = new();
+ for (int i = index; i < parts.Length; i++)
+ {
+ if (i != index)
+ sbRemainder.Append(' ');
+
+ sbRemainder.Append(parts[i]);
+ }
+
+ string remainder = sbRemainder.ToString();
+ parts = remainder.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ MemoryRegionProtection protect = default;
+ index = 0;
+ while (index < parts.Length - 1)
+ {
+ if (Enum.TryParse(parts[index], ignoreCase: true, out MemoryRegionProtection result))
+ {
+ protect |= result;
+ if (parts[index + 1] == "|")
+ index++;
+ }
+ else
+ {
+ break;
+ }
+
+ index++;
+ }
+
+ string description = parts[index++].Trim();
+
+ // On Linux, !address is reporting this as MEM_PRIVATE or MEM_UNKNOWN
+ if (description == "Image")
+ type = MemoryRegionType.MEM_IMAGE;
+
+ // On Linux, !address is reporting this as nothing
+ if (type == MemoryRegionType.MEM_UNKNOWN && state == MemoryRegionState.MEM_UNKNOWN && protect == MemoryRegionProtection.PAGE_UNKNOWN)
+ {
+ state = MemoryRegionState.MEM_FREE;
+ protect = MemoryRegionProtection.PAGE_NOACCESS;
+ }
+
+ string image = null;
+ if (type == MemoryRegionType.MEM_IMAGE)
+ {
+ image = parts[index].Substring(1, parts[index].Length - 2);
+ index++;
+ }
+
+ if (description.Equals("<unknown>", StringComparison.OrdinalIgnoreCase))
+ description = "";
+
+ MemoryRegionUsage usage = description switch
+ {
+ "" => MemoryRegionUsage.Unknown,
+
+ "Free" => MemoryRegionUsage.Free,
+ "Image" => MemoryRegionUsage.Image,
+
+ "PEB" => MemoryRegionUsage.Peb,
+ "PEB32" => MemoryRegionUsage.Peb,
+ "PEB64" => MemoryRegionUsage.Peb,
+
+ "TEB" => MemoryRegionUsage.Teb,
+ "TEB32" => MemoryRegionUsage.Teb,
+ "TEB64" => MemoryRegionUsage.Teb,
+
+ "Stack" => MemoryRegionUsage.Stack,
+ "Stack32" => MemoryRegionUsage.Stack,
+ "Stack64" => MemoryRegionUsage.Stack,
+
+ "Heap" => MemoryRegionUsage.Heap,
+ "Heap32" => MemoryRegionUsage.Heap,
+ "Heap64" => MemoryRegionUsage.Heap,
+
+
+ "PageHeap" => MemoryRegionUsage.PageHeap,
+ "PageHeap64" => MemoryRegionUsage.PageHeap,
+ "PageHeap32" => MemoryRegionUsage.PageHeap,
+
+ "MappedFile" => MemoryRegionUsage.FileMapping,
+ "CLR" => MemoryRegionUsage.CLR,
+
+ "Other" => MemoryRegionUsage.Other,
+ "Other32" => MemoryRegionUsage.Other,
+ "Other64" => MemoryRegionUsage.Other,
+
+ _ => MemoryRegionUsage.Unknown
+ };
+
+ yield return new AddressMemoryRange()
+ {
+ Start = start,
+ End = end,
+ Type = type,
+ State = state,
+ Protection = protect,
+ Usage = usage,
+ Image = image
+ };
+ }
+ }
+
+ if (!foundHeader)
+ throw new InvalidOperationException($"!address did not produce a standard header.\nThis may mean symbols could not be resolved for ntdll.\nPlease run !address and make sure the output looks correct.");
+
+ }
+
+ private (int hresult, string output) RunCommandWithOutput(string command)
+ {
+ using DbgEngOutputHolder dbgengOutput = new(_client);
+ StringBuilder sb = new(1024);
+ dbgengOutput.OutputReceived += (mask, text) => sb.Append(text);
+
+ int hr = _control.ExecuteWide(DEBUG_OUTCTL.THIS_CLIENT, command, DEBUG_EXECUTE.DEFAULT);
+
+ return (hr, sb.ToString());
+ }
+
+ private class AddressMemoryRange : IMemoryRegion
+ {
+ public ulong Start { get; internal set; }
+
+ public ulong End { get; internal set; }
+
+ public ulong Size { get; internal set; }
+
+ public MemoryRegionType Type { get; internal set; }
+
+ public MemoryRegionState State { get; internal set; }
+
+ public MemoryRegionProtection Protection { get; internal set; }
+
+ public MemoryRegionUsage Usage { get; internal set; }
+
+ public string Image { get; internal set; }
+ }
+ }
+}
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Runtime.InteropServices;
+using System.IO;
namespace SOS.Extensions
{
private readonly ModuleServiceFromDebuggerServices _moduleService;
private Version _version;
private string _versionString;
+ private SymbolStatus _symbolStatus = SymbolStatus.Unknown;
public ModuleFromDebuggerServices(
ModuleServiceFromDebuggerServices moduleService,
return true;
}
+ SymbolStatus IModuleSymbols.GetSymbolStatus()
+ {
+ if (_symbolStatus != SymbolStatus.Unknown)
+ return _symbolStatus;
+
+ // GetSymbolStatus is not implemented for anything other than DbgEng for now.
+ IDebugClient client = _moduleService._debuggerServices.DebugClient;
+ if (client is null || client is not IDebugSymbols5 symbols)
+ return SymbolStatus.Unknown;
+
+ return _symbolStatus = GetSymbolStatusFromDbgEng(symbols);
+ }
#endregion
protected override bool TryGetSymbolAddressInner(string name, out ulong address)
}
protected override ModuleService ModuleService => _moduleService;
+
+ private SymbolStatus GetSymbolStatusFromDbgEng(IDebugSymbols5 symbols)
+ {
+ // First, see if the symbol is already loaded. Note that getting the symbol type
+ // from DbgEng won't force a symbol load, it will only tell us if it's already
+ // been loaded or not.
+ DEBUG_SYMTYPE symType = GetSymType(symbols, ImageBase);
+ if (symType != DEBUG_SYMTYPE.NONE && symType != DEBUG_SYMTYPE.DEFERRED)
+ return DebugToSymbolStatus(symType);
+
+ // At this point, the symbol type is DEFERRED or NONE and we haven't tried reloading
+ // the symbol yet. Try a reload, and then ask one last time what the symbol is.
+ if (!string.IsNullOrWhiteSpace(FileName))
+ {
+ string module = Path.GetFileName(FileName);
+ module = module.Replace('+', '_'); // Reload doesn't like '+' in module names
+ HResult hr = symbols.Reload(module);
+ if (!hr)
+ {
+ // Ugh, Reload might not like the module name that GetModuleName gives us.
+ // Instead, force DbgEng to look up the base address as a symbol which will
+ // force symbol load as well.
+ symbols.GetNameByOffset(ImageBase, null, 0, out _, out _);
+ }
+ }
+
+ // Whether we successfully reloaded or not, get the final symbol type.
+ symType = GetSymType(symbols, ImageBase);
+ return DebugToSymbolStatus(symType);
+ }
+
+ private static SymbolStatus DebugToSymbolStatus(DEBUG_SYMTYPE symType)
+ {
+ // By the time we get here, we've already tried forcing a symbol load.
+ // If it's NONE or DEFERRED at this point then we can't load it. We
+ // will never return SymbolStatus.Unknown, so GetSymbolStatusFromDbgEng
+ // will only ever be called once per module.
+ return symType switch
+ {
+ DEBUG_SYMTYPE.NONE => SymbolStatus.NotLoaded,
+ DEBUG_SYMTYPE.DEFERRED => SymbolStatus.NotLoaded,
+ DEBUG_SYMTYPE.EXPORT => SymbolStatus.ExportOnly,
+ _ => SymbolStatus.Loaded,
+ };
+ }
+
+ private static DEBUG_SYMTYPE GetSymType(IDebugSymbols symbols, ulong imageBase)
+ {
+ DEBUG_MODULE_PARAMETERS[] moduleParams = new DEBUG_MODULE_PARAMETERS[1];
+ HResult hr = symbols.GetModuleParameters(1, new ulong[] { imageBase }, 0, moduleParams);
+
+ var symType = hr ? moduleParams[0].SymbolType : DEBUG_SYMTYPE.NONE;
+ return symType;
+ }
}
private readonly DebuggerServices _debuggerServices;
AddRef();
return S_OK;
}
- if (InterfaceId == __uuidof(IDebugEventCallbacks))
+ else if (InterfaceId == __uuidof(IDebugEventCallbacks))
{
*Interface = static_cast<IDebugEventCallbacks*>(this);
AddRef();
return S_OK;
}
+ else if (m_client != nullptr)
+ {
+ return m_client->QueryInterface(InterfaceId, Interface);
+ }
else
{
*Interface = nullptr;