diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs index 0b9bfe91dd9785..e248cd26e4f047 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs @@ -252,8 +252,54 @@ public int GetModulePath(ulong vmModule, nint pStrFilename, Interop.BOOL* pResul public int GetMetadata(ulong vmModule, DacDbiTargetBuffer* pTargetBuffer) => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetMetadata(vmModule, pTargetBuffer) : HResults.E_NOTIMPL; - public int GetSymbolsBuffer(ulong vmModule, DacDbiTargetBuffer* pTargetBuffer, int* pSymbolFormat) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetSymbolsBuffer(vmModule, pTargetBuffer, pSymbolFormat) : HResults.E_NOTIMPL; + public int GetSymbolsBuffer(ulong vmModule, DacDbiTargetBuffer* pTargetBuffer, SymbolFormat* pSymbolFormat) + { + int hr = HResults.S_OK; + try + { + if (pTargetBuffer == null) + throw new ArgumentNullException(nameof(pTargetBuffer)); + + if (pSymbolFormat == null) + throw new ArgumentNullException(nameof(pSymbolFormat)); + + *pTargetBuffer = default; + *pSymbolFormat = SymbolFormat.None; + + if (vmModule == 0) + throw new ArgumentException("Module pointer must be non-zero.", nameof(vmModule)); + + Contracts.ILoader loader = _target.Contracts.Loader; + Contracts.ModuleHandle handle = loader.GetModuleHandleFromModulePtr(new TargetPointer(vmModule)); + + if (loader.TryGetSymbolStream(handle, out TargetPointer buffer, out uint size) && size != 0) + { + pTargetBuffer->pAddress = buffer.Value; + pTargetBuffer->cbSize = size; + *pSymbolFormat = SymbolFormat.Pdb; + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null) + { + DacDbiTargetBuffer bufferLocal; + SymbolFormat formatLocal; + int hrLocal = _legacy.GetSymbolsBuffer(vmModule, &bufferLocal, &formatLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + { + Debug.Assert(pTargetBuffer->pAddress == bufferLocal.pAddress, $"pAddress: cDAC: {pTargetBuffer->pAddress:x}, DAC: {bufferLocal.pAddress:x}"); + Debug.Assert(pTargetBuffer->cbSize == bufferLocal.cbSize, $"cbSize: cDAC: {pTargetBuffer->cbSize}, DAC: {bufferLocal.cbSize}"); + Debug.Assert(*pSymbolFormat == formatLocal, $"pSymbolFormat: cDAC: {*pSymbolFormat}, DAC: {formatLocal}"); + } + } +#endif + return hr; + } public int GetModuleData(ulong vmModule, DacDbiModuleInfo* pData) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs index 79c0e36953db9f..6e296072920b15 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs @@ -176,6 +176,12 @@ public enum CorDebugUserState USER_THREADPOOL = 0x100, } +public enum SymbolFormat +{ + None = 0, + Pdb = 1, +} + public enum CorDebugGenerationTypes { CorDebug_Gen0 = 0, @@ -231,7 +237,7 @@ public unsafe partial interface IDacDbiInterface int GetMetadata(ulong vmModule, DacDbiTargetBuffer* pTargetBuffer); [PreserveSig] - int GetSymbolsBuffer(ulong vmModule, DacDbiTargetBuffer* pTargetBuffer, int* pSymbolFormat); + int GetSymbolsBuffer(ulong vmModule, DacDbiTargetBuffer* pTargetBuffer, SymbolFormat* pSymbolFormat); [PreserveSig] int GetModuleData(ulong vmModule, DacDbiModuleInfo* pData); diff --git a/src/native/managed/cdac/tests/DacDbiImplTests.cs b/src/native/managed/cdac/tests/DacDbiImplTests.cs index 0f9268acbeb255..192a485d54f6f7 100644 --- a/src/native/managed/cdac/tests/DacDbiImplTests.cs +++ b/src/native/managed/cdac/tests/DacDbiImplTests.cs @@ -397,4 +397,69 @@ public void EnumerateAssembliesInAppDomain_NoAssemblies(MockTarget.Architecture Assert.Equal(System.HResults.S_OK, hr); Assert.Empty(assemblies); } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetSymbolsBuffer_NoStream(MockTarget.Architecture arch) + { + TargetPointer moduleAddr = TargetPointer.Null; + var (dacDbi, _) = CreateDacDbiWithLoader(arch, (loader, _) => + { + moduleAddr = new TargetPointer(loader.AddModule().Address); + }); + + DacDbiTargetBuffer targetBuffer; + SymbolFormat symbolFormat; + int hr = dacDbi.GetSymbolsBuffer(moduleAddr, &targetBuffer, &symbolFormat); + Assert.Equal(System.HResults.S_OK, hr); + Assert.Equal(0UL, targetBuffer.pAddress); + Assert.Equal(0u, targetBuffer.cbSize); + Assert.Equal(SymbolFormat.None, symbolFormat); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetSymbolsBuffer_WithSymbols(MockTarget.Architecture arch) + { + byte[] symbolBytes = [0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE]; + TargetPointer moduleAddr = TargetPointer.Null; + ulong expectedBufferAddr = 0; + var (dacDbi, _) = CreateDacDbiWithLoader(arch, (loader, _) => + { + MockLoaderModule module = loader.AddModule(); + MockCGrowableSymbolStream stream = loader.AddInMemorySymbolStream(module, symbolBytes); + moduleAddr = new TargetPointer(module.Address); + expectedBufferAddr = stream.Buffer; + }); + + DacDbiTargetBuffer targetBuffer; + SymbolFormat symbolFormat; + int hr = dacDbi.GetSymbolsBuffer(moduleAddr, &targetBuffer, &symbolFormat); + Assert.Equal(System.HResults.S_OK, hr); + Assert.Equal(expectedBufferAddr, targetBuffer.pAddress); + Assert.Equal((uint)symbolBytes.Length, targetBuffer.cbSize); + Assert.Equal(SymbolFormat.Pdb, symbolFormat); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetSymbolsBuffer_EmptyStream(MockTarget.Architecture arch) + { + // Stream object exists but contains no bytes - treated like no symbols. + TargetPointer moduleAddr = TargetPointer.Null; + var (dacDbi, _) = CreateDacDbiWithLoader(arch, (loader, _) => + { + MockLoaderModule module = loader.AddModule(); + loader.AddInMemorySymbolStream(module, symbols: null); + moduleAddr = new TargetPointer(module.Address); + }); + + DacDbiTargetBuffer targetBuffer; + SymbolFormat symbolFormat; + int hr = dacDbi.GetSymbolsBuffer(moduleAddr, &targetBuffer, &symbolFormat); + Assert.Equal(System.HResults.S_OK, hr); + Assert.Equal(0UL, targetBuffer.pAddress); + Assert.Equal(0u, targetBuffer.cbSize); + Assert.Equal(SymbolFormat.None, symbolFormat); + } } diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiMultiModuleDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiMultiModuleDumpTests.cs new file mode 100644 index 00000000000000..3fda2f72d21b0c --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiMultiModuleDumpTests.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Dump-based integration tests for DacDbiImpl methods that need a debuggee with +/// extra loader features. Uses the MultiModule debuggee (heap dump), which +/// additionally loads an assembly from in-memory bytes with an in-memory PDB +/// so that the GetSymbolsBuffer / TryGetSymbolStream code path can +/// be exercised against an actual module with in-memory symbols. +/// +public class DacDbiMultiModuleDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "MultiModule"; + + private DacDbiImpl CreateDacDbi() => new DacDbiImpl(Target, legacyObj: null); + + private IEnumerable GetAllModules() + { + ILoader loader = Target.Contracts.Loader; + TargetPointer appDomainPtr = Target.ReadGlobalPointer(Constants.Globals.AppDomain); + ulong appDomain = Target.ReadPointer(appDomainPtr); + return loader.GetModuleHandles(new TargetPointer(appDomain), + AssemblyIterationFlags.IncludeLoaded | AssemblyIterationFlags.IncludeExecution); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public unsafe void GetSymbolsBuffer_FindsInMemorySymbols(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + ILoader loader = Target.Contracts.Loader; + + bool foundInMemorySymbols = false; + foreach (ModuleHandle module in GetAllModules()) + { + TargetPointer moduleAddr = loader.GetModule(module); + + DacDbiTargetBuffer targetBuffer; + SymbolFormat symbolFormat; + int hr = dbi.GetSymbolsBuffer(moduleAddr.Value, &targetBuffer, &symbolFormat); + Assert.Equal(System.HResults.S_OK, hr); + + if (symbolFormat == SymbolFormat.Pdb) + { + // When PDB symbols are reported, the buffer must be non-empty. + Assert.NotEqual(0UL, targetBuffer.pAddress); + Assert.NotEqual(0u, targetBuffer.cbSize); + foundInMemorySymbols = true; + } + else + { + Assert.Equal(0UL, targetBuffer.pAddress); + Assert.Equal(0u, targetBuffer.cbSize); + } + } + + Assert.True(foundInMemorySymbols, + "Expected at least one module in the MultiModule debuggee dump to have an in-memory PDB symbol stream."); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/MultiModule/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/MultiModule/Program.cs index 927c2dce9c257f..21652fbd734962 100644 --- a/src/native/managed/cdac/tests/DumpTests/Debuggees/MultiModule/Program.cs +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/MultiModule/Program.cs @@ -2,12 +2,16 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.IO; using System.Reflection; using System.Runtime.Loader; /// /// Debuggee for cDAC dump tests — exercises the Loader contract. /// Loads assemblies from multiple AssemblyLoadContexts then crashes. +/// Also loads an assembly from in-memory bytes with an in-memory PDB +/// stream so the Loader contract's TryGetSymbolStream / +/// DacDbi GetSymbolsBuffer path can be exercised against a real module. /// Also includes metadata features for MetaDataImport dump tests: /// - Non-const fields (for ELEMENT_TYPE_VOID default value testing) /// - String literals (user strings in #US heap) @@ -40,6 +44,16 @@ private static void Main() // Also load System.Xml to have another module present var xmlAssembly = Assembly.Load("System.Private.Xml"); + // Load this debuggee's own assembly from in-memory bytes alongside its + // portable PDB. The runtime stores the PDB bytes on the resulting + // Module as a CGrowableSymbolStream, which is what the DacDbi + // GetSymbolsBuffer / Loader contract TryGetSymbolStream APIs read. + string asmPath = typeof(Program).Assembly.Location; + string pdbPath = Path.ChangeExtension(asmPath, ".pdb"); + byte[] inMemoryAssemblyBytes = File.ReadAllBytes(asmPath); + byte[] inMemorySymbolBytes = File.ReadAllBytes(pdbPath); + Assembly inMemoryAssembly = Assembly.Load(inMemoryAssemblyBytes, inMemorySymbolBytes); + // Use the non-const field so it doesn't get optimized away s_nonConstField = contexts.Length; @@ -51,6 +65,9 @@ private static void Main() GC.KeepAlive(contexts); GC.KeepAlive(loadedAssemblies); GC.KeepAlive(xmlAssembly); + GC.KeepAlive(inMemoryAssembly); + GC.KeepAlive(inMemoryAssemblyBytes); + GC.KeepAlive(inMemorySymbolBytes); GC.KeepAlive(userString); GC.KeepAlive(s_nonConstField); diff --git a/src/native/managed/cdac/tests/LoaderTests.cs b/src/native/managed/cdac/tests/LoaderTests.cs index e283a7ece00154..60e718b51b751d 100644 --- a/src/native/managed/cdac/tests/LoaderTests.cs +++ b/src/native/managed/cdac/tests/LoaderTests.cs @@ -27,6 +27,7 @@ public unsafe class LoaderTests [DataType.Module] = TargetTestHelpers.CreateTypeInfo(loader.ModuleLayout), [DataType.Assembly] = TargetTestHelpers.CreateTypeInfo(loader.AssemblyLayout), [DataType.EEConfig] = TargetTestHelpers.CreateTypeInfo(loader.EEConfigLayout), + [DataType.CGrowableSymbolStream] = TargetTestHelpers.CreateTypeInfo(loader.CGrowableSymbolStreamLayout), }; private static ILoader CreateLoaderContract(MockTarget.Architecture arch, Action configure) @@ -1021,4 +1022,65 @@ public void GetCompilerFlags(uint rawFlags, Interop.BOOL expectedAllowJITOpts, I Assert.Equal(expectedAllowJITOpts, allowJITOpts); Assert.Equal(expectedEnableEnC, enableEnC); } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void TryGetSymbolStream_NoStream(MockTarget.Architecture arch) + { + TargetPointer moduleAddr = TargetPointer.Null; + ILoader contract = CreateLoaderContract(arch, loader => + { + // Module with no GrowableSymbolStream (left as null pointer). + moduleAddr = loader.AddModule().Address; + }); + + Contracts.ModuleHandle handle = contract.GetModuleHandleFromModulePtr(moduleAddr); + bool result = contract.TryGetSymbolStream(handle, out TargetPointer buffer, out uint size); + Assert.False(result); + Assert.Equal(TargetPointer.Null, buffer); + Assert.Equal(0u, size); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void TryGetSymbolStream_WithSymbols(MockTarget.Architecture arch) + { + byte[] symbolBytes = [1, 2, 3, 4, 5, 6, 7, 8]; + TargetPointer moduleAddr = TargetPointer.Null; + TargetPointer expectedBuffer = TargetPointer.Null; + ILoader contract = CreateLoaderContract(arch, loader => + { + MockLoaderModule module = loader.AddModule(); + MockCGrowableSymbolStream stream = loader.AddInMemorySymbolStream(module, symbolBytes); + moduleAddr = module.Address; + expectedBuffer = new TargetPointer(stream.Buffer); + }); + + Contracts.ModuleHandle handle = contract.GetModuleHandleFromModulePtr(moduleAddr); + bool result = contract.TryGetSymbolStream(handle, out TargetPointer buffer, out uint size); + Assert.True(result); + Assert.Equal(expectedBuffer, buffer); + Assert.Equal((uint)symbolBytes.Length, size); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void TryGetSymbolStream_EmptyStream(MockTarget.Architecture arch) + { + TargetPointer moduleAddr = TargetPointer.Null; + ILoader contract = CreateLoaderContract(arch, loader => + { + MockLoaderModule module = loader.AddModule(); + // Stream object is present but has no bytes. + loader.AddInMemorySymbolStream(module, symbols: null); + moduleAddr = module.Address; + }); + + Contracts.ModuleHandle handle = contract.GetModuleHandleFromModulePtr(moduleAddr); + bool result = contract.TryGetSymbolStream(handle, out TargetPointer buffer, out uint size); + // The stream pointer is non-null so the API returns true even though the buffer is empty. + Assert.True(result); + Assert.Equal(TargetPointer.Null, buffer); + Assert.Equal(0u, size); + } } diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Loader.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Loader.cs index 51b2a8a623d31c..079a149e1df332 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Loader.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Loader.cs @@ -145,6 +145,12 @@ public ulong ReadyToRunInfo get => ReadPointerField(ReadyToRunInfoFieldName); set => WritePointerField(ReadyToRunInfoFieldName, value); } + + public ulong GrowableSymbolStream + { + get => ReadPointerField(GrowableSymbolStreamFieldName); + set => WritePointerField(GrowableSymbolStreamFieldName, value); + } } internal sealed class MockLoaderAssembly : TypedView @@ -189,6 +195,30 @@ public uint ModifiableAssemblies } } +internal sealed class MockCGrowableSymbolStream : TypedView +{ + private const string BufferFieldName = "Buffer"; + private const string SizeFieldName = "Size"; + + public static Layout CreateLayout(MockTarget.Architecture architecture) + => new SequentialLayoutBuilder("CGrowableSymbolStream", architecture) + .AddPointerField(BufferFieldName) + .AddUInt32Field(SizeFieldName) + .Build(); + + public ulong Buffer + { + get => ReadPointerField(BufferFieldName); + set => WritePointerField(BufferFieldName, value); + } + + public uint Size + { + get => ReadUInt32Field(SizeFieldName); + set => WriteUInt32Field(SizeFieldName, value); + } +} + internal sealed class MockLoaderBuilder { private const ulong DefaultAllocationRangeStart = 0x0001_0000; @@ -200,6 +230,7 @@ internal sealed class MockLoaderBuilder internal Layout EEConfigLayout { get; } internal Layout LoaderHeapLayout { get; } internal Layout LoaderHeapBlockLayout { get; } + internal Layout CGrowableSymbolStreamLayout { get; } private readonly MockMemorySpace.BumpAllocator _allocator; @@ -220,6 +251,7 @@ public MockLoaderBuilder(MockMemorySpace.Builder builder, (ulong Start, ulong En EEConfigLayout = MockEEConfig.CreateLayout(builder.TargetTestHelpers.Arch); LoaderHeapLayout = MockLoaderHeap.CreateLayout(builder.TargetTestHelpers.Arch); LoaderHeapBlockLayout = MockLoaderHeapBlock.CreateLayout(builder.TargetTestHelpers.Arch); + CGrowableSymbolStreamLayout = MockCGrowableSymbolStream.CreateLayout(builder.TargetTestHelpers.Arch); } internal MockLoaderHeap AddLoaderHeap(ulong firstBlockAddress = 0) @@ -281,6 +313,32 @@ internal MockEEConfig AddEEConfig(uint modifiableAssemblies) return config; } + /// + /// Allocates a CGrowableSymbolStream with an associated symbol byte buffer and attaches it + /// to via the GrowableSymbolStream field. + /// + /// Module to attach the symbol stream to. + /// Symbol bytes to allocate in the target. If null, only the stream object is created. + /// The stream allocation, with Buffer/Size populated. + internal MockCGrowableSymbolStream AddInMemorySymbolStream(MockLoaderModule module, byte[]? symbols) + { + MockCGrowableSymbolStream stream = CGrowableSymbolStreamLayout.Create(_allocator.Allocate((ulong)CGrowableSymbolStreamLayout.Size, "CGrowableSymbolStream")); + if (symbols is not null && symbols.Length > 0) + { + MockMemorySpace.HeapFragment bufferFragment = _allocator.Allocate((ulong)symbols.Length, "CGrowableSymbolStream buffer"); + symbols.CopyTo(bufferFragment.Data); + stream.Buffer = bufferFragment.Address; + stream.Size = (uint)symbols.Length; + } + else + { + stream.Buffer = 0; + stream.Size = 0; + } + module.GrowableSymbolStream = stream.Address; + return stream; + } + private ulong AddNullTerminatedUtf8(ReadOnlySpan bytes, string name) { MockMemorySpace.HeapFragment fragment = _allocator.Allocate((ulong)bytes.Length + 1, name);