From 2294a557ca7e01ca65bfde59de88d5b4375bad52 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:18:00 -0400 Subject: [PATCH 1/2] Forward ICLRSymbolProvider calls to a host-supplied symbol resolver Plumbs ClrMD's IClrSymbolProvider hook through the diagnostics host so the (c)DAC can map runtime addresses back to module-qualified symbols via the host's IModuleService / IModuleSymbols. Also unifies SOS's existing legacy-DAC symbol-provider thunks onto the same shared adapter. * HostSymbolProvider (new): IClrSymbolProvider adapter over IModuleService; registered as a per-target service via [ServiceExport]. * RuntimeProvider: sets DataTargetOptions.SymbolProvider from the registered service so ClrMD's COM data target wrapper forwards ICLRSymbolProvider calls to the host. * SOS.Hosting/DataTargetWrapper: exposes ICLRSymbolProvider on the legacy-DAC CCW (was missing), delegating to the same IClrSymbolProvider service for a single resolver code path. Aligns the CCW HRESULT contract with ClrMD's LegacyDacDataTargetWrapper (E_NOTIMPL when no provider, E_FAIL on miss, S_FALSE on truncation, null-terminated buffer, ! reserved as module separator). * Runtime: extracts the ClrFlavor -> RuntimeType mapping into a single GetRuntimeType helper; adds a generic GetDacFilePath fallback so runtimes that ship only a cDAC (no legacy DAC binary) load via the cDAC without the user having to set ForceUseContractReader. * IHostExtension (new) + ServiceManager: extension assemblies can implement IHostExtension to register process-wide state (e.g. a ClrMD IClrInfoProvider) at load time. Discovery is folded into the existing [ServiceExport]/[ProviderExport] type scan in RegisterExportedServices, so no new reflection pass is added. Depends on a ClrMD package that contains IClrSymbolProvider and DataTargetOptions.SymbolProvider (microsoft/clrmd#PR-merged; the package-version bump in eng/Version.Details.xml has landed via darc). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../HostSymbolProvider.cs | 123 ++++++++++++++++ .../Runtime.cs | 28 ++-- .../RuntimeProvider.cs | 3 +- .../ServiceManager.cs | 20 +++ .../IHostExtension.cs | 23 +++ src/SOS/SOS.Hosting/DataTargetWrapper.cs | 131 ++++++++++++++++++ 6 files changed, 318 insertions(+), 10 deletions(-) create mode 100644 src/Microsoft.Diagnostics.DebugServices.Implementation/HostSymbolProvider.cs create mode 100644 src/Microsoft.Diagnostics.DebugServices/IHostExtension.cs diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/HostSymbolProvider.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/HostSymbolProvider.cs new file mode 100644 index 0000000000..8103e37847 --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/HostSymbolProvider.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using Microsoft.Diagnostics.Runtime; + +namespace Microsoft.Diagnostics.DebugServices.Implementation +{ + /// + /// Adapter exposing the host's / + /// through ClrMD's + /// contract. Registered as a per-target + /// service so both ClrMD (via ) + /// and SOS's data-target CCW can resolve symbols through one shared + /// implementation. Returns symbols in "Module!Symbol" form. + /// + [ServiceExport(Type = typeof(IClrSymbolProvider), Scope = ServiceScope.Target)] + public sealed class HostSymbolProvider : IClrSymbolProvider + { + private readonly IModuleService _moduleService; + + public HostSymbolProvider(IModuleService moduleService) + { + _moduleService = moduleService ?? throw new ArgumentNullException(nameof(moduleService)); + } + + public bool TryGetSymbolName(ulong address, out string symbolName, out ulong displacement) + { + symbolName = null; + displacement = 0; + + IModule module; + try + { + module = _moduleService.GetModuleFromAddress(address); + } + catch (DiagnosticsException) + { + return false; + } + if (module is null) + { + return false; + } + + IModuleSymbols symbols = module.Services.GetService(); + if (symbols is null) + { + return false; + } + + if (!symbols.TryGetSymbolName(address, out string bareName, out displacement) + || string.IsNullOrEmpty(bareName)) + { + return false; + } + + // Strip any module! qualifier the lower-level service might have + // prepended — the new contract returns bare names only. + int bang = bareName.IndexOf('!'); + symbolName = bang >= 0 && bang + 1 < bareName.Length ? bareName.Substring(bang + 1) : bareName; + return true; + } + + public bool TryGetSymbolAddress(ulong moduleBase, string name, out ulong address) + { + address = 0; + if (string.IsNullOrEmpty(name)) + { + return false; + } + + // moduleBase != 0 restricts the search to a single module. + if (moduleBase != 0) + { + IModule scopedModule; + try + { + scopedModule = _moduleService.GetModuleFromBaseAddress(moduleBase); + } + catch (DiagnosticsException) + { + return false; + } + if (scopedModule is null) + { + return false; + } + + IModuleSymbols scopedSymbols = scopedModule.Services.GetService(); + if (scopedSymbols is null) + { + return false; + } + + if (scopedSymbols.TryGetSymbolAddress(name, out ulong scopedAddr) && scopedAddr != 0) + { + address = scopedAddr; + return true; + } + return false; + } + + foreach (IModule module in _moduleService.EnumerateModules()) + { + IModuleSymbols symbols = module.Services.GetService(); + if (symbols is null) + { + continue; + } + + if (symbols.TryGetSymbolAddress(name, out ulong addr) && addr != 0) + { + address = addr; + return true; + } + } + + return false; + } + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs index 05db19b715..9cdfc03022 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs @@ -39,15 +39,7 @@ public Runtime(IServiceProvider services, int id, ClrInfo clrInfo) _settingsService = services.GetService() ?? throw new ArgumentException("ISettingsService required"); _symbolService = services.GetService() ?? throw new ArgumentException("ISymbolService required"); - RuntimeType = RuntimeType.Unknown; - if (clrInfo.Flavor == ClrFlavor.Core) - { - RuntimeType = RuntimeType.NetCore; - } - else if (clrInfo.Flavor == ClrFlavor.Desktop) - { - RuntimeType = RuntimeType.Desktop; - } + RuntimeType = GetRuntimeType(clrInfo.Flavor); RuntimeModule = services.GetService().GetModuleFromBaseAddress(clrInfo.ModuleInfo.ImageBase); ServiceContainerFactory containerFactory = services.GetService().CreateServiceContainerFactory(ServiceScope.Runtime, services); @@ -129,6 +121,15 @@ public string GetDacFilePath(out bool verifySignature) _verifySignature = _settingsService.DacSignatureVerificationEnabled; } } + if (_dacFilePath is null) + { + _cdacFilePath ??= GetLibraryPath(DebugLibraryKind.CDac); + if (_cdacFilePath is not null) + { + verifySignature = false; + return _cdacFilePath; + } + } verifySignature = _verifySignature; return _dacFilePath; } @@ -318,9 +319,18 @@ public override int GetHashCode() "Desktop .NET Framework", ".NET Core", ".NET Core (single-file)", + "Native AOT", "Other" }; + private static RuntimeType GetRuntimeType(ClrFlavor flavor) => flavor switch + { + ClrFlavor.Core => RuntimeType.NetCore, + ClrFlavor.Desktop => RuntimeType.Desktop, + ClrFlavor.NativeAOT => RuntimeType.NativeAOT, + _ => RuntimeType.Unknown, + }; + public override string ToString() { StringBuilder sb = new(); diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeProvider.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeProvider.cs index 00eb74c6cd..2994f71a18 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeProvider.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeProvider.cs @@ -36,7 +36,8 @@ public IEnumerable EnumerateRuntimes(int startingRuntimeId, RuntimeEnu DataTarget dataTarget = new(_services.GetService(), new DataTargetOptions() { ForceCompleteRuntimeEnumeration = (flags & RuntimeEnumerationFlags.All) != 0, - VerifyDacOnWindows = settingsService?.DacSignatureVerificationEnabled ?? true + VerifyDacOnWindows = settingsService?.DacSignatureVerificationEnabled ?? true, + SymbolProvider = _services.GetService(), }); for (int i = 0; i < dataTarget.ClrVersions.Length; i++) { diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs index ae69ea69ef..d088a2fdb0 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs @@ -121,6 +121,12 @@ public void RegisterExportedServices(Type serviceType) { throw new InvalidOperationException(); } + + if (typeof(IHostExtension).IsAssignableFrom(serviceType)) + { + InvokeHostExtension(serviceType); + } + for (Type currentType = serviceType; currentType is not null; currentType = currentType.BaseType) { if (currentType == typeof(object) || currentType == typeof(ValueType)) @@ -185,6 +191,20 @@ or FileLoadException } } + private static void InvokeHostExtension(Type type) + { + if (type.IsAbstract) + { + return; + } + if (type.GetConstructor(Type.EmptyTypes) is null) + { + throw new InvalidOperationException($"IHostExtension implementation '{type.FullName}' must have a public parameterless constructor."); + } + IHostExtension extension = (IHostExtension)Activator.CreateInstance(type); + extension.Initialize(); + } + /// /// Add service factory for the specific scope. /// diff --git a/src/Microsoft.Diagnostics.DebugServices/IHostExtension.cs b/src/Microsoft.Diagnostics.DebugServices/IHostExtension.cs new file mode 100644 index 0000000000..71bf63f3ca --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices/IHostExtension.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DebugServices +{ + /// + /// Implemented by extension assemblies to perform process-wide initialization + /// (for example registering an IClrInfoProvider) when the extension + /// is loaded. + /// + /// Implementations must have a public parameterless constructor. They are + /// discovered and invoked exactly once per extension assembly during + /// assembly registration, before any other + /// service or provider exports from the assembly are consumed. + /// + public interface IHostExtension + { + /// + /// Performs one-time initialization for the extension. + /// + void Initialize(); + } +} diff --git a/src/SOS/SOS.Hosting/DataTargetWrapper.cs b/src/SOS/SOS.Hosting/DataTargetWrapper.cs index 0a01ceea22..fda02b6202 100644 --- a/src/SOS/SOS.Hosting/DataTargetWrapper.cs +++ b/src/SOS/SOS.Hosting/DataTargetWrapper.cs @@ -19,6 +19,7 @@ internal sealed unsafe class DataTargetWrapper : COMCallableIUnknown private static readonly Guid IID_ICLRMetadataLocator = new("aa8fa804-bc05-4642-b2c5-c353ed22fc63"); private static readonly Guid IID_ICLRRuntimeLocator = new("b760bf44-9377-4597-8be7-58083bdc5146"); private static readonly Guid IID_ICLRContractLocator = new("17d5b8c6-34a9-407f-af4f-a930201d4e02"); + private static readonly Guid IID_ICLRSymbolProvider = new("c4f8b7e2-9d3a-4f6c-b1e5-8a2d7c3f9b1e"); // For ClrMD's magic hand shake private const ulong MagicCallbackConstant = 0x43; @@ -31,6 +32,7 @@ internal sealed unsafe class DataTargetWrapper : COMCallableIUnknown private readonly IModuleService _moduleService; private readonly IThreadUnwindService _threadUnwindService; private readonly IRemoteMemoryService _remoteMemoryService; + private readonly IClrSymbolProvider _symbolProvider; private readonly ulong _ignoreAddressBitsMask; public IntPtr IDataTarget { get; } @@ -47,6 +49,7 @@ public DataTargetWrapper(IServiceProvider services, IRuntime runtime) _threadUnwindService = services.GetService(); _moduleService = services.GetService(); _remoteMemoryService = services.GetService(); + _symbolProvider = services.GetService(); _ignoreAddressBitsMask = _memoryService.SignExtensionMask(); VTableBuilder builder = AddInterface(IID_ICLRDataTarget, false); @@ -73,6 +76,11 @@ public DataTargetWrapper(IServiceProvider services, IRuntime runtime) builder.AddMethod(new GetContractDescriptorDelegate(GetContractDescriptor)); builder.Complete(); + builder = AddInterface(IID_ICLRSymbolProvider, false); + builder.AddMethod(new TryGetSymbolNameDelegate(TryGetSymbolName)); + builder.AddMethod(new TryGetSymbolAddressDelegate(TryGetSymbolAddress)); + builder.Complete(); + AddRef(); } @@ -378,6 +386,109 @@ private int GetContractDescriptor( #endregion + #region ICLRSymbolProvider + + private int TryGetSymbolName( + IntPtr self, + ulong address, + uint cchName, + char* pName, + uint* pcchNameActual, + ulong* pDisplacement) + { + if (cchName > int.MaxValue) + { + return HResult.E_INVALIDARG; + } + + try + { + if (_symbolProvider is null) + { + return HResult.E_NOTIMPL; + } + + if (!_symbolProvider.TryGetSymbolName(address, out string symbolName, out ulong displacement) + || string.IsNullOrEmpty(symbolName)) + { + return HResult.E_FAIL; + } + + if (pcchNameActual != null) + { + *pcchNameActual = (uint)symbolName.Length + 1; + } + if (pDisplacement != null) + { + *pDisplacement = displacement; + } + + if (cchName == 0 || pName == null) + { + return HResult.S_OK; + } + + int copy = Math.Min(symbolName.Length, (int)cchName - 1); + for (int i = 0; i < copy; i++) + { + pName[i] = symbolName[i]; + } + pName[copy] = '\0'; + return copy < symbolName.Length ? HResult.S_FALSE : HResult.S_OK; + } + catch + { + return HResult.E_FAIL; + } + } + + private int TryGetSymbolAddress( + IntPtr self, + ulong moduleBase, + string name, + ulong* pAddress) + { + if (pAddress == null) + { + return HResult.E_INVALIDARG; + } + *pAddress = 0; + + try + { + if (_symbolProvider is null) + { + return HResult.E_NOTIMPL; + } + + if (string.IsNullOrEmpty(name)) + { + return HResult.E_INVALIDARG; + } + + // Bare symbol names only — '!' is reserved as the SOS module + // separator and is not produced by any of the mangling toolchains + // we target. + if (name.IndexOf('!') >= 0) + { + return HResult.E_INVALIDARG; + } + + if (_symbolProvider.TryGetSymbolAddress(moduleBase, name, out ulong address) && address != 0) + { + *pAddress = address; + return HResult.S_OK; + } + return HResult.E_FAIL; + } + catch + { + return HResult.E_FAIL; + } + } + + #endregion + #region ICLRDataTarget delegates [UnmanagedFunctionPointer(CallingConvention.Winapi)] @@ -522,5 +633,25 @@ private delegate int GetContractDescriptorDelegate( [Out] out ulong address); #endregion + + #region ICLRSymbolProvider delegates + + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + private delegate int TryGetSymbolNameDelegate( + [In] IntPtr self, + [In] ulong address, + [In] uint cchName, + [Out] char* pName, + [Out] uint* pcchNameActual, + [Out] ulong* pDisplacement); + + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + private delegate int TryGetSymbolAddressDelegate( + [In] IntPtr self, + [In] ulong moduleBase, + [In][MarshalAs(UnmanagedType.LPWStr)] string name, + [Out] ulong* pAddress); + + #endregion } } From 139973572bc26b3d955b75719ed5f40a6166e697 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:15:56 -0400 Subject: [PATCH 2/2] Address PR review feedback - HostSymbolProvider: remove unused System.IO using; fix XML doc (bare symbol names, qualifier stripped); inject IMemoryService and mask address/moduleBase via SignExtensionMask for x86 targets. - DataTargetWrapper.ICLRSymbolProvider: apply _ignoreAddressBitsMask to address/moduleBase for sign-extended x86 inputs (matches the pattern used by ReadVirtual/WriteVirtual in the same file). - ServiceManager.InvokeHostExtension: wrap Activator+Initialize and rethrow non-DiagnosticsException as DiagnosticsException so an extension constructor/initializer failure surfaces as a load failure instead of escaping RegisterAssembly. - IHostExtension: update doc to reflect per-implementing-type invocation (an assembly may contain multiple implementations; ordering is unspecified). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../HostSymbolProvider.cs | 14 ++++++++------ .../ServiceManager.cs | 11 +++++++++-- .../IHostExtension.cs | 12 ++++++++---- src/SOS/SOS.Hosting/DataTargetWrapper.cs | 4 ++++ 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/HostSymbolProvider.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/HostSymbolProvider.cs index 8103e37847..5ab9ffc3e7 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/HostSymbolProvider.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/HostSymbolProvider.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.IO; using Microsoft.Diagnostics.Runtime; namespace Microsoft.Diagnostics.DebugServices.Implementation @@ -10,19 +9,18 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// /// Adapter exposing the host's / /// through ClrMD's - /// contract. Registered as a per-target - /// service so both ClrMD (via ) - /// and SOS's data-target CCW can resolve symbols through one shared - /// implementation. Returns symbols in "Module!Symbol" form. + /// contract. /// [ServiceExport(Type = typeof(IClrSymbolProvider), Scope = ServiceScope.Target)] public sealed class HostSymbolProvider : IClrSymbolProvider { private readonly IModuleService _moduleService; + private readonly ulong _signExtensionMask; - public HostSymbolProvider(IModuleService moduleService) + public HostSymbolProvider(IModuleService moduleService, IMemoryService memoryService) { _moduleService = moduleService ?? throw new ArgumentNullException(nameof(moduleService)); + _signExtensionMask = memoryService?.SignExtensionMask() ?? ulong.MaxValue; } public bool TryGetSymbolName(ulong address, out string symbolName, out ulong displacement) @@ -30,6 +28,8 @@ public bool TryGetSymbolName(ulong address, out string symbolName, out ulong dis symbolName = null; displacement = 0; + address &= _signExtensionMask; + IModule module; try { @@ -71,6 +71,8 @@ public bool TryGetSymbolAddress(ulong moduleBase, string name, out ulong address return false; } + moduleBase &= _signExtensionMask; + // moduleBase != 0 restricts the search to a single module. if (moduleBase != 0) { diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs index d088a2fdb0..d4375b2f02 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs @@ -201,8 +201,15 @@ private static void InvokeHostExtension(Type type) { throw new InvalidOperationException($"IHostExtension implementation '{type.FullName}' must have a public parameterless constructor."); } - IHostExtension extension = (IHostExtension)Activator.CreateInstance(type); - extension.Initialize(); + try + { + IHostExtension extension = (IHostExtension)Activator.CreateInstance(type); + extension.Initialize(); + } + catch (Exception ex) when (ex is not DiagnosticsException) + { + throw new DiagnosticsException($"IHostExtension '{type.FullName}' initialization failed: {ex.Message}", ex); + } } /// diff --git a/src/Microsoft.Diagnostics.DebugServices/IHostExtension.cs b/src/Microsoft.Diagnostics.DebugServices/IHostExtension.cs index 71bf63f3ca..c6dedee9f6 100644 --- a/src/Microsoft.Diagnostics.DebugServices/IHostExtension.cs +++ b/src/Microsoft.Diagnostics.DebugServices/IHostExtension.cs @@ -8,10 +8,14 @@ namespace Microsoft.Diagnostics.DebugServices /// (for example registering an IClrInfoProvider) when the extension /// is loaded. /// - /// Implementations must have a public parameterless constructor. They are - /// discovered and invoked exactly once per extension assembly during - /// assembly registration, before any other - /// service or provider exports from the assembly are consumed. + /// Implementations must have a public parameterless constructor. Every + /// concrete implementation discovered in an + /// extension assembly is instantiated and its + /// method invoked once during assembly + /// registration, before any other service or provider exports from the + /// assembly are consumed. An assembly may contain multiple + /// implementations; invocation order across implementations is + /// unspecified. /// public interface IHostExtension { diff --git a/src/SOS/SOS.Hosting/DataTargetWrapper.cs b/src/SOS/SOS.Hosting/DataTargetWrapper.cs index fda02b6202..0a6690a9b2 100644 --- a/src/SOS/SOS.Hosting/DataTargetWrapper.cs +++ b/src/SOS/SOS.Hosting/DataTargetWrapper.cs @@ -401,6 +401,8 @@ private int TryGetSymbolName( return HResult.E_INVALIDARG; } + address &= _ignoreAddressBitsMask; + try { if (_symbolProvider is null) @@ -454,6 +456,8 @@ private int TryGetSymbolAddress( } *pAddress = 0; + moduleBase &= _ignoreAddressBitsMask; + try { if (_symbolProvider is null)