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);