From 7c419ecf4c929faf6b6b762972f31871a25b28e3 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 24 Apr 2026 12:17:42 -0400 Subject: [PATCH] [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)