From 7c419ecf4c929faf6b6b762972f31871a25b28e3 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 24 Apr 2026 12:17:42 -0400 Subject: [PATCH 1/5] [cDAC] Stack walk GC reference scanning and bug fixes Implement GC reference scanning for stub/transition frames and fix stack walker state machine bugs: - PromoteCallerStack/PromoteCallerStackUsingGCRefMap for transition frames - GCRefMap decoder for ReadyToRun import section resolution - FindGCRefMap with FindReadyToRunModule fallback - SOSDacImpl.GetStackReferences using cDAC contract - Fix IsFirst preserved for skipped frames - Fix skipped frame handling moved to UpdateState - GCInfoDecoder goto removal (ReportUntrackedAndSucceed local function) - RequiresInstArg, IsAsyncMethod, HasRetBuffArg on IRuntimeTypeSystem - ExceptionInfo ClauseForCatch fields for catch handler detection - Data descriptor additions for frame types and TransitionBlock layout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/GCInfo.md | 3 + .../design/datacontracts/RuntimeTypeSystem.md | 8 + docs/design/datacontracts/StackWalk.md | 14 + .../vm/datadescriptor/datadescriptor.inc | 33 ++ src/coreclr/vm/frames.h | 21 + src/coreclr/vm/readytoruninfo.h | 2 + .../Contracts/IExecutionManager.cs | 7 + .../Contracts/IGCInfo.cs | 3 + .../Contracts/IRuntimeTypeSystem.cs | 8 + .../DataType.cs | 3 + .../ExecutionManager/ExecutionManagerCore.cs | 13 + .../ExecutionManager/ExecutionManager_1.cs | 1 + .../ExecutionManager/ExecutionManager_2.cs | 1 + .../Contracts/GCInfo/GCInfoDecoder.cs | 34 +- .../Contracts/GCInfo/GCInfo_1.cs | 7 + .../Contracts/GCInfo/IGCInfoDecoder.cs | 7 + .../Contracts/RuntimeTypeSystem_1.cs | 94 +++ .../StackWalk/FrameHandling/FrameIterator.cs | 536 +++++++++++++++++- .../Contracts/StackWalk/GC/GCRefMapDecoder.cs | 123 ++++ .../StackWalk/GC/GcSignatureTypeProvider.cs | 64 +++ .../Contracts/StackWalk/StackWalk_1.cs | 324 ++++++++--- .../Data/ExceptionInfo.cs | 4 + .../Data/Frames/DynamicHelperFrame.cs | 18 + .../Data/Frames/ExternalMethodFrame.cs | 22 + .../Data/Frames/StubDispatchFrame.cs | 6 + .../Data/Frames/TransitionBlock.cs | 15 + .../Data/ReadyToRunInfo.cs | 7 + .../SOSDacImpl.cs | 70 ++- src/native/managed/cdac/cdac.slnx | 1 + ...iagnostics.DataContractReader.Tests.csproj | 2 +- .../MockDescriptors.ExecutionManager.cs | 5 + 31 files changed, 1338 insertions(+), 118 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs diff --git a/docs/design/datacontracts/GCInfo.md b/docs/design/datacontracts/GCInfo.md index c878957aea5001..b97f8dbbd3a525 100644 --- a/docs/design/datacontracts/GCInfo.md +++ b/docs/design/datacontracts/GCInfo.md @@ -19,6 +19,9 @@ IGCInfoHandle DecodeInterpreterGCInfo(TargetPointer gcInfoAddress, uint gcVersio // Fetches length of code as reported in GCInfo uint GetCodeLength(IGCInfoHandle handle); + +// Returns the list of interruptible code offset ranges from the GCInfo +IReadOnlyList GetInterruptibleRanges(IGCInfoHandle handle); ``` ## Version 1 diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index 83182979b22088..975537166a3395 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -185,6 +185,14 @@ partial interface IRuntimeTypeSystem : IContract // Return true if a MethodDesc represents an IL stub with a special MethodDesc context arg public virtual bool HasMDContextArg(MethodDescHandle); + // Return true if the method requires a hidden instantiation argument (generic context parameter). + // Corresponds to native MethodDesc::RequiresInstArg(). + public virtual bool RequiresInstArg(MethodDescHandle methodDesc); + + // Return true if the method uses the async calling convention. + // Corresponds to native MethodDesc::IsAsyncMethod(). + public virtual bool IsAsyncMethod(MethodDescHandle methodDesc); + // Return true if a MethodDesc is in a collectible module public virtual bool IsCollectibleMethod(MethodDescHandle methodDesc); diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 9d254591ddb0ac..6a5fa627ab06ab 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -60,9 +60,21 @@ This contract depends on the following descriptors: | `StubDispatchFrame` | `MethodDescPtr` | Pointer to Frame's method desc | | `StubDispatchFrame` | `RepresentativeMTPtr` | Pointer to Frame's method table pointer | | `StubDispatchFrame` | `RepresentativeSlot` | Frame's method table slot | +| `StubDispatchFrame` | `GCRefMap` | Cached pointer to GC reference map blob for caller stack promotion | +| `StubDispatchFrame` | `ZapModule` | Module pointer for lazy GCRefMap resolution via import sections | +| `StubDispatchFrame` | `Indirection` | Import slot pointer for lazy GCRefMap resolution | +| `ExternalMethodFrame` | `GCRefMap` | Cached pointer to GC reference map blob for caller stack promotion | +| `ExternalMethodFrame` | `Indirection` | Import slot pointer for lazy GCRefMap resolution | +| `ExternalMethodFrame` | `ZapModule` | Module pointer for lazy GCRefMap resolution via import sections | +| `DynamicHelperFrame` | `DynamicHelperFrameFlags` | Flags indicating which argument registers contain GC references | | `TransitionBlock` | `ReturnAddress` | Return address associated with the TransitionBlock | | `TransitionBlock` | `CalleeSavedRegisters` | Platform specific CalleeSavedRegisters struct associated with the TransitionBlock | | `TransitionBlock` (arm) | `ArgumentRegisters` | ARM specific `ArgumentRegisters` struct | +| `TransitionBlock` | `OffsetOfArgs` | Byte offset of stack arguments (first arg after registers) = `sizeof(TransitionBlock)` | +| `TransitionBlock` | `ArgumentRegistersOffset` | Byte offset of the ArgumentRegisters within the TransitionBlock | +| `TransitionBlock` | `FirstGCRefMapSlot` | Byte offset where GCRefMap slot enumeration begins. ARM64: RetBuffArgReg offset; others: ArgumentRegisters offset | +| `ReadyToRunInfo` | `ImportSections` | Pointer to array of `READYTORUN_IMPORT_SECTION` structs for GCRefMap resolution | +| `ReadyToRunInfo` | `NumImportSections` | Count of import sections in the array | | `FuncEvalFrame` | `DebuggerEvalPtr` | Pointer to the Frame's DebuggerEval object | | `DebuggerEval` | `TargetContext` | Context saved inside DebuggerEval | | `DebuggerEval` | `EvalDuringException` | Flag used in processing FuncEvalFrame | @@ -85,6 +97,8 @@ This contract depends on the following descriptors: | `ExceptionInfo` | `CallerOfActualHandlerFrame` | Stack frame of the caller of the catch handler | | `ExceptionInfo` | `PreviousNestedInfo` | Pointer to previous nested ExInfo | | `ExceptionInfo` | `PassNumber` | Exception handling pass (1 or 2) | +| `ExceptionInfo` | `ClauseForCatchHandlerStartPC` | Start PC offset of the catch handler clause, used for interruptible offset override | +| `ExceptionInfo` | `ClauseForCatchHandlerEndPC` | End PC offset of the catch handler clause, used for interruptible offset override | Global variables used: | Global Name | Type | Purpose | diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index b7e13c1f8574db..9b9d2d21f191eb 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -149,6 +149,8 @@ CDAC_TYPE_FIELD(ExceptionInfo, T_UINT8, PassNumber, offsetof(ExInfo, m_passNumbe CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, CSFEHClause, offsetof(ExInfo, m_csfEHClause)) CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, CSFEnclosingClause, offsetof(ExInfo, m_csfEnclosingClause)) CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, CallerOfActualHandlerFrame, offsetof(ExInfo, m_sfCallerOfActualHandlerFrame)) +CDAC_TYPE_FIELD(ExceptionInfo, T_UINT32, ClauseForCatchHandlerStartPC, offsetof(ExInfo, m_ClauseForCatch) + offsetof(EE_ILEXCEPTION_CLAUSE, HandlerStartPC)) +CDAC_TYPE_FIELD(ExceptionInfo, T_UINT32, ClauseForCatchHandlerEndPC, offsetof(ExInfo, m_ClauseForCatch) + offsetof(EE_ILEXCEPTION_CLAUSE, HandlerEndPC)) CDAC_TYPE_END(ExceptionInfo) CDAC_TYPE_BEGIN(ObjectHandle) @@ -724,6 +726,8 @@ CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, HotColdMap, cdac_data CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, DelayLoadMethodCallThunks, cdac_data::DelayLoadMethodCallThunks) CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, DebugInfoSection, cdac_data::DebugInfoSection) CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, ExceptionInfoSection, cdac_data::ExceptionInfoSection) +CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, ImportSections, cdac_data::ImportSections) +CDAC_TYPE_FIELD(ReadyToRunInfo, T_UINT32, NumImportSections, cdac_data::NumImportSections) CDAC_TYPE_FIELD(ReadyToRunInfo, TYPE(HashMap), EntryPointToMethodDescMap, cdac_data::EntryPointToMethodDescMap) CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, LoadedImageBase, cdac_data::LoadedImageBase) CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, Composite, cdac_data::Composite) @@ -969,6 +973,19 @@ CDAC_TYPE_FIELD(TransitionBlock, TYPE(CalleeSavedRegisters), CalleeSavedRegister #ifdef TARGET_ARM CDAC_TYPE_FIELD(TransitionBlock, TYPE(ArgumentRegisters), ArgumentRegisters, offsetof(TransitionBlock, m_argumentRegisters)) #endif // TARGET_ARM +// Offset to where stack arguments begin (just past the end of the TransitionBlock) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, OffsetOfArgs, sizeof(TransitionBlock)) +// Offset to where argument registers are saved in the TransitionBlock +#if (defined(TARGET_AMD64) && !defined(UNIX_AMD64_ABI)) || defined(TARGET_WASM) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegistersOffset, sizeof(TransitionBlock)) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, FirstGCRefMapSlot, sizeof(TransitionBlock)) +#elif defined(TARGET_ARM64) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegistersOffset, offsetof(TransitionBlock, m_argumentRegisters)) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, FirstGCRefMapSlot, offsetof(TransitionBlock, m_x8RetBuffReg)) +#else +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegistersOffset, offsetof(TransitionBlock, m_argumentRegisters)) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, FirstGCRefMapSlot, offsetof(TransitionBlock, m_argumentRegisters)) +#endif CDAC_TYPE_END(TransitionBlock) #ifdef DEBUGGING_SUPPORTED @@ -989,8 +1006,23 @@ CDAC_TYPE_SIZE(sizeof(StubDispatchFrame)) CDAC_TYPE_FIELD(StubDispatchFrame, T_POINTER, RepresentativeMTPtr, cdac_data::RepresentativeMTPtr) CDAC_TYPE_FIELD(StubDispatchFrame, T_POINTER, MethodDescPtr, cdac_data::MethodDescPtr) CDAC_TYPE_FIELD(StubDispatchFrame, T_UINT32, RepresentativeSlot, cdac_data::RepresentativeSlot) +CDAC_TYPE_FIELD(StubDispatchFrame, T_POINTER, GCRefMap, cdac_data::GCRefMap) +CDAC_TYPE_FIELD(StubDispatchFrame, T_POINTER, ZapModule, cdac_data::ZapModule) +CDAC_TYPE_FIELD(StubDispatchFrame, T_POINTER, Indirection, cdac_data::Indirection) CDAC_TYPE_END(StubDispatchFrame) +CDAC_TYPE_BEGIN(ExternalMethodFrame) +CDAC_TYPE_SIZE(sizeof(ExternalMethodFrame)) +CDAC_TYPE_FIELD(ExternalMethodFrame, T_POINTER, GCRefMap, cdac_data::GCRefMap) +CDAC_TYPE_FIELD(ExternalMethodFrame, T_POINTER, Indirection, cdac_data::Indirection) +CDAC_TYPE_FIELD(ExternalMethodFrame, T_POINTER, ZapModule, cdac_data::ZapModule) +CDAC_TYPE_END(ExternalMethodFrame) + +CDAC_TYPE_BEGIN(DynamicHelperFrame) +CDAC_TYPE_SIZE(sizeof(DynamicHelperFrame)) +CDAC_TYPE_FIELD(DynamicHelperFrame, T_INT32, DynamicHelperFrameFlags, cdac_data::DynamicHelperFrameFlags) +CDAC_TYPE_END(DynamicHelperFrame) + #ifdef FEATURE_HIJACK CDAC_TYPE_BEGIN(ResumableFrame) CDAC_TYPE_SIZE(sizeof(ResumableFrame)) @@ -1374,6 +1406,7 @@ CDAC_GLOBAL_POINTER(MetadataUpdatesApplied, &::g_metadataUpdatesApplied) #undef FRAME_TYPE_NAME CDAC_GLOBAL(MethodDescTokenRemainderBitCount, T_UINT8, METHOD_TOKEN_REMAINDER_BIT_COUNT) + #if FEATURE_COMINTEROP CDAC_GLOBAL(FeatureCOMInterop, T_UINT8, 1) #else diff --git a/src/coreclr/vm/frames.h b/src/coreclr/vm/frames.h index f3fccab5615efa..708b7eec57f983 100644 --- a/src/coreclr/vm/frames.h +++ b/src/coreclr/vm/frames.h @@ -1490,6 +1490,9 @@ struct cdac_data { static constexpr size_t RepresentativeMTPtr = offsetof(StubDispatchFrame, m_pRepresentativeMT); static constexpr uint32_t RepresentativeSlot = offsetof(StubDispatchFrame, m_representativeSlot); + static constexpr size_t GCRefMap = offsetof(StubDispatchFrame, m_pGCRefMap); + static constexpr size_t ZapModule = offsetof(StubDispatchFrame, m_pZapModule); + static constexpr size_t Indirection = offsetof(StubDispatchFrame, m_pIndirection); }; typedef DPTR(class StubDispatchFrame) PTR_StubDispatchFrame; @@ -1561,10 +1564,20 @@ class ExternalMethodFrame : public FramedMethodFrame #ifdef TARGET_X86 void UpdateRegDisplay_Impl(const PREGDISPLAY pRD, bool updateFloats = false); #endif + + friend struct ::cdac_data; }; typedef DPTR(class ExternalMethodFrame) PTR_ExternalMethodFrame; +template <> +struct cdac_data +{ + static constexpr size_t GCRefMap = offsetof(ExternalMethodFrame, m_pGCRefMap); + static constexpr size_t Indirection = offsetof(ExternalMethodFrame, m_pIndirection); + static constexpr size_t ZapModule = offsetof(ExternalMethodFrame, m_pZapModule); +}; + class DynamicHelperFrame : public FramedMethodFrame { int m_dynamicHelperFrameFlags; @@ -1583,10 +1596,18 @@ class DynamicHelperFrame : public FramedMethodFrame LIMITED_METHOD_DAC_CONTRACT; return TT_InternalCall; } + + friend struct ::cdac_data; }; typedef DPTR(class DynamicHelperFrame) PTR_DynamicHelperFrame; +template <> +struct cdac_data +{ + static constexpr size_t DynamicHelperFrameFlags = offsetof(DynamicHelperFrame, m_dynamicHelperFrameFlags); +}; + //------------------------------------------------------------------------ // This frame protects object references for the EE's convenience. // This frame type actually is created from C++. diff --git a/src/coreclr/vm/readytoruninfo.h b/src/coreclr/vm/readytoruninfo.h index 6963a5000311e7..64c3324d9b2acc 100644 --- a/src/coreclr/vm/readytoruninfo.h +++ b/src/coreclr/vm/readytoruninfo.h @@ -406,6 +406,8 @@ struct cdac_data static constexpr size_t DelayLoadMethodCallThunks = offsetof(ReadyToRunInfo, m_pSectionDelayLoadMethodCallThunks); static constexpr size_t DebugInfoSection = offsetof(ReadyToRunInfo, m_pSectionDebugInfo); static constexpr size_t ExceptionInfoSection = offsetof(ReadyToRunInfo, m_pSectionExceptionInfo); + static constexpr size_t ImportSections = offsetof(ReadyToRunInfo, m_pImportSections); + static constexpr size_t NumImportSections = offsetof(ReadyToRunInfo, m_nImportSections); static constexpr size_t EntryPointToMethodDescMap = offsetof(ReadyToRunInfo, m_entryPointToMethodDescMap); static constexpr size_t LoadedImageBase = offsetof(ReadyToRunInfo, m_pLoadedImageBase); static constexpr size_t Composite = offsetof(ReadyToRunInfo, m_pComposite); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs index 0dddd31417e5ad..bb31d1434b8832 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs @@ -101,6 +101,13 @@ public interface IExecutionManager : IContract List GetExceptionClauses(CodeBlockHandle codeInfoHandle) => throw new NotImplementedException(); JitManagerInfo GetEEJitManagerInfo() => throw new NotImplementedException(); IEnumerable GetCodeHeapInfos() => throw new NotImplementedException(); + + /// + /// Finds the R2R module that contains the given address. + /// Used by FindGCRefMap to resolve m_pZapModule when it's null. + /// Matches native ExecutionManager::FindReadyToRunModule (codeman.cpp). + /// + TargetPointer FindReadyToRunModule(TargetPointer address) => throw new NotImplementedException(); } public readonly struct ExecutionManager : IExecutionManager diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs index d94ed45048b256..70ab5217cd7976 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs @@ -2,10 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; namespace Microsoft.Diagnostics.DataContractReader.Contracts; public interface IGCInfoHandle { } +public readonly record struct InterruptibleRange(uint StartOffset, uint EndOffset); public interface IGCInfo : IContract { @@ -14,6 +16,7 @@ public interface IGCInfo : IContract IGCInfoHandle DecodePlatformSpecificGCInfo(TargetPointer gcInfoAddress, uint gcVersion) => throw new NotImplementedException(); IGCInfoHandle DecodeInterpreterGCInfo(TargetPointer gcInfoAddress, uint gcVersion) => throw new NotImplementedException(); uint GetCodeLength(IGCInfoHandle handle) => throw new NotImplementedException(); + IReadOnlyList GetInterruptibleRanges(IGCInfoHandle handle) => throw new NotImplementedException(); } public readonly struct GCInfo : IGCInfo diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs index 8cc74fc5d1ee72..7f700a0c479367 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs @@ -179,6 +179,14 @@ public interface IRuntimeTypeSystem : IContract bool IsGenericMethodDefinition(MethodDescHandle methodDesc) => throw new NotImplementedException(); ReadOnlySpan GetGenericMethodInstantiation(MethodDescHandle methodDesc) => throw new NotImplementedException(); + // Return true if the method requires a hidden instantiation argument (generic context parameter). + // This corresponds to native MethodDesc::RequiresInstArg(). + bool RequiresInstArg(MethodDescHandle methodDesc) => throw new NotImplementedException(); + + // Return true if the method uses the async calling convention (CORINFO_CALLCONV_ASYNCCALL). + // This corresponds to native MethodDesc::IsAsyncMethod(). + bool IsAsyncMethod(MethodDescHandle methodDesc) => throw new NotImplementedException(); + // Return mdtMethodDef (0x06000000) if the method doesn't have a token, otherwise return the token of the method uint GetMethodToken(MethodDescHandle methodDesc) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs index 989db1625b33f9..db776c50a53057 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -160,6 +160,9 @@ public enum DataType HijackFrame, TailCallFrame, StubDispatchFrame, + ExternalMethodFrame, + DynamicHelperFrame, + ComCallWrapper, SimpleComCallWrapper, ComMethodTable, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs index 43e89fbe2237e7..47f629ffd5700b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs @@ -390,6 +390,19 @@ TargetNUInt IExecutionManager.GetRelativeOffset(CodeBlockHandle codeInfoHandle) return info.RelativeOffset; } + TargetPointer IExecutionManager.FindReadyToRunModule(TargetPointer address) + { + // Use the range section map to find the RangeSection containing the address. + // The R2R range section covers the entire PE image (code + data), so this + // works for import section addresses used by FindGCRefMap. + TargetCodePointer codeAddr = CodePointerUtils.CodePointerFromAddress(address, _target); + RangeSection range = RangeSection.Find(_target, _topRangeSectionMap, _rangeSectionMapLookup, codeAddr); + if (range.Data is null) + return TargetPointer.Null; + + return range.Data.R2RModule; + } + JitManagerInfo IExecutionManager.GetEEJitManagerInfo() { TargetPointer eeJitManagerPtr = _target.ReadGlobalPointer(Constants.Globals.EEJitManagerAddress); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs index b636a2914c36ec..c082eb9ccdc969 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs @@ -34,5 +34,6 @@ internal ExecutionManager_1(Target target) public List GetExceptionClauses(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetExceptionClauses(codeInfoHandle); public JitManagerInfo GetEEJitManagerInfo() => _executionManagerCore.GetEEJitManagerInfo(); public IEnumerable GetCodeHeapInfos() => _executionManagerCore.GetCodeHeapInfos(); + public TargetPointer FindReadyToRunModule(TargetPointer address) => _executionManagerCore.FindReadyToRunModule(address); public void Flush() => _executionManagerCore.Flush(); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs index 6b84fda982ab5e..da5b1d6dc71f93 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs @@ -34,5 +34,6 @@ internal ExecutionManager_2(Target target) public List GetExceptionClauses(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetExceptionClauses(codeInfoHandle); public JitManagerInfo GetEEJitManagerInfo() => _executionManagerCore.GetEEJitManagerInfo(); public IEnumerable GetCodeHeapInfos() => _executionManagerCore.GetCodeHeapInfos(); + public TargetPointer FindReadyToRunModule(TargetPointer address) => _executionManagerCore.FindReadyToRunModule(address); public void Flush() => _executionManagerCore.Flush(); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs index d6a6a0da8b39f4..20a300df238ec7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs @@ -68,8 +68,6 @@ internal enum GcStackSlotBase : uint GC_SPBASE_LAST = GC_FRAMEREG_REL, } - public readonly record struct InterruptibleRange(uint StartOffset, uint EndOffset); - public readonly record struct GcSlotDesc { /* Register Slot */ @@ -569,7 +567,7 @@ public bool EnumerateLiveSlots( uint numTracked = NumTrackedSlots; if (numTracked == 0) - goto ReportUntracked; + return ReportUntrackedAndSucceed(); uint normBreakOffset = TTraits.NormalizeCodeOffset(instructionOffset); @@ -655,7 +653,7 @@ public bool EnumerateLiveSlots( fReport = !fReport; } Debug.Assert(readSlots == numTracked); - goto ReportUntracked; + return ReportUntrackedAndSucceed(); } // Normal 1-bit-per-slot encoding follows } @@ -669,7 +667,7 @@ public bool EnumerateLiveSlots( if (_reader.ReadBits(1, ref bitOffset) != 0) ReportSlot(slotIndex, reportScratchSlots, reportFpBasedSlotsOnly, reportSlot); } - goto ReportUntracked; + return ReportUntrackedAndSucceed(); } else { @@ -682,7 +680,7 @@ public bool EnumerateLiveSlots( bitOffset += (int)(_numSafePoints * numTracked); if (_numInterruptibleRanges == 0) - goto ReportUntracked; + return ReportUntrackedAndSucceed(); } // ---- Fully-interruptible path ---- @@ -695,7 +693,7 @@ public bool EnumerateLiveSlots( uint numBitsPerPointer = (uint)_reader.DecodeVarLengthUnsigned(TTraits.POINTER_SIZE_ENCBASE, ref bitOffset); if (numBitsPerPointer == 0) - goto ReportUntracked; + return ReportUntrackedAndSucceed(); int pointerTablePos = bitOffset; @@ -709,7 +707,7 @@ public bool EnumerateLiveSlots( if (chunkPointer != 0) break; if (chunk-- == 0) - goto ReportUntracked; + return ReportUntrackedAndSucceed(); } int chunksStartPos = (int)(((uint)pointerTablePos + numChunks * numBitsPerPointer + 7) & (~7u)); @@ -815,14 +813,22 @@ public bool EnumerateLiveSlots( } } - ReportUntracked: - if (_numUntrackedSlots > 0 && (flags & (CodeManagerFlags.ParentOfFuncletStackFrame | CodeManagerFlags.NoReportUntracked)) == 0) + return ReportUntrackedAndSucceed(); + + bool ReportUntrackedAndSucceed() { - for (uint slotIndex = numTracked; slotIndex < _numSlots; slotIndex++) - ReportSlot(slotIndex, reportScratchSlots, reportFpBasedSlotsOnly, reportSlot); + if (_numUntrackedSlots > 0 && (flags & (CodeManagerFlags.ParentOfFuncletStackFrame | CodeManagerFlags.NoReportUntracked)) == 0) + { + // Native passes reportScratchSlots=true for untracked slots (see native + // ReportUntrackedSlots: "Report everything (although there should *never* + // be any scratch slots that are untracked)"). In practice the JIT can + // produce untracked scratch register slots for interior pointers, so they + // must be reported regardless of whether this is a leaf frame. + for (uint slotIndex = numTracked; slotIndex < _numSlots; slotIndex++) + ReportSlot(slotIndex, reportScratchSlots: true, reportFpBasedSlotsOnly, reportSlot); + } + return true; } - - return true; } private void ReportSlot(uint slotIndex, bool reportScratchSlots, bool reportFpBasedSlotsOnly, Action reportSlot) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs index f34292572a936e..9e6ce0252128bd 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; namespace Microsoft.Diagnostics.DataContractReader.Contracts; @@ -27,6 +28,12 @@ uint IGCInfo.GetCodeLength(IGCInfoHandle gcInfoHandle) return handle.GetCodeLength(); } + IReadOnlyList IGCInfo.GetInterruptibleRanges(IGCInfoHandle gcInfoHandle) + { + IGCInfoDecoder handle = AssertCorrectHandle(gcInfoHandle); + return handle.GetInterruptibleRanges(); + } + private static IGCInfoDecoder AssertCorrectHandle(IGCInfoHandle gcInfoHandle) { if (gcInfoHandle is not IGCInfoDecoder handle) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs index 86f4210a7cb91d..e21bc79661062f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; @@ -24,6 +26,11 @@ internal interface IGCInfoDecoder : IGCInfoHandle uint GetCodeLength(); uint StackBaseRegister { get; } + /// + /// Gets the interruptible code ranges decoded from the GC info. + /// + IReadOnlyList GetInterruptibleRanges(); + /// /// Enumerates all live GC slots at the given instruction offset. /// diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index 27e971fc0cd615..72b46fc4813ade 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -155,6 +155,7 @@ internal enum DynamicMethodDescExtendedFlags : uint internal enum AsyncMethodFlags : uint { None = 0, + AsyncCall = 0x1, Thunk = 16, } @@ -1251,6 +1252,99 @@ public ReadOnlySpan GetGenericMethodInstantiation(MethodDescHandle m return AsInstantiatedMethodDesc(methodDesc).Instantiation; } + /// + /// Returns true if the method requires a hidden instantiation argument (generic context parameter). + /// Matches native MethodDesc::RequiresInstArg(). + /// + public bool RequiresInstArg(MethodDescHandle methodDescHandle) + { + MethodDesc methodDesc = _methodDescs[methodDescHandle.Address]; + + // RequiresInstArg = IsSharedByGenericInstantiations && (HasMethodInstantiation || IsStatic || IsValueType || IsInterface) + if (!IsSharedByGenericInstantiations(methodDesc)) + return false; + + if (HasMethodInstantiation(methodDesc)) + return true; + + MethodTable mt = _methodTables[methodDesc.MethodTable]; + if (mt.Flags.IsInterface) + return true; + + if (mt.Flags.IsValueType) + return true; + + if (IsStaticMethod(methodDesc)) + return true; + + return false; + } + + /// + /// Matches native MethodDesc::IsStatic(). + /// + private bool IsStaticMethod(MethodDesc methodDesc) + { + try + { + uint token = methodDesc.Token; + if (token != 0x06000000) + { + TypeHandle typeHandle = GetTypeHandle(methodDesc.MethodTable); + TargetPointer modulePtr = GetModule(typeHandle); + ILoader loader = _target.Contracts.Loader; + ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(modulePtr); + MetadataReader? mdReader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); + if (mdReader is not null) + { + MethodDefinitionHandle methodDefHandle = + MetadataTokens.MethodDefinitionHandle((int)(token & 0x00FFFFFF)); + MethodDefinition methodDef = mdReader.GetMethodDefinition(methodDefHandle); + return (methodDef.Attributes & MethodAttributes.Static) != 0; + } + } + } + catch + { + } + + return false; + } + + private bool IsSharedByGenericInstantiations(MethodDesc methodDesc) + { + // Check method-level sharing: InstantiatedMethodDesc with SharedMethodInstantiation + if (methodDesc.Classification == MethodClassification.Instantiated) + { + InstantiatedMethodDesc imd = AsInstantiatedMethodDesc(methodDesc); + if (imd.IsWrapperStubWithInstantiations) + return false; + + // Check SharedMethodInstantiation flag + Data.InstantiatedMethodDesc imdData = _target.ProcessedData.GetOrAdd(methodDesc.Address); + if ((imdData.Flags2 & (ushort)InstantiatedMethodDescFlags2.KindMask) + == (ushort)InstantiatedMethodDescFlags2.SharedMethodInstantiation) + return true; + } + + // Check class-level sharing: canonical MethodTable with generic instantiation + MethodTable mt = _methodTables[methodDesc.MethodTable]; + return mt.IsCanonMT && mt.Flags.HasInstantiation; + } + + public bool IsAsyncMethod(MethodDescHandle methodDescHandle) + { + MethodDesc methodDesc = _methodDescs[methodDescHandle.Address]; + if (!methodDesc.HasAsyncMethodData) + return false; + + // AsyncMethodData is the last optional slot, placed after NativeCodeSlot. + // Read the AsyncMethodFlags (first field) and check for AsyncCall. + TargetPointer asyncDataAddr = methodDesc.GetAddressOfAsyncMethodData(); + uint asyncFlags = _target.Read(asyncDataAddr); + return (asyncFlags & (uint)AsyncMethodFlags.AsyncCall) != 0; + } + public uint GetMethodToken(MethodDescHandle methodDescHandle) { MethodDesc methodDesc = _methodDescs[methodDescHandle.Address]; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs index 4edd821c203dc9..62954af93c4e4e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using Microsoft.Diagnostics.DataContractReader.Data; namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; @@ -132,20 +135,81 @@ public void UpdateContextFromFrame(IPlatformAgnosticContext context) } } - public bool IsInlineCallFrameWithActiveCall() + /// + /// Returns the return address for the current Frame, matching native Frame::GetReturnAddress(). + /// Returns TargetPointer.Null if the Frame has no return address (e.g., non-active ICF, + /// base Frame types, FuncEvalFrame during exception eval). + /// + public TargetPointer GetReturnAddress() { - if (GetFrameType(target, CurrentFrame.Identifier) != FrameType.InlinedCallFrame) + FrameType frameType = GetCurrentFrameType(); + switch (frameType) { - return false; - } - Data.InlinedCallFrame inlinedCallFrame = target.ProcessedData.GetOrAdd(currentFramePointer); - return InlinedCallFrameHasActiveCall(inlinedCallFrame); - } + // InlinedCallFrame: returns 0 if inactive, else m_pCallerReturnAddress + case FrameType.InlinedCallFrame: + Data.InlinedCallFrame icf = target.ProcessedData.GetOrAdd(currentFramePointer); + return InlinedCallFrameHasActiveCall(icf) ? new TargetPointer(icf.CallerReturnAddress) : TargetPointer.Null; - public static bool IsInlinedCallFrame(Target target, TargetPointer framePointer) - { - Data.Frame frame = target.ProcessedData.GetOrAdd(framePointer); - return GetFrameType(target, frame.Identifier) == FrameType.InlinedCallFrame; + // TransitionFrame types: read return address from the transition block + case FrameType.FramedMethodFrame: + case FrameType.PInvokeCalliFrame: + case FrameType.PrestubMethodFrame: + case FrameType.StubDispatchFrame: + case FrameType.CallCountingHelperFrame: + case FrameType.ExternalMethodFrame: + case FrameType.DynamicHelperFrame: + Data.FramedMethodFrame fmf = target.ProcessedData.GetOrAdd(currentFramePointer); + Data.TransitionBlock tb = target.ProcessedData.GetOrAdd(fmf.TransitionBlockPtr); + return tb.ReturnAddress; + + // SoftwareExceptionFrame: stored m_ReturnAddress + case FrameType.SoftwareExceptionFrame: + Data.SoftwareExceptionFrame sef = target.ProcessedData.GetOrAdd(currentFramePointer); + return sef.ReturnAddress; + + // ResumableFrame / RedirectedThreadFrame: RIP from captured context + case FrameType.ResumableFrame: + case FrameType.RedirectedThreadFrame: + { + Data.ResumableFrame rf = target.ProcessedData.GetOrAdd(currentFramePointer); + IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(target); + ctx.ReadFromAddress(target, rf.TargetContextPtr); + return ctx.InstructionPointer; + } + + // FaultingExceptionFrame: RIP from embedded context + case FrameType.FaultingExceptionFrame: + { + Data.FaultingExceptionFrame fef = target.ProcessedData.GetOrAdd(currentFramePointer); + IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(target); + ctx.ReadFromAddress(target, fef.TargetContext); + return ctx.InstructionPointer; + } + + // HijackFrame: stored m_ReturnAddress + case FrameType.HijackFrame: + Data.HijackFrame hf = target.ProcessedData.GetOrAdd(currentFramePointer); + return hf.ReturnAddress; + + // TailCallFrame: stored m_ReturnAddress + case FrameType.TailCallFrame: + Data.TailCallFrame tcf = target.ProcessedData.GetOrAdd(currentFramePointer); + return tcf.ReturnAddress; + + // FuncEvalFrame: returns 0 during exception eval, else from transition block + case FrameType.FuncEvalFrame: + Data.FuncEvalFrame funcEval = target.ProcessedData.GetOrAdd(currentFramePointer); + Data.DebuggerEval dbgEval = target.ProcessedData.GetOrAdd(funcEval.DebuggerEvalPtr); + if (dbgEval.EvalDuringException) + return TargetPointer.Null; + Data.FramedMethodFrame funcEvalFmf = target.ProcessedData.GetOrAdd(currentFramePointer); + Data.TransitionBlock funcEvalTb = target.ProcessedData.GetOrAdd(funcEvalFmf.TransitionBlockPtr); + return funcEvalTb.ReturnAddress; + + // Base Frame and unknown types: return 0 (matches native Frame::GetReturnAddressPtr_Impl) + default: + return TargetPointer.Null; + } } public static string GetFrameName(Target target, TargetPointer frameIdentifier) @@ -160,7 +224,7 @@ public static string GetFrameName(Target target, TargetPointer frameIdentifier) public FrameType GetCurrentFrameType() => GetFrameType(target, CurrentFrame.Identifier); - private static FrameType GetFrameType(Target target, TargetPointer frameIdentifier) + internal static FrameType GetFrameType(Target target, TargetPointer frameIdentifier) { foreach (FrameType frameType in Enum.GetValues()) { @@ -233,35 +297,455 @@ public static TargetPointer GetMethodDescPtr(Target target, TargetPointer frameP } } - public static TargetPointer GetReturnAddress(Target target, TargetPointer framePtr) + private static bool InlinedCallFrameHasFunction(Data.InlinedCallFrame frame, Target target) { - Data.Frame frame = target.ProcessedData.GetOrAdd(framePtr); - FrameType frameType = GetFrameType(target, frame.Identifier); + if (target.PointerSize == sizeof(ulong)) + { + return frame.Datum != TargetPointer.Null && (frame.Datum.Value & 0x1) == 0; + } + else + { + return ((long)frame.Datum.Value & ~0xffff) != 0; + } + } + + private static bool InlinedCallFrameHasActiveCall(Data.InlinedCallFrame frame) + { + return frame.CallerReturnAddress != TargetPointer.Null; + } + + // ===== Frame GC Root Scanning ===== + + /// + /// Scans GC roots for a Frame based on its type. + /// Dispatches to the appropriate scanning method (GCRefMap, MetaSig, or custom). + /// Matches native Frame::GcScanRoots_Impl virtual dispatch. + /// + internal void GcScanRoots(TargetPointer frameAddress, GcScanContext scanContext) + { + if (frameAddress == TargetPointer.Null) + return; + + Data.Frame frameData = target.ProcessedData.GetOrAdd(frameAddress); + FrameType frameType = GetFrameType(target, frameData.Identifier); + switch (frameType) { - case FrameType.InlinedCallFrame: - Data.InlinedCallFrame inlinedCallFrame = target.ProcessedData.GetOrAdd(frame.Address); - return InlinedCallFrameHasActiveCall(inlinedCallFrame) ? inlinedCallFrame.CallerReturnAddress : TargetPointer.Null; + case FrameType.StubDispatchFrame: + { + Data.FramedMethodFrame fmf = target.ProcessedData.GetOrAdd(frameAddress); + Data.StubDispatchFrame sdf = target.ProcessedData.GetOrAdd(frameAddress); + TargetPointer gcRefMap = sdf.GCRefMap; + + // Resolve GCRefMap via indirection if not yet cached + if (gcRefMap == TargetPointer.Null && sdf.Indirection != TargetPointer.Null) + gcRefMap = FindGCRefMap(sdf.ZapModule, sdf.Indirection); + + if (gcRefMap != TargetPointer.Null) + PromoteCallerStackUsingGCRefMap(fmf.TransitionBlockPtr, gcRefMap, scanContext); + else + PromoteCallerStack(frameAddress, fmf.TransitionBlockPtr, scanContext); + break; + } + + case FrameType.ExternalMethodFrame: + { + Data.FramedMethodFrame fmf = target.ProcessedData.GetOrAdd(frameAddress); + Data.ExternalMethodFrame emf = target.ProcessedData.GetOrAdd(frameAddress); + TargetPointer gcRefMap = emf.GCRefMap; + + // Resolve GCRefMap via FindGCRefMap if not yet cached by the runtime + if (gcRefMap == TargetPointer.Null && emf.Indirection != TargetPointer.Null) + gcRefMap = FindGCRefMap(emf.ZapModule, emf.Indirection); + + if (gcRefMap != TargetPointer.Null) + PromoteCallerStackUsingGCRefMap(fmf.TransitionBlockPtr, gcRefMap, scanContext); + else + PromoteCallerStack(frameAddress, fmf.TransitionBlockPtr, scanContext); + break; + } + + case FrameType.DynamicHelperFrame: + { + Data.FramedMethodFrame fmf = target.ProcessedData.GetOrAdd(frameAddress); + Data.DynamicHelperFrame dhf = target.ProcessedData.GetOrAdd(frameAddress); + ScanDynamicHelperFrame(fmf.TransitionBlockPtr, dhf.DynamicHelperFrameFlags, scanContext); + break; + } + + case FrameType.CallCountingHelperFrame: + case FrameType.PrestubMethodFrame: + { + Data.FramedMethodFrame fmf = target.ProcessedData.GetOrAdd(frameAddress); + PromoteCallerStack(frameAddress, fmf.TransitionBlockPtr, scanContext); + break; + } + + case FrameType.HijackFrame: + // TODO(stackref): Implement HijackFrame scanning (X86 only with FEATURE_HIJACK) + break; + + case FrameType.ProtectValueClassFrame: + // TODO(stackref): Implement ProtectValueClassFrame scanning + break; + default: - // NotImplemented for other frame types + // Base Frame::GcScanRoots_Impl is a no-op for most frame types. + break; + } + } + + /// + /// Decodes a GCRefMap bitstream and reports GC references in the transition block. + /// Port of native TransitionFrame::PromoteCallerStackUsingGCRefMap (frames.cpp). + /// + private void PromoteCallerStackUsingGCRefMap( + TargetPointer transitionBlock, + TargetPointer gcRefMapBlob, + GcScanContext scanContext) + { + GCRefMapDecoder decoder = new(target, gcRefMapBlob); + + if (target.PointerSize == 4) + decoder.ReadStackPop(); + + while (!decoder.AtEnd) + { + int pos = decoder.CurrentPos; + GCRefMapToken token = decoder.ReadToken(); + uint offset = OffsetFromGCRefMapPos(pos); + TargetPointer slotAddress = new(transitionBlock.Value + offset); + + switch (token) + { + case GCRefMapToken.Skip: + break; + case GCRefMapToken.Ref: + scanContext.GCReportCallback(slotAddress, GcScanFlags.None); + break; + case GCRefMapToken.Interior: + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + break; + case GCRefMapToken.MethodParam: + case GCRefMapToken.TypeParam: + break; + case GCRefMapToken.VASigCookie: + // TODO(stackref): Implement VASIG_COOKIE handling + break; + } + } + } + + /// + /// Scans GC roots for a DynamicHelperFrame based on its flags. + /// Port of native DynamicHelperFrame::GcScanRoots_Impl (frames.cpp). + /// + private void ScanDynamicHelperFrame( + TargetPointer transitionBlock, + int dynamicHelperFrameFlags, + GcScanContext scanContext) + { + const int DynamicHelperFrameFlags_ObjectArg = 1; + const int DynamicHelperFrameFlags_ObjectArg2 = 2; + + Target.TypeInfo tbType = target.GetTypeInfo(DataType.TransitionBlock); + uint argRegOffset = (uint)tbType.Fields[nameof(Data.TransitionBlock.ArgumentRegistersOffset)].Offset; + + if ((dynamicHelperFrameFlags & DynamicHelperFrameFlags_ObjectArg) != 0) + { + TargetPointer argAddr = new(transitionBlock.Value + argRegOffset); + scanContext.GCReportCallback(argAddr, GcScanFlags.None); + } + + if ((dynamicHelperFrameFlags & DynamicHelperFrameFlags_ObjectArg2) != 0) + { + TargetPointer argAddr = new(transitionBlock.Value + argRegOffset + (uint)target.PointerSize); + scanContext.GCReportCallback(argAddr, GcScanFlags.None); + } + } + + /// + /// Resolves the GCRefMap for a Frame with m_pIndirection set but m_pGCRefMap not yet cached. + /// Port of native FindGCRefMap (frames.cpp:853). + /// + private TargetPointer FindGCRefMap(TargetPointer zapModule, TargetPointer indirection) + { + if (indirection == TargetPointer.Null) + return TargetPointer.Null; + + // If ZapModule is null, resolve it from the indirection address. + // Matches native GetGCRefMap which calls FindModuleForGCRefMap(m_pIndirection) + // → ExecutionManager::FindReadyToRunModule. + if (zapModule == TargetPointer.Null) + { + IExecutionManager eman = target.Contracts.ExecutionManager; + zapModule = eman.FindReadyToRunModule(indirection); + if (zapModule == TargetPointer.Null) return TargetPointer.Null; } + + // Get the ReadyToRunInfo from the module + Data.Module module = target.ProcessedData.GetOrAdd(zapModule); + if (module.ReadyToRunInfo == TargetPointer.Null) + return TargetPointer.Null; + + Data.ReadyToRunInfo r2rInfo = target.ProcessedData.GetOrAdd(module.ReadyToRunInfo); + if (r2rInfo.ImportSections == TargetPointer.Null || r2rInfo.NumImportSections == 0) + return TargetPointer.Null; + + // Compute RVA = indirection - imageBase + ulong imageBase = r2rInfo.LoadedImageBase.Value; + if (indirection.Value < imageBase) + return TargetPointer.Null; + ulong diff = indirection.Value - imageBase; + if (diff > uint.MaxValue) + return TargetPointer.Null; + uint rva = (uint)diff; + + // READYTORUN_IMPORT_SECTION layout: + // IMAGE_DATA_DIRECTORY Section (VirtualAddress:4, Size:4) = 8 bytes + // ReadyToRunImportSectionFlags Flags (2 bytes) + // ReadyToRunImportSectionType Type (1 byte) + // BYTE EntrySize (1 byte) + // DWORD Signatures (4 bytes) + // DWORD AuxiliaryData (4 bytes) + // Total: 20 bytes + const int ImportSectionSize = 20; + const int SectionVAOffset = 0; + const int SectionSizeOffset = 4; + const int EntrySizeOffset = 11; + const int AuxiliaryDataOffset = 16; + + TargetPointer sectionsBase = r2rInfo.ImportSections; + for (uint i = 0; i < r2rInfo.NumImportSections; i++) + { + TargetPointer sectionAddr = new(sectionsBase.Value + i * ImportSectionSize); + uint sectionVA = target.Read(sectionAddr + SectionVAOffset); + uint sectionSize = target.Read(sectionAddr + SectionSizeOffset); + + if (rva >= sectionVA && rva < sectionVA + sectionSize) + { + byte entrySize = target.Read(sectionAddr + EntrySizeOffset); + if (entrySize == 0) + return TargetPointer.Null; + + uint index = (rva - sectionVA) / entrySize; + uint auxDataRva = target.Read(sectionAddr + AuxiliaryDataOffset); + if (auxDataRva == 0) + return TargetPointer.Null; + + TargetPointer gcRefMapBase = new(imageBase + auxDataRva); + + // GCRefMap starts with a lookup index for stride-based access. + // GCREFMAP_LOOKUP_STRIDE is 1024 in the native code. + const uint GCREFMAP_LOOKUP_STRIDE = 1024; + uint lookupIndex = index / GCREFMAP_LOOKUP_STRIDE; + uint remaining = index % GCREFMAP_LOOKUP_STRIDE; + + // Read the offset from the lookup table (array of DWORDs) + uint lookupOffset = target.Read(new TargetPointer(gcRefMapBase.Value + lookupIndex * 4)); + TargetPointer p = new(gcRefMapBase.Value + lookupOffset); + + // Linear scan past 'remaining' entries + while (remaining > 0) + { + // Each entry is a variable-length sequence of bytes where the high bit + // indicates continuation. Skip until we find a byte without the high bit set. + while ((target.Read(p) & 0x80) != 0) + p = new(p.Value + 1); + p = new(p.Value + 1); // skip the final byte of this entry + + remaining--; + } + + return p; + } + } + + return TargetPointer.Null; } - private static bool InlinedCallFrameHasFunction(Data.InlinedCallFrame frame, Target target) + /// + /// Entry point for promoting caller stack GC references via method signature. + /// Matches native TransitionFrame::PromoteCallerStack (frames.cpp:1494). + /// + private void PromoteCallerStack( + TargetPointer frameAddress, + TargetPointer transitionBlock, + GcScanContext scanContext) { - if (target.PointerSize == sizeof(ulong)) + Data.FramedMethodFrame fmf = target.ProcessedData.GetOrAdd(frameAddress); + TargetPointer methodDescPtr = fmf.MethodDescPtr; + if (methodDescPtr == TargetPointer.Null) + return; + + ReadOnlySpan signature; + try { - return frame.Datum != TargetPointer.Null && (frame.Datum.Value & 0x1) == 0; + signature = GetMethodSignatureBytes(methodDescPtr); } - else + catch (System.Exception) { - return ((long)frame.Datum.Value & ~0xffff) != 0; + return; } + + if (signature.IsEmpty) + return; + + MethodSignature methodSig; + try + { + unsafe + { + fixed (byte* pSig = signature) + { + BlobReader blobReader = new(pSig, signature.Length); + SignatureDecoder decoder = new( + GcSignatureTypeProvider.Instance, metadataReader: null!, genericContext: null); + methodSig = decoder.DecodeMethodSignature(ref blobReader); + } + } + } + catch (System.Exception) + { + // If signature decoding fails (e.g., ELEMENT_TYPE_INTERNAL), skip this frame. + // The GCRefMap path handles these cases when available. + return; + } + + if (methodSig.Header.CallingConvention is SignatureCallingConvention.VarArgs) + { + // TODO(stackref): VarArg path — read VASigCookie from frame + return; + } + + IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + MethodDescHandle mdh = rts.GetMethodDescHandle(methodDescPtr); + + bool hasThis = methodSig.Header.IsInstance; + bool hasRetBuf = methodSig.ReturnType is GcTypeKind.Other; + bool requiresInstArg = false; + bool isAsync = false; + bool isValueTypeThis = false; + + try + { + requiresInstArg = rts.RequiresInstArg(mdh); + isAsync = rts.IsAsyncMethod(mdh); + + // TODO(stackref): Detect value type 'this' (needs IRuntimeTypeSystem.IsValueType) + // TODO(stackref): String constructor clears HasThis + } + catch + { + } + + PromoteCallerStackHelper(transitionBlock, methodSig, hasThis, hasRetBuf, + requiresInstArg, isAsync, isValueTypeThis, scanContext); } - private static bool InlinedCallFrameHasActiveCall(Data.InlinedCallFrame frame) + /// + /// Core logic for promoting caller stack GC references. + /// Matches native TransitionFrame::PromoteCallerStackHelper (frames.cpp:1560). + /// + private void PromoteCallerStackHelper( + TargetPointer transitionBlock, + MethodSignature methodSig, + bool hasThis, + bool hasRetBuf, + bool requiresInstArg, + bool isAsync, + bool isValueTypeThis, + GcScanContext scanContext) { - return frame.CallerReturnAddress != TargetPointer.Null; + int numRegistersUsed = 0; + if (hasThis) + numRegistersUsed++; + if (hasRetBuf) + numRegistersUsed++; + if (requiresInstArg) + numRegistersUsed++; + if (isAsync) + numRegistersUsed++; + + bool isArm64 = IsTargetArm64(); + if (isArm64) + numRegistersUsed++; + + if (hasThis) + { + int thisPos = isArm64 ? 1 : 0; + uint thisOffset = OffsetFromGCRefMapPos(thisPos); + TargetPointer thisAddr = new(transitionBlock.Value + thisOffset); + GcScanFlags thisFlags = isValueTypeThis ? GcScanFlags.GC_CALL_INTERIOR : GcScanFlags.None; + scanContext.GCReportCallback(thisAddr, thisFlags); + } + + // TODO(stackref): Promote async continuation pointer at its specific offset + + int pos = numRegistersUsed; + foreach (GcTypeKind kind in methodSig.ParameterTypes) + { + uint offset = OffsetFromGCRefMapPos(pos); + TargetPointer slotAddress = new(transitionBlock.Value + offset); + + switch (kind) + { + case GcTypeKind.Ref: + scanContext.GCReportCallback(slotAddress, GcScanFlags.None); + break; + case GcTypeKind.Interior: + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + break; + case GcTypeKind.Other: + // TODO(stackref): Value type GCDesc scanning + break; + case GcTypeKind.None: + break; + } + pos++; + } + } + + private ReadOnlySpan GetMethodSignatureBytes(TargetPointer methodDescPtr) + { + IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + MethodDescHandle mdh = rts.GetMethodDescHandle(methodDescPtr); + + if (rts.IsStoredSigMethodDesc(mdh, out ReadOnlySpan storedSig)) + return storedSig; + + uint methodToken = rts.GetMethodToken(mdh); + if (methodToken == 0x06000000) + return default; + + TargetPointer methodTablePtr = rts.GetMethodTable(mdh); + TypeHandle typeHandle = rts.GetTypeHandle(methodTablePtr); + TargetPointer modulePtr = rts.GetModule(typeHandle); + + ILoader loader = target.Contracts.Loader; + ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(modulePtr); + + IEcmaMetadata ecmaMetadata = target.Contracts.EcmaMetadata; + MetadataReader? mdReader = ecmaMetadata.GetMetadata(moduleHandle); + if (mdReader is null) + return default; + + MethodDefinitionHandle methodDefHandle = MetadataTokens.MethodDefinitionHandle((int)(methodToken & 0x00FFFFFF)); + MethodDefinition methodDef = mdReader.GetMethodDefinition(methodDefHandle); + BlobReader blobReader = mdReader.GetBlobReader(methodDef.Signature); + return blobReader.ReadBytes(blobReader.Length); + } + + private uint OffsetFromGCRefMapPos(int pos) + { + Target.TypeInfo tbType = target.GetTypeInfo(DataType.TransitionBlock); + uint firstSlotOffset = (uint)tbType.Fields[nameof(Data.TransitionBlock.FirstGCRefMapSlot)].Offset; + return firstSlotOffset + (uint)(pos * target.PointerSize); + } + + private bool IsTargetArm64() + { + return target.Contracts.RuntimeInfo.GetTargetArchitecture() is RuntimeInfoArchitecture.Arm64; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs new file mode 100644 index 00000000000000..6815878ec65c86 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.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. + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +/// +/// Token values from CORCOMPILE_GCREFMAP_TOKENS (corcompile.h). +/// These indicate the type of GC reference at each transition block slot. +/// +internal enum GCRefMapToken +{ + Skip = 0, + Ref = 1, + Interior = 2, + MethodParam = 3, + TypeParam = 4, + VASigCookie = 5, +} + +/// +/// Managed port of the native GCRefMapDecoder (gcrefmap.h). +/// +/// A GCRefMap is a compact bitstream that describes which transition block slots +/// contain GC references for a given call site (e.g., in ReadyToRun stubs). +/// It is used by ExternalMethodFrame and StubDispatchFrame to report GC roots +/// without needing the full MethodDesc/signature decoding path. +/// +/// Encoding: each slot is encoded as a variable-length integer using 3 bits per +/// token (see ), with a high-bit continuation flag. +/// A "skip" token advances the slot position without reporting. The stream ends +/// when all slots have been consumed (indicated by a zero byte after the last token). +/// +/// The native implementation lives in coreclr/inc/gcrefmap.h (GCRefMapDecoder class). +/// +internal ref struct GCRefMapDecoder +{ + private readonly Target _target; + private TargetPointer _currentByte; + private int _pendingByte; + private int _pos; + + public GCRefMapDecoder(Target target, TargetPointer blob) + { + _target = target; + _currentByte = blob; + _pendingByte = 0x80; // Forces first byte read + _pos = 0; + } + + public readonly bool AtEnd => _pendingByte == 0; + + public readonly int CurrentPos => _pos; + + private int GetBit() + { + int x = _pendingByte; + if ((x & 0x80) != 0) + { + x = _target.Read(_currentByte); + _currentByte = new TargetPointer(_currentByte.Value + 1); + x |= (x & 0x80) << 7; + } + _pendingByte = x >> 1; + return x & 1; + } + + private int GetTwoBit() + { + int result = GetBit(); + result |= GetBit() << 1; + return result; + } + + private int GetInt() + { + int result = 0; + int bit = 0; + do + { + result |= GetBit() << (bit++); + result |= GetBit() << (bit++); + result |= GetBit() << (bit++); + } + while (GetBit() != 0); + return result; + } + + /// + /// x86 only: Read the stack pop count from the stream. + /// + public uint ReadStackPop() + { + int x = GetTwoBit(); + if (x == 3) + x = GetInt() + 3; + return (uint)x; + } + + /// + /// Read the next GC reference token from the stream. + /// Advances CurrentPos as appropriate. + /// + public GCRefMapToken ReadToken() + { + int val = GetTwoBit(); + if (val == 3) + { + int ext = GetInt(); + if ((ext & 1) == 0) + { + _pos += (ext >> 1) + 4; + return GCRefMapToken.Skip; + } + else + { + _pos++; + return (GCRefMapToken)((ext >> 1) + 3); + } + } + _pos++; + return (GCRefMapToken)val; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs new file mode 100644 index 00000000000000..4edb08421a317a --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs @@ -0,0 +1,64 @@ +// 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.Immutable; +using System.Reflection.Metadata; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +/// +/// Classification of a signature type for GC scanning purposes. +/// +internal enum GcTypeKind +{ + /// Not a GC reference (primitives, pointers). + None, + /// Object reference (class, string, array). + Ref, + /// Interior pointer (byref). + Interior, + /// Value type that may contain embedded GC references. + Other, +} + +/// +/// Classifies signature types for GC scanning purposes. +/// Implements for use +/// with SRM's . +/// +internal sealed class GcSignatureTypeProvider + : ISignatureTypeProvider +{ + public static readonly GcSignatureTypeProvider Instance = new(); + + public GcTypeKind GetPrimitiveType(PrimitiveTypeCode typeCode) + => typeCode switch + { + PrimitiveTypeCode.String or PrimitiveTypeCode.Object => GcTypeKind.Ref, + PrimitiveTypeCode.TypedReference => GcTypeKind.Other, + _ => GcTypeKind.None, + }; + + public GcTypeKind GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + => rawTypeKind == 0x11 ? GcTypeKind.Other : GcTypeKind.Ref; + + public GcTypeKind GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + => rawTypeKind == 0x11 ? GcTypeKind.Other : GcTypeKind.Ref; + + public GcTypeKind GetTypeFromSpecification(MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind) + => rawTypeKind == 0x11 ? GcTypeKind.Other : GcTypeKind.Ref; + + public GcTypeKind GetSZArrayType(GcTypeKind elementType) => GcTypeKind.Ref; + public GcTypeKind GetArrayType(GcTypeKind elementType, ArrayShape shape) => GcTypeKind.Ref; + public GcTypeKind GetByReferenceType(GcTypeKind elementType) => GcTypeKind.Interior; + public GcTypeKind GetPointerType(GcTypeKind elementType) => GcTypeKind.None; + + public GcTypeKind GetGenericInstantiation(GcTypeKind genericType, ImmutableArray typeArguments) + => genericType; + + public GcTypeKind GetGenericMethodParameter(object? genericContext, int index) => GcTypeKind.Ref; + public GcTypeKind GetGenericTypeParameter(object? genericContext, int index) => GcTypeKind.Ref; + public GcTypeKind GetFunctionPointerType(MethodSignature signature) => GcTypeKind.None; + public GcTypeKind GetModifiedType(GcTypeKind modifier, GcTypeKind unmodifiedType, bool isRequired) => unmodifiedType; + public GcTypeKind GetPinnedType(GcTypeKind elementType) => elementType; +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs index e7a3222806464b..1b4fef66afe058 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs @@ -62,6 +62,19 @@ private class StackWalkData(IPlatformAgnosticContext context, StackWalkState sta // set back to true when encountering a ResumableFrame (FRAME_ATTR_RESUMABLE). public bool IsFirst { get; set; } = true; + // Track isInterrupted like native CrawlFrame::isInterrupted. + // Set in UpdateState when transitioning to SW_FRAMELESS after processing a Frame + // with FRAME_ATTR_EXCEPTION (e.g., FaultingExceptionFrame). When true, the managed + // frame reached via that Frame's return address was interrupted by an exception, + // and EnumGcRefs should use ExecutionAborted to skip live slot reporting at + // non-interruptible offsets. + public bool IsInterrupted { get; set; } + + // The frame type of the last SW_FRAME processed by Next(). + // Used by UpdateState to detect exception frames (FRAME_ATTR_EXCEPTION) and + // set IsInterrupted when transitioning to a managed frame. + public FrameIterator.FrameType? LastProcessedFrameType { get; set; } + public bool IsCurrentFrameResumable() { if (State is not (StackWalkState.SW_FRAME or StackWalkState.SW_SKIPPED_FRAME)) @@ -71,9 +84,10 @@ public bool IsCurrentFrameResumable() // Only frame types with FRAME_ATTR_RESUMABLE set isFirst=true. // FaultingExceptionFrame has FRAME_ATTR_FAULTED (sets hasFaulted) // but NOT FRAME_ATTR_RESUMABLE, so it must not be included here. - // TODO: HijackFrame only has FRAME_ATTR_RESUMABLE on non-x86 platforms. - // When x86 stack walking is supported, this should be conditioned on - // the target architecture. + // Note: HijackFrame only has FRAME_ATTR_RESUMABLE on non-x86 platforms + // (see frames.h). On x86 it uses GcScanRoots_Impl instead of the + // resumable frame pattern. When x86 cDAC stack walking is supported, + // HijackFrame should be conditioned on the target architecture. return ft is FrameIterator.FrameType.ResumableFrame or FrameIterator.FrameType.RedirectedThreadFrame or FrameIterator.FrameType.HijackFrame; @@ -81,9 +95,11 @@ or FrameIterator.FrameType.RedirectedThreadFrame /// /// Update the IsFirst state for the NEXT frame, matching native stackwalk.cpp: - /// - After a frameless frame: isFirst = false (line 2202) - /// - After a ResumableFrame: isFirst = true (line 2235) - /// - After other Frames: isFirst = false (implicit in line 2235 assignment) + /// - After a frameless frame: isFirst = false + /// - After a ResumableFrame: isFirst = true + /// - After other Frames: isFirst = false + /// - After a skipped frame: isFirst unchanged (native never modifies isFirst + /// in the SFITER_SKIPPED_FRAME_FUNCTION path — it keeps the value from Init) /// public void AdvanceIsFirst() { @@ -91,6 +107,14 @@ public void AdvanceIsFirst() { IsFirst = false; } + else if (State == StackWalkState.SW_SKIPPED_FRAME) + { + // Native SFITER_SKIPPED_FRAME_FUNCTION (stackwalk.cpp:2086-2128) does NOT + // modify isFirst. It stays true from Init() so the subsequent managed frame + // gets IsActiveFunc()=true. This is important because skipped frames are + // explicit Frames embedded within the active managed frame (e.g. InlinedCallFrame + // from PInvoke), and the managed frame should still be treated as the leaf. + } else { IsFirst = IsCurrentFrameResumable(); @@ -106,47 +130,12 @@ public StackDataFrameHandle ToDataFrame() } IEnumerable IStackWalk.CreateStackWalk(ThreadData threadData) - => CreateStackWalkCore(threadData, skipInitialFrames: false); - - /// - /// Core stack walk implementation. - /// - /// Thread to walk. - /// - /// When true, pre-advances the FrameIterator past explicit Frames below the initial - /// managed frame's caller SP. This matches the native DacStackReferenceWalker behavior - /// for GC reference enumeration, where these frames are within the current managed - /// frame's stack range and don't contribute additional GC roots. - /// - /// Must be false for ClrDataStackWalk, which advances the cDAC and legacy DAC in - /// lockstep and must yield the same frame sequence (including initial skipped frames). - /// - private IEnumerable CreateStackWalkCore(ThreadData threadData, bool skipInitialFrames) { IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); FillContextFromThread(context, threadData); StackWalkState state = IsManaged(context.InstructionPointer, out _) ? StackWalkState.SW_FRAMELESS : StackWalkState.SW_FRAME; FrameIterator frameIterator = new(_target, threadData); - if (skipInitialFrames) - { - TargetPointer skipBelowSP; - if (state == StackWalkState.SW_FRAMELESS) - { - IPlatformAgnosticContext callerCtx = context.Clone(); - callerCtx.Unwind(_target); - skipBelowSP = callerCtx.StackPointer; - } - else - { - skipBelowSP = context.StackPointer; - } - while (frameIterator.IsValid() && frameIterator.CurrentFrameAddress.Value < skipBelowSP.Value) - { - frameIterator.Next(); - } - } - // if the next Frame is not valid and we are not in managed code, there is nothing to return if (state == StackWalkState.SW_FRAME && !frameIterator.IsValid()) { @@ -158,7 +147,7 @@ private IEnumerable CreateStackWalkCore(ThreadData thread // Mirror native Init() -> ProcessCurrentFrame() -> CheckForSkippedFrames(): // When the initial frame is managed (SW_FRAMELESS), check if there are explicit // Frames below the caller SP that should be reported first. The native walker - // yields skipped frames BEFORE the containing managed frame on non-x86. + // yields skipped frames BEFORE the containing managed frame. if (state == StackWalkState.SW_FRAMELESS && CheckForSkippedFrames(stackWalkData)) { stackWalkData.State = StackWalkState.SW_SKIPPED_FRAME; @@ -176,18 +165,32 @@ private IEnumerable CreateStackWalkCore(ThreadData thread IReadOnlyList IStackWalk.WalkStackReferences(ThreadData threadData) { - IEnumerable stackFrames = CreateStackWalkCore(threadData, skipInitialFrames: true); - IEnumerable frames = stackFrames.Select(AssertCorrectHandle); - IEnumerable gcFrames = Filter(frames); + // Initialize the walk data directly + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); + FillContextFromThread(context, threadData); + StackWalkState state = IsManaged(context.InstructionPointer, out _) ? StackWalkState.SW_FRAMELESS : StackWalkState.SW_FRAME; + FrameIterator frameIterator = new(_target, threadData); + + if (state == StackWalkState.SW_FRAME && !frameIterator.IsValid()) + return []; + + StackWalkData walkData = new(context, state, frameIterator, threadData); + + // Mirror native Init() -> ProcessCurrentFrame() -> CheckForSkippedFrames(): + // When the initial frame is managed (SW_FRAMELESS), check if there are explicit + // Frames below the caller SP that should be reported first. The native walker + // yields skipped frames BEFORE the containing managed frame. + if (walkData.State == StackWalkState.SW_FRAMELESS && CheckForSkippedFrames(walkData)) + walkData.State = StackWalkState.SW_SKIPPED_FRAME; GcScanContext scanContext = new(_target, resolveInteriorPointers: false); - foreach (GCFrameData gcFrame in gcFrames) + // Filter drives Next() directly, matching native Filter()+NextRaw() integration. + // This prevents funclet-to-parent transitions from re-visiting already-walked frames. + foreach (GCFrameData gcFrame in Filter(walkData)) { try { - _ = ((IStackWalk)this).GetMethodDescPtr(gcFrame.Frame); - bool reportGcReferences = gcFrame.ShouldCrawlFrameReportGCReferences; TargetPointer pFrame = ((IStackWalk)this).GetFrameAddress(gcFrame.Frame); @@ -207,22 +210,45 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre ? CodeManagerFlags.ActiveStackFrame : 0; + // If the frame was interrupted by an exception (reached via a + // FaultingExceptionFrame), set ExecutionAborted so the GcInfoDecoder + // skips live slot reporting at non-interruptible offsets. This matches + // native CrawlFrame::GetCodeManagerFlags (stackwalk.h). + if (gcFrame.IsInterrupted) + codeManagerFlags |= CodeManagerFlags.ExecutionAborted; + if (gcFrame.ShouldParentToFuncletSkipReportingGCReferences) codeManagerFlags |= CodeManagerFlags.ParentOfFuncletStackFrame; - // TODO: When ShouldParentFrameUseUnwindTargetPCforGCReporting is set, - // use FindFirstInterruptiblePoint on the catch handler clause range - // to override the relOffset for GC liveness lookup. This mirrors - // native gcenv.ee.common.cpp behavior for catch-handler resumption. + uint? relOffsetOverride = null; + if (gcFrame.ShouldParentFrameUseUnwindTargetPCforGCReporting) + { + _eman.GetGCInfo(cbh.Value, out TargetPointer gcInfoAddr, out uint gcVersion); + IGCInfoHandle gcHandle = _target.Contracts.GCInfo.DecodePlatformSpecificGCInfo(gcInfoAddr, gcVersion); + uint startPC = gcFrame.ClauseForCatchHandlerStartPC; + uint endPC = gcFrame.ClauseForCatchHandlerEndPC; + foreach (var range in _target.Contracts.GCInfo.GetInterruptibleRanges(gcHandle)) + { + if (range.EndOffset <= startPC) + continue; + if (startPC >= range.StartOffset && startPC < range.EndOffset) + { + relOffsetOverride = startPC; + break; + } + if (range.StartOffset < endPC) + { + relOffsetOverride = range.StartOffset; + break; + } + } + } - GcScanner gcScanner = new(_target); - gcScanner.EnumGcRefs(gcFrame.Frame.Context, cbh.Value, codeManagerFlags, scanContext); + EnumGcRefsForManagedFrame(gcFrame.Frame.Context, cbh.Value, codeManagerFlags, scanContext, relOffsetOverride); } else { - // TODO: Frame-based GC root scanning (ScanFrameRoots) not yet implemented. - // Frames that call PromoteCallerStack (StubDispatchFrame, ExternalMethodFrame, - // DynamicHelperFrame, etc.) will be handled in a follow-up PR. + walkData.FrameIter.GcScanRoots(gcFrame.Frame.FrameAddress, scanContext); } } } @@ -260,11 +286,25 @@ public GCFrameData(StackDataFrameHandle frame) public bool ShouldParentToFuncletSkipReportingGCReferences { get; set; } public bool ShouldCrawlFrameReportGCReferences { get; set; } // required public bool ShouldParentFrameUseUnwindTargetPCforGCReporting { get; set; } + public uint ClauseForCatchHandlerStartPC { get; set; } + public uint ClauseForCatchHandlerEndPC { get; set; } + // Set when the frame was reached via an exception Frame (FRAME_ATTR_EXCEPTION). + // Causes ExecutionAborted to be passed to EnumGcRefs. + public bool IsInterrupted { get; set; } } - private IEnumerable Filter(IEnumerable handles) + /// + /// Port of native StackFrameIterator::Filter (GC_FUNCLET_REFERENCE_REPORTING mode). + /// Unlike the previous implementation that passively consumed pre-generated frames, + /// this version drives Next() directly — matching native Filter() which calls NextRaw() + /// internally to skip frames. This prevents funclet-to-parent transitions from + /// re-visiting already-walked frames. + /// +#pragma warning disable IDE0059 // Unnecessary assignment — false positives from goto case + do/while pattern + private IEnumerable Filter(StackWalkData walkData) { - // StackFrameIterator::Filter assuming GC_FUNCLET_REFERENCE_REPORTING is defined + // Process the initial frame, then loop calling Next() for subsequent frames. + // This matches native: Init() produces the first frame, then Filter()+NextRaw() loop. // global tracking variables bool processNonFilterFunclet = false; @@ -272,11 +312,19 @@ private IEnumerable Filter(IEnumerable handle bool didFuncletReportGCReferences = true; TargetPointer parentStackFrame = TargetPointer.Null; TargetPointer funcletParentStackFrame = TargetPointer.Null; - TargetPointer intermediaryFuncletParentStackFrame; + TargetPointer intermediaryFuncletParentStackFrame = TargetPointer.Null; - foreach (StackDataFrameHandle handle in handles) + // Process the initial frame, then advance with Next() + bool isValid = walkData.State is not (StackWalkState.SW_ERROR or StackWalkState.SW_COMPLETE); + while (isValid) { - GCFrameData gcFrame = new(handle); + StackDataFrameHandle handle = walkData.ToDataFrame(); + walkData.AdvanceIsFirst(); + + GCFrameData gcFrame = new(handle) + { + IsInterrupted = walkData.IsInterrupted, + }; // per-frame tracking variables bool stop = false; @@ -494,6 +542,9 @@ private IEnumerable Filter(IEnumerable handle didFuncletReportGCReferences = true; gcFrame.ShouldParentFrameUseUnwindTargetPCforGCReporting = true; + + gcFrame.ClauseForCatchHandlerStartPC = exInfo.ClauseForCatchHandlerStartPC; + gcFrame.ClauseForCatchHandlerEndPC = exInfo.ClauseForCatchHandlerEndPC; } else if (!IsFunclet(handle)) { @@ -568,8 +619,14 @@ private IEnumerable Filter(IEnumerable handle if (stop) yield return gcFrame; + + // Advance the iterator - matching native Filter() calling NextRaw() + // When a frame was skipped (stop=false), this advances past it. + // When a frame was yielded (stop=true), this advances to the next frame. + isValid = Next(walkData); } } +#pragma warning restore IDE0059 private bool IsUnwoundToTargetParentFrame(StackDataFrameHandle handle, TargetPointer targetParentFrame) { @@ -586,6 +643,18 @@ private bool Next(StackWalkData handle) switch (handle.State) { case StackWalkState.SW_FRAMELESS: + // Native assertion (stackwalk.cpp): current SP must be below the next Frame. + // FaultingExceptionFrame is a special case where it gets pushed after the frame is running. + Debug.Assert( + !handle.FrameIter.IsValid() || + handle.Context.StackPointer.Value < handle.FrameIter.CurrentFrameAddress.Value || + handle.FrameIter.GetCurrentFrameType() == FrameIterator.FrameType.FaultingExceptionFrame, + $"SP (0x{handle.Context.StackPointer:X}) should be below next Frame (0x{handle.FrameIter.CurrentFrameAddress:X})"); + + // Reset interrupted state after processing a managed frame. + // Native stackwalk.cpp:2203-2205: isInterrupted = false; hasFaulted = false; + handle.IsInterrupted = false; + try { handle.Context.Unwind(_target); @@ -597,13 +666,33 @@ private bool Next(StackWalkData handle) } break; case StackWalkState.SW_SKIPPED_FRAME: + // Advance past the skipped frame, then let UpdateState detect + // whether there are more skipped frames or we've reached the managed method. handle.FrameIter.Next(); break; case StackWalkState.SW_FRAME: - handle.FrameIter.UpdateContextFromFrame(handle.Context); - if (!handle.FrameIter.IsInlineCallFrameWithActiveCall()) + // Native SFITER_FRAME_FUNCTION gates ProcessIp + UpdateRegDisplay on + // GetReturnAddress() != 0, and gates GotoNextFrame on !pInlinedFrame. + // pInlinedFrame is set only for active InlinedCallFrames. { - handle.FrameIter.Next(); + var frameType = handle.FrameIter.GetCurrentFrameType(); + + TargetPointer returnAddress = handle.FrameIter.GetReturnAddress(); + bool isActiveICF = frameType == FrameIterator.FrameType.InlinedCallFrame + && returnAddress != TargetPointer.Null; + + // Record the frame type so UpdateState can detect exception frames + // and set IsInterrupted when transitioning to the managed frame. + handle.LastProcessedFrameType = frameType; + + if (returnAddress != TargetPointer.Null) + { + handle.FrameIter.UpdateContextFromFrame(handle.Context); + } + if (!isActiveICF) + { + handle.FrameIter.Next(); + } } break; case StackWalkState.SW_ERROR: @@ -629,6 +718,18 @@ private void UpdateState(StackWalkData handle) if (isManaged) { handle.State = StackWalkState.SW_FRAMELESS; + + // Detect exception frames (FRAME_ATTR_EXCEPTION) when transitioning to managed. + // Both FaultingExceptionFrame (hardware) and SoftwareExceptionFrame (managed throw) + // have FRAME_ATTR_EXCEPTION set. The resulting managed frame gets ExecutionAborted, + // causing GcInfoDecoder to skip live slot reporting at non-interruptible offsets. + if (handle.LastProcessedFrameType is FrameIterator.FrameType.FaultingExceptionFrame + or FrameIterator.FrameType.SoftwareExceptionFrame) + { + handle.IsInterrupted = true; + } + handle.LastProcessedFrameType = null; + if (CheckForSkippedFrames(handle)) { handle.State = StackWalkState.SW_SKIPPED_FRAME; @@ -707,15 +808,17 @@ TargetPointer IStackWalk.GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHa // 4) the return address method has a MDContext arg bool reportInteropMD = false; - if (FrameIterator.IsInlinedCallFrame(_target, framePtr) && + Data.Frame frameData = _target.ProcessedData.GetOrAdd(framePtr); + FrameIterator.FrameType frameType = FrameIterator.GetFrameType(_target, frameData.Identifier); + + if (frameType == FrameIterator.FrameType.InlinedCallFrame && handle.State == StackWalkState.SW_SKIPPED_FRAME) { IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; - // FrameIterator.GetReturnAddress is currently only implemented for InlinedCallFrame - // This is fine as this check is only needed for that frame type - TargetPointer returnAddress = FrameIterator.GetReturnAddress(_target, framePtr); - if (_eman.GetCodeBlockHandle(returnAddress.Value) is CodeBlockHandle cbh) + Data.InlinedCallFrame icf = _target.ProcessedData.GetOrAdd(framePtr); + TargetPointer returnAddress = icf.CallerReturnAddress; + if (returnAddress != TargetPointer.Null && _eman.GetCodeBlockHandle(returnAddress.Value) is CodeBlockHandle cbh) { MethodDescHandle returnMethodDesc = rts.GetMethodDescHandle(_eman.GetMethodDesc(cbh)); reportInteropMD = rts.HasMDContextArg(returnMethodDesc); @@ -802,4 +905,85 @@ private static StackDataFrameHandle AssertCorrectHandle(IStackDataFrameHandle st return handle; } + + /// + /// Enumerates live GC slots for a managed (frameless) code frame. + /// Port of native EECodeManager::EnumGcRefs (eetwain.cpp). + /// + private void EnumGcRefsForManagedFrame( + IPlatformAgnosticContext context, + CodeBlockHandle cbh, + CodeManagerFlags flags, + GcScanContext scanContext, + uint? relOffsetOverride = null) + { + TargetNUInt relativeOffset = _eman.GetRelativeOffset(cbh); + _eman.GetGCInfo(cbh, out TargetPointer gcInfoAddr, out uint gcVersion); + + if (_eman.IsFilterFunclet(cbh)) + flags |= CodeManagerFlags.NoReportUntracked; + + IGCInfoHandle handle = _target.Contracts.GCInfo.DecodePlatformSpecificGCInfo(gcInfoAddr, gcVersion); + if (handle is not IGCInfoDecoder decoder) + return; + + uint stackBaseRegister = decoder.StackBaseRegister; + TargetPointer? callerSP = null; + uint offsetToUse = relOffsetOverride ?? (uint)relativeOffset.Value; + + decoder.EnumerateLiveSlots( + offsetToUse, + flags, + (bool isRegister, uint registerNumber, int spOffset, uint spBase, uint gcFlags) => + { + GcScanFlags scanFlags = GcScanFlags.None; + if ((gcFlags & 0x1) != 0) + scanFlags |= GcScanFlags.GC_CALL_INTERIOR; + if ((gcFlags & 0x2) != 0) + scanFlags |= GcScanFlags.GC_CALL_PINNED; + + if (isRegister) + { + if (!context.TryReadRegister((int)registerNumber, out TargetNUInt regValue)) + return; + GcScanSlotLocation loc = new((int)registerNumber, 0, false); + scanContext.GCEnumCallback(new TargetPointer(regValue.Value), scanFlags, loc); + } + else + { + int spReg = context.StackPointerRegister; + int reg = spBase switch + { + 1 => spReg, + 2 => (int)stackBaseRegister, + 0 => -(spReg + 1), + _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), + }; + TargetPointer baseAddr = spBase switch + { + 1 => context.StackPointer, + 2 => context.TryReadRegister((int)stackBaseRegister, out TargetNUInt val) + ? new TargetPointer(val.Value) + : throw new InvalidOperationException($"Failed to read register {stackBaseRegister}"), + 0 => GetCallerSP(context, ref callerSP), + _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), + }; + + TargetPointer addr = new(baseAddr.Value + (ulong)(long)spOffset); + GcScanSlotLocation loc = new(reg, spOffset, true); + scanContext.GCEnumCallback(addr, scanFlags, loc); + } + }); + } + + private TargetPointer GetCallerSP(IPlatformAgnosticContext context, ref TargetPointer? cached) + { + if (cached is null) + { + IPlatformAgnosticContext callerContext = context.Clone(); + callerContext.Unwind(_target); + cached = callerContext.StackPointer; + } + return cached.Value; + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs index c8d30ded52e678..d582523b5159ec 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs @@ -23,6 +23,8 @@ public ExceptionInfo(Target target, TargetPointer address) CSFEHClause = target.ReadPointerField(address, type, nameof(CSFEHClause)); CSFEnclosingClause = target.ReadPointerField(address, type, nameof(CSFEnclosingClause)); CallerOfActualHandlerFrame = target.ReadPointerField(address, type, nameof(CallerOfActualHandlerFrame)); + ClauseForCatchHandlerStartPC = target.ReadField(address, type, nameof(ClauseForCatchHandlerStartPC)); + ClauseForCatchHandlerEndPC = target.ReadField(address, type, nameof(ClauseForCatchHandlerEndPC)); } public TargetPointer PreviousNestedInfo { get; } @@ -35,4 +37,6 @@ public ExceptionInfo(Target target, TargetPointer address) public TargetPointer CSFEHClause { get; } public TargetPointer CSFEnclosingClause { get; } public TargetPointer CallerOfActualHandlerFrame { get; } + public uint ClauseForCatchHandlerStartPC { get; } + public uint ClauseForCatchHandlerEndPC { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs new file mode 100644 index 00000000000000..625c616d42616e --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs @@ -0,0 +1,18 @@ +// 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.DataContractReader.Data; + +internal class DynamicHelperFrame : IData +{ + static DynamicHelperFrame IData.Create(Target target, TargetPointer address) + => new DynamicHelperFrame(target, address); + + public DynamicHelperFrame(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.DynamicHelperFrame); + DynamicHelperFrameFlags = target.ReadField(address, type, nameof(DynamicHelperFrameFlags)); + } + + public int DynamicHelperFrameFlags { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs new file mode 100644 index 00000000000000..cfc3e92be93297 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs @@ -0,0 +1,22 @@ +// 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.DataContractReader.Data; + +internal class ExternalMethodFrame : IData +{ + static ExternalMethodFrame IData.Create(Target target, TargetPointer address) + => new ExternalMethodFrame(target, address); + + public ExternalMethodFrame(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.ExternalMethodFrame); + GCRefMap = target.ReadPointerField(address, type, nameof(GCRefMap)); + Indirection = target.ReadPointerField(address, type, nameof(Indirection)); + ZapModule = target.ReadPointerField(address, type, nameof(ZapModule)); + } + + public TargetPointer GCRefMap { get; } + public TargetPointer Indirection { get; } + public TargetPointer ZapModule { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs index c49f6919353255..da2e1a493f602b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs @@ -14,6 +14,9 @@ public StubDispatchFrame(Target target, TargetPointer address) MethodDescPtr = target.ReadPointerField(address, type, nameof(MethodDescPtr)); RepresentativeMTPtr = target.ReadPointerField(address, type, nameof(RepresentativeMTPtr)); RepresentativeSlot = target.ReadField(address, type, nameof(RepresentativeSlot)); + GCRefMap = target.ReadPointerField(address, type, nameof(GCRefMap)); + ZapModule = target.ReadPointerField(address, type, nameof(ZapModule)); + Indirection = target.ReadPointerField(address, type, nameof(Indirection)); Address = address; } @@ -21,4 +24,7 @@ public StubDispatchFrame(Target target, TargetPointer address) public TargetPointer MethodDescPtr { get; } public TargetPointer RepresentativeMTPtr { get; } public uint RepresentativeSlot { get; } + public TargetPointer GCRefMap { get; } + public TargetPointer ZapModule { get; } + public TargetPointer Indirection { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs index b2c0c71cb47ef9..97443b11f43645 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs @@ -18,6 +18,11 @@ public TransitionBlock(Target target, TargetPointer address) { ArgumentRegisters = address + (ulong)type.Fields[nameof(ArgumentRegisters)].Offset; } + + // These are offsets relative to the TransitionBlock pointer, stored as field "offsets" + // in the data descriptor. They represent computed layout positions, not actual memory reads. + FirstGCRefMapSlot = (uint)type.Fields[nameof(FirstGCRefMapSlot)].Offset; + ArgumentRegistersOffset = (uint)type.Fields[nameof(ArgumentRegistersOffset)].Offset; } public TargetPointer ReturnAddress { get; } @@ -27,4 +32,14 @@ public TransitionBlock(Target target, TargetPointer address) /// Only available on ARM targets. /// public TargetPointer? ArgumentRegisters { get; } + + /// + /// Offset to the first slot covered by the GCRefMap, relative to the TransitionBlock pointer. + /// + public uint FirstGCRefMapSlot { get; } + + /// + /// Offset to the argument registers area, relative to the TransitionBlock pointer. + /// + public uint ArgumentRegistersOffset { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs index 3241fa45b965a0..6557ee7aa99a1c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs @@ -33,6 +33,11 @@ public ReadyToRunInfo(Target target, TargetPointer address) DebugInfoSection = target.ReadPointerField(address, type, nameof(DebugInfoSection)); ExceptionInfoSection = target.ReadPointerField(address, type, nameof(ExceptionInfoSection)); + NumImportSections = target.Read(address + (ulong)type.Fields[nameof(NumImportSections)].Offset); + ImportSections = NumImportSections > 0 + ? target.ReadPointer(address + (ulong)type.Fields[nameof(ImportSections)].Offset) + : TargetPointer.Null; + // Map is from the composite info pointer (set to itself for non-multi-assembly composite images) EntryPointToMethodDescMap = CompositeInfo + (ulong)type.Fields[nameof(EntryPointToMethodDescMap)].Offset; LoadedImageBase = target.ReadPointerField(address, type, nameof(LoadedImageBase)); @@ -55,4 +60,6 @@ public ReadyToRunInfo(Target target, TargetPointer address) public TargetPointer EntryPointToMethodDescMap { get; } public TargetPointer LoadedImageBase { get; } public TargetPointer Composite { get; } + public uint NumImportSections { get; } + public TargetPointer ImportSections { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index d67adff3ab3489..3eb7727d3d3fc2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -4034,13 +4034,69 @@ int ISOSEnum.GetCount(uint* pCount) int ISOSDacInterface.GetStackReferences(int osThreadID, DacComNullableByRef ppEnum) { - // Stack reference enumeration is not yet complete in the cDAC — capital-F Frame - // GC root scanning (ScanFrameRoots) is still pending. Fall through to the legacy - // DAC so that consumers (dump tests, SOS) continue to work while the implementation - // is in progress. - return _legacyImpl is not null - ? _legacyImpl.GetStackReferences(osThreadID, ppEnum) - : HResults.E_NOTIMPL; + int hr = HResults.S_OK; + try + { + IThread threadContract = _target.Contracts.Thread; + IStackWalk stackWalkContract = _target.Contracts.StackWalk; + ThreadData? matchingThread = null; + + ThreadStoreData threadStore = threadContract.GetThreadStoreData(); + TargetPointer threadAddr = threadStore.FirstThread; + while (threadAddr != TargetPointer.Null) + { + ThreadData td = threadContract.GetThreadData(threadAddr); + if (td.OSId.Value == (ulong)osThreadID) + { + matchingThread = td; + break; + } + threadAddr = td.NextThread; + } + + if (matchingThread is null) + { + return HResults.E_INVALIDARG; + } + + IReadOnlyList refs = stackWalkContract.WalkStackReferences(matchingThread.Value); + + SOSStackRefData[] sosRefs = new SOSStackRefData[refs.Count]; + for (int i = 0; i < refs.Count; i++) + { + sosRefs[i] = new SOSStackRefData + { + HasRegisterInformation = refs[i].HasRegisterInformation ? 1 : 0, + Register = refs[i].Register, + Offset = refs[i].Offset, + Address = refs[i].Address.Value, + Object = refs[i].Object.Value, + Flags = refs[i].Flags, + Source = refs[i].Source.Value, + SourceType = refs[i].IsStackSourceFrame + ? SOSStackSourceType.SOS_StackSourceFrame + : SOSStackSourceType.SOS_StackSourceIP, + StackPointer = refs[i].StackPointer.Value, + }; + } + + ppEnum.Interface = new SOSStackRefEnum(sosRefs); + } + catch (System.Exception) + { + hr = HResults.E_FAIL; + } +#if DEBUG + if (_legacyImpl is not null) + { + // Validate that the legacy DAC produces the same HResult. + // We pass isNullRef: false to request actual enumeration, but we don't + // compare individual refs — that's done by cdacstress.cpp at runtime. + int hrLocal = _legacyImpl.GetStackReferences(osThreadID, new DacComNullableByRef(isNullRef: false)); + Debug.ValidateHResult(hr, hrLocal); + } +#endif + return hr; } int ISOSDacInterface.GetStressLogAddress(ClrDataAddress* stressLog) diff --git a/src/native/managed/cdac/cdac.slnx b/src/native/managed/cdac/cdac.slnx index 7449d30624ec2d..3243195e0855eb 100644 --- a/src/native/managed/cdac/cdac.slnx +++ b/src/native/managed/cdac/cdac.slnx @@ -14,5 +14,6 @@ + diff --git a/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj b/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj index 5b33a365154275..a3951ba48e1a21 100644 --- a/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj +++ b/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs index c72126d962f939..d054e97d09de27 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs @@ -331,6 +331,9 @@ internal sealed class MockReadyToRunInfo : TypedView private const string LoadedImageBaseFieldName = "LoadedImageBase"; private const string CompositeFieldName = "Composite"; + private const string ImportSectionsFieldName = "ImportSections"; + private const string NumImportSectionsFieldName = "NumImportSections"; + public static Layout CreateLayout(MockTarget.Architecture architecture, int hashMapStride) => new SequentialLayoutBuilder("ReadyToRunInfo", architecture) .AddPointerField(ReadyToRunHeaderFieldName) @@ -342,6 +345,8 @@ public static Layout CreateLayout(MockTarget.Architecture ar .AddPointerField(DelayLoadMethodCallThunksFieldName) .AddPointerField(DebugInfoSectionFieldName) .AddPointerField(ExceptionInfoSectionFieldName) + .AddPointerField(ImportSectionsFieldName) + .AddUInt32Field(NumImportSectionsFieldName) .AddField(EntryPointToMethodDescMapFieldName, hashMapStride) .AddPointerField(LoadedImageBaseFieldName) .AddPointerField(CompositeFieldName) From 99fe809420bb8f32d4d4ae4c47dfecef184eda11 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 27 Apr 2026 19:43:01 -0400 Subject: [PATCH 2/5] Address PR review feedback - Use ex.HResult instead of E_FAIL in GetStackReferences catch block - Remove ZapModule references from StackWalk.md documentation - Replace magic 0x11 with (byte)SignatureTypeKind.ValueType - Remove StressTests csproj reference from cdac.slnx (PR5 only) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/StackWalk.md | 6 ++---- .../Contracts/StackWalk/GC/GcSignatureTypeProvider.cs | 6 +++--- .../SOSDacImpl.cs | 4 ++-- src/native/managed/cdac/cdac.slnx | 1 - 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 6a5fa627ab06ab..5bcfd1ef60fcf8 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -61,11 +61,9 @@ This contract depends on the following descriptors: | `StubDispatchFrame` | `RepresentativeMTPtr` | Pointer to Frame's method table pointer | | `StubDispatchFrame` | `RepresentativeSlot` | Frame's method table slot | | `StubDispatchFrame` | `GCRefMap` | Cached pointer to GC reference map blob for caller stack promotion | -| `StubDispatchFrame` | `ZapModule` | Module pointer for lazy GCRefMap resolution via import sections | -| `StubDispatchFrame` | `Indirection` | Import slot pointer for lazy GCRefMap resolution | +| `StubDispatchFrame` | `Indirection` | Import slot pointer for lazy GCRefMap resolution via `FindReadyToRunModule` | | `ExternalMethodFrame` | `GCRefMap` | Cached pointer to GC reference map blob for caller stack promotion | -| `ExternalMethodFrame` | `Indirection` | Import slot pointer for lazy GCRefMap resolution | -| `ExternalMethodFrame` | `ZapModule` | Module pointer for lazy GCRefMap resolution via import sections | +| `ExternalMethodFrame` | `Indirection` | Import slot pointer for lazy GCRefMap resolution via `FindReadyToRunModule` | | `DynamicHelperFrame` | `DynamicHelperFrameFlags` | Flags indicating which argument registers contain GC references | | `TransitionBlock` | `ReturnAddress` | Return address associated with the TransitionBlock | | `TransitionBlock` | `CalleeSavedRegisters` | Platform specific CalleeSavedRegisters struct associated with the TransitionBlock | diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs index 4edb08421a317a..46e6c8af2de24c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs @@ -40,13 +40,13 @@ public GcTypeKind GetPrimitiveType(PrimitiveTypeCode typeCode) }; public GcTypeKind GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) - => rawTypeKind == 0x11 ? GcTypeKind.Other : GcTypeKind.Ref; + => rawTypeKind == (byte)SignatureTypeKind.ValueType ? GcTypeKind.Other : GcTypeKind.Ref; public GcTypeKind GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) - => rawTypeKind == 0x11 ? GcTypeKind.Other : GcTypeKind.Ref; + => rawTypeKind == (byte)SignatureTypeKind.ValueType ? GcTypeKind.Other : GcTypeKind.Ref; public GcTypeKind GetTypeFromSpecification(MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind) - => rawTypeKind == 0x11 ? GcTypeKind.Other : GcTypeKind.Ref; + => rawTypeKind == (byte)SignatureTypeKind.ValueType ? GcTypeKind.Other : GcTypeKind.Ref; public GcTypeKind GetSZArrayType(GcTypeKind elementType) => GcTypeKind.Ref; public GcTypeKind GetArrayType(GcTypeKind elementType, ArrayShape shape) => GcTypeKind.Ref; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index 3eb7727d3d3fc2..dab97673335e99 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -4082,9 +4082,9 @@ int ISOSDacInterface.GetStackReferences(int osThreadID, DacComNullableByRef - From 3b8bd21ab1e5356039e17f21d491a81ffaf2c287 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 28 Apr 2026 16:46:59 -0400 Subject: [PATCH 3/5] add docs for FindReadyToRunModule --- docs/design/datacontracts/ExecutionManager.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index cf7163569789f9..b2d29e3a7a220e 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -57,6 +57,11 @@ struct CodeBlockHandle bool IsFunclet(CodeBlockHandle codeInfoHandle); // Returns true if the code block is specifically a filter funclet bool IsFilterFunclet(CodeBlockHandle codeInfoHandle); + + // Finds the ReadyToRun module that contains the given address. + // Used to resolve the containing module from an import section address + // during GCRefMap lookup in the StackWalk contract. + TargetPointer FindReadyToRunModule(TargetPointer address); ``` ```csharp @@ -501,6 +506,24 @@ After obtaining the clause array bounds, the common iteration logic classifies e `IsFilterFunclet` first checks `IsFunclet`. If the code block is a funclet, it retrieves the EH clauses for the method and checks whether any filter clause's handler offset matches the funclet's relative offset. If a match is found, the funclet is a filter funclet. +### FindReadyToRunModule + +`FindReadyToRunModule` locates the ReadyToRun module whose PE image contains the given address. Unlike `GetCodeBlockHandle` (which only matches code regions), this API matches against the full PE image range — including data sections such as import tables. This is necessary because GCRefMap resolution requires finding the module that owns an import section indirection address, which is in the data section rather than the code section. + +```csharp +TargetPointer IExecutionManager.FindReadyToRunModule(TargetPointer address) +{ + // Use the RangeSectionMap to find the RangeSection containing the address. + // ReadyToRun range sections cover the entire PE image (code + data), + // so this works for import section addresses used by GCRefMap lookup. + RangeSection range = RangeSection.Find(target, topRangeSectionMap, address); + if (range.Data is null) + return TargetPointer.Null; + + return range.Data.R2RModule; +} +``` + ### EE JIT Manager and Code Heap Info ```csharp From 762145c9fd8ccb4154fc9984a32c04038a82f62a Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 28 Apr 2026 16:47:22 -0400 Subject: [PATCH 4/5] add docs for GCInfo parsing (including InterruptibleRanges --- docs/design/datacontracts/GCInfo.md | 185 ++++++++++++++++++++++++++-- 1 file changed, 176 insertions(+), 9 deletions(-) diff --git a/docs/design/datacontracts/GCInfo.md b/docs/design/datacontracts/GCInfo.md index b97f8dbbd3a525..ac8733c48689e3 100644 --- a/docs/design/datacontracts/GCInfo.md +++ b/docs/design/datacontracts/GCInfo.md @@ -2,6 +2,9 @@ This contract is for fetching information related to GCInfo associated with native code. Currently, this contract does not support x86 architecture. +The GCInfo contract has platform specific implementations as GCInfo differs per architecture. With the exception of x86, all platforms have a common encoding scheme with different encoding lengths and normalization functions for data. x86 uses an entirely different scheme which is not currently supported by this contract. + + ## APIs of contract ```csharp @@ -47,10 +50,6 @@ Constants: | `NO_PSP_SYM` | Indicates no PSP symbol | -1 | -## Implementation - -The GCInfo contract has platform specific implementations as GCInfo differs per architecture. With the exception of x86, all platforms have a common encoding scheme with different encoding lengths and normalization functions for data. x86 uses an entirely different scheme which is not currently supported by this contract. - ### GCInfo Format The GCInfo format consists of a header structure and following data types. The header is either 'slim' for simple methods that can use the compact encoding scheme or a 'fat' header containing more details. @@ -315,7 +314,7 @@ Signed values use the same encoding as unsigned, but with sign considerations: ### Implementation -The GCInfo contract implementation follows this process: +The GCInfo decoder uses **lazy sequential decoding** — data is decoded on demand as APIs are called, and each section of the bitstream is decoded at most once. The decoder tracks a set of `DecodePoints` that represent completion of each section. When an API like `GetCodeLength()` or `GetInterruptibleRanges()` is called, the decoder advances through the bitstream until the requested data has been decoded. ```csharp IGCInfoHandle DecodePlatformSpecificGCInfo(TargetPointer gcInfoAddress, uint gcVersion) @@ -329,13 +328,181 @@ IGCInfoHandle DecodeInterpreterGCInfo(TargetPointer gcInfoAddress, uint gcVersio // Create a new decoder instance using the interpreter encoding return new GcInfoDecoder(target, gcInfoAddress, gcVersion); } +``` + +#### Header Decoding + +The first bit of the GCInfo bitstream determines whether the header is **slim** or **fat**. + +**Slim Header** (first bit = 0): + +The slim header is a compact encoding for simple methods. It reads only a few fields: + +```csharp +isSlimHeader = ReadBits(1) // 0 = slim +usingStackBaseRegister = ReadBits(1) +if usingStackBaseRegister: + stackBaseRegister = DenormalizeStackBaseRegister(0) +codeLength = DenormalizeCodeLength(DecodeVarLengthUnsigned(CODE_LENGTH_ENCBASE)) +numSafePoints = DecodeVarLengthUnsigned(NUM_SAFE_POINTS_ENCBASE) +numInterruptibleRanges = 0 // slim header never has interruptible ranges +``` + +All optional fields (GS cookie, PSP symbol, generics context, EnC info, reverse P/Invoke) default to their sentinel "not present" values. + +**Fat Header** (first bit = 1): + +The fat header contains a full flags bitfield and conditionally-present optional fields: + +```csharp +isSlimHeader = ReadBits(1) // 1 = fat +headerFlags = ReadBits(GC_INFO_FLAGS_BIT_SIZE) // 10 bits +codeLength = DenormalizeCodeLength(DecodeVarLengthUnsigned(CODE_LENGTH_ENCBASE)) + +// Prolog/epilog sizes (conditional on GS cookie or generics context) +if HAS_GS_COOKIE: + normPrologSize = DecodeVarLengthUnsigned(NORM_PROLOG_SIZE_ENCBASE) + 1 + normEpilogSize = DecodeVarLengthUnsigned(NORM_EPILOG_SIZE_ENCBASE) +elif HAS_GENERICS_INST_CONTEXT: + normPrologSize = DecodeVarLengthUnsigned(NORM_PROLOG_SIZE_ENCBASE) + 1 + +// Optional fields (each conditional on its header flag) +if HAS_GS_COOKIE: + gsCookieStackSlot = DenormalizeStackSlot(DecodeVarLengthSigned(GS_COOKIE_STACK_SLOT_ENCBASE)) +if HAS_GENERICS_INST_CONTEXT: + genericsInstContextStackSlot = DenormalizeStackSlot(DecodeVarLengthSigned(...)) +if HAS_STACK_BASE_REGISTER: + stackBaseRegister = DenormalizeStackBaseRegister(DecodeVarLengthUnsigned(...)) +if HAS_EDIT_AND_CONTINUE_INFO: + sizeOfEnCPreservedArea = DecodeVarLengthUnsigned(...) + if ARM64: sizeOfEnCFixedStackFrame = DecodeVarLengthUnsigned(...) +if REVERSE_PINVOKE_FRAME: + reversePInvokeFrameStackSlot = DenormalizeStackSlot(DecodeVarLengthSigned(...)) +if HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA: // platform-dependent + fixedStackParameterScratchArea = DenormalizeSizeOfStackArea(DecodeVarLengthUnsigned(...)) + +numSafePoints = DecodeVarLengthUnsigned(NUM_SAFE_POINTS_ENCBASE) +numInterruptibleRanges = DecodeVarLengthUnsigned(NUM_INTERRUPTIBLE_RANGES_ENCBASE) +``` + +#### Body Decoding + +Following the header, the GCInfo body contains data sections that must be decoded in strict order: + +##### 1. Safe Point Offsets + +Safe points (also called call sites) are code offsets where the GC can safely interrupt execution for partially-interruptible methods. Each offset is encoded as a fixed-width bitfield: + +```csharp +numBitsPerOffset = CeilOfLog2(NormalizeCodeOffset(codeLength)) +for each safe point: + offset = ReadBits(numBitsPerOffset) // normalized code offset +``` + +The offsets are stored in sorted order to enable binary search during `EnumerateLiveSlots`. + +##### 2. Interruptible Ranges + +Interruptible ranges define code regions where the method is **fully interruptible** — the GC can interrupt at any instruction within these ranges. Each range is encoded as a pair of delta-compressed, normalized offsets: + +```csharp +lastStopNormalized = 0 + +for each range: + startDelta = DecodeVarLengthUnsigned(INTERRUPTIBLE_RANGE_DELTA1_ENCBASE) + stopDelta = DecodeVarLengthUnsigned(INTERRUPTIBLE_RANGE_DELTA2_ENCBASE) + 1 + startNormalized = lastStopNormalized + startDelta + stopNormalized = startNormalized + stopDelta + + startOffset = DenormalizeCodeOffset(startNormalized) + stopOffset = DenormalizeCodeOffset(stopNormalized) + + emit InterruptibleRange(startOffset, stopOffset) + lastStopNormalized = stopNormalized +``` + +##### 3. Slot Table + +The slot table describes all GC-tracked locations used by the method. It has three sections decoded in order: register slots, tracked stack slots, and untracked stack slots. + +**Slot counts** are encoded with presence bits: + +```csharp +if ReadBits(1): // has register slots + numRegisters = DecodeVarLengthUnsigned(NUM_REGISTERS_ENCBASE) +if ReadBits(1): // has stack/untracked slots + numStackSlots = DecodeVarLengthUnsigned(NUM_STACK_SLOTS_ENCBASE) + numUntrackedSlots = DecodeVarLengthUnsigned(NUM_UNTRACKED_SLOTS_ENCBASE) +``` + +**Register slots** use delta encoding when consecutive slots share the same flags: + +```csharp +// First slot: absolute register number + 2-bit flags +regNum = DecodeVarLengthUnsigned(REGISTER_ENCBASE) +flags = ReadBits(2) + +// Subsequent slots: +if previousFlags != 0: + regNum = DecodeVarLengthUnsigned(REGISTER_ENCBASE) // absolute + flags = ReadBits(2) +else: + regNum += DecodeVarLengthUnsigned(REGISTER_DELTA_ENCBASE) + 1 // delta + // flags inherited from previous +``` + +**Stack slots** follow a similar delta encoding pattern: + +```csharp +// First slot: base (2 bits) + normalized offset + flags (2 bits) +spBase = ReadBits(2) // CALLER_SP_REL, SP_REL, or FRAMEREG_REL +normSpOffset = DecodeVarLengthSigned(STACK_SLOT_ENCBASE) +spOffset = DenormalizeStackSlot(normSpOffset) +flags = ReadBits(2) + +// Subsequent slots: +spBase = ReadBits(2) +if previousFlags != 0: + normSpOffset = DecodeVarLengthSigned(STACK_SLOT_ENCBASE) // absolute + flags = ReadBits(2) +else: + normSpOffset += DecodeVarLengthUnsigned(STACK_SLOT_DELTA_ENCBASE) // delta + // flags inherited from previous +``` + +Untracked slots use the same encoding as tracked stack slots. + +The 2-bit slot flags are: + +| Flag | Value | Meaning | +| --- | --- | --- | +| `GC_SLOT_BASE` | 0x0 | Normal object reference | +| `GC_SLOT_INTERIOR` | 0x1 | Interior pointer (points inside an object) | +| `GC_SLOT_PINNED` | 0x2 | Pinned object reference | + +##### 4. Live State Data + +Following the slot table, the remaining bitstream contains per-safe-point and per-chunk liveness information used by `EnumerateLiveSlots` to determine which slots are live at a given instruction offset. This data uses either a direct 1-bit-per-slot encoding or RLE (run-length encoding) compression for methods with many tracked slots. + +For **partially interruptible** methods (at safe points), each safe point has a bitvector indicating which tracked slots are live. An optional indirection table allows sharing identical bitvectors across safe points. + +For **fully interruptible** methods (within interruptible ranges), the interruptible region is divided into fixed-size chunks (`NUM_NORM_CODE_OFFSETS_PER_CHUNK = 64` normalized offsets). Each chunk records a "could be live" bitvector, a final state bitvector, and transition points within the chunk where slot liveness changes. + +#### API Implementations + +```csharp uint GetCodeLength(IGCInfoHandle handle) { - // Cast to the appropriate decoder type and return the decoded code length - GcInfoDecoder decoder = (GcInfoDecoder)handle; + // Decodes the header up through CodeLength if not already decoded + GcInfoDecoder decoder = (GcInfoDecoder)handle; return decoder.GetCodeLength(); } -``` -The decoder reads and parses the GCInfo data structure sequentially, using the platform-specific encoding bases and normalization rules to reconstruct the original method metadata. +IReadOnlyList GetInterruptibleRanges(IGCInfoHandle handle) +{ + // Decodes through the header and body up through interruptible ranges + GcInfoDecoder decoder = (GcInfoDecoder)handle; + return decoder.GetInterruptibleRanges(); +} +``` From b8c28e63fa9e941603a6d327562e57a156426066 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 29 Apr 2026 13:57:38 -0400 Subject: [PATCH 5/5] clean up PR --- docs/design/datacontracts/GCInfo.md | 83 +++++++++++-- docs/design/datacontracts/StackWalk.md | 99 ++++++++++++++- .../vm/datadescriptor/datadescriptor.inc | 4 - src/coreclr/vm/frames.h | 4 - .../Contracts/IExecutionManager.cs | 6 - .../Contracts/IGCInfo.cs | 36 ++++++ .../Contracts/GCInfo/GCInfoDecoder.cs | 45 +++---- .../Contracts/GCInfo/GCInfo_1.cs | 12 ++ .../Contracts/GCInfo/IGCInfoDecoder.cs | 34 +---- .../StackWalk/FrameHandling/FrameIterator.cs | 34 +++-- .../Contracts/StackWalk/GC/GcScanner.cs | 109 ---------------- .../Contracts/StackWalk/StackWalk_1.cs | 116 ++++++++---------- .../Data/Frames/ExternalMethodFrame.cs | 4 - .../Data/Frames/StubDispatchFrame.cs | 4 - 14 files changed, 303 insertions(+), 287 deletions(-) delete mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs diff --git a/docs/design/datacontracts/GCInfo.md b/docs/design/datacontracts/GCInfo.md index ac8733c48689e3..1e92cf91259909 100644 --- a/docs/design/datacontracts/GCInfo.md +++ b/docs/design/datacontracts/GCInfo.md @@ -23,8 +23,14 @@ IGCInfoHandle DecodeInterpreterGCInfo(TargetPointer gcInfoAddress, uint gcVersio // Fetches length of code as reported in GCInfo uint GetCodeLength(IGCInfoHandle handle); +// Returns the stack base register number decoded from GCInfo +uint GetStackBaseRegister(IGCInfoHandle handle); + // Returns the list of interruptible code offset ranges from the GCInfo IReadOnlyList GetInterruptibleRanges(IGCInfoHandle handle); + +// Enumerates all live GC slots at the given instruction offset, invoking the callback for each +bool EnumerateLiveSlots(IGCInfoHandle handle, uint instructionOffset, CodeManagerFlags flags, LiveSlotCallback reportSlot); ``` ## Version 1 @@ -338,7 +344,7 @@ The first bit of the GCInfo bitstream determines whether the header is **slim** The slim header is a compact encoding for simple methods. It reads only a few fields: -```csharp +``` isSlimHeader = ReadBits(1) // 0 = slim usingStackBaseRegister = ReadBits(1) if usingStackBaseRegister: @@ -354,7 +360,7 @@ All optional fields (GS cookie, PSP symbol, generics context, EnC info, reverse The fat header contains a full flags bitfield and conditionally-present optional fields: -```csharp +``` isSlimHeader = ReadBits(1) // 1 = fat headerFlags = ReadBits(GC_INFO_FLAGS_BIT_SIZE) // 10 bits codeLength = DenormalizeCodeLength(DecodeVarLengthUnsigned(CODE_LENGTH_ENCBASE)) @@ -393,7 +399,7 @@ Following the header, the GCInfo body contains data sections that must be decode Safe points (also called call sites) are code offsets where the GC can safely interrupt execution for partially-interruptible methods. Each offset is encoded as a fixed-width bitfield: -```csharp +``` numBitsPerOffset = CeilOfLog2(NormalizeCodeOffset(codeLength)) for each safe point: offset = ReadBits(numBitsPerOffset) // normalized code offset @@ -405,7 +411,7 @@ The offsets are stored in sorted order to enable binary search during `Enumerate Interruptible ranges define code regions where the method is **fully interruptible** — the GC can interrupt at any instruction within these ranges. Each range is encoded as a pair of delta-compressed, normalized offsets: -```csharp +``` lastStopNormalized = 0 for each range: @@ -428,7 +434,7 @@ The slot table describes all GC-tracked locations used by the method. It has thr **Slot counts** are encoded with presence bits: -```csharp +``` if ReadBits(1): // has register slots numRegisters = DecodeVarLengthUnsigned(NUM_REGISTERS_ENCBASE) if ReadBits(1): // has stack/untracked slots @@ -438,7 +444,7 @@ if ReadBits(1): // has stack/untracked slots **Register slots** use delta encoding when consecutive slots share the same flags: -```csharp +``` // First slot: absolute register number + 2-bit flags regNum = DecodeVarLengthUnsigned(REGISTER_ENCBASE) flags = ReadBits(2) @@ -454,7 +460,7 @@ else: **Stack slots** follow a similar delta encoding pattern: -```csharp +``` // First slot: base (2 bits) + normalized offset + flags (2 bits) spBase = ReadBits(2) // CALLER_SP_REL, SP_REL, or FRAMEREG_REL normSpOffset = DecodeVarLengthSigned(STACK_SLOT_ENCBASE) @@ -489,20 +495,71 @@ For **partially interruptible** methods (at safe points), each safe point has a For **fully interruptible** methods (within interruptible ranges), the interruptible region is divided into fixed-size chunks (`NUM_NORM_CODE_OFFSETS_PER_CHUNK = 64` normalized offsets). Each chunk records a "could be live" bitvector, a final state bitvector, and transition points within the chunk where slot liveness changes. +### EnumerateLiveSlots + +`EnumerateLiveSlots` determines which GC-tracked slots (registers and stack locations) are live at a given instruction offset, then reports each live slot via a callback. The algorithm handles two distinct cases depending on whether the instruction offset falls at a **safe point** (partially-interruptible) or within an **interruptible range** (fully-interruptible). + +**Input**: instruction offset, `CodeManagerFlags`, slot report callback. + +**Step 1 — Find safe point**: Search the safe point offset table for an exact match against the normalized instruction offset. If found, the safe point index is used for the partially-interruptible path. + +**Step 2 — Partially-interruptible path** (safe point found, not `ExecutionAborted`): + +Each safe point has a bitvector with one bit per tracked slot. If the bit is set, the slot is live. An optional **indirection table** allows sharing identical bitvectors across safe points — when present, each safe point stores an offset into a deduplicated bitvector table. The bitvectors may use either direct 1-bit-per-slot encoding or **RLE** (run-length encoding) for methods with many tracked slots. + +**Step 3 — Fully-interruptible path** (no safe point match, offset is within an interruptible range): + +The total interruptible length is computed by summing all interruptible range sizes. A **pseudo-offset** maps the instruction offset into this linear space. The interruptible region is divided into fixed-size **chunks** of 64 normalized offsets each. + +For each chunk, the encoding stores: +- A **couldBeLive** bitvector identifying which slots may be live anywhere in the chunk (1-bit-per-slot or RLE). +- A **finalState** bit per couldBeLive slot indicating liveness at the end of the chunk. +- **Transition points** within the chunk where each slot's liveness toggles. + +To determine liveness at the target offset: start from the chunk's final state, then apply any transitions that occur *after* the target offset (toggling the state backwards). A slot is live if its final state (after toggle adjustment) is 1. + +**Step 4 — Report untracked slots**: Untracked slots are always live (they represent stack locations the JIT doesn't track at each safe point). They are reported unconditionally unless `ParentOfFuncletStackFrame` or `NoReportUntracked` flags are set. Untracked slots are reported with `reportScratchSlots=true` since the JIT may produce untracked scratch register slots for interior pointers. + +**Slot filtering**: Before reporting any slot, the algorithm checks: +- **Scratch registers**: Only reported for the active/leaf frame (`ActiveStackFrame` flag). +- **Scratch stack slots**: Only reported for the active/leaf frame (slots in the outgoing/scratch area). +- **FP-based-only mode** (`ReportFPBasedSlotsOnly`): Only frame-register-relative stack slots are reported; all register slots and non-frame-relative stack slots are skipped. + #### API Implementations +All APIs use lazy decoding — the GCInfo bitstream is decoded up to the required point on first access, and cached for subsequent calls. + ```csharp uint GetCodeLength(IGCInfoHandle handle) { - // Decodes the header up through CodeLength if not already decoded - GcInfoDecoder decoder = (GcInfoDecoder)handle; - return decoder.GetCodeLength(); + // Ensure header is decoded, then return the code length field. } IReadOnlyList GetInterruptibleRanges(IGCInfoHandle handle) { - // Decodes through the header and body up through interruptible ranges - GcInfoDecoder decoder = (GcInfoDecoder)handle; - return decoder.GetInterruptibleRanges(); + // Ensure header and body are decoded through interruptible ranges, + // then return the decoded range list. +} + +uint GetStackBaseRegister(IGCInfoHandle handle) +{ + // Ensure header is decoded through the stack base register field, + // then return the denormalized register number (e.g., RBP on x64). +} + +bool EnumerateLiveSlots(IGCInfoHandle handle, uint instructionOffset, + CodeManagerFlags flags, LiveSlotCallback reportSlot) +{ + // Ensure header, body, and slot table are fully decoded. + // Then execute the EnumerateLiveSlots algorithm described above: + // 1. Find safe point match for the normalized instruction offset + // 2. If found: read the per-safe-point bitvector (partially-interruptible path) + // 3. If not found: compute pseudo-offset into interruptible ranges, + // locate the chunk, read couldBeLive/finalState/transitions + // (fully-interruptible path) + // 4. Report untracked slots unconditionally (unless suppressed by flags) + // 5. Apply slot filtering (scratch registers, FP-based-only mode) + // Invokes reportSlot callback for each live slot with: + // isRegister, registerNumber, spOffset, spBase, gcFlags } ``` diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 5bcfd1ef60fcf8..1eb5610479fcb3 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -28,6 +28,10 @@ TargetPointer GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHandle); // Gets the instruction pointer from the current frame's context. TargetPointer GetInstructionPointer(IStackDataFrameHandle stackDataFrameHandle); + +// Walks the stack and returns all GC references found on each frame. +// This is the primary API for GC reference enumeration, used by SOSDacImpl.GetStackReferences. +IReadOnlyList WalkStackReferences(ThreadData threadData); ``` ## Version 1 @@ -60,10 +64,8 @@ This contract depends on the following descriptors: | `StubDispatchFrame` | `MethodDescPtr` | Pointer to Frame's method desc | | `StubDispatchFrame` | `RepresentativeMTPtr` | Pointer to Frame's method table pointer | | `StubDispatchFrame` | `RepresentativeSlot` | Frame's method table slot | -| `StubDispatchFrame` | `GCRefMap` | Cached pointer to GC reference map blob for caller stack promotion | -| `StubDispatchFrame` | `Indirection` | Import slot pointer for lazy GCRefMap resolution via `FindReadyToRunModule` | -| `ExternalMethodFrame` | `GCRefMap` | Cached pointer to GC reference map blob for caller stack promotion | -| `ExternalMethodFrame` | `Indirection` | Import slot pointer for lazy GCRefMap resolution via `FindReadyToRunModule` | +| `StubDispatchFrame` | `Indirection` | Import slot pointer for GCRefMap resolution via `FindReadyToRunModule` | +| `ExternalMethodFrame` | `Indirection` | Import slot pointer for GCRefMap resolution via `FindReadyToRunModule` | | `DynamicHelperFrame` | `DynamicHelperFrameFlags` | Flags indicating which argument registers contain GC references | | `TransitionBlock` | `ReturnAddress` | Return address associated with the TransitionBlock | | `TransitionBlock` | `CalleeSavedRegisters` | Platform specific CalleeSavedRegisters struct associated with the TransitionBlock | @@ -114,6 +116,7 @@ Contracts used: | `ExecutionManager` | | `Thread` | | `RuntimeTypeSystem` | +| `GCInfo` | ### Stackwalk Algorithm @@ -289,10 +292,14 @@ InlinedCallFrames store and update only the IP, SP, and FP of a given context. I * On ARM, the InlinedCallFrame stores the value of the SP after the prolog (`SPAfterProlog`) to allow unwinding for functions with stackalloc. When a function uses stackalloc, the CallSiteSP can already have been adjusted. This value should be placed in R9. +**Return Address**: `CallerReturnAddress`, but only when the frame has an active call (i.e., `CallerReturnAddress != 0`). Returns null otherwise. + #### SoftwareExceptionFrame SoftwareExceptionFrames store a copy of the context struct. The IP, SP, and all ABI specified (platform specific) callee-saved registers are copied from the stored context to the working context. +**Return Address**: Read from the `ReturnAddress` field on the frame. + #### TransitionFrame TransitionFrames hold a pointer to a `TransitionBlock`. The TransitionBlock holds a return address along with a `CalleeSavedRegisters` struct which has values for all ABI specified callee-saved registers. The SP can be found using the address of the TransitionBlock. Since the TransitionBlock will be the lowest element on the stack, the SP is the address of the TransitionBlock + sizeof(TransitionBlock). @@ -301,6 +308,8 @@ When updating the context from a TransitionFrame, the IP, SP, and all ABI specif * On ARM, the additional register values stored in `ArgumentRegisters` are copied over. The `TransitionBlock` holds a pointer to the `ArgumentRegister` struct containing these values. +**Return Address**: Read from `TransitionBlock.ReturnAddress`. This applies to all frame types that use the TransitionFrame mechanism. + The following Frame types also use this mechanism: * FramedMethodFrame * PInvokeCallIFrame @@ -314,12 +323,16 @@ The following Frame types also use this mechanism: FuncEvalFrames hold a pointer to a `DebuggerEval`. The DebuggerEval holds a full context which is completely copied over to the working context when updating. +**Return Address**: Returns null during exception evaluation (`EvalDuringException`). Otherwise, read from `TransitionBlock.ReturnAddress` like other TransitionFrame types. + #### ResumableFrame ResumableFrames hold a pointer to a context object (Note this is different from SoftwareExceptionFrames which hold the context directly). The entire context object is copied over to the working context when updating. RedirectedThreadFrames also use this mechanism. +**Return Address**: Extracted from the saved context's instruction pointer (`TargetContextPtr` -> context IP). + #### FaultingExceptionFrame FaultingExceptionFrames have two different implementations. One for Windows x86 and another for all other builds (with funclets). @@ -328,10 +341,14 @@ Given the cDAC does not yet support Windows x86, this version is not supported. The other version stores a context struct. To update the working context, the entire stored context is copied over. In addition the `ContextFlags` are updated to ensure the `CONTEXT_XSTATE` bit is not set given the debug version of the contexts can not store extended state. This bit is architecture specific. +**Return Address**: Extracted from the saved context's instruction pointer (`TargetContext` -> context IP). + #### HijackFrame HijackFrames carry a IP (ReturnAddress) and a pointer to `HijackArgs`. All platforms update the IP and use the platform specific HijackArgs to update further registers. The following details currently implemented platforms. +**Return Address**: Read from the `ReturnAddress` field directly. + * x64 - On x64, HijackArgs contains a CalleeSavedRegister struct. The saved registers values contained in the struct are copied over to the working context. * Windows - On Windows, HijackArgs also contains the SP value directly which is copied over to the working context. * Non-Windows - On OS's other than Windows, HijackArgs does not contain an SP value. Instead since the HijackArgs struct lives on the stack, the SP is `&hijackArgs + sizeof(HijackArgs)`. This value is also copied over. @@ -343,6 +360,8 @@ HijackFrames carry a IP (ReturnAddress) and a pointer to `HijackArgs`. All platf TailCallFrames only appear on x86 Windows. They hold a `CalleeSavedRegisters` struct as well as a `ReturnAddress`. While the stack pointer is not directly contained in the TailCallFrame structure, it will be on the stack immediately following the Frame (found at the address of the Frame + size of the Frame). To process these Frames, update all of the registers in `CalleeSavedRegisters`, the instruction pointer from the stored return address, and the stack pointer from the address saved on the stack. +**Return Address**: Read from the `ReturnAddress` field directly. + ### APIs The majority of the contract's complexity is the stack walking algorithm (detailed above) implemented as part of `CreateStackWalk`. @@ -411,6 +430,78 @@ TargetPointer GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHandle) TargetPointer GetInstructionPointer(IStackDataFrameHandle stackDataFrameHandle) ``` +`WalkStackReferences` walks the entire managed stack and enumerates all live GC references at each frame. It returns a list of `StackReferenceData` describing each GC-tracked slot (its address, whether it's an interior pointer, and the register/stack location). This API is the primary consumer for `SOSDacImpl.GetStackReferences`. + +```csharp +IReadOnlyList WalkStackReferences(ThreadData threadData) +``` + +The implementation uses the same stack walk algorithm as `CreateStackWalk`, but integrates the GC-aware `Filter` directly (rather than consuming pre-generated frames) and performs GC reference enumeration at each frame. See [GC Stack Reference Scanning](#gc-stack-reference-scanning) for details. + +### GC Stack Reference Scanning + +`WalkStackReferences` scans the stack for GC references by walking through each frame and reporting live object references and interior pointers. The native equivalent is `DacStackReferenceWalker` which calls `GcStackCrawlCallBack` at each frame. + +#### Stack Walk Integration + +The GC reference walk uses the `Filter` function to drive the stack walk. `Filter` is a port of native `StackFrameIterator::Filter` (with `GC_FUNCLET_REFERENCE_REPORTING` mode) that handles funclet-to-parent frame transitions, exception tracker correlation, and determines whether each frame should report GC references. Unlike `CreateStackWalk` which yields all frames, `Filter` calls `Next()` directly and may skip frames that don't contribute GC roots. + +Key state tracked during the walk: + +- **IsInterrupted**: Set when transitioning to a managed frame from a `FaultingExceptionFrame` or `SoftwareExceptionFrame` (frames with `FRAME_ATTR_EXCEPTION`). When true, the managed frame's GC enumeration uses `ExecutionAborted` mode, which causes the GcInfoDecoder to skip live slot reporting at non-interruptible offsets. +- **LastProcessedFrameType**: Records the frame type when processing `SW_FRAME` state, so `UpdateState` can detect exception frames during the transition to `SW_FRAMELESS`. +- **IsFirst**: Preserved during skipped frame processing (native `SFITER_SKIPPED_FRAME_FUNCTION` does not modify `IsFirst`), ensuring the subsequent managed frame is still treated as the leaf/active frame. +- **GetReturnAddress gating**: In `SW_FRAME` state, `UpdateContextFromFrame` is only called when `GetReturnAddress()` returns a non-null value. This matches native behavior where the context is only updated when the frame has a valid return address. + +#### Per-Frame GC Enumeration + +At each frame yielded by `Filter`, the walk determines whether to scan for GC references: + +**Managed (frameless) frames** use `EnumGcRefsForManagedFrame`: + +1. Get the code block handle and relative offset from the `ExecutionManager` contract. +2. Decode the GCInfo for the code block via the `GCInfo` contract. +3. Determine `CodeManagerFlags`: set `ActiveStackFrame` if this is the leaf frame (`IsFirst`), `ExecutionAborted` if the frame was interrupted, `ParentOfFuncletStackFrame` if funclet GC reporting was delegated to the parent, `NoReportUntracked` if the code block is a filter funclet. +4. **Catch handler offset override**: When `ShouldParentFrameUseUnwindTargetPCforGCReporting` is set (parent frame resuming from a catch handler), the GC liveness offset is overridden to the first interruptible point within the catch handler clause range. This uses `GetInterruptibleRanges` from the `GCInfo` contract. See [How EH affects GC info/reporting](../coreclr/botr/clr-abi.md#how-eh-affects-gc-inforeporting) for background on why this override is needed. +5. Call `GcInfoDecoder.EnumerateLiveSlots` with the computed offset and flags to report all live register and stack slots. See the [GCInfo contract — EnumerateLiveSlots](./GCInfo.md#enumerateliveslots) for details on the algorithm. + +**Capital "F" Frames** use `GcScanRoots`, which dispatches based on frame type: + +- **StubDispatchFrame / ExternalMethodFrame**: Resolve GCRefMap via `FindGCRefMap` using the frame's `Indirection` pointer, otherwise fall back to signature-based scanning. +- **DynamicHelperFrame**: Use flag-based scanning (`DynamicHelperFrameFlags`). +- **PrestubMethodFrame / CallCountingHelperFrame**: Use signature-based scanning. +- Other frame types: No GC roots to report. + +See [GCRefMap Format and Resolution](#gcrefmap-format-and-resolution) for the GCRefMap scanning path details. + +### GCRefMap Format and Resolution + +A **GCRefMap** is a compact per-callsite encoding that describes which stack slots in a `TransitionBlock` contain GC references. GCRefMaps are pre-computed by the ReadyToRun compiler and stored in the PE image's import section auxiliary data. + +The GCRefMap encoding format — including token values, bit encoding, lookup table structure, and per-architecture position semantics — is documented in the [ReadyToRun format specification](../coreclr/botr/readytorun-format.md#readytorun_import_sectionsauxiliarydata). + +#### Resolution Flow + +GCRefMap resolution from a frame's `Indirection` pointer proceeds as follows: + +1. Call `FindReadyToRunModule(indirection)` (see [ExecutionManager contract](./ExecutionManager.md)) to find the ReadyToRun module containing the import slot. +2. Load the module's `ReadyToRunInfo` to access the import section array. +3. Compute the RVA of the indirection address: `rva = indirection - imageBase`. +4. Search through `READYTORUN_IMPORT_SECTION` entries to find the section containing the RVA. +5. Compute the slot index within the section: `index = (rva - sectionVA) / entrySize`. +6. Use the section's `AuxiliaryData` RVA to locate the GCRefMap lookup table. +7. Use stride-based lookup (stride = 1024) plus linear scan to find the specific GCRefMap entry. + +#### Slot Mapping + +GCRefMap positions map to `TransitionBlock` offsets using the formula: + +```csharp +slotAddress = transitionBlockPtr + FirstGCRefMapSlot + (position * pointerSize) +``` + +Where `FirstGCRefMapSlot` is the byte offset in the `TransitionBlock` where GCRefMap slot enumeration begins (platform-dependent: on ARM64 it is the return buffer argument register offset; on other platforms it is the argument registers offset). + ### x86 Specifics The x86 platform has some major differences to other platforms. In general this stems from the platform being older and not having a defined unwinding codes. Instead, to unwind managed frames, we rely on GCInfo associated with JITted code. For the unwind, we do not defer to a 'Windows like' native unwinder, instead the custom unwinder implementation was ported to managed code. diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 9b9d2d21f191eb..114b9dd2bbb022 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1006,16 +1006,12 @@ CDAC_TYPE_SIZE(sizeof(StubDispatchFrame)) CDAC_TYPE_FIELD(StubDispatchFrame, T_POINTER, RepresentativeMTPtr, cdac_data::RepresentativeMTPtr) CDAC_TYPE_FIELD(StubDispatchFrame, T_POINTER, MethodDescPtr, cdac_data::MethodDescPtr) CDAC_TYPE_FIELD(StubDispatchFrame, T_UINT32, RepresentativeSlot, cdac_data::RepresentativeSlot) -CDAC_TYPE_FIELD(StubDispatchFrame, T_POINTER, GCRefMap, cdac_data::GCRefMap) -CDAC_TYPE_FIELD(StubDispatchFrame, T_POINTER, ZapModule, cdac_data::ZapModule) CDAC_TYPE_FIELD(StubDispatchFrame, T_POINTER, Indirection, cdac_data::Indirection) CDAC_TYPE_END(StubDispatchFrame) CDAC_TYPE_BEGIN(ExternalMethodFrame) CDAC_TYPE_SIZE(sizeof(ExternalMethodFrame)) -CDAC_TYPE_FIELD(ExternalMethodFrame, T_POINTER, GCRefMap, cdac_data::GCRefMap) CDAC_TYPE_FIELD(ExternalMethodFrame, T_POINTER, Indirection, cdac_data::Indirection) -CDAC_TYPE_FIELD(ExternalMethodFrame, T_POINTER, ZapModule, cdac_data::ZapModule) CDAC_TYPE_END(ExternalMethodFrame) CDAC_TYPE_BEGIN(DynamicHelperFrame) diff --git a/src/coreclr/vm/frames.h b/src/coreclr/vm/frames.h index 708b7eec57f983..00f4d86f2578c3 100644 --- a/src/coreclr/vm/frames.h +++ b/src/coreclr/vm/frames.h @@ -1490,8 +1490,6 @@ struct cdac_data { static constexpr size_t RepresentativeMTPtr = offsetof(StubDispatchFrame, m_pRepresentativeMT); static constexpr uint32_t RepresentativeSlot = offsetof(StubDispatchFrame, m_representativeSlot); - static constexpr size_t GCRefMap = offsetof(StubDispatchFrame, m_pGCRefMap); - static constexpr size_t ZapModule = offsetof(StubDispatchFrame, m_pZapModule); static constexpr size_t Indirection = offsetof(StubDispatchFrame, m_pIndirection); }; @@ -1573,9 +1571,7 @@ typedef DPTR(class ExternalMethodFrame) PTR_ExternalMethodFrame; template <> struct cdac_data { - static constexpr size_t GCRefMap = offsetof(ExternalMethodFrame, m_pGCRefMap); static constexpr size_t Indirection = offsetof(ExternalMethodFrame, m_pIndirection); - static constexpr size_t ZapModule = offsetof(ExternalMethodFrame, m_pZapModule); }; class DynamicHelperFrame : public FramedMethodFrame diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs index bb31d1434b8832..1856c2c3a4ea9d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs @@ -101,12 +101,6 @@ public interface IExecutionManager : IContract List GetExceptionClauses(CodeBlockHandle codeInfoHandle) => throw new NotImplementedException(); JitManagerInfo GetEEJitManagerInfo() => throw new NotImplementedException(); IEnumerable GetCodeHeapInfos() => throw new NotImplementedException(); - - /// - /// Finds the R2R module that contains the given address. - /// Used by FindGCRefMap to resolve m_pZapModule when it's null. - /// Matches native ExecutionManager::FindReadyToRunModule (codeman.cpp). - /// TargetPointer FindReadyToRunModule(TargetPointer address) => throw new NotImplementedException(); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs index 70ab5217cd7976..8a70ba34aae189 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs @@ -7,16 +7,52 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts; public interface IGCInfoHandle { } + +/// +/// Describes a code region where the GC can safely interrupt execution. +/// +/// Start of the interruptible region, as a byte offset from the method start. +/// End of the interruptible region (exclusive), as a byte offset from the method start. public readonly record struct InterruptibleRange(uint StartOffset, uint EndOffset); +/// +/// Describes a live GC slot at a given instruction offset. +/// +/// True if the slot is a CPU register; false if it is a stack location. +/// Register number (meaningful only when IsRegister is true). +/// Stack offset from the base (meaningful only when IsRegister is false). +/// Stack base: 0 = CALLER_SP_REL, 1 = SP_REL, 2 = FRAMEREG_REL. +/// GC slot flags: 0x1 = interior pointer, 0x2 = pinned. +public readonly record struct LiveSlot(bool IsRegister, uint RegisterNumber, int SpOffset, uint SpBase, uint GcFlags); + +/// +/// Options controlling which GC slots are reported by . +/// +public record struct GcSlotEnumerationOptions +{ + /// True if this is the active (leaf) stack frame. When false, scratch register and stack slots are excluded. + public bool IsActiveFrame { get; set; } + /// True if execution was aborted (e.g., interrupted by exception). Skips live slot reporting at non-interruptible offsets. + public bool IsExecutionAborted { get; set; } + /// True if the frame is a parent of a funclet that already reported GC references. + public bool IsParentOfFuncletStackFrame { get; set; } + /// True to suppress reporting of untracked slots (e.g., for filter funclets). + public bool SuppressUntrackedSlots { get; set; } + /// True to report only frame-register-relative stack slots (skips all register slots and non-frame-relative stack slots). + public bool ReportFPBasedSlotsOnly { get; set; } +} + public interface IGCInfo : IContract { static string IContract.Name { get; } = nameof(GCInfo); IGCInfoHandle DecodePlatformSpecificGCInfo(TargetPointer gcInfoAddress, uint gcVersion) => throw new NotImplementedException(); IGCInfoHandle DecodeInterpreterGCInfo(TargetPointer gcInfoAddress, uint gcVersion) => throw new NotImplementedException(); + uint GetCodeLength(IGCInfoHandle handle) => throw new NotImplementedException(); + uint GetStackBaseRegister(IGCInfoHandle handle) => throw new NotImplementedException(); IReadOnlyList GetInterruptibleRanges(IGCInfoHandle handle) => throw new NotImplementedException(); + IReadOnlyList EnumerateLiveSlots(IGCInfoHandle handle, uint instructionOffset, GcSlotEnumerationOptions options) => throw new NotImplementedException(); } public readonly struct GCInfo : IGCInfo diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs index 20a300df238ec7..aa7d919b8aa8d6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs @@ -512,57 +512,50 @@ public uint GetCodeLength() return _codeLength; } - public IReadOnlyList GetInterruptibleRanges() + public uint GetStackBaseRegister() { - EnsureDecodedTo(DecodePoints.InterruptibleRanges); - return _interruptibleRanges; + EnsureDecodedTo(DecodePoints.ReversePInvoke); + return _stackBaseRegister; } - public uint StackBaseRegister + public IReadOnlyList GetInterruptibleRanges() { - get - { - EnsureDecodedTo(DecodePoints.ReversePInvoke); - return _stackBaseRegister; - } + EnsureDecodedTo(DecodePoints.InterruptibleRanges); + return _interruptibleRanges; } public uint NumTrackedSlots => _numSlots - _numUntrackedSlots; - bool IGCInfoDecoder.EnumerateLiveSlots( + IReadOnlyList IGCInfoDecoder.EnumerateLiveSlots( uint instructionOffset, - CodeManagerFlags flags, - LiveSlotCallback reportSlot) + GcSlotEnumerationOptions options) { - return EnumerateLiveSlots(instructionOffset, flags, + List result = []; + EnumerateLiveSlots(instructionOffset, options, (uint slotIndex, GcSlotDesc slot, uint gcFlags) => { - reportSlot(slot.IsRegister, slot.RegisterNumber, slot.SpOffset, (uint)slot.Base, gcFlags); + result.Add(new LiveSlot(slot.IsRegister, slot.RegisterNumber, slot.SpOffset, (uint)slot.Base, gcFlags)); }); + return result; } /// /// Enumerates all GC slots that are live at the given instruction offset, invoking the callback for each. /// This is the managed equivalent of the native GcInfoDecoder::EnumerateLiveSlots. /// - /// The current instruction offset (relative to method start). - /// CodeManagerFlags controlling reporting behavior. - /// Called for each live slot with (slotIndex, slotDesc, gcFlags). - /// gcFlags contains GC_SLOT_INTERIOR/GC_SLOT_PINNED from the slot descriptor. - /// True if enumeration succeeded. - public bool EnumerateLiveSlots( + private bool EnumerateLiveSlots( uint instructionOffset, - CodeManagerFlags flags, + GcSlotEnumerationOptions options, Action reportSlot) { EnsureDecodedTo(DecodePoints.SlotTable); - bool executionAborted = flags.HasFlag(CodeManagerFlags.ExecutionAborted); - bool reportScratchSlots = flags.HasFlag(CodeManagerFlags.ActiveStackFrame); - bool reportFpBasedSlotsOnly = flags.HasFlag(CodeManagerFlags.ReportFPBasedSlotsOnly); + bool executionAborted = options.IsExecutionAborted; + bool reportScratchSlots = options.IsActiveFrame; + bool reportFpBasedSlotsOnly = options.ReportFPBasedSlotsOnly; // WantsReportOnlyLeaf is always true for non-legacy formats - if (flags.HasFlag(CodeManagerFlags.ParentOfFuncletStackFrame)) + if (options.IsParentOfFuncletStackFrame) return true; uint numTracked = NumTrackedSlots; @@ -817,7 +810,7 @@ public bool EnumerateLiveSlots( bool ReportUntrackedAndSucceed() { - if (_numUntrackedSlots > 0 && (flags & (CodeManagerFlags.ParentOfFuncletStackFrame | CodeManagerFlags.NoReportUntracked)) == 0) + if (_numUntrackedSlots > 0 && !options.IsParentOfFuncletStackFrame && !options.SuppressUntrackedSlots) { // Native passes reportScratchSlots=true for untracked slots (see native // ReportUntrackedSlots: "Report everything (although there should *never* diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs index 9e6ce0252128bd..db1dc4dd79d519 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs @@ -28,12 +28,24 @@ uint IGCInfo.GetCodeLength(IGCInfoHandle gcInfoHandle) return handle.GetCodeLength(); } + uint IGCInfo.GetStackBaseRegister(IGCInfoHandle gcInfoHandle) + { + IGCInfoDecoder handle = AssertCorrectHandle(gcInfoHandle); + return handle.GetStackBaseRegister(); + } + IReadOnlyList IGCInfo.GetInterruptibleRanges(IGCInfoHandle gcInfoHandle) { IGCInfoDecoder handle = AssertCorrectHandle(gcInfoHandle); return handle.GetInterruptibleRanges(); } + IReadOnlyList IGCInfo.EnumerateLiveSlots(IGCInfoHandle gcInfoHandle, uint instructionOffset, GcSlotEnumerationOptions options) + { + IGCInfoDecoder handle = AssertCorrectHandle(gcInfoHandle); + return handle.EnumerateLiveSlots(instructionOffset, options); + } + private static IGCInfoDecoder AssertCorrectHandle(IGCInfoHandle gcInfoHandle) { if (gcInfoHandle is not IGCInfoDecoder handle) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs index e21bc79661062f..fcf7aa46c691bb 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs @@ -7,40 +7,10 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; -/// -/// Flags controlling GC reference reporting behavior. -/// These match the native ICodeManager flags in eetwain.h. -/// -[Flags] -internal enum CodeManagerFlags : uint -{ - ActiveStackFrame = 0x1, - ExecutionAborted = 0x2, - ParentOfFuncletStackFrame = 0x40, - NoReportUntracked = 0x80, - ReportFPBasedSlotsOnly = 0x200, -} - internal interface IGCInfoDecoder : IGCInfoHandle { uint GetCodeLength(); - uint StackBaseRegister { get; } - - /// - /// Gets the interruptible code ranges decoded from the GC info. - /// + uint GetStackBaseRegister(); IReadOnlyList GetInterruptibleRanges(); - - /// - /// Enumerates all live GC slots at the given instruction offset. - /// - /// Relative offset from method start. - /// CodeManagerFlags controlling reporting. - /// Callback: (isRegister, registerNumber, spOffset, spBase, gcFlags). - bool EnumerateLiveSlots( - uint instructionOffset, - CodeManagerFlags flags, - LiveSlotCallback reportSlot); + IReadOnlyList EnumerateLiveSlots(uint instructionOffset, GcSlotEnumerationOptions options); } - -internal delegate void LiveSlotCallback(bool isRegister, uint registerNumber, int spOffset, uint spBase, uint gcFlags); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs index 62954af93c4e4e..1a2f18a31b8cef 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs @@ -335,11 +335,11 @@ internal void GcScanRoots(TargetPointer frameAddress, GcScanContext scanContext) { Data.FramedMethodFrame fmf = target.ProcessedData.GetOrAdd(frameAddress); Data.StubDispatchFrame sdf = target.ProcessedData.GetOrAdd(frameAddress); - TargetPointer gcRefMap = sdf.GCRefMap; - // Resolve GCRefMap via indirection if not yet cached - if (gcRefMap == TargetPointer.Null && sdf.Indirection != TargetPointer.Null) - gcRefMap = FindGCRefMap(sdf.ZapModule, sdf.Indirection); + // Always resolve GCRefMap via FindGCRefMap from the indirection address. + TargetPointer gcRefMap = sdf.Indirection != TargetPointer.Null + ? FindGCRefMap(sdf.Indirection) + : TargetPointer.Null; if (gcRefMap != TargetPointer.Null) PromoteCallerStackUsingGCRefMap(fmf.TransitionBlockPtr, gcRefMap, scanContext); @@ -352,11 +352,11 @@ internal void GcScanRoots(TargetPointer frameAddress, GcScanContext scanContext) { Data.FramedMethodFrame fmf = target.ProcessedData.GetOrAdd(frameAddress); Data.ExternalMethodFrame emf = target.ProcessedData.GetOrAdd(frameAddress); - TargetPointer gcRefMap = emf.GCRefMap; - // Resolve GCRefMap via FindGCRefMap if not yet cached by the runtime - if (gcRefMap == TargetPointer.Null && emf.Indirection != TargetPointer.Null) - gcRefMap = FindGCRefMap(emf.ZapModule, emf.Indirection); + // Always resolve GCRefMap via FindGCRefMap from the indirection address. + TargetPointer gcRefMap = emf.Indirection != TargetPointer.Null + ? FindGCRefMap(emf.Indirection) + : TargetPointer.Null; if (gcRefMap != TargetPointer.Null) PromoteCallerStackUsingGCRefMap(fmf.TransitionBlockPtr, gcRefMap, scanContext); @@ -465,24 +465,20 @@ private void ScanDynamicHelperFrame( } /// - /// Resolves the GCRefMap for a Frame with m_pIndirection set but m_pGCRefMap not yet cached. - /// Port of native FindGCRefMap (frames.cpp:853). + /// Resolves the GCRefMap for a Frame with m_pIndirection. + /// Port of native FindGCRefMap (frames.cpp). /// - private TargetPointer FindGCRefMap(TargetPointer zapModule, TargetPointer indirection) + private TargetPointer FindGCRefMap(TargetPointer indirection) { if (indirection == TargetPointer.Null) return TargetPointer.Null; - // If ZapModule is null, resolve it from the indirection address. + // Resolve the module from the indirection address. // Matches native GetGCRefMap which calls FindModuleForGCRefMap(m_pIndirection) - // → ExecutionManager::FindReadyToRunModule. + IExecutionManager eman = target.Contracts.ExecutionManager; + TargetPointer zapModule = eman.FindReadyToRunModule(indirection); if (zapModule == TargetPointer.Null) - { - IExecutionManager eman = target.Contracts.ExecutionManager; - zapModule = eman.FindReadyToRunModule(indirection); - if (zapModule == TargetPointer.Null) - return TargetPointer.Null; - } + return TargetPointer.Null; // Get the ReadyToRunInfo from the module Data.Module module = target.ProcessedData.GetOrAdd(zapModule); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs deleted file mode 100644 index fa72eb606fad75..00000000000000 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ /dev/null @@ -1,109 +0,0 @@ -// 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 Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; - -namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; - -internal class GcScanner -{ - private readonly Target _target; - private readonly IExecutionManager _eman; - private readonly IGCInfo _gcInfo; - - internal GcScanner(Target target) - { - _target = target; - _eman = target.Contracts.ExecutionManager; - _gcInfo = target.Contracts.GCInfo; - } - - public bool EnumGcRefs( - IPlatformAgnosticContext context, - CodeBlockHandle cbh, - CodeManagerFlags flags, - GcScanContext scanContext) - { - TargetNUInt relativeOffset = _eman.GetRelativeOffset(cbh); - _eman.GetGCInfo(cbh, out TargetPointer gcInfoAddr, out uint gcVersion); - - if (_eman.IsFilterFunclet(cbh)) - flags |= CodeManagerFlags.NoReportUntracked; - - IGCInfoHandle handle = _gcInfo.DecodePlatformSpecificGCInfo(gcInfoAddr, gcVersion); - if (handle is not IGCInfoDecoder decoder) - return false; - - uint stackBaseRegister = decoder.StackBaseRegister; - - // Lazily compute the caller SP for GC_CALLER_SP_REL slots. - // The native code uses GET_CALLER_SP(pRD) which comes from EnsureCallerContextIsValid. - TargetPointer? callerSP = null; - - return decoder.EnumerateLiveSlots( - (uint)relativeOffset.Value, - flags, - (bool isRegister, uint registerNumber, int spOffset, uint spBase, uint gcFlags) => - { - GcScanFlags scanFlags = GcScanFlags.None; - if ((gcFlags & 0x1) != 0) // GC_SLOT_INTERIOR - scanFlags |= GcScanFlags.GC_CALL_INTERIOR; - if ((gcFlags & 0x2) != 0) // GC_SLOT_PINNED - scanFlags |= GcScanFlags.GC_CALL_PINNED; - - if (isRegister) - { - TargetPointer regValue = ReadRegisterValue(context, (int)registerNumber); - GcScanSlotLocation loc = new((int)registerNumber, 0, false); - scanContext.GCEnumCallback(regValue, scanFlags, loc); - } - else - { - int spReg = context.StackPointerRegister; - int reg = spBase switch - { - 1 => spReg, // GC_SP_REL → SP register number - 2 => (int)stackBaseRegister, // GC_FRAMEREG_REL → frame base register - 0 => -(spReg + 1), // GC_CALLER_SP_REL → -(SP + 1) - _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), - }; - TargetPointer baseAddr = spBase switch - { - 1 => context.StackPointer, // GC_SP_REL - 2 => ReadRegisterValue(context, (int)stackBaseRegister), // GC_FRAMEREG_REL - 0 => GetCallerSP(context, ref callerSP), // GC_CALLER_SP_REL - _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), - }; - - TargetPointer addr = new(baseAddr.Value + (ulong)(long)spOffset); - GcScanSlotLocation loc = new(reg, spOffset, true); - scanContext.GCEnumCallback(addr, scanFlags, loc); - } - }); - } - - /// - /// Compute the caller's SP by unwinding the current context one frame. - /// Cached in to avoid repeated unwinds for the same frame. - /// - private TargetPointer GetCallerSP(IPlatformAgnosticContext context, ref TargetPointer? cached) - { - if (cached is null) - { - IPlatformAgnosticContext callerContext = context.Clone(); - callerContext.Unwind(_target); - cached = callerContext.StackPointer; - } - return cached.Value; - } - - private static TargetPointer ReadRegisterValue(IPlatformAgnosticContext context, int registerNumber) - { - if (!context.TryReadRegister(registerNumber, out TargetNUInt value)) - throw new ArgumentOutOfRangeException(nameof(registerNumber), $"Register number {registerNumber} not found"); - - return new TargetPointer(value.Value); - } - -} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs index 1b4fef66afe058..76428214d54df4 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs @@ -8,7 +8,6 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; -using Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; using Microsoft.Diagnostics.DataContractReader.Data; using System.Linq; @@ -206,19 +205,18 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre if (!IsManaged(gcFrame.Frame.Context.InstructionPointer, out CodeBlockHandle? cbh)) throw new InvalidOperationException("Expected managed code"); - CodeManagerFlags codeManagerFlags = gcFrame.Frame.IsActiveFrame - ? CodeManagerFlags.ActiveStackFrame - : 0; - - // If the frame was interrupted by an exception (reached via a - // FaultingExceptionFrame), set ExecutionAborted so the GcInfoDecoder - // skips live slot reporting at non-interruptible offsets. This matches - // native CrawlFrame::GetCodeManagerFlags (stackwalk.h). - if (gcFrame.IsInterrupted) - codeManagerFlags |= CodeManagerFlags.ExecutionAborted; + GcSlotEnumerationOptions gcOptions = new() + { + IsActiveFrame = gcFrame.Frame.IsActiveFrame, - if (gcFrame.ShouldParentToFuncletSkipReportingGCReferences) - codeManagerFlags |= CodeManagerFlags.ParentOfFuncletStackFrame; + // If the frame was interrupted by an exception (reached via a + // FaultingExceptionFrame), set ExecutionAborted so the GcInfoDecoder + // skips live slot reporting at non-interruptible offsets. This matches + // native CrawlFrame::GetCodeManagerFlags (stackwalk.h). + IsExecutionAborted = gcFrame.IsInterrupted, + IsParentOfFuncletStackFrame = gcFrame.ShouldParentToFuncletSkipReportingGCReferences, + SuppressUntrackedSlots = _eman.IsFilterFunclet(cbh.Value), + }; uint? relOffsetOverride = null; if (gcFrame.ShouldParentFrameUseUnwindTargetPCforGCReporting) @@ -244,7 +242,7 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre } } - EnumGcRefsForManagedFrame(gcFrame.Frame.Context, cbh.Value, codeManagerFlags, scanContext, relOffsetOverride); + EnumGcRefsForManagedFrame(gcFrame.Frame.Context, cbh.Value, gcOptions, scanContext, relOffsetOverride); } else { @@ -913,67 +911,61 @@ private static StackDataFrameHandle AssertCorrectHandle(IStackDataFrameHandle st private void EnumGcRefsForManagedFrame( IPlatformAgnosticContext context, CodeBlockHandle cbh, - CodeManagerFlags flags, + GcSlotEnumerationOptions options, GcScanContext scanContext, uint? relOffsetOverride = null) { TargetNUInt relativeOffset = _eman.GetRelativeOffset(cbh); _eman.GetGCInfo(cbh, out TargetPointer gcInfoAddr, out uint gcVersion); - if (_eman.IsFilterFunclet(cbh)) - flags |= CodeManagerFlags.NoReportUntracked; - - IGCInfoHandle handle = _target.Contracts.GCInfo.DecodePlatformSpecificGCInfo(gcInfoAddr, gcVersion); - if (handle is not IGCInfoDecoder decoder) - return; + IGCInfo gcInfo = _target.Contracts.GCInfo; + IGCInfoHandle handle = gcInfo.DecodePlatformSpecificGCInfo(gcInfoAddr, gcVersion); - uint stackBaseRegister = decoder.StackBaseRegister; + uint stackBaseRegister = gcInfo.GetStackBaseRegister(handle); TargetPointer? callerSP = null; uint offsetToUse = relOffsetOverride ?? (uint)relativeOffset.Value; - decoder.EnumerateLiveSlots( - offsetToUse, - flags, - (bool isRegister, uint registerNumber, int spOffset, uint spBase, uint gcFlags) => - { - GcScanFlags scanFlags = GcScanFlags.None; - if ((gcFlags & 0x1) != 0) - scanFlags |= GcScanFlags.GC_CALL_INTERIOR; - if ((gcFlags & 0x2) != 0) - scanFlags |= GcScanFlags.GC_CALL_PINNED; + IReadOnlyList liveSlots = gcInfo.EnumerateLiveSlots(handle, offsetToUse, options); + foreach (LiveSlot slot in liveSlots) + { + GcScanFlags scanFlags = GcScanFlags.None; + if ((slot.GcFlags & 0x1) != 0) + scanFlags |= GcScanFlags.GC_CALL_INTERIOR; + if ((slot.GcFlags & 0x2) != 0) + scanFlags |= GcScanFlags.GC_CALL_PINNED; - if (isRegister) + if (slot.IsRegister) + { + if (!context.TryReadRegister((int)slot.RegisterNumber, out TargetNUInt regValue)) + continue; + GcScanSlotLocation loc = new((int)slot.RegisterNumber, 0, false); + scanContext.GCEnumCallback(new TargetPointer(regValue.Value), scanFlags, loc); + } + else + { + int spReg = context.StackPointerRegister; + int reg = slot.SpBase switch { - if (!context.TryReadRegister((int)registerNumber, out TargetNUInt regValue)) - return; - GcScanSlotLocation loc = new((int)registerNumber, 0, false); - scanContext.GCEnumCallback(new TargetPointer(regValue.Value), scanFlags, loc); - } - else + 1 => spReg, + 2 => (int)stackBaseRegister, + 0 => -(spReg + 1), + _ => throw new InvalidOperationException($"Unknown stack slot base: {slot.SpBase}"), + }; + TargetPointer baseAddr = slot.SpBase switch { - int spReg = context.StackPointerRegister; - int reg = spBase switch - { - 1 => spReg, - 2 => (int)stackBaseRegister, - 0 => -(spReg + 1), - _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), - }; - TargetPointer baseAddr = spBase switch - { - 1 => context.StackPointer, - 2 => context.TryReadRegister((int)stackBaseRegister, out TargetNUInt val) - ? new TargetPointer(val.Value) - : throw new InvalidOperationException($"Failed to read register {stackBaseRegister}"), - 0 => GetCallerSP(context, ref callerSP), - _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), - }; - - TargetPointer addr = new(baseAddr.Value + (ulong)(long)spOffset); - GcScanSlotLocation loc = new(reg, spOffset, true); - scanContext.GCEnumCallback(addr, scanFlags, loc); - } - }); + 1 => context.StackPointer, + 2 => context.TryReadRegister((int)stackBaseRegister, out TargetNUInt val) + ? new TargetPointer(val.Value) + : throw new InvalidOperationException($"Failed to read register {stackBaseRegister}"), + 0 => GetCallerSP(context, ref callerSP), + _ => throw new InvalidOperationException($"Unknown stack slot base: {slot.SpBase}"), + }; + + TargetPointer addr = new(baseAddr.Value + (ulong)(long)slot.SpOffset); + GcScanSlotLocation loc = new(reg, slot.SpOffset, true); + scanContext.GCEnumCallback(addr, scanFlags, loc); + } + } } private TargetPointer GetCallerSP(IPlatformAgnosticContext context, ref TargetPointer? cached) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs index cfc3e92be93297..108fe0d0a5b70d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs @@ -11,12 +11,8 @@ static ExternalMethodFrame IData.Create(Target target, Targ public ExternalMethodFrame(Target target, TargetPointer address) { Target.TypeInfo type = target.GetTypeInfo(DataType.ExternalMethodFrame); - GCRefMap = target.ReadPointerField(address, type, nameof(GCRefMap)); Indirection = target.ReadPointerField(address, type, nameof(Indirection)); - ZapModule = target.ReadPointerField(address, type, nameof(ZapModule)); } - public TargetPointer GCRefMap { get; } public TargetPointer Indirection { get; } - public TargetPointer ZapModule { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs index da2e1a493f602b..a063f9f3a0dc52 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs @@ -14,8 +14,6 @@ public StubDispatchFrame(Target target, TargetPointer address) MethodDescPtr = target.ReadPointerField(address, type, nameof(MethodDescPtr)); RepresentativeMTPtr = target.ReadPointerField(address, type, nameof(RepresentativeMTPtr)); RepresentativeSlot = target.ReadField(address, type, nameof(RepresentativeSlot)); - GCRefMap = target.ReadPointerField(address, type, nameof(GCRefMap)); - ZapModule = target.ReadPointerField(address, type, nameof(ZapModule)); Indirection = target.ReadPointerField(address, type, nameof(Indirection)); Address = address; } @@ -24,7 +22,5 @@ public StubDispatchFrame(Target target, TargetPointer address) public TargetPointer MethodDescPtr { get; } public TargetPointer RepresentativeMTPtr { get; } public uint RepresentativeSlot { get; } - public TargetPointer GCRefMap { get; } - public TargetPointer ZapModule { get; } public TargetPointer Indirection { get; } }