From dc705ce4d5358fa6cf4ce749131d96dd95b95f43 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Thu, 17 Sep 2026 12:07:12 -0400 Subject: [PATCH 1/2] Fix LLDB module PE probing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b013d849-0633-4b39-87ad-36002a21ae4f --- .../Module.cs | 12 ++++++++++-- .../ModuleService.cs | 3 +-- .../ModuleServiceFromDebuggerServices.cs | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Module.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Module.cs index 7eecd08205..b7ce19a0b4 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Module.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Module.cs @@ -34,16 +34,24 @@ public enum Flags : byte } private Flags _flags; - private IEnumerable _pdbFileInfos; + private IEnumerable _pdbFileInfos = []; private string _symbolFileName; protected ImmutableArray _buildId; protected readonly ServiceContainer _serviceContainer; public Module(IServiceProvider services) + : this(services, isPEModuleProbeSupported: true) + { + } + + protected Module(IServiceProvider services, bool isPEModuleProbeSupported) { ServiceContainerFactory containerFactory = services.GetService().CreateServiceContainerFactory(ServiceScope.Module, services); - containerFactory.AddServiceFactory((services) => ModuleService.GetPEInfo(ImageBase, ImageSize, out _pdbFileInfos, ref _flags)); + if (isPEModuleProbeSupported) + { + containerFactory.AddServiceFactory((services) => ModuleService.GetPEInfo(ImageBase, ImageSize, out _pdbFileInfos, ref _flags)); + } _serviceContainer = containerFactory.Build(); _serviceContainer.AddService(this); _serviceContainer.AddService(this); diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleService.cs index ad1e40dc9e..c4e6bbc8ab 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleService.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleService.cs @@ -250,8 +250,7 @@ internal PEFile GetPEInfo(ulong address, ulong size, out IEnumerable(); moduleFlags &= ~(Module.Flags.IsPEImage | Module.Flags.IsManaged | Module.Flags.IsLoadedLayout | Module.Flags.IsFileLayout); - // None of the modules that lldb (on either Linux/MacOS) provides are PEs - if (size > 0 && Target.Host.HostType != HostType.Lldb) + if (size > 0) { // First try getting the PE info as loaded layout (native Windows DLLs and most managed PEs). peFile = GetPEInfo(isVirtual: true, address, size, out List pdbs, out Module.Flags flags); diff --git a/src/SOS/SOS.Extensions/ModuleServiceFromDebuggerServices.cs b/src/SOS/SOS.Extensions/ModuleServiceFromDebuggerServices.cs index 371de58dd3..4114a1c468 100644 --- a/src/SOS/SOS.Extensions/ModuleServiceFromDebuggerServices.cs +++ b/src/SOS/SOS.Extensions/ModuleServiceFromDebuggerServices.cs @@ -81,7 +81,7 @@ public ModuleFromDebuggerServices( ulong imageSize, uint indexFileSize, uint indexTimeStamp) - : base(moduleService.Services) + : base(moduleService.Services, moduleService.Target.Host.HostType != HostType.Lldb) { _moduleService = moduleService; ModuleIndex = moduleIndex; From 8ef7c76f471e16fb3b8e0c9b837e6023f1e92c54 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Thu, 17 Sep 2026 12:07:20 -0400 Subject: [PATCH 2/2] Add managed module mapping coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b013d849-0633-4b39-87ad-36002a21ae4f --- .../SOS.TestHarness/ChildEngineClient.cs | 2 +- src/tests/SOS.TestHarness/DbgEngHostBase.cs | 2 +- src/tests/SOS.TestHarness/IDebuggerHost.cs | 5 +- src/tests/SOS.Tests/MemoryAndDecodeTests.cs | 92 +++++++++++++++++++ src/tests/SOS.Tests/ModuleCommandParsing.cs | 76 +++++++++++++-- src/tests/SOS.Tests/RuntimeInfoTests.cs | 2 +- src/tests/SOS.Tests/SOS.Tests.csproj | 1 + 7 files changed, 167 insertions(+), 13 deletions(-) diff --git a/src/tests/SOS.TestHarness/ChildEngineClient.cs b/src/tests/SOS.TestHarness/ChildEngineClient.cs index bc40f98bb0..c14df0a52f 100644 --- a/src/tests/SOS.TestHarness/ChildEngineClient.cs +++ b/src/tests/SOS.TestHarness/ChildEngineClient.cs @@ -121,7 +121,7 @@ public void LoadSos() public SosOutput Execute(string command) => new(Name, command, Send(command)); - public SosOutput Sos(string command) => new(Name, command, Send("!" + command)); + public SosOutput Sos(string command) => new(Name, command, Send("!sos " + command)); /// Live only: set a managed breakpoint and run to it (handled inside the child). public SosOutput RunToBpmd(string module, string method) => diff --git a/src/tests/SOS.TestHarness/DbgEngHostBase.cs b/src/tests/SOS.TestHarness/DbgEngHostBase.cs index e95ac59cfe..3e7c72b92b 100644 --- a/src/tests/SOS.TestHarness/DbgEngHostBase.cs +++ b/src/tests/SOS.TestHarness/DbgEngHostBase.cs @@ -158,7 +158,7 @@ private static bool ChainContainsSos(string chain) => public SosOutput Execute(string command) => new(Name, command, Invoke(() => RunCore(command))); - public SosOutput Sos(string command) => new(Name, command, Invoke(() => RunCore("!" + command))); + public SosOutput Sos(string command) => new(Name, command, Invoke(() => RunCore("!sos " + command))); /// Worker-thread command execution returning captured output. protected string RunCore(string command) diff --git a/src/tests/SOS.TestHarness/IDebuggerHost.cs b/src/tests/SOS.TestHarness/IDebuggerHost.cs index 522c2cd55f..ca84df60e5 100644 --- a/src/tests/SOS.TestHarness/IDebuggerHost.cs +++ b/src/tests/SOS.TestHarness/IDebuggerHost.cs @@ -28,9 +28,8 @@ public interface IDebuggerHost : IDisposable SosOutput Execute(string command); /// - /// Run a SOS command. The host applies whatever prefixing it needs (dbgeng wants a - /// leading !; dotnet-dump takes the bare command), so the test author writes - /// Sos("clrstack") once and it works everywhere. + /// Run a SOS command. The host applies whatever prefixing it needs, so the test author + /// writes Sos("clrstack") once and it works everywhere. /// SosOutput Sos(string command); } diff --git a/src/tests/SOS.Tests/MemoryAndDecodeTests.cs b/src/tests/SOS.Tests/MemoryAndDecodeTests.cs index d644e77eb0..adb5e03c97 100644 --- a/src/tests/SOS.Tests/MemoryAndDecodeTests.cs +++ b/src/tests/SOS.Tests/MemoryAndDecodeTests.cs @@ -2,6 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text.RegularExpressions; +using Microsoft.FileFormats; +using Microsoft.FileFormats.ELF; +using Microsoft.FileFormats.MachO; +using Microsoft.FileFormats.Minidump; using SOS.TestHarness; using Xunit; @@ -18,6 +22,12 @@ public sealed class MemoryAndDecodeTests public static TheoryData ScenariosMatrix => TestConfig.BuildMatrix([TargetCatalog.Scenarios]); public static TheoryData NestedExceptionMatrix => TestMatrices.HeapEnumeration([TargetCatalog.NestedException]); public static TheoryData DotnetDumpMatrix => TestConfig.BuildMatrix([TargetCatalog.Scenarios], Flavor.AllValid, Host.DotnetDump); + public static TheoryData MiniDumpMatrix => TestConfig.BuildMatrix( + [TargetCatalog.Scenarios], + Flavor.Core, + Host.AllValid, + Liveness.Dump, + dumpKind: DumpKind.Mini); [SosTheory] [MemberData(nameof(DotnetDumpMatrix))] @@ -57,6 +67,88 @@ public async Task MemoryDumpers_ShowKnownFieldBytes(TestConfig config) target.Sos($"db {marker:x}").AssertContains(":"); // byte dump prints ": " } + [SosTheory] + [MemberData(nameof(MiniDumpMatrix))] + public async Task MemoryDumper_MapsOmittedManagedModuleData(TestConfig config) + { + using Target target = await Targets.GetTargetAsync(config); + target.GoToStopPoint(TargetCatalog.StopHeap); + + ClrModuleInfo coreLib = target.ClrModules().SingleByName("System.Private.CoreLib.dll"); + ulong imageAddress = FindOmittedImageAddress(target.DumpPath, coreLib.ImageBase, coreLib.ImageSize); + + if (config.Host == Host.Lldb) + { + SosOutput nativeRead = target.Execute($"memory read --size 1 --count 16 0x{imageAddress:x}"); + Assert.Contains("core file does not contain", nativeRead.Text, StringComparison.OrdinalIgnoreCase); + } + + SosOutput mappedRead = target.Sos($"db {imageAddress:x}"); + Assert.Matches( + $@"(?im)^{imageAddress:x16}:(?: [0-9a-f]{{2}}){{16}}", + mappedRead.Text); + } + + private static ulong FindOmittedImageAddress(string dumpPath, ulong imageBase, ulong imageSize) + { + const ulong ReadSize = 16; + + using StreamAddressSpace dataSource = new(File.OpenRead(dumpPath)); + (ulong Start, ulong End)[] savedRanges; + + if (OperatingSystem.IsWindows()) + { + Minidump dump = new(dataSource); + savedRanges = dump.Segments + .Select(segment => (segment.VirtualAddress, segment.VirtualAddress + segment.Size)) + .ToArray(); + } + else if (!OperatingSystem.IsMacOS()) + { + ELFCoreFile dump = new(dataSource); + Assert.True(dump.IsValid(), $"'{dumpPath}' is not an ELF core dump"); + savedRanges = dump.Segments + .Where(segment => segment.Header.Type == ELFProgramHeaderType.Load && segment.Header.FileSize > 0) + .Select(segment => (segment.Header.VirtualAddress.Value, segment.Header.VirtualAddress + segment.Header.FileSize)) + .ToArray(); + } + else + { + MachOFile dump = new(dataSource); + Assert.True(dump.IsValid() && dump.Header.FileType == MachHeaderFileType.Core, $"'{dumpPath}' is not a Mach-O core dump"); + savedRanges = dump.Segments + .Where(segment => segment.LoadCommand.FileSize > 0) + .Select(segment => ((ulong)segment.LoadCommand.VMAddress, segment.LoadCommand.VMAddress + segment.LoadCommand.FileSize)) + .ToArray(); + } + + ulong imageEnd = imageBase + imageSize; + ulong address = imageBase; + foreach ((ulong start, ulong end) in savedRanges.OrderBy(range => range.Start)) + { + if (end <= address) + { + continue; + } + if (start >= imageEnd) + { + break; + } + if (address + ReadSize <= start) + { + return address; + } + address = Math.Max(address, end); + } + if (address + ReadSize <= imageEnd) + { + return address; + } + + throw new InvalidOperationException( + $"No omitted {ReadSize}-byte range was found in the CoreLib image."); + } + [SosTheory] [MemberData(nameof(ScenariosMatrix))] public async Task ThreadState_DecodesStateFlags(TestConfig config) diff --git a/src/tests/SOS.Tests/ModuleCommandParsing.cs b/src/tests/SOS.Tests/ModuleCommandParsing.cs index eecb88c4f4..dfd51b24bd 100644 --- a/src/tests/SOS.Tests/ModuleCommandParsing.cs +++ b/src/tests/SOS.Tests/ModuleCommandParsing.cs @@ -8,16 +8,20 @@ namespace SOS.Tests; /// -/// Structured parsers for the module-keyed SOS commands: !dumpdomain, !dumpassembly, -/// !dumpmodule (with and without -mt), !name2ee and !token2ee. Each builds a -/// typed model (domains → assemblies → modules; module fields + type tables; EE name/token resolutions) -/// instead of matching raw lines, and the structural parsers fail loudly on a line they don't recognize so -/// a layout change can never be silently dropped. The models let the tests round-trip addresses across -/// commands (a dumpdomain assembly address into dumpassembly, a dumpmodule type-table token into token2ee, -/// etc.) and assert exact equality rather than the legacy scripts' "is it a hex value". +/// Structured parsers for the module-keyed SOS commands: !clrmodules, !dumpdomain, +/// !dumpassembly, !dumpmodule (with and without -mt), !name2ee and +/// !token2ee. Each builds a typed model (domains → assemblies → modules; module fields + type tables; +/// EE name/token resolutions) instead of matching raw lines, and the structural parsers fail loudly on a +/// line they don't recognize so a layout change can never be silently dropped. The models let the tests +/// round-trip addresses across commands (a dumpdomain assembly address into dumpassembly, a dumpmodule +/// type-table token into token2ee, etc.) and assert exact equality rather than the legacy scripts' +/// "is it a hex value". /// internal static class ModuleCommandParsing { + /// Run !clrmodules and parse the managed module list. + public static ClrModulesResult ClrModules(this Target target) => new(target.Sos("clrmodules")); + /// Run !dumpdomain and parse the full domain/assembly/module tree. public static DumpDomainResult DumpDomain(this Target target) => new(target.Sos("dumpdomain")); @@ -39,6 +43,64 @@ public static EEResult Token2EE(this Target target, string module, uint token) = new(target.Sos($"token2ee {module} 0x{token:x}")); } +/// One managed module row from !clrmodules. +public sealed record ClrModuleInfo(ulong ImageBase, ulong ImageSize, string Name); + +/// The parsed !clrmodules output. +public sealed class ClrModulesResult +{ + private static readonly Regex s_moduleRow = + new(@"^\s*([0-9a-fA-F`]+)\s+([0-9a-fA-F]+)(?:\s+(.*?))?\s*$", RegexOptions.Compiled); + + public ClrModulesResult(SosOutput output) + { + Output = output; + + List modules = new(); + foreach (string raw in output.Lines) + { + string line = raw.TrimEnd(); + if (line.Length == 0) + { + continue; + } + + Match row = s_moduleRow.Match(line); + if (!row.Success) + { + throw output.Fail($"clrmodules row to be recognized (was \"{line}\")"); + } + + modules.Add(new ClrModuleInfo( + DumpDomainResult.ParseHex(row.Groups[1].Value), + DumpDomainResult.ParseHex(row.Groups[2].Value), + row.Groups[3].Value.Trim())); + } + + Modules = modules; + } + + public SosOutput Output { get; } + public IReadOnlyList Modules { get; } + + public ClrModuleInfo SingleByName(string name) + { + List matches = Modules + .Where(module => module.Name.EndsWith(name, StringComparison.OrdinalIgnoreCase)) + .ToList(); + if (matches.Count != 1) + { + string matchList = matches.Count == 0 + ? "" + : string.Join(", ", matches.Select(module => + $"{module.ImageBase:X16} {module.ImageSize:X8} {module.Name}")); + throw Output.Fail( + $"exactly one module named \"{name}\" (got {matches.Count}: {matchList})"); + } + return matches[0]; + } +} + /// Which kind of domain a !dumpdomain block describes. public enum DomainKind { diff --git a/src/tests/SOS.Tests/RuntimeInfoTests.cs b/src/tests/SOS.Tests/RuntimeInfoTests.cs index 5806b68959..e69bb13811 100644 --- a/src/tests/SOS.Tests/RuntimeInfoTests.cs +++ b/src/tests/SOS.Tests/RuntimeInfoTests.cs @@ -40,7 +40,7 @@ public async Task ClrModulesAndAssemblies_ListDebuggeeModule(TestConfig config) target.GoToStopPoint(TargetCatalog.StopHeap); // The CLR module list and the assembly list both include the debuggee, on every host. - target.Sos("clrmodules").AssertContains("SosHarnessScenarios"); + target.ClrModules().SingleByName(TargetCatalog.Get(TargetCatalog.Scenarios).ModuleFor(config.Flavor)); target.Sos("assemblies").AssertContains("SosHarnessScenarios"); } diff --git a/src/tests/SOS.Tests/SOS.Tests.csproj b/src/tests/SOS.Tests/SOS.Tests.csproj index 11d35b5556..3d910defc9 100644 --- a/src/tests/SOS.Tests/SOS.Tests.csproj +++ b/src/tests/SOS.Tests/SOS.Tests.csproj @@ -15,6 +15,7 @@ +