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..62714b3266a672 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,31 @@ 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 +// Negative offset to where float argument registers are saved (relative to TransitionBlock pointer). +// This is -sizeof(FloatArgumentRegisters) (-padding) on platforms that have them, 0 otherwise. +#ifdef CALLDESCR_FPARGREGS +#ifdef TARGET_ARM +// ARM has 8-byte alignment padding after FloatArgumentRegisters +CDAC_TYPE_FIELD(TransitionBlock, T_INT32, OffsetOfFloatArgumentRegisters, -(int)(sizeof(FloatArgumentRegisters) + TARGET_POINTER_SIZE)) +#else +CDAC_TYPE_FIELD(TransitionBlock, T_INT32, OffsetOfFloatArgumentRegisters, -(int)sizeof(FloatArgumentRegisters)) +#endif +#else +CDAC_TYPE_FIELD(TransitionBlock, T_INT32, OffsetOfFloatArgumentRegisters, 0) +#endif CDAC_TYPE_END(TransitionBlock) #ifdef DEBUGGING_SUPPORTED @@ -989,8 +1018,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 +1418,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/CallingConvention/ArgIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIterator.cs new file mode 100644 index 00000000000000..cbb42062124f0a --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIterator.cs @@ -0,0 +1,797 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Provides an abstraction over platform specific calling conventions. +// Ported from crossgen2's ArgIterator.cs. + +using System; +using System.Diagnostics; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.CallingConvention; + +/// +/// Enumerates method arguments and maps each to a register or stack offset +/// within a TransitionBlock. +/// Ported from crossgen2's ArgIterator. +/// +internal struct ArgIterator +{ + private readonly CallingConventionInfo _ccInfo; + private readonly ArgIteratorData _argData; + private readonly bool _hasThis; + private readonly bool _hasParamType; + private readonly bool _hasAsyncContinuation; + private readonly bool _extraFunctionPointerArg; + private readonly bool[] _forcedByRefParams; + private readonly bool _skipFirstArg; + private readonly bool _extraObjectFirstArg; + + // Iteration state + private bool _ITERATION_STARTED; + private bool _SIZE_OF_ARG_STACK_COMPUTED; + private bool _RETURN_FLAGS_COMPUTED; + private bool _RETURN_HAS_RET_BUFFER; + + private CorElementType _argType; + private ArgTypeInfo _argTypeHandle; + private ArgTypeInfo _argTypeHandleOfByRefParam; + private int _argSize; + private int _argNum; + private bool _argForceByRef; + private int _nSizeOfArgStack; + + // Per-architecture register allocation state + // x86 + private int _x86NumRegistersUsed; + private int _x86OfsStack; + + // x64 Windows + private int _x64WindowsCurOfs; + + // x64 Unix + private int _x64UnixIdxGenReg; + private int _x64UnixIdxStack; + private int _x64UnixIdxFPReg; + + // ARM32 + private int _armIdxGenReg; + private int _armOfsStack; + private ushort _armWFPRegs; + private bool _armRequires64BitAlignment; + + // ARM64 + private int _arm64IdxGenReg; + private int _arm64OfsStack; + private int _arm64IdxFPReg; + + // LoongArch64 / RISC-V64 + private int _rvLa64IdxGenReg; + private int _rvLa64OfsStack; + private int _rvLa64IdxFPReg; + + // Struct-in-registers tracking + private bool _hasArgLocDescForStructInRegs; +#pragma warning disable CS0649 // Assigned in platform-specific paths (ARM64 HFA, Unix AMD64 struct-in-regs) + private ArgLocDesc _argLocDescForStructInRegs; +#pragma warning restore CS0649 + + // x86 param type location + private enum ParamTypeLocation + { + Stack, + Ecx, + Edx, + } + private ParamTypeLocation _paramTypeLoc; + + private enum AsyncContinuationLocation + { + Stack, + Ecx, + Edx, + } + private AsyncContinuationLocation _asyncContinuationLoc; + + public bool HasThis => _hasThis; + public bool IsVarArg => _argData.IsVarArg(); + public bool HasParamType => _hasParamType; + public bool HasAsyncContinuation => _hasAsyncContinuation; + public int NumFixedArgs => _argData.NumFixedArgs() + (_extraFunctionPointerArg ? 1 : 0) + (_extraObjectFirstArg ? 1 : 0); + + public ArgIterator( + CallingConventionInfo ccInfo, + ArgIteratorData argData, + bool hasParamType, + bool hasAsyncContinuation, + bool[] forcedByRefParams, + bool skipFirstArg = false, + bool extraObjectFirstArg = false, + bool extraFunctionPointerArg = false) + { + this = default; + _ccInfo = ccInfo; + _argData = argData; + _hasThis = argData.HasThis(); + _hasParamType = hasParamType; + _hasAsyncContinuation = hasAsyncContinuation; + _extraFunctionPointerArg = extraFunctionPointerArg; + _forcedByRefParams = forcedByRefParams; + _skipFirstArg = skipFirstArg; + _extraObjectFirstArg = extraObjectFirstArg; + } + + public CorElementType GetArgumentType(int argNum, out ArgTypeInfo thArgType, out bool forceByRefReturn) + { + forceByRefReturn = false; + + if (_extraObjectFirstArg && argNum == 0) + { + thArgType = ArgTypeInfo.ForPrimitive(CorElementType.Class, _ccInfo.PointerSize); + return CorElementType.Class; + } + + argNum = _extraObjectFirstArg ? argNum - 1 : argNum; + + if (_forcedByRefParams is not null && (argNum + 1) < _forcedByRefParams.Length) + forceByRefReturn = _forcedByRefParams[argNum + 1]; + + if (_extraFunctionPointerArg && argNum == _argData.NumFixedArgs()) + { + thArgType = ArgTypeInfo.ForPrimitive(CorElementType.I, _ccInfo.PointerSize); + return CorElementType.I; + } + + return _argData.GetArgumentType(argNum, out thArgType); + } + + public CorElementType GetReturnType(out ArgTypeInfo thRetType, out bool forceByRefReturn) + { + forceByRefReturn = _forcedByRefParams is not null && _forcedByRefParams.Length > 0 && _forcedByRefParams[0]; + return _argData.GetReturnType(out thRetType); + } + + public void Reset() + { + _argType = default; + _argTypeHandle = default; + _argSize = 0; + _argNum = 0; + _argForceByRef = false; + _ITERATION_STARTED = false; + } + + private uint SizeOfArgStack() + { + if (!_SIZE_OF_ARG_STACK_COMPUTED) + ForceSigWalk(); + Debug.Assert(_SIZE_OF_ARG_STACK_COMPUTED); + return (uint)_nSizeOfArgStack; + } + + public int SizeOfFrameArgumentArray() + { + uint size = SizeOfArgStack(); + + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X64 && !_ccInfo.IsX64UnixABI) + { + size += (uint)_ccInfo.SizeOfArgumentRegisters; + } + + return (int)size; + } + + public uint CbStackPop() + { + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X86) + { + return IsVarArg ? 0 : SizeOfArgStack(); + } + throw new NotImplementedException(); + } + + public bool HasRetBuffArg() + { + if (!_RETURN_FLAGS_COMPUTED) + ComputeReturnFlags(); + return _RETURN_HAS_RET_BUFFER; + } + + public int GetThisOffset() => _ccInfo.ThisOffset; + + public int GetVASigCookieOffset() + { + Debug.Assert(IsVarArg); + + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X86) + { + return (int)_ccInfo.SizeOfTransitionBlock; + } + + int ret = (int)_ccInfo.ArgumentRegistersOffset; + if (HasThis) + ret += _ccInfo.PointerSize; + if (HasRetBuffArg() && _ccInfo.IsRetBuffPassedAsFirstArg) + ret += _ccInfo.PointerSize; + return ret; + } + + public int GetParamTypeArgOffset() + { + Debug.Assert(HasParamType); + + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X86) + { + if (!_SIZE_OF_ARG_STACK_COMPUTED) + ForceSigWalk(); + + return _paramTypeLoc switch + { + ParamTypeLocation.Ecx => (int)_ccInfo.ArgumentRegistersOffset + _ccInfo.PointerSize, // ECX offset + ParamTypeLocation.Edx => (int)_ccInfo.ArgumentRegistersOffset, // EDX offset + _ => (int)_ccInfo.SizeOfTransitionBlock, + }; + } + + int ret = (int)_ccInfo.ArgumentRegistersOffset; + if (HasThis) ret += _ccInfo.PointerSize; + if (HasRetBuffArg() && _ccInfo.IsRetBuffPassedAsFirstArg) ret += _ccInfo.PointerSize; + return ret; + } + + public int GetAsyncContinuationArgOffset() + { + Debug.Assert(HasAsyncContinuation); + + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X86) + { + if (!_SIZE_OF_ARG_STACK_COMPUTED) + ForceSigWalk(); + + return _asyncContinuationLoc switch + { + AsyncContinuationLocation.Ecx => (int)_ccInfo.ArgumentRegistersOffset + _ccInfo.PointerSize, + AsyncContinuationLocation.Edx => (int)_ccInfo.ArgumentRegistersOffset, + _ => HasParamType && _paramTypeLoc == ParamTypeLocation.Stack + ? (int)_ccInfo.SizeOfTransitionBlock + _ccInfo.PointerSize + : (int)_ccInfo.SizeOfTransitionBlock, + }; + } + + int ret = (int)_ccInfo.ArgumentRegistersOffset; + if (HasThis) ret += _ccInfo.PointerSize; + if (HasRetBuffArg() && _ccInfo.IsRetBuffPassedAsFirstArg) ret += _ccInfo.PointerSize; + if (HasParamType) ret += _ccInfo.PointerSize; + return ret; + } + + public bool IsArgPassedByRef() + { + if (_argForceByRef) + return true; + if (_argType == CorElementType.Byref) + return true; + + if (_ccInfo.EnregisteredParamTypeMaxSize != 0) + { + return _ccInfo.Architecture switch + { + RuntimeInfoArchitecture.X64 => _ccInfo.IsArgPassedByRef(_argSize), + RuntimeInfoArchitecture.Arm64 => _argType == CorElementType.ValueType + && (_argSize > _ccInfo.EnregisteredParamTypeMaxSize) && (!_argTypeHandle.IsHomogeneousAggregate || IsVarArg), + RuntimeInfoArchitecture.LoongArch64 or RuntimeInfoArchitecture.RiscV64 => _argType == CorElementType.ValueType + && _argSize > _ccInfo.EnregisteredParamTypeMaxSize, + _ => false, + }; + } + return false; + } + + public ArgLocDesc? GetArgLoc(int _) + { + return _hasArgLocDescForStructInRegs ? _argLocDescForStructInRegs : null; + } + + public int GetArgSize() => _argSize; + + /// + /// Returns the next argument's offset in the transition block, or + /// when all arguments + /// have been enumerated. + /// + public int GetNextOffset() + { + if (!_ITERATION_STARTED) + { + int numRegistersUsed = 0; + + if (HasThis) numRegistersUsed++; + if (HasRetBuffArg() && _ccInfo.IsRetBuffPassedAsFirstArg) numRegistersUsed++; + + Debug.Assert(!IsVarArg || !HasParamType); + + if (_ccInfo.Architecture != RuntimeInfoArchitecture.X86) + { + if (HasParamType) numRegistersUsed++; + if (HasAsyncContinuation) numRegistersUsed++; + } + + if (_ccInfo.Architecture != RuntimeInfoArchitecture.X86 && IsVarArg) + numRegistersUsed++; + + switch (_ccInfo.Architecture) + { + case RuntimeInfoArchitecture.X86: + if (IsVarArg) numRegistersUsed = _ccInfo.NumArgumentRegisters; + _x86NumRegistersUsed = numRegistersUsed; + _x86OfsStack = (int)(_ccInfo.OffsetOfArgs + SizeOfArgStack()); + break; + + case RuntimeInfoArchitecture.X64: + if (_ccInfo.IsX64UnixABI) + { + _x64UnixIdxGenReg = numRegistersUsed; + _x64UnixIdxStack = 0; + _x64UnixIdxFPReg = 0; + } + else + { + _x64WindowsCurOfs = (int)_ccInfo.OffsetOfArgs + numRegistersUsed * _ccInfo.PointerSize; + } + break; + + case RuntimeInfoArchitecture.Arm: + _armIdxGenReg = numRegistersUsed; + _armOfsStack = 0; + _armWFPRegs = 0; + break; + + case RuntimeInfoArchitecture.Arm64: + _arm64IdxGenReg = numRegistersUsed; + _arm64OfsStack = 0; + _arm64IdxFPReg = 0; + break; + + case RuntimeInfoArchitecture.LoongArch64: + case RuntimeInfoArchitecture.RiscV64: + _rvLa64IdxGenReg = numRegistersUsed; + _rvLa64OfsStack = 0; + _rvLa64IdxFPReg = 0; + break; + + default: + throw new NotSupportedException(_ccInfo.Architecture.ToString()); + } + + _argNum = _skipFirstArg ? 1 : 0; + _ITERATION_STARTED = true; + } + + if (_argNum >= NumFixedArgs) + return CallingConventionInfo.InvalidOffset; + + CorElementType argType = GetArgumentType(_argNum, out _argTypeHandle, out _argForceByRef); + _argTypeHandleOfByRefParam = argType == CorElementType.Byref ? _argData.GetByRefArgumentType(_argNum) : default; + _argNum++; + + int argSize = ArgTypeInfo.GetElemSize(argType, _argTypeHandle, _ccInfo.PointerSize); + _argType = argType; + _argSize = argSize; + + argType = _argForceByRef ? CorElementType.Byref : argType; + argSize = _argForceByRef ? _ccInfo.PointerSize : argSize; + + _hasArgLocDescForStructInRegs = false; + + switch (_ccInfo.Architecture) + { + case RuntimeInfoArchitecture.X64: + return GetNextOffsetX64(argType, argSize); + + case RuntimeInfoArchitecture.Arm64: + return GetNextOffsetArm64(argType, argSize); + + case RuntimeInfoArchitecture.X86: + return GetNextOffsetX86(argType, argSize); + + case RuntimeInfoArchitecture.Arm: + return GetNextOffsetArm32(argType, argSize); + + case RuntimeInfoArchitecture.LoongArch64: + case RuntimeInfoArchitecture.RiscV64: + return GetNextOffsetRiscVLoongArch(argType, argSize); + + default: + throw new NotSupportedException(_ccInfo.Architecture.ToString()); + } + } + + // ---- Per-architecture GetNextOffset implementations ---- + // These match crossgen2's ArgIterator.GetNextOffset() switch cases. + + private int GetNextOffsetX64(CorElementType argType, int argSize) + { + if (_ccInfo.IsX64UnixABI) + { + // TODO: Full Unix AMD64 implementation with SystemV struct classification + // For now, simplified: all args go through GP regs then stack + int cbArg = _ccInfo.StackElemSize(argSize); + int cGenRegs = cbArg / 8; + + if (argType is CorElementType.R4 or CorElementType.R8) + { + if (_x64UnixIdxFPReg < _ccInfo.NumFloatArgumentRegisters) + { + int argOfs = _ccInfo.OffsetOfFloatArgumentRegisters + _x64UnixIdxFPReg * _ccInfo.FloatRegisterSize; + _x64UnixIdxFPReg++; + return argOfs; + } + } + else if (cGenRegs > 0 && _x64UnixIdxGenReg + cGenRegs <= _ccInfo.NumArgumentRegisters) + { + int argOfs = (int)_ccInfo.ArgumentRegistersOffset + _x64UnixIdxGenReg * _ccInfo.PointerSize; + _x64UnixIdxGenReg += cGenRegs; + return argOfs; + } + + int stackOfs = (int)_ccInfo.OffsetOfArgs + _x64UnixIdxStack * _ccInfo.PointerSize; + _x64UnixIdxStack += _ccInfo.StackElemSize(argSize) / _ccInfo.PointerSize; + return stackOfs; + } + else + { + // Windows x64: each arg takes exactly one slot + int cFPRegs = argType is CorElementType.R4 or CorElementType.R8 ? 1 : 0; + + int argOfs = _x64WindowsCurOfs - (int)_ccInfo.OffsetOfArgs; + _x64WindowsCurOfs += _ccInfo.PointerSize; + + if (cFPRegs == 0 || argOfs >= _ccInfo.SizeOfArgumentRegisters) + { + return argOfs + (int)_ccInfo.OffsetOfArgs; + } + else + { + int idxFpReg = argOfs / _ccInfo.PointerSize; + return _ccInfo.OffsetOfFloatArgumentRegisters + idxFpReg * 16; // SizeOfM128A + } + } + } + + private int GetNextOffsetArm64(CorElementType argType, int argSize) + { + int cFPRegs = 0; + bool isFloatHFA = false; + + switch (argType) + { + case CorElementType.R4: + case CorElementType.R8: + cFPRegs = 1; + break; + + case CorElementType.ValueType: + if (_argTypeHandle.IsHomogeneousAggregate) + { + int haElementSize = _argTypeHandle.HomogeneousAggregateElementSize; + if (haElementSize == 4) isFloatHFA = true; + cFPRegs = argSize / haElementSize; + } + else if (argSize > _ccInfo.EnregisteredParamTypeMaxSize) + { + argSize = _ccInfo.PointerSize; + } + break; + } + + bool isValueType = argType == CorElementType.ValueType; + int cbArg = _ccInfo.StackElemSize(argSize, isValueType, isFloatHFA); + + if (cFPRegs > 0 && !IsVarArg) + { + if (cFPRegs + _arm64IdxFPReg <= 8) + { + int argOfs = _ccInfo.OffsetOfFloatArgumentRegisters + _arm64IdxFPReg * 16; + _arm64IdxFPReg += cFPRegs; + return argOfs; + } + else + { + _arm64IdxFPReg = 8; + } + } + else + { + int regSlots = CallingConventionInfo.AlignUp(cbArg, _ccInfo.PointerSize) / _ccInfo.PointerSize; + if (_arm64IdxGenReg + regSlots <= 8) + { + int argOfs = (int)_ccInfo.ArgumentRegistersOffset + _arm64IdxGenReg * 8; + _arm64IdxGenReg += regSlots; + return argOfs; + } + else + { + _arm64IdxGenReg = 8; + } + } + + if (_ccInfo.IsAppleArm64ABI) + { + int alignment = isValueType ? (isFloatHFA ? 4 : 8) : cbArg; + _arm64OfsStack = CallingConventionInfo.AlignUp(_arm64OfsStack, alignment); + } + + int result = (int)_ccInfo.OffsetOfArgs + _arm64OfsStack; + _arm64OfsStack += cbArg; + return result; + } + + private int GetNextOffsetX86(CorElementType argType, int argSize) + { + if (_x86NumRegistersUsed < _ccInfo.NumArgumentRegisters + && argType is not CorElementType.ValueType + and not CorElementType.R4 + and not CorElementType.R8 + and not CorElementType.I8 + and not CorElementType.U8) + { + _x86NumRegistersUsed++; + return (int)_ccInfo.ArgumentRegistersOffset + + (_ccInfo.NumArgumentRegisters - _x86NumRegistersUsed) * _ccInfo.PointerSize; + } + + int cbArg = _ccInfo.StackElemSize(argSize); + _x86OfsStack -= cbArg; + return _x86OfsStack; + } + + private int GetNextOffsetArm32(CorElementType argType, int argSize) + { + bool fFloatingPoint = false; + bool fRequiresAlign64Bit = false; + + switch (argType) + { + case CorElementType.I8: + case CorElementType.U8: + fRequiresAlign64Bit = true; + break; + case CorElementType.R4: + fFloatingPoint = true; + break; + case CorElementType.R8: + fFloatingPoint = true; + fRequiresAlign64Bit = true; + break; + case CorElementType.ValueType: + fRequiresAlign64Bit = _argTypeHandle.RequiresAlign8; + if (_argTypeHandle.IsHomogeneousAggregate) fFloatingPoint = true; + break; + } + + _armRequires64BitAlignment = fRequiresAlign64Bit; + int cbArg = _ccInfo.StackElemSize(argSize); + + if (fFloatingPoint && _ccInfo.IsArmhfABI && !IsVarArg) + { + ushort wAllocMask = checked((ushort)((1 << (cbArg / 4)) - 1)); + ushort cSteps = (ushort)(fRequiresAlign64Bit ? 9 - (cbArg / 8) : 17 - (cbArg / 4)); + ushort cShift = fRequiresAlign64Bit ? (ushort)2 : (ushort)1; + + for (ushort i = 0; i < cSteps; i++) + { + if ((_armWFPRegs & wAllocMask) == 0) + { + _armWFPRegs |= wAllocMask; + return _ccInfo.OffsetOfFloatArgumentRegisters + (i * cShift * 4); + } + wAllocMask <<= cShift; + } + + _armWFPRegs = 0xffff; + + if (fRequiresAlign64Bit) + _armOfsStack = CallingConventionInfo.AlignUp(_armOfsStack, _ccInfo.PointerSize * 2); + + int argOfs = (int)_ccInfo.OffsetOfArgs + _armOfsStack; + _armOfsStack += cbArg; + return argOfs; + } + + if (_armIdxGenReg < 4) + { + if (fRequiresAlign64Bit) + _armIdxGenReg = CallingConventionInfo.AlignUp(_armIdxGenReg, 2); + + int argOfs = (int)_ccInfo.ArgumentRegistersOffset + _armIdxGenReg * 4; + int cRemainingRegs = 4 - _armIdxGenReg; + + if (cbArg <= cRemainingRegs * _ccInfo.PointerSize) + { + _armIdxGenReg += CallingConventionInfo.AlignUp(cbArg, _ccInfo.PointerSize) / _ccInfo.PointerSize; + return argOfs; + } + + _armIdxGenReg = 4; + + if (_armOfsStack == 0) + { + _armOfsStack += cbArg - cRemainingRegs * _ccInfo.PointerSize; + return argOfs; + } + } + + if (fRequiresAlign64Bit) + _armOfsStack = CallingConventionInfo.AlignUp(_armOfsStack, _ccInfo.PointerSize * 2); + + int result = (int)_ccInfo.OffsetOfArgs + _armOfsStack; + _armOfsStack += cbArg; + return result; + } + + private int GetNextOffsetRiscVLoongArch(CorElementType argType, int argSize) + { + // Simplified: no FP struct detection, just use integer calling convention + int cFPRegs = argType is CorElementType.R4 or CorElementType.R8 ? 1 : 0; + + if (argType == CorElementType.ValueType && argSize > _ccInfo.EnregisteredParamTypeMaxSize) + argSize = _ccInfo.PointerSize; + + int cbArg = _ccInfo.StackElemSize(argSize); + + if (cFPRegs > 0 && !IsVarArg && cFPRegs + _rvLa64IdxFPReg <= _ccInfo.NumFloatArgumentRegisters) + { + int argOfs = _ccInfo.OffsetOfFloatArgumentRegisters + _rvLa64IdxFPReg * _ccInfo.FloatRegisterSize; + _rvLa64IdxFPReg += cFPRegs; + return argOfs; + } + + int regSlots = CallingConventionInfo.AlignUp(cbArg, _ccInfo.PointerSize) / _ccInfo.PointerSize; + if (_rvLa64IdxGenReg + regSlots <= _ccInfo.NumArgumentRegisters) + { + int argOfs = (int)_ccInfo.ArgumentRegistersOffset + _rvLa64IdxGenReg * _ccInfo.PointerSize; + _rvLa64IdxGenReg += regSlots; + return argOfs; + } + else + { + _rvLa64IdxGenReg = _ccInfo.NumArgumentRegisters; + } + + int result = (int)_ccInfo.OffsetOfArgs + _rvLa64OfsStack; + _rvLa64OfsStack += cbArg; + return result; + } + + // ---- Return type computation ---- + + private void ComputeReturnFlags() + { + _RETURN_FLAGS_COMPUTED = true; + CorElementType retType = GetReturnType(out ArgTypeInfo thRetType, out bool forceByRef); + + if (forceByRef) + { + _RETURN_HAS_RET_BUFFER = true; + return; + } + + switch (retType) + { + case CorElementType.TypedByRef: + _RETURN_HAS_RET_BUFFER = true; + break; + + case CorElementType.ValueType: + if (thRetType.Size > _ccInfo.EnregisteredParamTypeMaxSize && _ccInfo.EnregisteredParamTypeMaxSize > 0) + { + _RETURN_HAS_RET_BUFFER = true; + } + else if (_ccInfo.Architecture is RuntimeInfoArchitecture.X86 or RuntimeInfoArchitecture.X64) + { + int size = thRetType.Size; + if ((size & (size - 1)) != 0) // not power of 2 + _RETURN_HAS_RET_BUFFER = true; + } + break; + } + } + + private void ForceSigWalk() + { + Debug.Assert(!_ITERATION_STARTED); + + int numRegistersUsed = 0; + int nSizeOfArgStack = 0; + + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X86) + { + if (HasThis) numRegistersUsed++; + if (HasRetBuffArg() && _ccInfo.IsRetBuffPassedAsFirstArg) numRegistersUsed++; + if (IsVarArg) + { + nSizeOfArgStack += _ccInfo.PointerSize; + numRegistersUsed = _ccInfo.NumArgumentRegisters; + } + + int nArgs = NumFixedArgs; + for (int i = _skipFirstArg ? 1 : 0; i < nArgs; i++) + { + CorElementType type = GetArgumentType(i, out ArgTypeInfo thArgType, out bool argForced); + if (argForced) type = CorElementType.Byref; + + // Simplified: assume all non-trivial types go to stack + int structSize = ArgTypeInfo.GetElemSize(type, thArgType, _ccInfo.PointerSize); + nSizeOfArgStack += _ccInfo.StackElemSize(structSize); + } + + if (HasAsyncContinuation) + { + if (numRegistersUsed < _ccInfo.NumArgumentRegisters) + { + numRegistersUsed++; + _asyncContinuationLoc = numRegistersUsed == 1 ? AsyncContinuationLocation.Ecx : AsyncContinuationLocation.Edx; + } + else + { + nSizeOfArgStack += _ccInfo.PointerSize; + _asyncContinuationLoc = AsyncContinuationLocation.Stack; + } + } + + if (HasParamType) + { + if (numRegistersUsed < _ccInfo.NumArgumentRegisters) + { + numRegistersUsed++; + _paramTypeLoc = numRegistersUsed == 1 ? ParamTypeLocation.Ecx : ParamTypeLocation.Edx; + } + else + { + nSizeOfArgStack += _ccInfo.PointerSize; + _paramTypeLoc = ParamTypeLocation.Stack; + } + } + } + else + { + // Non-x86: iterate through GetNextOffset to compute stack size + int maxOffset = (int)_ccInfo.OffsetOfArgs; + int ofs; + while (CallingConventionInfo.InvalidOffset != (ofs = GetNextOffset())) + { + int stackElemSize; + if (_ccInfo.Architecture == RuntimeInfoArchitecture.X64) + { + stackElemSize = _ccInfo.IsX64UnixABI + ? _ccInfo.StackElemSize(GetArgSize()) + : _ccInfo.PointerSize; + } + else + { + stackElemSize = _ccInfo.StackElemSize(GetArgSize()); + } + + int endOfs = ofs + stackElemSize; + if (IsArgumentRegisterOffset(ofs)) + continue; + if (CallingConventionInfo.IsFloatArgumentRegisterOffset(ofs)) + continue; + if (ofs == CallingConventionInfo.StructInRegsOffset) + continue; + if (endOfs > maxOffset) + maxOffset = endOfs; + } + + nSizeOfArgStack = maxOffset - (int)_ccInfo.OffsetOfArgs; + Reset(); + } + + _nSizeOfArgStack = nSizeOfArgStack; + _SIZE_OF_ARG_STACK_COMPUTED = true; + } + + private bool IsArgumentRegisterOffset(int offset) + { + return _ccInfo.IsArgumentRegisterOffset(offset); + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIteratorData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIteratorData.cs new file mode 100644 index 00000000000000..b48a86f5b2e0ee --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgIteratorData.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Ported from crossgen2's ArgIterator.cs — data holder types. + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.CallingConvention; + +/// +/// Describes how a single argument is laid out in registers and/or stack locations. +/// Ported from crossgen2's ArgLocDesc. +/// +internal struct ArgLocDesc +{ + public int m_idxFloatReg; + public int m_cFloatReg; + + public int m_idxGenReg; + public short m_cGenReg; + + public bool m_fRequires64BitAlignment; + + public int m_byteStackIndex; + public int m_byteStackSize; + + public void Init() + { + m_idxFloatReg = -1; + m_cFloatReg = 0; + m_idxGenReg = -1; + m_cGenReg = 0; + m_byteStackIndex = -1; + m_byteStackSize = 0; + m_fRequires64BitAlignment = false; + } +} + +/// +/// Holds parsed method signature data for . +/// Ported from crossgen2's ArgIteratorData. +/// +internal sealed class ArgIteratorData +{ + private readonly bool _hasThis; + private readonly bool _isVarArg; + private readonly ArgTypeInfo[] _parameterTypes; + private readonly ArgTypeInfo _returnType; + + public ArgIteratorData( + bool hasThis, + bool isVarArg, + ArgTypeInfo[] parameterTypes, + ArgTypeInfo returnType) + { + _hasThis = hasThis; + _isVarArg = isVarArg; + _parameterTypes = parameterTypes; + _returnType = returnType; + } + + public bool HasThis() => _hasThis; + public bool IsVarArg() => _isVarArg; + public int NumFixedArgs() => _parameterTypes.Length; + + public CorElementType GetArgumentType(int argNum, out ArgTypeInfo thArgType) + { + thArgType = _parameterTypes[argNum]; + return thArgType.CorElementType; + } + + public ArgTypeInfo GetByRefArgumentType(int argNum) + { + if (argNum < _parameterTypes.Length && _parameterTypes[argNum].CorElementType == CorElementType.Byref) + return _parameterTypes[argNum]; + return default; + } + + public CorElementType GetReturnType(out ArgTypeInfo thRetType) + { + thRetType = _returnType; + return thRetType.CorElementType; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgTypeInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgTypeInfo.cs new file mode 100644 index 00000000000000..bd3ca6ecf632d5 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/ArgTypeInfo.cs @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Type information needed by ArgIterator for calling convention analysis. +// Ported from crossgen2's TypeHandle struct in ArgIterator.cs. + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.CallingConvention; + +/// +/// Pre-computed type information needed by for +/// calling convention analysis. This is a value type to avoid allocations +/// during argument iteration. +/// +/// +/// Mirrors crossgen2's TypeHandle struct in ArgIterator.cs, but uses +/// data from the cDAC's rather than +/// crossgen2's TypeDesc. +/// +internal readonly struct ArgTypeInfo +{ + public CorElementType CorElementType { get; init; } + public int Size { get; init; } + public bool IsValueType { get; init; } + public bool RequiresAlign8 { get; init; } + public bool IsHomogeneousAggregate { get; init; } + public int HomogeneousAggregateElementSize { get; init; } + + /// + /// The TypeHandle from the target runtime, used for value type field enumeration + /// and SystemV struct classification. + /// + public TypeHandle RuntimeTypeHandle { get; init; } + + public bool IsNull => CorElementType == default && Size == 0; + + /// + /// Gets the element size for a given CorElementType, matching crossgen2's + /// TypeHandle.GetElemSize. Returns the type's actual size for value + /// types, or pointer size for reference types. + /// + public static int GetElemSize(CorElementType t, ArgTypeInfo thValueType, int pointerSize) + { + if ((int)t <= 0x1d) + { + int elemSize = s_elemSizes[(int)t]; + if (elemSize == -1) + return thValueType.Size; + if (elemSize == -2) + return pointerSize; + return elemSize; + } + return 0; + } + + private static readonly int[] s_elemSizes = + [ + 0, // ELEMENT_TYPE_END 0x0 + 0, // ELEMENT_TYPE_VOID 0x1 + 1, // ELEMENT_TYPE_BOOLEAN 0x2 + 2, // ELEMENT_TYPE_CHAR 0x3 + 1, // ELEMENT_TYPE_I1 0x4 + 1, // ELEMENT_TYPE_U1 0x5 + 2, // ELEMENT_TYPE_I2 0x6 + 2, // ELEMENT_TYPE_U2 0x7 + 4, // ELEMENT_TYPE_I4 0x8 + 4, // ELEMENT_TYPE_U4 0x9 + 8, // ELEMENT_TYPE_I8 0xa + 8, // ELEMENT_TYPE_U8 0xb + 4, // ELEMENT_TYPE_R4 0xc + 8, // ELEMENT_TYPE_R8 0xd + -2, // ELEMENT_TYPE_STRING 0xe + -2, // ELEMENT_TYPE_PTR 0xf + -2, // ELEMENT_TYPE_BYREF 0x10 + -1, // ELEMENT_TYPE_VALUETYPE 0x11 + -2, // ELEMENT_TYPE_CLASS 0x12 + 0, // ELEMENT_TYPE_VAR 0x13 + -2, // ELEMENT_TYPE_ARRAY 0x14 + 0, // ELEMENT_TYPE_GENERICINST 0x15 + 0, // ELEMENT_TYPE_TYPEDBYREF 0x16 + 0, // UNUSED 0x17 + -2, // ELEMENT_TYPE_I 0x18 + -2, // ELEMENT_TYPE_U 0x19 + 0, // UNUSED 0x1a + -2, // ELEMENT_TYPE_FPTR 0x1b + -2, // ELEMENT_TYPE_OBJECT 0x1c + -2, // ELEMENT_TYPE_SZARRAY 0x1d + ]; + + /// + /// Creates an from a target TypeHandle using the + /// runtime type system contract. + /// + public static ArgTypeInfo FromTypeHandle(Target target, TypeHandle th) + { + IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + CorElementType corType = rts.GetSignatureCorElementType(th); + + bool isValueType = corType is CorElementType.ValueType; + int size = isValueType + ? (int)rts.GetBaseSize(th) - 2 * target.PointerSize // InstanceFieldSize = BaseSize - ObjHeader - MethodTable ptr + : target.PointerSize; + + bool requiresAlign8 = false; + bool isHfa = false; + int hfaElemSize = 0; + + if (isValueType) + { + // TODO: Implement RequiresAlign8 via IRuntimeTypeSystem + // TODO: Implement IsHomogeneousAggregate via IRuntimeTypeSystem + } + + return new ArgTypeInfo + { + CorElementType = corType, + Size = size, + IsValueType = isValueType, + RequiresAlign8 = requiresAlign8, + IsHomogeneousAggregate = isHfa, + HomogeneousAggregateElementSize = hfaElemSize, + RuntimeTypeHandle = th, + }; + } + + /// + /// Creates an for a primitive type that doesn't need + /// type handle resolution. + /// + public static ArgTypeInfo ForPrimitive(CorElementType corType, int pointerSize) + { + return new ArgTypeInfo + { + CorElementType = corType, + Size = GetElemSize(corType, default, pointerSize), + IsValueType = false, + RequiresAlign8 = false, + IsHomogeneousAggregate = false, + HomogeneousAggregateElementSize = 0, + RuntimeTypeHandle = default, + }; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/CallingConventionInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/CallingConventionInfo.cs new file mode 100644 index 00000000000000..ee89c064c24a27 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/CallingConvention/CallingConventionInfo.cs @@ -0,0 +1,274 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Provides an abstraction over platform-specific calling conventions +// (specifically, the managed calling convention utilized by the JIT). +// Ported from crossgen2's TransitionBlock.cs. + +using System; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.CallingConvention; + +/// +/// Architecture-specific calling convention constants and methods for +/// mapping method arguments to register and stack locations. +/// +/// +/// Layout-dependent values (offsets, sizes) come from the data descriptor +/// (). ABI-invariant values (register counts, +/// alignment rules) are hardcoded per architecture since they are defined by +/// the hardware/OS ABI and never change. +/// +internal sealed class CallingConventionInfo +{ + // Layout values from the data descriptor + private readonly uint _sizeOfTransitionBlock; + private readonly uint _argumentRegistersOffset; + private readonly uint _firstGCRefMapSlot; + private readonly uint _offsetOfArgs; + private readonly int _offsetOfFloatArgumentRegisters; + + // ABI invariants + public int PointerSize { get; } + public int NumArgumentRegisters { get; } + public int NumFloatArgumentRegisters { get; } + public int FloatRegisterSize { get; } + public int EnregisteredParamTypeMaxSize { get; } + public int StackSlotSize { get; } + public bool IsRetBuffPassedAsFirstArg { get; } + public bool IsX64UnixABI { get; } + public bool IsAppleArm64ABI { get; } + public bool IsArmhfABI { get; } + + public RuntimeInfoArchitecture Architecture { get; } + + // Convenience accessors + public uint SizeOfTransitionBlock => _sizeOfTransitionBlock; + public uint ArgumentRegistersOffset => _argumentRegistersOffset; + public uint FirstGCRefMapSlot => _firstGCRefMapSlot; + public uint OffsetOfArgs => _offsetOfArgs; + public int OffsetOfFloatArgumentRegisters => _offsetOfFloatArgumentRegisters; + public int SizeOfArgumentRegisters => NumArgumentRegisters * PointerSize; + + public const int InvalidOffset = -1; + public const int StructInRegsOffset = -2; + + /// + /// Creates a for the given target, reading + /// layout data from the data descriptor and filling in ABI constants from + /// the target's architecture and OS. + /// + public CallingConventionInfo(Target target) + { + IRuntimeInfo runtimeInfo = target.Contracts.RuntimeInfo; + Architecture = runtimeInfo.GetTargetArchitecture(); + RuntimeInfoOperatingSystem os = runtimeInfo.GetTargetOperatingSystem(); + PointerSize = target.PointerSize; + + // Read layout values from the data descriptor + Target.TypeInfo tbType = target.GetTypeInfo(DataType.TransitionBlock); + _sizeOfTransitionBlock = (uint)tbType.Size!; + _argumentRegistersOffset = (uint)tbType.Fields["ArgumentRegistersOffset"].Offset; + _firstGCRefMapSlot = (uint)tbType.Fields["FirstGCRefMapSlot"].Offset; + _offsetOfArgs = (uint)tbType.Fields["OffsetOfArgs"].Offset; + _offsetOfFloatArgumentRegisters = tbType.Fields["OffsetOfFloatArgumentRegisters"].Offset; + + // Fill in ABI invariants based on architecture + switch (Architecture) + { + case RuntimeInfoArchitecture.X86: + NumArgumentRegisters = 2; // ECX, EDX + NumFloatArgumentRegisters = 0; + FloatRegisterSize = 0; + EnregisteredParamTypeMaxSize = 0; + StackSlotSize = 4; + IsRetBuffPassedAsFirstArg = true; + break; + + case RuntimeInfoArchitecture.X64: + if (os is RuntimeInfoOperatingSystem.Unix or RuntimeInfoOperatingSystem.Apple) + { + // Unix/Apple AMD64 ABI (SysV) + NumArgumentRegisters = 6; // RDI, RSI, RDX, RCX, R8, R9 + NumFloatArgumentRegisters = 8; // XMM0-XMM7 + FloatRegisterSize = 16; // M128A + EnregisteredParamTypeMaxSize = 16; + IsX64UnixABI = true; + } + else + { + // Windows AMD64 ABI + NumArgumentRegisters = 4; // RCX, RDX, R8, R9 + NumFloatArgumentRegisters = 0; // Shared with GP regs on Windows + FloatRegisterSize = 16; + EnregisteredParamTypeMaxSize = 8; + } + StackSlotSize = 8; + IsRetBuffPassedAsFirstArg = true; + break; + + case RuntimeInfoArchitecture.Arm: + NumArgumentRegisters = 4; // R0-R3 + NumFloatArgumentRegisters = 16; // 16 single-precision slots (D0-D7 / S0-S15) + FloatRegisterSize = 4; + EnregisteredParamTypeMaxSize = 0; + StackSlotSize = 4; + IsRetBuffPassedAsFirstArg = true; + IsArmhfABI = true; // TODO: detect armel + break; + + case RuntimeInfoArchitecture.Arm64: + NumArgumentRegisters = 8; // X0-X7 + NumFloatArgumentRegisters = 8; // V0-V7 + FloatRegisterSize = 16; + EnregisteredParamTypeMaxSize = 16; + StackSlotSize = 8; + IsRetBuffPassedAsFirstArg = false; // ARM64 uses X8 for retbuf + IsAppleArm64ABI = os == RuntimeInfoOperatingSystem.Apple; + break; + + case RuntimeInfoArchitecture.LoongArch64: + NumArgumentRegisters = 8; // A0-A7 + NumFloatArgumentRegisters = 8; // FA0-FA7 + FloatRegisterSize = 8; + EnregisteredParamTypeMaxSize = 16; + StackSlotSize = 8; + IsRetBuffPassedAsFirstArg = true; + break; + + case RuntimeInfoArchitecture.RiscV64: + NumArgumentRegisters = 8; // a0-a7 + NumFloatArgumentRegisters = 8; // fa0-fa7 + FloatRegisterSize = 8; + EnregisteredParamTypeMaxSize = 16; + StackSlotSize = 8; + IsRetBuffPassedAsFirstArg = true; + break; + + default: + throw new NotSupportedException($"Architecture {Architecture} is not supported for calling convention analysis."); + } + } + + // ---- Derived methods ---- + + /// + /// Returns the byte offset of the 'this' pointer in the transition block. + /// + public int ThisOffset + { + get + { + if (Architecture == RuntimeInfoArchitecture.X86) + { + // ECX offset within ArgumentRegisters: ECX is at offset PointerSize (after EDX at 0) + return (int)ArgumentRegistersOffset + PointerSize; + } + return (int)ArgumentRegistersOffset; + } + } + + /// + /// Rounds up a parameter size to the stack slot size for the platform. + /// + public int StackElemSize(int parmSize, bool isValueType = false, bool isFloatHfa = false) + { + if (IsAppleArm64ABI) + { + if (!isValueType) + { + // Primitives use their natural size, no padding + return parmSize; + } + if (isFloatHfa) + { + // Float HFA: 4-byte alignment + return parmSize; + } + } + return AlignUp(parmSize, StackSlotSize); + } + + /// + /// Maps a GCRefMap position index to the byte offset in the transition block. + /// + public int OffsetFromGCRefMapPos(int pos) + { + if (Architecture == RuntimeInfoArchitecture.X86) + { + if (pos < NumArgumentRegisters) + { + return (int)ArgumentRegistersOffset + SizeOfArgumentRegisters - (pos + 1) * PointerSize; + } + return (int)OffsetOfArgs + (pos - NumArgumentRegisters) * PointerSize; + } + return (int)FirstGCRefMapSlot + pos * PointerSize; + } + + /// + /// Returns true if the argument at the given offset is a float register. + /// Float register offsets are negative. + /// + public static bool IsFloatArgumentRegisterOffset(int offset) => offset < 0; + + /// + /// Returns true if the argument at the given offset is in a general-purpose register. + /// + public bool IsArgumentRegisterOffset(int offset) + { + return offset >= (int)ArgumentRegistersOffset + && offset < (int)ArgumentRegistersOffset + SizeOfArgumentRegisters; + } + + /// + /// Returns true if the argument at the given offset is on the stack. + /// + public bool IsStackArgumentOffset(int offset) + { + return offset >= (int)ArgumentRegistersOffset + SizeOfArgumentRegisters; + } + + /// + /// Checks if a value type of the given size should be passed by reference + /// (applies to x64 and ARM64). + /// + public bool IsArgPassedByRef(int size) + { + if (EnregisteredParamTypeMaxSize == 0) + return false; + + if (Architecture == RuntimeInfoArchitecture.X64) + { + // On x64, also check power-of-2 rule + return size > EnregisteredParamTypeMaxSize || (size & (size - 1)) != 0; + } + + return size > EnregisteredParamTypeMaxSize; + } + + /// + /// Returns the byte offset of the return buffer argument. + /// + public int GetRetBuffArgOffset(bool hasThis) + { + if (Architecture == RuntimeInfoArchitecture.X86) + { + // x86: retbuf goes in EDX if hasThis (this in ECX), else ECX + return hasThis + ? (int)ArgumentRegistersOffset // EDX offset = 0 + : (int)ArgumentRegistersOffset + PointerSize; // ECX offset + } + if (Architecture == RuntimeInfoArchitecture.Arm64) + { + // ARM64: retbuf is in X8, which is at FirstGCRefMapSlot + return (int)FirstGCRefMapSlot; + } + // Default: retbuf is after 'this' in the argument registers + return (int)ArgumentRegistersOffset + (hasThis ? PointerSize : 0); + } + + internal static int AlignUp(int value, int alignment) + { + return (value + (alignment - 1)) & ~(alignment - 1); + } +} 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..88bfcaece90d1d 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,10 @@ // 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.Contracts.StackWalkHelpers.CallingConvention; +using Microsoft.Diagnostics.DataContractReader.Data; namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; @@ -47,11 +51,15 @@ internal enum FrameType private readonly Target target; private readonly TargetPointer terminator; private TargetPointer currentFramePointer; + private CallingConventionInfo? _callingConventionInfo; internal Data.Frame CurrentFrame => target.ProcessedData.GetOrAdd(currentFramePointer); public TargetPointer CurrentFrameAddress => currentFramePointer; + private CallingConventionInfo GetCallingConventionInfo() + => _callingConventionInfo ??= new CallingConventionInfo(target); + public FrameIterator(Target target, ThreadData threadData) { this.target = target; @@ -132,20 +140,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 +229,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 +302,488 @@ 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(); + int offset = GetCallingConventionInfo().OffsetFromGCRefMapPos(pos); + TargetPointer slotAddress = new(transitionBlock.Value + (ulong)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; + MetadataReader? metadataReader; + try { - return frame.Datum != TargetPointer.Null && (frame.Datum.Value & 0x1) == 0; + signature = GetMethodSignatureBytes(methodDescPtr, out metadataReader); } - else + catch (System.Exception) { - return ((long)frame.Datum.Value & ~0xffff) != 0; + return; + } + + if (signature.IsEmpty) + return; + + MethodSignature methodSig; + try + { + RuntimeSignatureDecoder decoder = new( + GcSignatureTypeProvider.Instance, target, genericContext: null, + new SpanSignatureReader(signature, target.IsLittleEndian), metadataReader); + methodSig = decoder.DecodeMethodSignature(); + } + catch (System.Exception) + { + // If signature decoding fails for any reason, 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 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, + requiresInstArg, isAsync, isValueTypeThis, scanContext); } - private static bool InlinedCallFrameHasActiveCall(Data.InlinedCallFrame frame) + /// + /// Core logic for promoting caller stack GC references. + /// Uses to correctly map arguments to their + /// register/stack locations per the target's calling convention. + /// Matches native TransitionFrame::PromoteCallerStackHelper (frames.cpp:1546). + /// + private void PromoteCallerStackHelper( + TargetPointer transitionBlock, + MethodSignature methodSig, + bool hasThis, + bool requiresInstArg, + bool isAsync, + bool isValueTypeThis, + GcScanContext scanContext) { - return frame.CallerReturnAddress != TargetPointer.Null; + CallingConventionInfo ccInfo; + try + { + ccInfo = GetCallingConventionInfo(); + } + catch + { + return; + } + + // Build ArgTypeInfo array from decoded signature + ArgTypeInfo[] paramTypes = new ArgTypeInfo[methodSig.ParameterTypes.Length]; + for (int i = 0; i < paramTypes.Length; i++) + { + paramTypes[i] = GcTypeKindToArgTypeInfo(methodSig.ParameterTypes[i], ccInfo.PointerSize); + } + ArgTypeInfo returnTypeInfo = GcTypeKindToArgTypeInfo(methodSig.ReturnType, ccInfo.PointerSize); + + ArgIteratorData argData = new(hasThis, methodSig.Header.CallingConvention is SignatureCallingConvention.VarArgs, paramTypes, returnTypeInfo); + CallingConvention.ArgIterator argit = new(ccInfo, argData, hasParamType: requiresInstArg, hasAsyncContinuation: isAsync, forcedByRefParams: System.Array.Empty()); + + // Promote 'this' for non-static methods + if (argit.HasThis) + { + int thisOffset = argit.GetThisOffset(); + TargetPointer thisAddr = new(transitionBlock.Value + (ulong)thisOffset); + GcScanFlags thisFlags = isValueTypeThis ? GcScanFlags.GC_CALL_INTERIOR : GcScanFlags.None; + scanContext.GCReportCallback(thisAddr, thisFlags); + } + + // Promote async continuation + if (argit.HasAsyncContinuation) + { + int asyncOffset = argit.GetAsyncContinuationArgOffset(); + TargetPointer asyncAddr = new(transitionBlock.Value + (ulong)asyncOffset); + scanContext.GCReportCallback(asyncAddr, GcScanFlags.None); + } + + // Walk each argument using ArgIterator for correct offsets + int argIndex = 0; + int argOffset; + while ((argOffset = argit.GetNextOffset()) != CallingConventionInfo.InvalidOffset) + { + if (argIndex >= methodSig.ParameterTypes.Length) + break; + + GcTypeKind kind = methodSig.ParameterTypes[argIndex]; + TargetPointer slotAddress = new(transitionBlock.Value + (ulong)argOffset); + + 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: + // Value type: if passed by ref, report as interior pointer + if (argit.IsArgPassedByRef()) + { + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + } + // TODO: For value types passed by value, enumerate fields for embedded GC refs + break; + case GcTypeKind.None: + break; + } + argIndex++; + } + } + + /// + /// Converts a to a minimal for + /// ArgIterator consumption. This is a bridge until we have a full type-aware provider. + /// + private static ArgTypeInfo GcTypeKindToArgTypeInfo(GcTypeKind kind, int pointerSize) + { + return kind switch + { + GcTypeKind.None => ArgTypeInfo.ForPrimitive(CorElementType.I, pointerSize), + GcTypeKind.Ref => ArgTypeInfo.ForPrimitive(CorElementType.Class, pointerSize), + GcTypeKind.Interior => ArgTypeInfo.ForPrimitive(CorElementType.Byref, pointerSize), + GcTypeKind.Other => new ArgTypeInfo + { + CorElementType = CorElementType.ValueType, + Size = pointerSize, // Conservative: assume pointer-sized for now + IsValueType = true, + }, + _ => ArgTypeInfo.ForPrimitive(CorElementType.I, pointerSize), + }; + } + + private ReadOnlySpan GetMethodSignatureBytes(TargetPointer methodDescPtr, out MetadataReader? metadataReader) + { + metadataReader = null; + 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 = ecmaMetadata.GetMetadata(moduleHandle); + if (metadataReader is null) + return default; + + MethodDefinitionHandle methodDefHandle = MetadataTokens.MethodDefinitionHandle((int)(methodToken & 0x00FFFFFF)); + MethodDefinition methodDef = metadataReader.GetMethodDefinition(methodDefHandle); + BlobReader blobReader = metadataReader.GetBlobReader(methodDef.Signature); + return blobReader.ReadBytes(blobReader.Length); + } + + 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..46658212eb81e4 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs @@ -0,0 +1,101 @@ +// 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 which +/// is a superset of SRM's , +/// adding support for ELEMENT_TYPE_INTERNAL. +/// +internal sealed class GcSignatureTypeProvider + : IRuntimeSignatureTypeProvider +{ + 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 GetInternalModifiedType(Target target, TargetPointer typeHandlePointer, GcTypeKind unmodifiedType, bool isRequired) => unmodifiedType; + public GcTypeKind GetPinnedType(GcTypeKind elementType) => elementType; + + public GcTypeKind GetInternalType(Target target, TargetPointer typeHandlePointer) + { + if (typeHandlePointer == TargetPointer.Null) + return GcTypeKind.None; + + try + { + IRuntimeTypeSystem rts = target.Contracts.RuntimeTypeSystem; + TypeHandle th = rts.GetTypeHandle(typeHandlePointer); + CorElementType corType = rts.GetSignatureCorElementType(th); + + return corType switch + { + CorElementType.Void or CorElementType.Boolean or CorElementType.Char + or CorElementType.I1 or CorElementType.U1 + or CorElementType.I2 or CorElementType.U2 + or CorElementType.I4 or CorElementType.U4 + or CorElementType.I8 or CorElementType.U8 + or CorElementType.R4 or CorElementType.R8 + or CorElementType.I or CorElementType.U + or CorElementType.FnPtr or CorElementType.Ptr + => GcTypeKind.None, + + CorElementType.Byref => GcTypeKind.Interior, + CorElementType.ValueType => GcTypeKind.Other, + + _ => GcTypeKind.Ref, + }; + } + catch + { + return GcTypeKind.Ref; + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/RuntimeSignatureDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/RuntimeSignatureDecoder.cs new file mode 100644 index 00000000000000..9998176d7e4f04 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/RuntimeSignatureDecoder.cs @@ -0,0 +1,422 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers.Binary; +using System.Collections.Immutable; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +/// +/// Superset of SRM's +/// that adds support for runtime-internal type codes (ELEMENT_TYPE_INTERNAL). +/// +/// +/// Providers implementing this interface automatically satisfy SRM's +/// and can be used +/// with both SRM's SignatureDecoder and our +/// . +/// +internal interface IRuntimeSignatureTypeProvider + : ISignatureTypeProvider +{ + /// + /// Classify an ELEMENT_TYPE_INTERNAL (0x21) type by resolving the + /// embedded TypeHandle pointer via the target's runtime type system. + /// + TType GetInternalType(Target target, TargetPointer typeHandlePointer); + + /// + /// Classify an ELEMENT_TYPE_CMOD_INTERNAL (0x22) custom modifier by + /// resolving the embedded TypeHandle pointer via the target's runtime type system. + /// + TType GetInternalModifiedType(Target target, TargetPointer typeHandlePointer, TType unmodifiedType, bool isRequired); +} + +/// +/// Abstraction for reading bytes from a signature blob. +/// +/// +/// Allows the decoder to read from different sources (in-memory spans, +/// target process memory) without allocating intermediate byte arrays. +/// +internal interface ISignatureReader +{ + byte ReadByte(); + byte PeekByte(); + int Remaining { get; } + + /// Reads a pointer-sized unsigned value (4 or 8 bytes). + ulong ReadPointerSized(int pointerSize); +} + +/// +/// Reads signature bytes from a . +/// +internal ref struct SpanSignatureReader : ISignatureReader +{ + private readonly ReadOnlySpan _blob; + private readonly bool _isLittleEndian; + private int _offset; + + public SpanSignatureReader(ReadOnlySpan blob, bool isLittleEndian = true) + { + _blob = blob; + _isLittleEndian = isLittleEndian; + _offset = 0; + } + + public int Remaining => _blob.Length - _offset; + + public byte ReadByte() + { + if (_offset >= _blob.Length) + throw new BadImageFormatException("Unexpected end of signature blob"); + return _blob[_offset++]; + } + + public byte PeekByte() + { + if (_offset >= _blob.Length) + throw new BadImageFormatException("Unexpected end of signature blob"); + return _blob[_offset]; + } + + public ulong ReadPointerSized(int pointerSize) + { + if (_offset + pointerSize > _blob.Length) + throw new BadImageFormatException("Unexpected end of signature blob"); + + ReadOnlySpan slice = _blob.Slice(_offset, pointerSize); + ulong val = pointerSize == 8 + ? (_isLittleEndian ? BinaryPrimitives.ReadUInt64LittleEndian(slice) : BinaryPrimitives.ReadUInt64BigEndian(slice)) + : (_isLittleEndian ? BinaryPrimitives.ReadUInt32LittleEndian(slice) : BinaryPrimitives.ReadUInt32BigEndian(slice)); + _offset += pointerSize; + return val; + } +} + +/// +/// Decodes method and local variable signatures, handling both standard ECMA-335 +/// types and runtime-internal types like ELEMENT_TYPE_INTERNAL (0x21). +/// +/// +/// +/// Handles the same ECMA-335 type codes as SRM's +/// , plus runtime-internal +/// types (ELEMENT_TYPE_INTERNAL 0x21 and ELEMENT_TYPE_CMOD_INTERNAL 0x22). +/// +/// +/// Internal custom modifiers (ELEMENT_TYPE_CMOD_INTERNAL) are skipped since +/// they carry runtime TypeHandle pointers that are not meaningful for type classification. +/// Standard custom modifiers (modreq/modopt) are decoded and dispatched +/// to . +/// +/// +internal ref struct RuntimeSignatureDecoder + where TReader : ISignatureReader, allows ref struct +{ + private const byte ELEMENT_TYPE_PTR = 0x0f; + private const byte ELEMENT_TYPE_BYREF = 0x10; + private const byte ELEMENT_TYPE_VALUETYPE = 0x11; + private const byte ELEMENT_TYPE_CLASS = 0x12; + private const byte ELEMENT_TYPE_VAR = 0x13; + private const byte ELEMENT_TYPE_ARRAY = 0x14; + private const byte ELEMENT_TYPE_GENERICINST = 0x15; + private const byte ELEMENT_TYPE_FNPTR = 0x1b; + private const byte ELEMENT_TYPE_SZARRAY = 0x1d; + private const byte ELEMENT_TYPE_MVAR = 0x1e; + private const byte ELEMENT_TYPE_CMOD_REQD = 0x1f; + private const byte ELEMENT_TYPE_CMOD_OPT = 0x20; + private const byte ELEMENT_TYPE_INTERNAL = 0x21; + private const byte ELEMENT_TYPE_CMOD_INTERNAL = 0x22; + private const byte ELEMENT_TYPE_SENTINEL = 0x41; + private const byte ELEMENT_TYPE_PINNED = 0x45; + + private readonly IRuntimeSignatureTypeProvider _provider; + private readonly MetadataReader? _metadataReader; + private readonly Target _target; + private readonly TGenericContext _genericContext; + private TReader _reader; + + public RuntimeSignatureDecoder( + IRuntimeSignatureTypeProvider provider, + Target target, + TGenericContext genericContext, + TReader reader, + MetadataReader? metadataReader = null) + { + _provider = provider; + _metadataReader = metadataReader; + _target = target; + _genericContext = genericContext; + _reader = reader; + } + + /// Decodes a method signature (MethodDefSig/MethodRefSig). + public MethodSignature DecodeMethodSignature() + { + byte rawHeader = _reader.ReadByte(); + SignatureHeader header = new(rawHeader); + + if (header.Kind is not SignatureKind.Method and not SignatureKind.Property) + throw new BadImageFormatException($"Unexpected signature header kind: {header.Kind}"); + + int genericParameterCount = 0; + if (header.IsGeneric) + genericParameterCount = ReadCompressedUInt(); + + int parameterCount = ReadCompressedUInt(); + if (parameterCount > _reader.Remaining) + throw new BadImageFormatException($"Parameter count {parameterCount} exceeds remaining signature bytes"); + TType returnType = DecodeType(); + + var parameterTypes = ImmutableArray.CreateBuilder(parameterCount); + int requiredParameterCount = parameterCount; + bool sentinelSeen = false; + + for (int i = 0; i < parameterCount; i++) + { + if (_reader.Remaining > 0 && _reader.PeekByte() == ELEMENT_TYPE_SENTINEL) + { + if (sentinelSeen) + throw new BadImageFormatException("Multiple sentinels in method signature"); + sentinelSeen = true; + requiredParameterCount = i; + _reader.ReadByte(); + } + parameterTypes.Add(DecodeType()); + } + + return new MethodSignature( + header, returnType, requiredParameterCount, genericParameterCount, + parameterTypes.MoveToImmutable()); + } + + /// Decodes a local variable signature (LocalVarSig). + public ImmutableArray DecodeLocalSignature() + { + byte header = _reader.ReadByte(); + if (header != 0x07) // IMAGE_CEE_CS_CALLCONV_LOCAL_SIG + throw new BadImageFormatException($"Expected LocalVarSig header (0x07), got 0x{header:X2}"); + + int count = ReadCompressedUInt(); + if (count == 0) + throw new BadImageFormatException("Local variable signature must have at least one entry"); + if (count > _reader.Remaining) + throw new BadImageFormatException($"Local count {count} exceeds remaining signature bytes"); + var locals = ImmutableArray.CreateBuilder(count); + for (int i = 0; i < count; i++) + locals.Add(DecodeType()); + return locals.MoveToImmutable(); + } + + /// Decodes a single type embedded in a signature. + public TType DecodeType() + { + // Handle custom modifiers (standard and internal) + while (_reader.Remaining > 0) + { + byte peek = _reader.PeekByte(); + if (peek is ELEMENT_TYPE_CMOD_REQD or ELEMENT_TYPE_CMOD_OPT) + { + bool isRequired = peek == ELEMENT_TYPE_CMOD_REQD; + _reader.ReadByte(); + TType modifier = DecodeTypeDefOrRefOrSpec(0); + TType unmodifiedType = DecodeType(); + return _provider.GetModifiedType(modifier, unmodifiedType, isRequired); + } + else if (peek == ELEMENT_TYPE_CMOD_INTERNAL) + { + _reader.ReadByte(); + bool isRequired = _reader.ReadByte() != 0; + ulong val = _reader.ReadPointerSized(_target.PointerSize); + TType unmodifiedType = DecodeType(); + return _provider.GetInternalModifiedType( + _target, new TargetPointer(val), unmodifiedType, isRequired); + } + else + { + break; + } + } + + byte typeCode = _reader.ReadByte(); + + switch (typeCode) + { + case (byte)SignatureTypeCode.Boolean: + case (byte)SignatureTypeCode.Char: + case (byte)SignatureTypeCode.SByte: + case (byte)SignatureTypeCode.Byte: + case (byte)SignatureTypeCode.Int16: + case (byte)SignatureTypeCode.UInt16: + case (byte)SignatureTypeCode.Int32: + case (byte)SignatureTypeCode.UInt32: + case (byte)SignatureTypeCode.Int64: + case (byte)SignatureTypeCode.UInt64: + case (byte)SignatureTypeCode.Single: + case (byte)SignatureTypeCode.Double: + case (byte)SignatureTypeCode.IntPtr: + case (byte)SignatureTypeCode.UIntPtr: + case (byte)SignatureTypeCode.Object: + case (byte)SignatureTypeCode.String: + case (byte)SignatureTypeCode.Void: + case (byte)SignatureTypeCode.TypedReference: + return _provider.GetPrimitiveType((PrimitiveTypeCode)typeCode); + + case ELEMENT_TYPE_CLASS: + case ELEMENT_TYPE_VALUETYPE: + return DecodeTypeDefOrRefOrSpec(typeCode); + + case ELEMENT_TYPE_PTR: + return _provider.GetPointerType(DecodeType()); + + case ELEMENT_TYPE_BYREF: + return _provider.GetByReferenceType(DecodeType()); + + case ELEMENT_TYPE_SZARRAY: + return _provider.GetSZArrayType(DecodeType()); + + case ELEMENT_TYPE_ARRAY: + { + TType elementType = DecodeType(); + ArrayShape shape = DecodeArrayShape(); + return _provider.GetArrayType(elementType, shape); + } + + case ELEMENT_TYPE_GENERICINST: + { + TType baseType = DecodeType(); + int count = ReadCompressedUInt(); + if (count == 0) + throw new BadImageFormatException("Generic instantiation must have at least one type argument"); + if (count > _reader.Remaining) + throw new BadImageFormatException($"Generic argument count {count} exceeds remaining signature bytes"); + var args = ImmutableArray.CreateBuilder(count); + for (int i = 0; i < count; i++) + args.Add(DecodeType()); + return _provider.GetGenericInstantiation(baseType, args.MoveToImmutable()); + } + + case ELEMENT_TYPE_VAR: + return _provider.GetGenericTypeParameter(_genericContext, ReadCompressedUInt()); + + case ELEMENT_TYPE_MVAR: + return _provider.GetGenericMethodParameter(_genericContext, ReadCompressedUInt()); + + case ELEMENT_TYPE_FNPTR: + { + MethodSignature fnSig = DecodeMethodSignature(); + return _provider.GetFunctionPointerType(fnSig); + } + + case ELEMENT_TYPE_PINNED: + return _provider.GetPinnedType(DecodeType()); + + case ELEMENT_TYPE_INTERNAL: + { + ulong val = _reader.ReadPointerSized(_target.PointerSize); + return _provider.GetInternalType(_target, new TargetPointer(val)); + } + + default: + throw new BadImageFormatException($"Unexpected signature type code: 0x{typeCode:X2}"); + } + } + + /// + /// Decodes a TypeDefOrRefOrSpecEncoded token (ECMA-335 II.23.2.8). + /// The compressed value encodes tag in the low 2 bits and RID in the upper bits. + /// + private TType DecodeTypeDefOrRefOrSpec(byte rawTypeKind) + { + int coded = ReadCompressedUInt(); + int tag = coded & 0x3; + int rid = coded >> 2; + + if (rid == 0) + throw new BadImageFormatException("Nil TypeDefOrRefOrSpecEncoded handle in signature"); + + if (rid > 0x00FFFFFF) + throw new BadImageFormatException($"TypeDefOrRefOrSpecEncoded RID out of range: {rid}"); + + return tag switch + { + 0 => _provider.GetTypeFromDefinition(_metadataReader!, MetadataTokens.TypeDefinitionHandle(rid), rawTypeKind), + 1 => _provider.GetTypeFromReference(_metadataReader!, MetadataTokens.TypeReferenceHandle(rid), rawTypeKind), + 2 => _provider.GetTypeFromSpecification(_metadataReader!, _genericContext, MetadataTokens.TypeSpecificationHandle(rid), rawTypeKind), + _ => _provider.GetPrimitiveType(PrimitiveTypeCode.Object), // tag=3 is BaseType in native + }; + } + + private ArrayShape DecodeArrayShape() + { + int rank = ReadCompressedUInt(); + int numSizes = ReadCompressedUInt(); + if (numSizes > _reader.Remaining) + throw new BadImageFormatException($"Array size count {numSizes} exceeds remaining signature bytes"); + var sizes = ImmutableArray.CreateBuilder(numSizes); + for (int i = 0; i < numSizes; i++) + sizes.Add(ReadCompressedUInt()); + int numLoBounds = ReadCompressedUInt(); + if (numLoBounds > _reader.Remaining) + throw new BadImageFormatException($"Array lower bound count {numLoBounds} exceeds remaining signature bytes"); + var loBounds = ImmutableArray.CreateBuilder(numLoBounds); + for (int i = 0; i < numLoBounds; i++) + loBounds.Add(ReadCompressedSignedInt()); + return new ArrayShape(rank, sizes.MoveToImmutable(), loBounds.MoveToImmutable()); + } + + /// + /// Reads a compressed unsigned integer per ECMA-335 II.23.2. + /// + private int ReadCompressedUInt() + { + byte first = _reader.ReadByte(); + if ((first & 0x80) == 0) + return first; + if ((first & 0xC0) == 0x80) + return ((first & 0x3F) << 8) | _reader.ReadByte(); + if ((first & 0xE0) == 0xC0) + return ((first & 0x1F) << 24) | (_reader.ReadByte() << 16) | (_reader.ReadByte() << 8) | _reader.ReadByte(); + + throw new BadImageFormatException("Invalid compressed integer encoding"); + } + + /// + /// Reads a compressed signed integer per ECMA-335 II.23.2. + /// Uses sign extension based on encoded width, matching SRM's BlobReader.ReadCompressedSignedInteger. + /// + private int ReadCompressedSignedInt() + { + byte first = _reader.ReadByte(); + + if ((first & 0x80) == 0) + { + // 1-byte: 7 bits, sign bit is bit 0 of the encoded value + int value = first >> 1; + return (first & 1) != 0 ? value - 0x40 : value; + } + + if ((first & 0xC0) == 0x80) + { + // 2-byte: 14 bits + int raw = ((first & 0x3F) << 8) | _reader.ReadByte(); + int value = raw >> 1; + return (raw & 1) != 0 ? value - 0x2000 : value; + } + + if ((first & 0xE0) == 0xC0) + { + // 4-byte: 29 bits + int raw = ((first & 0x1F) << 24) | (_reader.ReadByte() << 16) | (_reader.ReadByte() << 8) | _reader.ReadByte(); + int value = raw >> 1; + return (raw & 1) != 0 ? value - 0x10000000 : value; + } + + throw new BadImageFormatException("Invalid compressed signed integer encoding"); + } +} 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..e727286cc7de06 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,12 @@ 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; + OffsetOfFloatArgumentRegisters = type.Fields[nameof(OffsetOfFloatArgumentRegisters)].Offset; } public TargetPointer ReturnAddress { get; } @@ -27,4 +33,21 @@ 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; } + + /// + /// Offset to the float argument registers area, relative to the TransitionBlock pointer. + /// Negative on most platforms (float regs are stored before the TransitionBlock). + /// Zero on platforms without float argument registers (x86, Windows x64). + /// + public int OffsetOfFloatArgumentRegisters { 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)