Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,24 @@ public enum Flags : byte
}

private Flags _flags;
private IEnumerable<PdbFileInfo> _pdbFileInfos;
private IEnumerable<PdbFileInfo> _pdbFileInfos = [];
private string _symbolFileName;

protected ImmutableArray<byte> _buildId;
protected readonly ServiceContainer _serviceContainer;

public Module(IServiceProvider services)
: this(services, isPEModuleProbeSupported: true)
{
}

protected Module(IServiceProvider services, bool isPEModuleProbeSupported)
{
ServiceContainerFactory containerFactory = services.GetService<IServiceManager>().CreateServiceContainerFactory(ServiceScope.Module, services);
containerFactory.AddServiceFactory<PEFile>((services) => ModuleService.GetPEInfo(ImageBase, ImageSize, out _pdbFileInfos, ref _flags));
if (isPEModuleProbeSupported)
{
containerFactory.AddServiceFactory<PEFile>((services) => ModuleService.GetPEInfo(ImageBase, ImageSize, out _pdbFileInfos, ref _flags));
}
_serviceContainer = containerFactory.Build();
_serviceContainer.AddService<IModule>(this);
_serviceContainer.AddService<IExportSymbols>(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,7 @@ internal PEFile GetPEInfo(ulong address, ulong size, out IEnumerable<PdbFileInfo
pdbFileInfos = Array.Empty<PdbFileInfo>();
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<PdbFileInfo> pdbs, out Module.Flags flags);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/tests/SOS.TestHarness/ChildEngineClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Comment thread
max-charlamb marked this conversation as resolved.
public SosOutput Sos(string command) => new(Name, command, Send("!sos " + command));

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modifying this for cdb. The managed db command conflicts with cdb's native db command so it needs the !sos prefix. Now the target.Sos(...) method will always use the extension command and never conflict with native debugger commands.


/// <summary>Live only: set a managed breakpoint and run to it (handled inside the child).</summary>
public SosOutput RunToBpmd(string module, string method) =>
Expand Down
2 changes: 1 addition & 1 deletion src/tests/SOS.TestHarness/DbgEngHostBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)));

/// <summary>Worker-thread command execution returning captured output.</summary>
protected string RunCore(string command)
Expand Down
5 changes: 2 additions & 3 deletions src/tests/SOS.TestHarness/IDebuggerHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,8 @@ public interface IDebuggerHost : IDisposable
SosOutput Execute(string command);

/// <summary>
/// Run a SOS command. The host applies whatever prefixing it needs (dbgeng wants a
/// leading <c>!</c>; dotnet-dump takes the bare command), so the test author writes
/// <c>Sos("clrstack")</c> once and it works everywhere.
/// Run a SOS command. The host applies whatever prefixing it needs, so the test author
/// writes <c>Sos("clrstack")</c> once and it works everywhere.
/// </summary>
SosOutput Sos(string command);
}
92 changes: 92 additions & 0 deletions src/tests/SOS.Tests/MemoryAndDecodeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -18,6 +22,12 @@ public sealed class MemoryAndDecodeTests
public static TheoryData<TestConfig> ScenariosMatrix => TestConfig.BuildMatrix([TargetCatalog.Scenarios]);
public static TheoryData<TestConfig> NestedExceptionMatrix => TestMatrices.HeapEnumeration([TargetCatalog.NestedException]);
public static TheoryData<TestConfig> DotnetDumpMatrix => TestConfig.BuildMatrix([TargetCatalog.Scenarios], Flavor.AllValid, Host.DotnetDump);
public static TheoryData<TestConfig> MiniDumpMatrix => TestConfig.BuildMatrix(
[TargetCatalog.Scenarios],
Flavor.Core,
Host.AllValid,
Liveness.Dump,
dumpKind: DumpKind.Mini);

[SosTheory]
[MemberData(nameof(DotnetDumpMatrix))]
Expand Down Expand Up @@ -57,6 +67,88 @@ public async Task MemoryDumpers_ShowKnownFieldBytes(TestConfig config)
target.Sos($"db {marker:x}").AssertContains(":"); // byte dump prints "<addr>: <bytes>"
}

[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)
Expand Down
76 changes: 69 additions & 7 deletions src/tests/SOS.Tests/ModuleCommandParsing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,20 @@
namespace SOS.Tests;

/// <summary>
/// Structured parsers for the module-keyed SOS commands: <c>!dumpdomain</c>, <c>!dumpassembly</c>,
/// <c>!dumpmodule</c> (with and without <c>-mt</c>), <c>!name2ee</c> and <c>!token2ee</c>. 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: <c>!clrmodules</c>, <c>!dumpdomain</c>,
/// <c>!dumpassembly</c>, <c>!dumpmodule</c> (with and without <c>-mt</c>), <c>!name2ee</c> and
/// <c>!token2ee</c>. 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".
/// </summary>
internal static class ModuleCommandParsing
{
/// <summary>Run <c>!clrmodules</c> and parse the managed module list.</summary>
public static ClrModulesResult ClrModules(this Target target) => new(target.Sos("clrmodules"));

/// <summary>Run <c>!dumpdomain</c> and parse the full domain/assembly/module tree.</summary>
public static DumpDomainResult DumpDomain(this Target target) => new(target.Sos("dumpdomain"));

Expand All @@ -39,6 +43,64 @@ public static EEResult Token2EE(this Target target, string module, uint token) =
new(target.Sos($"token2ee {module} 0x{token:x}"));
}

/// <summary>One managed module row from <c>!clrmodules</c>.</summary>
public sealed record ClrModuleInfo(ulong ImageBase, ulong ImageSize, string Name);

/// <summary>The parsed <c>!clrmodules</c> output.</summary>
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<ClrModuleInfo> 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<ClrModuleInfo> Modules { get; }

public ClrModuleInfo SingleByName(string name)
{
List<ClrModuleInfo> matches = Modules
.Where(module => module.Name.EndsWith(name, StringComparison.OrdinalIgnoreCase))
.ToList();
if (matches.Count != 1)
{
string matchList = matches.Count == 0
? "<none>"
: 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];
}
}

/// <summary>Which kind of domain a <c>!dumpdomain</c> block describes.</summary>
public enum DomainKind
{
Expand Down
2 changes: 1 addition & 1 deletion src/tests/SOS.Tests/RuntimeInfoTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down
1 change: 1 addition & 0 deletions src/tests/SOS.Tests/SOS.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\Microsoft.FileFormats\Microsoft.FileFormats.csproj" />
<ProjectReference Include="..\SOS.TestHarness\SOS.TestHarness.csproj" />
<ProjectReference Include="..\SOS.TestHarness.SourceGen\SOS.TestHarness.SourceGen.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
Expand Down
Loading