From d8b570c1d47c46ae5bcabfe7711233e41359b5ad Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 27 Feb 2026 14:23:01 -0500 Subject: [PATCH 01/30] cdac: Stack walk GC scanning, exception handling, and IsFilterFunclet support Squash of cdac-stackreferences branch changes onto main: - Implement stack reference enumeration (EnumerateStackRefs) - Add GC scanning support (GcScanner, GcScanContext, BitStreamReader) - Add exception handling for stack walks (ExceptionHandling) - Add IsFunclet/IsFilterFunclet to execution manager - Add EH clause retrieval for ReadyToRun - Add data types: EEILExceptionClause, CorCompileExceptionClause, CorCompileExceptionLookupEntry, LastReportedFuncletInfo - Update datadescriptor.inc with new type layouts - Update SOSDacImpl with improved stack walk support Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/StackWalk.md | 18 +- src/coreclr/vm/codeman.cpp | 2 +- .../vm/datadescriptor/datadescriptor.inc | 41 +- src/coreclr/vm/exinfo.h | 6 + src/coreclr/vm/readytoruninfo.cpp | 1 + src/coreclr/vm/readytoruninfo.h | 3 + .../Extensions/IExecutionManagerExtensions.cs | 12 - .../Contracts/IExecutionManager.cs | 2 + .../Contracts/IStackWalk.cs | 1 + .../Contracts/IThread.cs | 14 +- .../DataType.cs | 4 + .../TargetPointer.cs | 3 + .../ExecutionManagerCore.EEJitManager.cs | 33 ++ ...ecutionManagerCore.ReadyToRunJitManager.cs | 81 +++ .../ExecutionManager/ExecutionManagerCore.cs | 62 +++ .../ExecutionManager/ExecutionManager_1.cs | 4 +- .../ExecutionManager/ExecutionManager_2.cs | 4 +- .../Contracts/GCInfo/BitStreamReader.cs | 279 ++++++++++ .../Contracts/GCInfo/GCInfoDecoder.cs | 6 + .../Contracts/GCInfo/GCInfo_1.cs | 1 + .../Contracts/StackWalk/ExceptionHandling.cs | 171 ++++++ .../Contracts/StackWalk/GC/GcScanContext.cs | 117 ++++ .../Contracts/StackWalk/GC/GcScanFlags.cs | 13 + .../StackWalk/GC/GcScanSlotLocation.cs | 8 + .../Contracts/StackWalk/GC/GcScanner.cs | 50 ++ .../Contracts/StackWalk/GC/StackRefData.cs | 25 + .../Contracts/StackWalk/StackWalk_1.cs | 500 +++++++++++++++++- .../Contracts/Thread_1.cs | 23 +- .../Data/CorCompileExceptionClause.cs | 21 + .../Data/CorCompileExceptionLookupEntry.cs | 21 + .../Data/EEILExceptionClause.cs | 21 + .../Data/ExceptionInfo.cs | 23 +- .../Data/LastReportedFuncletInfo.cs | 19 + .../Data/ReadyToRunInfo.cs | 2 + .../Data/RealCodeHeader.cs | 14 +- .../SOSDacImpl.cs | 65 +-- 36 files changed, 1577 insertions(+), 93 deletions(-) delete mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/Extensions/IExecutionManagerExtensions.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/BitStreamReader.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/ExceptionHandling.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanFlags.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanSlotLocation.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CorCompileExceptionClause.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CorCompileExceptionLookupEntry.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEILExceptionClause.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/LastReportedFuncletInfo.cs diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index bc83127e4df2c8..6795924f2006b2 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -85,7 +85,6 @@ Contracts used: | --- | | `ExecutionManager` | | `Thread` | -| `RuntimeTypeSystem` | ### Stackwalk Algorithm @@ -360,21 +359,10 @@ string GetFrameName(TargetPointer frameIdentifier); TargetPointer GetMethodDescPtr(TargetPointer framePtr) ``` -`GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHandle)` returns the method desc pointer associated with a `IStackDataFrameHandle`. Note there are two major differences between this API and the one above that operates on a TargetPointer. -* This API can either be at a capital 'F' frame or a managed frame unlike the TargetPointer overload which only works at capital 'F' frames. -* This API handles the special ReportInteropMD case which happens under the following conditions - 1. The dataFrame is at an `InlinedCallFrame` - 2. The dataFrame is in a `SW_SKIPPED_FRAME` state - 3. The InlinedCallFrame's return address is managed code - 4. The InlinedCallFrame's return address method has a MDContext arg - - In this case, we report the actual interop MethodDesc. A pointer to the MethodDesc immediately follows the InlinedCallFrame in memory. +`GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHandle)` returns the method desc pointer associated with a `IStackDataFrameHandle`. Note this can either be at a capital 'F' frame or a managed frame unlike the above API which works only at capital 'F' frames. This API is implemeted as follows: -1. Try to get the current frame address `framePtr` with `GetFrameAddress`. -2. If the address is not null, compute `reportInteropMD` as listed above. Otherwise skip to step 5. -3. If `reportInteropMD`, dereference the pointer immediately following the InlinedCallFrame and return that value. -4. If `!reportIteropMD`, return `GetMethodDescPtr(framePtr)`. -5. Check if the current context IP is a managed context using the ExecutionManager contract. If it is a managed context, use the ExecutionManager context to find the related MethodDesc and return the pointer to it. +1. Try to get the current frame address with `GetFrameAddress`. If the address is not null, return `GetMethodDescPtr()`. +2. Check if the current context IP is a managed context using the ExecutionManager contract. If it is a managed contet, use the ExecutionManager context to find the related MethodDesc and return the pointer to it. ```csharp TargetPointer GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHandle) ``` diff --git a/src/coreclr/vm/codeman.cpp b/src/coreclr/vm/codeman.cpp index 3b57576853be3e..7df406e1b9127f 100644 --- a/src/coreclr/vm/codeman.cpp +++ b/src/coreclr/vm/codeman.cpp @@ -6403,7 +6403,7 @@ unsigned ReadyToRunJitManager::InitializeEHEnumeration(const METHODTOKEN& Method ReadyToRunInfo * pReadyToRunInfo = JitTokenToReadyToRunInfo(MethodToken); - IMAGE_DATA_DIRECTORY * pExceptionInfoDir = pReadyToRunInfo->FindSection(ReadyToRunSectionType::ExceptionInfo); + IMAGE_DATA_DIRECTORY * pExceptionInfoDir = pReadyToRunInfo->GetExceptionInfoSection(); if (pExceptionInfoDir == NULL) return 0; diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index b516297965ea79..2ee065576f42c9 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -132,13 +132,25 @@ CDAC_TYPE_END(Exception) CDAC_TYPE_BEGIN(ExceptionInfo) CDAC_TYPE_INDETERMINATE(ExceptionInfo) +CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, PreviousNestedInfo, offsetof(ExInfo, m_pPrevNestedInfo)) CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, ThrownObjectHandle, offsetof(ExInfo, m_hThrowable)) -CDAC_TYPE_FIELD(PreviousNestedInfo, /*pointer*/, PreviousNestedInfo, offsetof(ExInfo, m_pPrevNestedInfo)) +CDAC_TYPE_FIELD(ExceptionInfo, /*uint32*/, ExceptionFlags, offsetof(ExInfo, m_ExceptionFlags.m_flags)) +CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, StackLowBound, cdac_data::StackLowBound) +CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, StackHighBound, cdac_data::StackHighBound) #ifndef TARGET_UNIX -CDAC_TYPE_FIELD(ExceptionWatsonBucketTrackerBuckets, /*pointer*/, ExceptionWatsonBucketTrackerBuckets, cdac_data::ExceptionWatsonBucketTrackerBuckets) +CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, ExceptionWatsonBucketTrackerBuckets, cdac_data::ExceptionWatsonBucketTrackerBuckets) #endif // TARGET_UNIX +CDAC_TYPE_FIELD(ExceptionInfo, /*uint8*/, PassNumber, offsetof(ExInfo, m_passNumber)) +CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, CSFEHClause, offsetof(ExInfo, m_csfEHClause)) +CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, CSFEnclosingClause, offsetof(ExInfo, m_csfEnclosingClause)) +CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, CallerOfActualHandlerFrame, offsetof(ExInfo, m_sfCallerOfActualHandlerFrame)) +CDAC_TYPE_FIELD(ExceptionInfo, /*LastReportedFuncletInfo*/, LastReportedFuncletInfo, offsetof(ExInfo, m_lastReportedFunclet)) CDAC_TYPE_END(ExceptionInfo) +CDAC_TYPE_BEGIN(LastReportedFuncletInfo) +CDAC_TYPE_INDETERMINATE(LastReportedFuncletInfo) +CDAC_TYPE_FIELD(LastReportedFuncletInfo, /*PCODE*/, IP, offsetof(LastReportedFuncletInfo, IP)) +CDAC_TYPE_END(LastReportedFuncletInfo) CDAC_TYPE_BEGIN(GCHandle) CDAC_TYPE_SIZE(sizeof(OBJECTHANDLE)) @@ -662,6 +674,7 @@ CDAC_TYPE_FIELD(ReadyToRunInfo, /*uint32*/, NumHotColdMap, cdac_data::HotColdMap) CDAC_TYPE_FIELD(ReadyToRunInfo, /*pointer*/, DelayLoadMethodCallThunks, cdac_data::DelayLoadMethodCallThunks) CDAC_TYPE_FIELD(ReadyToRunInfo, /*pointer*/, DebugInfoSection, cdac_data::DebugInfoSection) +CDAC_TYPE_FIELD(ReadyToRunInfo, /*pointer*/, ExceptionInfoSection, cdac_data::ExceptionInfoSection) CDAC_TYPE_FIELD(ReadyToRunInfo, /*HashMap*/, EntryPointToMethodDescMap, cdac_data::EntryPointToMethodDescMap) CDAC_TYPE_FIELD(ReadyToRunInfo, /*pointer*/, LoadedImageBase, cdac_data::LoadedImageBase) CDAC_TYPE_FIELD(ReadyToRunInfo, /*pointer*/, Composite, cdac_data::Composite) @@ -702,6 +715,12 @@ CDAC_TYPE_FIELD(ImageDataDirectory, /*uint32*/, VirtualAddress, offsetof(IMAGE_D CDAC_TYPE_FIELD(ImageDataDirectory, /*uint32*/, Size, offsetof(IMAGE_DATA_DIRECTORY, Size)) CDAC_TYPE_END(ImageDataDirectory) +CDAC_TYPE_BEGIN(CorCompileExceptionLookupEntry) +CDAC_TYPE_SIZE(sizeof(CORCOMPILE_EXCEPTION_LOOKUP_TABLE_ENTRY)) +CDAC_TYPE_FIELD(CorCompileExceptionLookupEntry, /*uint32*/, MethodStartRva, offsetof(CORCOMPILE_EXCEPTION_LOOKUP_TABLE_ENTRY, MethodStartRVA)) +CDAC_TYPE_FIELD(CorCompileExceptionLookupEntry, /*uint32*/, ExceptionInfoRva, offsetof(CORCOMPILE_EXCEPTION_LOOKUP_TABLE_ENTRY, ExceptionInfoRVA)) +CDAC_TYPE_END(CorCompileExceptionLookupEntry) + CDAC_TYPE_BEGIN(RuntimeFunction) CDAC_TYPE_SIZE(sizeof(RUNTIME_FUNCTION)) CDAC_TYPE_FIELD(RuntimeFunction, /*uint32*/, BeginAddress, offsetof(RUNTIME_FUNCTION, BeginAddress)) @@ -763,6 +782,7 @@ CDAC_TYPE_BEGIN(RealCodeHeader) CDAC_TYPE_INDETERMINATE(RealCodeHeader) CDAC_TYPE_FIELD(RealCodeHeader, /*pointer*/, MethodDesc, offsetof(RealCodeHeader, phdrMDesc)) CDAC_TYPE_FIELD(RealCodeHeader, /*pointer*/, DebugInfo, offsetof(RealCodeHeader, phdrDebugInfo)) +CDAC_TYPE_FIELD(RealCodeHeader, /*uint16*/, EHInfo, offsetof(RealCodeHeader, phdrJitEHInfo)) CDAC_TYPE_FIELD(RealCodeHeader, /*pointer*/, GCInfo, offsetof(RealCodeHeader, phdrJitGCInfo)) CDAC_TYPE_FIELD(RealCodeHeader, /*uint32*/, NumUnwindInfos, offsetof(RealCodeHeader, nUnwindInfos)) CDAC_TYPE_FIELD(RealCodeHeader, /* T_RUNTIME_FUNCTION */, UnwindInfos, offsetof(RealCodeHeader, unwindInfos)) @@ -794,6 +814,23 @@ CDAC_TYPE_INDETERMINATE(EEILException) CDAC_TYPE_FIELD(EEILException, /* EE_ILEXCEPTION_CLAUSE */, Clauses, offsetof(EE_ILEXCEPTION, Clauses)) CDAC_TYPE_END(EEILException) +CDAC_TYPE_BEGIN(EEILExceptionClause) +CDAC_TYPE_SIZE(sizeof(EE_ILEXCEPTION_CLAUSE)) +CDAC_TYPE_FIELD(EEILExceptionClause, /*uint32*/, Flags, offsetof(EE_ILEXCEPTION_CLAUSE, Flags)) +CDAC_TYPE_FIELD(EEILExceptionClause, /*uint32*/, FilterOffset, offsetof(EE_ILEXCEPTION_CLAUSE, FilterOffset)) +CDAC_TYPE_END(EEILExceptionClause) + +CDAC_TYPE_BEGIN(CorCompileExceptionClause) +CDAC_TYPE_SIZE(sizeof(CORCOMPILE_EXCEPTION_CLAUSE)) +CDAC_TYPE_FIELD(CorCompileExceptionClause, /*uint32*/, Flags, offsetof(CORCOMPILE_EXCEPTION_CLAUSE, Flags)) +CDAC_TYPE_FIELD(CorCompileExceptionClause, /*uint32*/, FilterOffset, offsetof(CORCOMPILE_EXCEPTION_CLAUSE, FilterOffset)) +CDAC_TYPE_END(CorCompileExceptionClause) + +CDAC_TYPE_BEGIN(PatchpointInfo) +CDAC_TYPE_SIZE(sizeof(PatchpointInfo)) +CDAC_TYPE_FIELD(PatchpointInfo, /*uint32*/, LocalCount, cdac_data::LocalCount) +CDAC_TYPE_END(PatchpointInfo) + CDAC_TYPE_BEGIN(CodeHeapListNode) CDAC_TYPE_FIELD(CodeHeapListNode, /*pointer*/, Next, offsetof(HeapList, hpNext)) CDAC_TYPE_FIELD(CodeHeapListNode, /*pointer*/, StartAddress, offsetof(HeapList, startAddress)) diff --git a/src/coreclr/vm/exinfo.h b/src/coreclr/vm/exinfo.h index 302975c5d7ec04..a409fef070407b 100644 --- a/src/coreclr/vm/exinfo.h +++ b/src/coreclr/vm/exinfo.h @@ -57,6 +57,8 @@ struct ExInfo class StackRange { + friend struct ::cdac_data; + public: StackRange(); void Reset(); @@ -364,6 +366,10 @@ struct cdac_data { static constexpr size_t ExceptionWatsonBucketTrackerBuckets = offsetof(ExInfo, m_WatsonBucketTracker) + offsetof(EHWatsonBucketTracker, m_WatsonUnhandledInfo.m_pUnhandledBuckets); + static constexpr size_t StackLowBound = offsetof(ExInfo, m_ScannedStackRange) + + offsetof(ExInfo::StackRange, m_sfLowBound); + static constexpr size_t StackHighBound = offsetof(ExInfo, m_ScannedStackRange) + + offsetof(ExInfo::StackRange, m_sfHighBound); }; #endif // TARGET_UNIX diff --git a/src/coreclr/vm/readytoruninfo.cpp b/src/coreclr/vm/readytoruninfo.cpp index f97915cd71bea4..26e99db74dd71f 100644 --- a/src/coreclr/vm/readytoruninfo.cpp +++ b/src/coreclr/vm/readytoruninfo.cpp @@ -896,6 +896,7 @@ ReadyToRunInfo::ReadyToRunInfo(Module * pModule, LoaderAllocator* pLoaderAllocat m_pSectionDelayLoadMethodCallThunks = m_pComposite->FindSection(ReadyToRunSectionType::DelayLoadMethodCallThunks); m_pSectionDebugInfo = m_pComposite->FindSection(ReadyToRunSectionType::DebugInfo); + m_pSectionExceptionInfo = m_pComposite->FindSection(ReadyToRunSectionType::ExceptionInfo); IMAGE_DATA_DIRECTORY * pinstMethodsDir = m_pComposite->FindSection(ReadyToRunSectionType::InstanceMethodEntryPoints); if (pinstMethodsDir != NULL) diff --git a/src/coreclr/vm/readytoruninfo.h b/src/coreclr/vm/readytoruninfo.h index 7ffae129fb25ad..b0a5866b18dce7 100644 --- a/src/coreclr/vm/readytoruninfo.h +++ b/src/coreclr/vm/readytoruninfo.h @@ -129,6 +129,7 @@ class ReadyToRunInfo PTR_IMAGE_DATA_DIRECTORY m_pSectionDelayLoadMethodCallThunks; PTR_IMAGE_DATA_DIRECTORY m_pSectionDebugInfo; + PTR_IMAGE_DATA_DIRECTORY m_pSectionExceptionInfo; PTR_READYTORUN_IMPORT_SECTION m_pImportSections; DWORD m_nImportSections; @@ -172,6 +173,7 @@ class ReadyToRunInfo PTR_READYTORUN_HEADER GetReadyToRunHeader() const { return m_pHeader; } PTR_IMAGE_DATA_DIRECTORY GetDelayMethodCallThunksSection() const { return m_pSectionDelayLoadMethodCallThunks; } + PTR_IMAGE_DATA_DIRECTORY GetExceptionInfoSection() const { return m_pSectionExceptionInfo; } PTR_NativeImage GetNativeImage() const { return m_pNativeImage; } @@ -365,6 +367,7 @@ struct cdac_data static constexpr size_t HotColdMap = offsetof(ReadyToRunInfo, m_pHotColdMap); 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 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/Extensions/IExecutionManagerExtensions.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/Extensions/IExecutionManagerExtensions.cs deleted file mode 100644 index 303ff64eb444b1..00000000000000 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/Extensions/IExecutionManagerExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -// 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.Extensions; - -public static class IExecutionManagerExtensions -{ - public static bool IsFunclet(this IExecutionManager eman, CodeBlockHandle codeBlockHandle) - { - return eman.GetStartAddress(codeBlockHandle) != eman.GetFuncletStartAddress(codeBlockHandle); - } -} 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 d5fb9fb7622ccd..aa5f71c5072fdb 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 @@ -52,6 +52,8 @@ public interface IExecutionManager : IContract void GetMethodRegionInfo(CodeBlockHandle codeInfoHandle, out uint hotSize, out TargetPointer coldStart, out uint coldSize) => throw new NotImplementedException(); uint GetJITType(CodeBlockHandle codeInfoHandle) => throw new NotImplementedException(); TargetPointer NonVirtualEntry2MethodDesc(TargetCodePointer entrypoint) => throw new NotImplementedException(); + bool IsFunclet(CodeBlockHandle codeInfoHandle) => throw new NotImplementedException(); + bool IsFilterFunclet(CodeBlockHandle codeInfoHandle) => throw new NotImplementedException(); TargetPointer GetUnwindInfo(CodeBlockHandle codeInfoHandle) => throw new NotImplementedException(); TargetPointer GetUnwindInfoBaseAddress(CodeBlockHandle codeInfoHandle) => throw new NotImplementedException(); TargetPointer GetDebugInfo(CodeBlockHandle codeInfoHandle, out bool hasFlagByte) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs index bcddb9b7254a99..14110b2a6d7ad6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs @@ -13,6 +13,7 @@ public interface IStackWalk : IContract static string IContract.Name => nameof(StackWalk); public virtual IEnumerable CreateStackWalk(ThreadData threadData) => throw new NotImplementedException(); + void WalkStackReferences(ThreadData threadData) => throw new NotImplementedException(); byte[] GetRawContext(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException(); TargetPointer GetFrameAddress(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException(); string GetFrameName(TargetPointer frameIdentifier) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs index 296552dfa73836..671b9bf49d74c5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs @@ -20,15 +20,16 @@ public record struct ThreadStoreCounts( [Flags] public enum ThreadState { - Unknown = 0x00000000, - Hijacked = 0x00000080, // Return address has been hijacked - Background = 0x00000200, // Thread is a background thread - Unstarted = 0x00000400, // Thread has never been started - Dead = 0x00000800, // Thread is dead - ThreadPoolWorker = 0x01000000, // Thread is a thread pool worker thread + Unknown = 0x00000000, + Hijacked = 0x00000080, // Return address has been hijacked + Background = 0x00000200, // Thread is a background thread + Unstarted = 0x00000400, // Thread has never been started + Dead = 0x00000800, // Thread is dead + ThreadPoolWorker = 0x01000000, // Thread is a thread pool worker thread } public record struct ThreadData( + TargetPointer ThreadAddress, uint Id, TargetNUInt OSId, ThreadState State, @@ -53,6 +54,7 @@ void GetStackLimitData(TargetPointer threadPointer, out TargetPointer stackBase, out TargetPointer stackLimit, out TargetPointer frameAddress) => throw new NotImplementedException(); TargetPointer IdToThread(uint id) => throw new NotImplementedException(); TargetPointer GetThreadLocalStaticBase(TargetPointer threadPointer, TargetPointer tlsIndexPtr) => throw new NotImplementedException(); + bool IsInStackRegionUnwoundBySpecifiedException(TargetPointer threadAddress, TargetPointer stackPointer) => throw new NotImplementedException(); TargetPointer GetThrowableObject(TargetPointer threadPointer) => throw new NotImplementedException(); byte[] GetWatsonBuckets(TargetPointer threadPointer) => 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 ed2bba9751bd34..a9a975aa1f48db 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -37,6 +37,7 @@ public enum DataType ExceptionLookupTableEntry, EEILException, R2RExceptionClause, + LastReportedFuncletInfo, RuntimeThreadLocals, IdDispenser, Module, @@ -101,6 +102,9 @@ public enum DataType RangeSectionFragment, RangeSection, RealCodeHeader, + CorCompileExceptionLookupEntry, + CorCompileExceptionClause, + EEILExceptionClause, CodeHeapListNode, MethodDescVersioningState, ILCodeVersioningState, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/TargetPointer.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/TargetPointer.cs index d145347f3220a9..2c1f199bb8e1e7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/TargetPointer.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/TargetPointer.cs @@ -1,5 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. + using System; namespace Microsoft.Diagnostics.DataContractReader; @@ -19,6 +20,8 @@ namespace Microsoft.Diagnostics.DataContractReader; public static bool operator ==(TargetPointer left, TargetPointer right) => left.Value == right.Value; public static bool operator !=(TargetPointer left, TargetPointer right) => left.Value != right.Value; + public static TargetPointer PlatformMaxValue(Target target) => target.PointerSize == 4 ? Max32Bit : Max64Bit; + public override bool Equals(object? obj) => obj is TargetPointer pointer && Equals(pointer); public bool Equals(TargetPointer other) => Value == other.Value; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs index b275e10ab766fb..70875032b3a12d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; @@ -141,6 +142,38 @@ public override void GetGCInfo(RangeSection rangeSection, TargetCodePointer jitt gcInfo = realCodeHeader.GCInfo; } + public override IEnumerable GetEHClauses(RangeSection rangeSection, TargetCodePointer jittedCodeAddress) + { + if (rangeSection.IsRangeList) + yield break; + + if (rangeSection.Data == null) + throw new ArgumentException(nameof(rangeSection)); + + TargetPointer codeStart = FindMethodCode(rangeSection, jittedCodeAddress); + if (codeStart == TargetPointer.Null) + yield break; + Debug.Assert(codeStart.Value <= jittedCodeAddress.Value); + + if (!GetRealCodeHeader(rangeSection, codeStart, out Data.RealCodeHeader? realCodeHeader)) + yield break; + + // number of EH clauses is stored in a pointer sized integer just before the EHInfo array + TargetNUInt ehClauseCount = Target.ReadNUInt(realCodeHeader.EHInfo - (uint)Target.PointerSize); + uint ehClauseSize = Target.GetTypeInfo(DataType.EEILExceptionClause).Size ?? throw new InvalidOperationException("EEILExceptionClause size is not known"); + + for (uint i = 0; i < ehClauseCount.Value; i++) + { + TargetPointer clauseAddress = realCodeHeader.EHInfo + (i * ehClauseSize); + Data.EEILExceptionClause clause = Target.ProcessedData.GetOrAdd(clauseAddress); + yield return new EHClause() + { + Flags = (EHClause.CorExceptionFlag)clause.Flags, + FilterOffset = clause.FilterOffset + }; + } + } + private TargetPointer FindMethodCode(RangeSection rangeSection, TargetCodePointer jittedCodeAddress) { // EEJitManager::FindMethodCode diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs index ff08e588e2823e..021a92bb805426 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.Diagnostics.DataContractReader.Data; @@ -179,6 +180,86 @@ private uint GetR2RGCInfoVersion(Data.ReadyToRunInfo r2rInfo) }; } + private uint GetUnwindDataSize() + { + RuntimeInfoArchitecture arch = Target.Contracts.RuntimeInfo.GetTargetArchitecture(); + return arch switch + { + RuntimeInfoArchitecture.X86 => sizeof(uint), + _ => throw new NotSupportedException($"GetUnwindDataSize not supported for architecture: {arch}") + }; + } + + public override IEnumerable GetEHClauses(RangeSection rangeSection, TargetCodePointer jittedCodeAddress) + { + // ReadyToRunJitManager::GetEHClauses + Data.ReadyToRunInfo r2rInfo = GetReadyToRunInfo(rangeSection); + if (!GetRuntimeFunction(rangeSection, r2rInfo, jittedCodeAddress, out TargetPointer imageBase, out uint index)) + yield break; + + index = AdjustRuntimeFunctionIndexForHotCold(r2rInfo, index); + index = AdjustRuntimeFunctionToMethodStart(r2rInfo, imageBase, index, out _); + uint methodStartRva = _runtimeFunctions.GetRuntimeFunction(r2rInfo.RuntimeFunctions, index).BeginAddress; + + if (r2rInfo.ExceptionInfoSection == TargetPointer.Null) + yield break; + Data.ImageDataDirectory exceptionInfoData = Target.ProcessedData.GetOrAdd(r2rInfo.ExceptionInfoSection); + + // R2R images are always mapped so we can directly add the RVA to the base address + TargetPointer pExceptionLookupTable = imageBase + exceptionInfoData.VirtualAddress; + uint numEntries = exceptionInfoData.Size / Target.GetTypeInfo(DataType.CorCompileExceptionLookupEntry).Size + ?? throw new InvalidOperationException("CorCompileExceptionLookupEntry size is not known"); + + // at least 2 entries (1 valid + 1 sentinel) + Debug.Assert(numEntries >= 2); + Debug.Assert(GetExceptionLookupEntry(pExceptionLookupTable, numEntries - 1).MethodStartRva == uint.MaxValue); + + if (!BinaryThenLinearSearch.Search( + 0, + numEntries - 2, + Compare, + Match, + out uint ehInfoIndex)) + yield break; + + bool Compare(uint index) + { + Data.CorCompileExceptionLookupEntry exceptionEntry = GetExceptionLookupEntry(pExceptionLookupTable, index); + return methodStartRva < exceptionEntry.MethodStartRva; + } + + bool Match(uint index) + { + Data.CorCompileExceptionLookupEntry exceptionEntry = GetExceptionLookupEntry(pExceptionLookupTable, index); + return methodStartRva == exceptionEntry.MethodStartRva; + } + + Data.CorCompileExceptionLookupEntry entry = GetExceptionLookupEntry(pExceptionLookupTable, ehInfoIndex); + Data.CorCompileExceptionLookupEntry nextEntry = GetExceptionLookupEntry(pExceptionLookupTable, ehInfoIndex + 1); + uint exceptionInfoSize = nextEntry.ExceptionInfoRva - entry.ExceptionInfoRva; + uint clauseSize = Target.GetTypeInfo(DataType.CorCompileExceptionClause).Size + ?? throw new InvalidOperationException("CorCompileExceptionClause size is not known"); + uint numClauses = exceptionInfoSize / clauseSize; + + for (uint i = 0; i < numClauses; i++) + { + TargetPointer clauseAddress = imageBase + (i * clauseSize); + Data.CorCompileExceptionClause clause = Target.ProcessedData.GetOrAdd(clauseAddress); + yield return new EHClause() + { + Flags = (EHClause.CorExceptionFlag)clause.Flags, + FilterOffset = clause.FilterOffset + }; + } + } + + private Data.CorCompileExceptionLookupEntry GetExceptionLookupEntry(TargetPointer table, uint index) + { + TargetPointer entryAddress = table + (index * (Target.GetTypeInfo(DataType.CorCompileExceptionLookupEntry).Size + ?? throw new InvalidOperationException("CorCompileExceptionLookupEntry size is not known"))); + return Target.ProcessedData.GetOrAdd(entryAddress); + } + #region RuntimeFunction Helpers private Data.ReadyToRunInfo GetReadyToRunInfo(RangeSection rangeSection) 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 ad9d24248d3972..4514f59f8fce70 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 @@ -93,6 +93,7 @@ public abstract void GetMethodRegionInfo( public abstract TargetPointer GetDebugInfo(RangeSection rangeSection, TargetCodePointer jittedCodeAddress, out bool hasFlagByte); public abstract void GetGCInfo(RangeSection rangeSection, TargetCodePointer jittedCodeAddress, out TargetPointer gcInfo, out uint gcVersion); public abstract void GetExceptionClauses(RangeSection rangeSection, CodeBlockHandle codeInfoHandle, out TargetPointer startAddr, out TargetPointer endAddr); + public abstract IEnumerable GetEHClauses(RangeSection rangeSection, TargetCodePointer jittedCodeAddress); } private sealed class RangeSection @@ -146,6 +147,22 @@ internal static RangeSection Find(Target target, Data.RangeSectionMap topRangeSe } } + private sealed class EHClause + { + public enum CorExceptionFlag : uint + { + COR_ILEXCEPTION_CLAUSE_NONE = 0x0, + COR_ILEXCEPTION_CLAUSE_FILTER = 0x1, + COR_ILEXCEPTION_CLAUSE_FINALLY = 0x2, + COR_ILEXCEPTION_CLAUSE_FAULT = 0x4, + } + + public CorExceptionFlag Flags { get; init; } + public uint FilterOffset { get; init; } + + public bool IsFilterHandler => Flags.HasFlag(CorExceptionFlag.COR_ILEXCEPTION_CLAUSE_FILTER); + } + private JitManager GetJitManager(Data.RangeSection rangeSectionData) { if (rangeSectionData.R2RModule == TargetPointer.Null) @@ -301,6 +318,51 @@ TargetPointer IExecutionManager.NonVirtualEntry2MethodDesc(TargetCodePointer ent } return TargetPointer.Null; } + + bool IExecutionManager.IsFunclet(CodeBlockHandle codeInfoHandle) + { + return ((IExecutionManager)this).GetStartAddress(codeInfoHandle) == + ((IExecutionManager)this).GetFuncletStartAddress(codeInfoHandle); + } + + bool IExecutionManager.IsFilterFunclet(CodeBlockHandle codeInfoHandle) + { + if (!_codeInfos.TryGetValue(codeInfoHandle.Address, out CodeBlock? info)) + throw new InvalidOperationException($"{nameof(CodeBlock)} not found for {codeInfoHandle.Address}"); + + RangeSection range = RangeSection.Find(_target, _topRangeSectionMap, _rangeSectionMapLookup, codeInfoHandle.Address.Value); + if (range.Data == null) + throw new InvalidOperationException("Unable to get runtime function address"); + JitManager jitManager = GetJitManager(range.Data); + + IExecutionManager eman = this; + + if (eman.IsFunclet(codeInfoHandle) == false) + return false; + + TargetPointer codeAddress = info.StartAddress.Value + info.RelativeOffset.Value; + TargetPointer funcletStartAddress = eman.GetFuncletStartAddress(codeInfoHandle).AsTargetPointer; + + uint relativeOffsetInFunclet = (uint)(codeAddress - funcletStartAddress); + Debug.Assert(eman.GetRelativeOffset(codeInfoHandle).Value >= relativeOffsetInFunclet); + + uint funcletStartOffset = (uint)(eman.GetRelativeOffset(codeInfoHandle).Value - relativeOffsetInFunclet); + // can we calculate this much more simply?? + uint funcletStartOffset2 = (uint)(funcletStartAddress - info.StartAddress); + Debug.Assert(funcletStartOffset == funcletStartOffset2); + + IEnumerable ehClauses = jitManager.GetEHClauses(range, codeInfoHandle.Address.Value); + foreach (EHClause ehClause in ehClauses) + { + if (ehClause.IsFilterHandler && ehClause.FilterOffset == funcletStartOffset) + { + return true; + } + } + + return false; + } + TargetPointer IExecutionManager.GetUnwindInfo(CodeBlockHandle codeInfoHandle) { RangeSection range = RangeSectionFromCodeBlockHandle(codeInfoHandle); 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 76faf0d050c02a..e12ee5c7ee3c7f 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 @@ -19,9 +19,11 @@ internal ExecutionManager_1(Target target, Data.RangeSectionMap topRangeSectionM public TargetPointer GetMethodDesc(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetMethodDesc(codeInfoHandle); public TargetCodePointer GetStartAddress(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetStartAddress(codeInfoHandle); public TargetCodePointer GetFuncletStartAddress(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetFuncletStartAddress(codeInfoHandle); - public void GetMethodRegionInfo(CodeBlockHandle codeInfoHandle, out uint hotSize, out TargetPointer coldStart, out uint coldSize) => _executionManagerCore.GetMethodRegionInfo(codeInfoHandle, out hotSize, out coldStart, out coldSize); + void GetMethodRegionInfo(CodeBlockHandle codeInfoHandle, out uint hotSize, out TargetPointer coldStart, out uint coldSize) => _executionManagerCore.GetMethodRegionInfo(codeInfoHandle, out hotSize, out coldStart, out coldSize); public uint GetJITType(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetJITType(codeInfoHandle); public TargetPointer NonVirtualEntry2MethodDesc(TargetCodePointer entrypoint) => _executionManagerCore.NonVirtualEntry2MethodDesc(entrypoint); + public bool IsFunclet(CodeBlockHandle codeInfoHandle) => _executionManagerCore.IsFunclet(codeInfoHandle); + public bool IsFilterFunclet(CodeBlockHandle codeInfoHandle) => _executionManagerCore.IsFilterFunclet(codeInfoHandle); public TargetPointer GetUnwindInfo(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetUnwindInfo(codeInfoHandle); public TargetPointer GetUnwindInfoBaseAddress(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetUnwindInfoBaseAddress(codeInfoHandle); public TargetPointer GetDebugInfo(CodeBlockHandle codeInfoHandle, out bool hasFlagByte) => _executionManagerCore.GetDebugInfo(codeInfoHandle, out hasFlagByte); 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 8e7f6bb5267510..d5bfce8739456b 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 @@ -19,9 +19,11 @@ internal ExecutionManager_2(Target target, Data.RangeSectionMap topRangeSectionM public TargetPointer GetMethodDesc(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetMethodDesc(codeInfoHandle); public TargetCodePointer GetStartAddress(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetStartAddress(codeInfoHandle); public TargetCodePointer GetFuncletStartAddress(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetFuncletStartAddress(codeInfoHandle); - public void GetMethodRegionInfo(CodeBlockHandle codeInfoHandle, out uint hotSize, out TargetPointer coldStart, out uint coldSize) => _executionManagerCore.GetMethodRegionInfo(codeInfoHandle, out hotSize, out coldStart, out coldSize); + void GetMethodRegionInfo(CodeBlockHandle codeInfoHandle, out uint hotSize, out TargetPointer coldStart, out uint coldSize) => _executionManagerCore.GetMethodRegionInfo(codeInfoHandle, out hotSize, out coldStart, out coldSize); public uint GetJITType(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetJITType(codeInfoHandle); public TargetPointer NonVirtualEntry2MethodDesc(TargetCodePointer entrypoint) => _executionManagerCore.NonVirtualEntry2MethodDesc(entrypoint); + public bool IsFunclet(CodeBlockHandle codeInfoHandle) => _executionManagerCore.IsFunclet(codeInfoHandle); + public bool IsFilterFunclet(CodeBlockHandle codeInfoHandle) => _executionManagerCore.IsFilterFunclet(codeInfoHandle); public TargetPointer GetUnwindInfo(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetUnwindInfo(codeInfoHandle); public TargetPointer GetUnwindInfoBaseAddress(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetUnwindInfoBaseAddress(codeInfoHandle); public TargetPointer GetDebugInfo(CodeBlockHandle codeInfoHandle, out bool hasFlagByte) => _executionManagerCore.GetDebugInfo(codeInfoHandle, out hasFlagByte); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/BitStreamReader.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/BitStreamReader.cs new file mode 100644 index 00000000000000..64b5ffa35f8415 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/BitStreamReader.cs @@ -0,0 +1,279 @@ +// 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.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +/// +/// Managed implementation of the native BitStreamReader class for reading compressed GC info. +/// This class provides methods to read variable-length bit sequences from a memory buffer +/// accessed through the Target abstraction. +/// +internal struct BitStreamReader +{ + private static readonly int BitsPerSize = IntPtr.Size * 8; + + private readonly Target _target; + private readonly TargetPointer _buffer; + private readonly int _initialRelPos; + + private TargetPointer _current; + private int _relPos; + private nuint _currentValue; + + /// + /// Initializes a new BitStreamReader starting at the specified buffer address. + /// + /// The target process to read from + /// Pointer to the start of the bit stream data + public BitStreamReader(Target target, TargetPointer buffer) + { + ArgumentNullException.ThrowIfNull(target); + + if (buffer == TargetPointer.Null) + throw new ArgumentException("Buffer pointer cannot be null", nameof(buffer)); + + _target = target; + + // Align buffer to pointer size boundary (similar to native implementation) + nuint pointerMask = (nuint)target.PointerSize - 1; + TargetPointer alignedBuffer = new(buffer.Value & ~(ulong)pointerMask); + + _buffer = alignedBuffer; + _current = alignedBuffer; + _initialRelPos = (int)((buffer.Value % (ulong)target.PointerSize) * 8); + _relPos = _initialRelPos; + + // Prefetch the first word and position it correctly + _currentValue = ReadPointerSizedValue(_current); + _currentValue >>= _relPos; + } + + /// + /// Copy constructor + /// + /// The BitStreamReader to copy from + public BitStreamReader(BitStreamReader other) + { + _target = other._target; + _buffer = other._buffer; + _initialRelPos = other._initialRelPos; + _current = other._current; + _relPos = other._relPos; + _currentValue = other._currentValue; + } + + /// + /// Reads the specified number of bits from the stream. + /// + /// Number of bits to read (1 to pointer size in bits) + /// The value read from the stream + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public nuint Read(int numBits) + { + Debug.Assert(numBits > 0 && numBits <= BitsPerSize); + + nuint result = _currentValue; + _currentValue >>= numBits; + int newRelPos = _relPos + numBits; + + if (newRelPos > BitsPerSize) + { + // Need to read from next word + _current = new TargetPointer(_current.Value + (ulong)_target.PointerSize); + nuint nextValue = ReadPointerSizedValue(_current); + newRelPos -= BitsPerSize; + nuint extraBits = nextValue << (numBits - newRelPos); + result |= extraBits; + _currentValue = nextValue >> newRelPos; + } + + _relPos = newRelPos; + + // Mask to get only the requested bits + nuint mask = (nuint.MaxValue >> (BitsPerSize - numBits)); + result &= mask; + + return result; + } + + /// + /// Reads a single bit from the stream (optimized version). + /// + /// The bit value (0 or 1) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public nuint ReadOneFast() + { + // Check if we need to fetch the next word + if (_relPos == BitsPerSize) + { + _current = new TargetPointer(_current.Value + (ulong)_target.PointerSize); + _currentValue = ReadPointerSizedValue(_current); + _relPos = 0; + } + + _relPos++; + nuint result = _currentValue & 1; + _currentValue >>= 1; + + return result; + } + + /// + /// Gets the current position in bits from the start of the stream. + /// + /// Current bit position + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public nuint GetCurrentPos() + { + long wordOffset = ((long)_current.Value - (long)_buffer.Value) / _target.PointerSize; + return (nuint)(wordOffset * BitsPerSize + _relPos - _initialRelPos); + } + + /// + /// Sets the current position in the stream to the specified bit offset. + /// + /// Target bit position from the start of the stream + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetCurrentPos(nuint pos) + { + nuint adjPos = pos + (nuint)_initialRelPos; + nuint wordOffset = adjPos / (nuint)BitsPerSize; + int newRelPos = (int)(adjPos % (nuint)BitsPerSize); + + _current = new TargetPointer(_buffer.Value + wordOffset * (ulong)_target.PointerSize); + _relPos = newRelPos; + + // Prefetch the new word and position it correctly + _currentValue = ReadPointerSizedValue(_current) >> newRelPos; + } + + /// + /// Skips the specified number of bits in the stream. + /// + /// Number of bits to skip (can be negative) + public void Skip(nint numBitsToSkip) + { + nuint newPos = (nuint)((nint)GetCurrentPos() + numBitsToSkip); + + nuint adjPos = newPos + (nuint)_initialRelPos; + nuint wordOffset = adjPos / (nuint)BitsPerSize; + int newRelPos = (int)(adjPos % (nuint)BitsPerSize); + + _current = new TargetPointer(_buffer.Value + wordOffset * (ulong)_target.PointerSize); + _relPos = newRelPos; + + // Skipping ahead may go to a position at the edge-exclusive + // end of the stream. The location may have no more data. + // We will not prefetch on word boundary - in case + // the next word is in an unreadable page. + if (_relPos == 0) + { + _current = new TargetPointer(_current.Value - (ulong)_target.PointerSize); + _relPos = BitsPerSize; + _currentValue = 0; + } + else + { + _currentValue = ReadPointerSizedValue(_current) >> _relPos; + } + } + + /// + /// Decodes a variable-length unsigned integer. + /// + /// Base value for encoding (number of bits per chunk) + /// The decoded unsigned integer + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public nuint DecodeVarLengthUnsigned(int baseValue) + { + Debug.Assert(baseValue > 0 && baseValue < BitsPerSize); + + nuint result = Read(baseValue + 1); + if ((result & ((nuint)1 << baseValue)) != 0) + { + result ^= DecodeVarLengthUnsignedMore(baseValue); + } + + return result; + } + + /// + /// Helper method for decoding variable-length unsigned integers with extension bits. + /// + /// Base value for encoding + /// The additional bits for the decoded value + private nuint DecodeVarLengthUnsignedMore(int baseValue) + { + Debug.Assert(baseValue > 0 && baseValue < BitsPerSize); + + nuint numEncodings = (nuint)1 << baseValue; + nuint result = numEncodings; + + for (int shift = baseValue; ; shift += baseValue) + { + Debug.Assert(shift + baseValue <= BitsPerSize); + + nuint currentChunk = Read(baseValue + 1); + result ^= (currentChunk & (numEncodings - 1)) << shift; + + if ((currentChunk & numEncodings) == 0) + { + // Extension bit is not set, we're done + return result; + } + } + } + + /// + /// Decodes a variable-length signed integer. + /// + /// Base value for encoding (number of bits per chunk) + /// The decoded signed integer + public nint DecodeVarLengthSigned(int baseValue) + { + Debug.Assert(baseValue > 0 && baseValue < BitsPerSize); + + nuint numEncodings = (nuint)1 << baseValue; + nint result = 0; + + for (int shift = 0; ; shift += baseValue) + { + Debug.Assert(shift + baseValue <= BitsPerSize); + + nuint currentChunk = Read(baseValue + 1); + result |= (nint)(currentChunk & (numEncodings - 1)) << shift; + + if ((currentChunk & numEncodings) == 0) + { + // Extension bit is not set, sign-extend and we're done + int signBits = BitsPerSize - (shift + baseValue); + result <<= signBits; + result >>= signBits; // Arithmetic right shift for sign extension + return result; + } + } + } + + /// + /// Reads a pointer-sized value from the target at the specified address. + /// + /// Address to read from + /// The value read as nuint + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private nuint ReadPointerSizedValue(TargetPointer address) + { + if (_target.PointerSize == 4) + { + return _target.Read(address); + } + else + { + Debug.Assert(_target.PointerSize == 8); + return (nuint)_target.Read(address); + } + } +} 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 3b51810689bdac..32f0671cdefad3 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 @@ -497,6 +497,12 @@ public uint GetCodeLength() return _codeLength; } + public IReadOnlyList GetInterruptibleRanges() + { + EnsureDecodedTo(DecodePoints.InterruptibleRanges); + return _interruptibleRanges; + } + #endregion #region Helper Methods 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..397fba29665955 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 @@ -6,6 +6,7 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts; + internal class GCInfo_1 : IGCInfo where TTraits : IGCInfoTraits { private readonly Target _target; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/ExceptionHandling.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/ExceptionHandling.cs new file mode 100644 index 00000000000000..f950bfe321741e --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/ExceptionHandling.cs @@ -0,0 +1,171 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Diagnostics; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +internal partial class StackWalk_1 : IStackWalk +{ + /// + /// Given the CrawlFrame for a funclet frame, return the frame pointer of the enclosing funclet frame. + /// For filter funclet frames and normal method frames, this function returns a NULL StackFrame. + /// + /// + /// StackFrame.IsNull() - no skipping is necessary + /// StackFrame.IsMaxVal() - skip one frame and then ask again + /// Anything else - skip to the method frame indicated by the return value and ask again + /// + private TargetPointer FindParentStackFrameForStackWalk(StackDataFrameHandle handle, bool forGCReporting = false) + { + if (!forGCReporting && IsFilterFunclet(handle)) + { + return TargetPointer.Null; + } + else + { + return FindParentStackFrameHelper(handle, forGCReporting); + } + } + + private TargetPointer FindParentStackFrameHelper( + StackDataFrameHandle handle, + bool forGCReporting = false) + { + IPlatformAgnosticContext callerContext = handle.Context.Clone(); + callerContext.Unwind(_target); + TargetPointer callerStackFrame = callerContext.StackPointer; + + bool isFilterFunclet = IsFilterFunclet(handle); + + // Check for out-of-line finally funclets. Filter funclets can't be out-of-line. + if (!isFilterFunclet) + { + TargetPointer callerIp = callerContext.InstructionPointer; + + // In the runtime, on Windows, we check with that the IP is in the runtime + // TODO(stackref): make sure this difference doesn't matter + bool isCallerInVM = !IsManaged(callerIp, out CodeBlockHandle? _); + + if (!isCallerInVM) + { + if (!forGCReporting) + { + return TargetPointer.PlatformMaxValue(_target); + } + else + { + // ExInfo::GetCallerSPOfParentOfNonExceptionallyInvokedFunclet + IPlatformAgnosticContext callerCallerContext = callerContext.Clone(); + callerCallerContext.Unwind(_target); + return callerCallerContext.StackPointer; + } + } + } + + TargetPointer pExInfo = GetCurrentExceptionTracker(handle); + while (pExInfo != TargetPointer.Null) + { + Data.ExceptionInfo exInfo = _target.ProcessedData.GetOrAdd(pExInfo); + pExInfo = exInfo.PreviousNestedInfo; + + // ExInfo::StackRange::IsEmpty + if (exInfo.StackLowBound == TargetPointer.PlatformMaxValue(_target) && + exInfo.StackHighBound == TargetPointer.Null) + { + // This is ExInfo has just been created, skip it. + continue; + } + + if (callerStackFrame == exInfo.CSFEHClause) + { + return exInfo.CSFEnclosingClause; + } + } + + return TargetPointer.Null; + } + + + private bool IsFunclet(StackDataFrameHandle handle) + { + if (handle.State is StackWalkState.SW_FRAME or StackWalkState.SW_SKIPPED_FRAME) + { + return false; + } + + if (!IsManaged(handle.Context.InstructionPointer, out CodeBlockHandle? cbh)) + return false; + + return _eman.IsFunclet(cbh.Value); + } + + private bool IsFilterFunclet(StackDataFrameHandle handle) + { + if (handle.State is StackWalkState.SW_FRAME or StackWalkState.SW_SKIPPED_FRAME) + { + return false; + } + + if (!IsManaged(handle.Context.InstructionPointer, out CodeBlockHandle? cbh)) + return false; + + return _eman.IsFilterFunclet(cbh.Value); + } + + private TargetPointer GetCurrentExceptionTracker(StackDataFrameHandle handle) + { + Data.Thread thread = _target.ProcessedData.GetOrAdd(handle.ThreadData.ThreadAddress); + return thread.ExceptionTracker; + } + + private bool HasFrameBeenUnwoundByAnyActiveException(IStackDataFrameHandle stackDataFrameHandle) + { + StackDataFrameHandle handle = AssertCorrectHandle(stackDataFrameHandle); + + TargetPointer exInfo = GetCurrentExceptionTracker(handle); + while (exInfo != TargetPointer.Null) + { + Data.ExceptionInfo exceptionInfo = _target.ProcessedData.GetOrAdd(exInfo); + exInfo = exceptionInfo.PreviousNestedInfo; + + TargetPointer stackPointer; + if (handle.State is StackWalkState.SW_FRAMELESS) + { + IPlatformAgnosticContext callerContext = handle.Context.Clone(); + callerContext.Unwind(_target); + stackPointer = callerContext.StackPointer; + } + else + { + stackPointer = handle.Context.FramePointer; + } + if (IsInStackRegionUnwoundBySpecifiedException(handle.ThreadData, stackPointer)) + { + return true; + } + } + return false; + } + + private bool IsInStackRegionUnwoundBySpecifiedException(ThreadData threadData, TargetPointer stackPointer) + { + // See ExInfo::IsInStackRegionUnwoundBySpecifiedException for explanation + Data.Thread thread = _target.ProcessedData.GetOrAdd(threadData.ThreadAddress); + TargetPointer exInfo = thread.ExceptionTracker; + while (exInfo != TargetPointer.Null) + { + Data.ExceptionInfo exceptionInfo = _target.ProcessedData.GetOrAdd(exInfo); + if (exceptionInfo.StackLowBound < stackPointer && stackPointer <= exceptionInfo.StackHighBound) + { + return true; + } + exInfo = exceptionInfo.PreviousNestedInfo; + } + return false; + } + +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs new file mode 100644 index 00000000000000..ccde7a11b99612 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs @@ -0,0 +1,117 @@ +// 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.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Data; +using static Microsoft.Diagnostics.DataContractReader.Contracts.StackWalk_1; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +internal class GcScanContext +{ + + + private readonly Target _target; + public bool ResolveInteriorPointers { get; } + public List StackRefs { get; } = []; + public TargetPointer StackPointer { get; private set; } + public TargetPointer InstructionPointer { get; private set; } + public TargetPointer Frame { get; private set; } + + public GcScanContext(Target target, bool resolveInteriorPointers) + { + _target = target; + ResolveInteriorPointers = resolveInteriorPointers; + } + + public void UpdateScanContext(TargetPointer sp, TargetPointer ip, TargetPointer frame) + { + StackPointer = sp; + InstructionPointer = ip; + Frame = frame; + } + + public void GCEnumCallback(TargetPointer pObject, GcScanFlags flags, GcScanSlotLocation loc) + { + // Yuck. The GcInfoDecoder reports a local pointer for registers (as it's reading out of the REGDISPLAY + // in the stack walk), and it reports a TADDR for stack locations. This is architecturally difficulty + // to fix, so we are leaving it for now. + TargetPointer addr; + TargetPointer obj; + + if (loc.TargetPtr) + { + addr = pObject; + obj = _target.ReadPointer(addr); + } + else + { + addr = 0; + obj = pObject; + } + + if (flags.HasFlag(GcScanFlags.GC_CALL_INTERIOR) && ResolveInteriorPointers) + { + // TODO(stackref): handle interior pointers + throw new NotImplementedException(); + } + + StackRefData data = new() + { + HasRegisterInformation = true, + Register = loc.Reg, + Offset = loc.RegOffset, + Address = addr, + Object = obj, + Flags = flags, + StackPointer = StackPointer, + }; + + if (Frame != TargetPointer.Null) + { + data.SourceType = StackRefData.SourceTypes.StackSourceFrame; + data.Source = Frame; + } + else + { + data.SourceType = StackRefData.SourceTypes.StackSourceIP; + data.Source = InstructionPointer; + } + + StackRefs.Add(data); + } + + public void GCReportCallback(TargetPointer ppObj, GcScanFlags flags) + { + if (flags.HasFlag(GcScanFlags.GC_CALL_INTERIOR) && ResolveInteriorPointers) + { + // TODO(stackref): handle interior pointers + throw new NotImplementedException(); + } + + StackRefData data = new() + { + HasRegisterInformation = false, + Register = 0, + Offset = 0, + Address = ppObj, + Object = TargetPointer.Null, + Flags = flags, + StackPointer = StackPointer, + }; + + if (Frame != TargetPointer.Null) + { + data.SourceType = StackRefData.SourceTypes.StackSourceFrame; + data.Source = Frame; + } + else + { + data.SourceType = StackRefData.SourceTypes.StackSourceIP; + data.Source = InstructionPointer; + } + + StackRefs.Add(data); + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanFlags.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanFlags.cs new file mode 100644 index 00000000000000..85f7b666f1ef9e --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanFlags.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +[Flags] +internal enum GcScanFlags +{ + GC_CALL_INTERIOR = 0x1, + GC_CALL_PINNED = 0x2, +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanSlotLocation.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanSlotLocation.cs new file mode 100644 index 00000000000000..7e45bba9d19ce1 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanSlotLocation.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +internal readonly record struct GcScanSlotLocation(int Reg, int RegOffset, bool TargetPtr); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs new file mode 100644 index 00000000000000..45cd547b8e5313 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +internal class GcScanner +{ + public enum CodeManagerFlags : uint + { + ActiveStackFrame = 0x1, + ExecutionAborted = 0x2, + ParentOfFuncletStackFrame = 0x40, + NoReportUntracked = 0x80, + ReportFPBasedSlotsOnly = 0x200, + } + + private readonly Target _target; + private readonly IExecutionManager _eman; + private readonly IGCInfo _gcInfo; + + internal GcScanner(Target target) + { + _target = target; + _eman = target.Contracts.ExecutionManager; + _gcInfo = target.Contracts.GCInfo; + } + + public bool EnumGcRefs( + IPlatformAgnosticContext context, + CodeBlockHandle cbh, + CodeManagerFlags flags, + GcScanContext scanContext) + { + TargetNUInt curOffs = _eman.GetRelativeOffset(cbh); + + _eman.GetGCInfo(cbh, out TargetPointer pGcInfo, out uint gcVersion); + + if (_eman.IsFilterFunclet(cbh)) + { + // Filters are the only funclet that run during the 1st pass, and must have + // both the leaf and the parent frame reported. In order to avoid double + // reporting of the untracked variables, do not report them for the filter. + flags |= CodeManagerFlags.NoReportUntracked; + } + + return false; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs new file mode 100644 index 00000000000000..f7670e68a9f21c --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +internal class StackRefData +{ + public enum SourceTypes + { + StackSourceIP = 0, + StackSourceFrame = 1, + } + + public bool HasRegisterInformation { get; set; } + public int Register { get; set; } + public int Offset { get; set; } + public TargetPointer Address { get; set; } + public TargetPointer Object { get; set; } + public GcScanFlags Flags { get; set; } + public SourceTypes SourceType { get; set; } + public TargetPointer Source { get; set; } + public TargetPointer StackPointer { get; set; } +} 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 281bf58b6d3fe3..c7a3bc929e2ad4 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs @@ -8,16 +8,19 @@ using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; using Microsoft.Diagnostics.DataContractReader.Data; +using System.Linq; namespace Microsoft.Diagnostics.DataContractReader.Contracts; -internal readonly struct StackWalk_1 : IStackWalk +internal partial class StackWalk_1 : IStackWalk { private readonly Target _target; + private readonly IExecutionManager _eman; internal StackWalk_1(Target target) { _target = target; + _eman = target.Contracts.ExecutionManager; } public enum StackWalkState @@ -38,16 +41,18 @@ public enum StackWalkState private record StackDataFrameHandle( IPlatformAgnosticContext Context, StackWalkState State, - TargetPointer FrameAddress) : IStackDataFrameHandle + TargetPointer FrameAddress, + ThreadData ThreadData) : IStackDataFrameHandle { } - private class StackWalkData(IPlatformAgnosticContext context, StackWalkState state, FrameIterator frameIter) + private class StackWalkData(IPlatformAgnosticContext context, StackWalkState state, FrameIterator frameIter, ThreadData threadData) { public IPlatformAgnosticContext Context { get; set; } = context; public StackWalkState State { get; set; } = state; public FrameIterator FrameIter { get; set; } = frameIter; + public ThreadData ThreadData { get; set; } = threadData; - public StackDataFrameHandle ToDataFrame() => new(Context.Clone(), State, FrameIter.CurrentFrameAddress); + public StackDataFrameHandle ToDataFrame() => new(Context.Clone(), State, FrameIter.CurrentFrameAddress, ThreadData); } IEnumerable IStackWalk.CreateStackWalk(ThreadData threadData) @@ -63,7 +68,7 @@ IEnumerable IStackWalk.CreateStackWalk(ThreadData threadD yield break; } - StackWalkData stackWalkData = new(context, state, frameIterator); + StackWalkData stackWalkData = new(context, state, frameIterator, threadData); yield return stackWalkData.ToDataFrame(); @@ -73,6 +78,481 @@ IEnumerable IStackWalk.CreateStackWalk(ThreadData threadD } } + void IStackWalk.WalkStackReferences(ThreadData threadData) + { + // TODO(stackref): This isn't quite right. We need to check if the FilterContext or ProfilerFilterContext + // is set and prefer that if either is not null. + IEnumerable stackFrames = ((IStackWalk)this).CreateStackWalk(threadData); + IEnumerable frames = stackFrames.Select(AssertCorrectHandle); + IEnumerable gcFrames = Filter(frames); + + GcScanContext scanContext = new(_target, resolveInteriorPointers: false); + + foreach (GCFrameData gcFrame in gcFrames) + { + Console.WriteLine(gcFrame); + + TargetPointer pMethodDesc = ((IStackWalk)this).GetMethodDescPtr(gcFrame.Frame); + + bool reportGcReferences = gcFrame.ShouldCrawlFrameReportGCReferences; + + try + { + TargetPointer pFrame = ((IStackWalk)this).GetFrameAddress(gcFrame.Frame); + scanContext.UpdateScanContext( + gcFrame.Frame.Context.StackPointer, + gcFrame.Frame.Context.InstructionPointer, + pFrame); + + if (reportGcReferences) + { + if (IsFrameless(gcFrame.Frame)) + { + // TODO(stackref): are the "GetCodeManagerFlags" flags relevant? + if (!IsManaged(gcFrame.Frame.Context.InstructionPointer, out CodeBlockHandle? cbh)) + throw new InvalidOperationException("Expected managed code"); + GcScanner gcScanner = new(_target); + gcScanner.EnumGcRefs(gcFrame.Frame.Context, cbh.Value, scanContext); + } + else + { + + } + } + } + catch (System.Exception ex) + { + Debug.WriteLine($"Exception during WalkStackReferences: {ex}"); + // TODO(stackref): Handle exceptions properly + } + } + } + + private record GCFrameData + { + public GCFrameData(StackDataFrameHandle frame) + { + Frame = frame; + } + + public StackDataFrameHandle Frame { get; } + public bool IsFilterFunclet { get; set; } + public bool IsFilterFuncletCached { get; set; } + public bool ShouldParentToFuncletSkipReportingGCReferences { get; set; } + public bool ShouldCrawlFrameReportGCReferences { get; set; } // required + public bool ShouldParentFrameUseUnwindTargetPCforGCReporting { get; set; } + public bool ShouldSaveFuncletInfo { get; set; } + public bool ShouldParentToFuncletReportSavedFuncletSlots { get; set; } + } + + private enum ForceGcReportingStage + { + Off, + LookForManagedFrame, + LookForMarkerFrame, + } + + private IEnumerable Filter(IEnumerable handles) + { + // StackFrameIterator::Filter assuming GC_FUNCLET_REFERENCE_REPORTING is defined + + // global tracking variables + bool movedPastFirstExInfo = false; + bool processNonFilterFunclet = false; + bool processIntermediaryNonFilterFunclet = false; + bool didFuncletReportGCReferences = true; + bool funcletNotSeen = false; + TargetPointer parentStackFrame = TargetPointer.Null; + TargetPointer funcletParentStackFrame = TargetPointer.Null; + TargetPointer intermediaryFuncletParentStackFrame; + + ForceGcReportingStage forceReportingWhileSkipping = ForceGcReportingStage.Off; + bool foundFirstFunclet = false; + + foreach (StackDataFrameHandle handle in handles) + { + GCFrameData gcFrame = new(handle); + + // per-frame tracking variables + bool stop = false; + bool skippingFunclet = false; + bool recheckCurrentFrame = false; + bool skipFuncletCallback = true; + + TargetPointer pExInfo = GetCurrentExceptionTracker(handle); + TargetPointer frameSp = handle.State == StackWalkState.SW_FRAME ? handle.FrameAddress : handle.Context.StackPointer; + if (pExInfo != TargetPointer.Null && frameSp > pExInfo) + { + if (!movedPastFirstExInfo) + { + Data.ExceptionInfo exInfo = _target.ProcessedData.GetOrAdd(pExInfo); + if (exInfo.PassNumber == 2 && + exInfo.CSFEnclosingClause != TargetPointer.Null && + funcletParentStackFrame == TargetPointer.Null && + exInfo.LastReportedFuncletInfo.IP != TargetCodePointer.Null) + { + // We are in the 2nd pass and we have already called an exceptionally called + // finally funclet and reported that to GC in a previous GC run. But we have + // not seen any funclet on the call stack yet. + // Simulate that we have actualy seen a finally funclet during this pass and + // that it didn't report GC references to ensure that the references will be + // reported by the parent correctly. + funcletParentStackFrame = exInfo.CSFEnclosingClause; + parentStackFrame = exInfo.CSFEnclosingClause; + processNonFilterFunclet = true; + didFuncletReportGCReferences = false; + funcletNotSeen = true; + } + movedPastFirstExInfo = true; + } + } + + gcFrame.ShouldParentToFuncletReportSavedFuncletSlots = false; + + // by default, there is no funclet for the current frame + // that reported GC references + gcFrame.ShouldParentToFuncletSkipReportingGCReferences = false; + + // by default, assume that we are going to report GC references + gcFrame.ShouldCrawlFrameReportGCReferences = true; + + gcFrame.ShouldSaveFuncletInfo = false; + + // by default, assume that parent frame is going to report GC references from + // the actual location reported by the stack walk + gcFrame.ShouldParentFrameUseUnwindTargetPCforGCReporting = false; + + if (parentStackFrame != TargetPointer.Null) + { + // we are now skipping frames to get to the funclet's parent + skippingFunclet = true; + } + + switch (handle.State) + { + case StackWalkState.SW_FRAMELESS: + do + { + recheckCurrentFrame = false; + if (funcletParentStackFrame != TargetPointer.Null) + { + // Have we been processing a filter funclet without encountering any non-filter funclets? + if (!processNonFilterFunclet && !processIntermediaryNonFilterFunclet) + { + if (IsUnwoundToTargetParentFrame(handle, funcletParentStackFrame)) + { + gcFrame.ShouldParentToFuncletSkipReportingGCReferences = false; + + /* ResetGCRefReportingState */ + funcletParentStackFrame = TargetPointer.Null; + processNonFilterFunclet = false; + intermediaryFuncletParentStackFrame = TargetPointer.Null; + processIntermediaryNonFilterFunclet = false; + + // We have reached the parent of the filter funclet. + // It is possible this is another funclet (e.g. a catch/fault/finally), + // so reexamine this frame and see if it needs any skipping. + recheckCurrentFrame = true; + } + else + { + Debug.Assert(!IsFilterFunclet(handle)); + if (IsFunclet(handle)) + { + intermediaryFuncletParentStackFrame = FindParentStackFrameForStackWalk(handle, forGCReporting: true); + Debug.Assert(intermediaryFuncletParentStackFrame != TargetPointer.Null); + processIntermediaryNonFilterFunclet = true; + + // Set the parent frame so that the funclet skipping logic (below) can use it. + parentStackFrame = intermediaryFuncletParentStackFrame; + skippingFunclet = false; + + IPlatformAgnosticContext callerContext = handle.Context.Clone(); + callerContext.Unwind(_target); + if (!IsManaged(callerContext.InstructionPointer, out _)) + { + // Initiate force reporting of references in the new managed exception handling code frames. + // These frames are still alive when we are in a finally funclet. + forceReportingWhileSkipping = ForceGcReportingStage.LookForManagedFrame; + } + } + } + } + } + else + { + Debug.Assert(funcletParentStackFrame == TargetPointer.Null); + + // We don't have any funclet parent reference. Check if the current frame represents a funclet. + if (IsFunclet(handle)) + { + // Get a reference to the funclet's parent frame. + funcletParentStackFrame = FindParentStackFrameForStackWalk(handle, forGCReporting: true); + + bool frameWasUnwound = HasFrameBeenUnwoundByAnyActiveException(handle); + + if (funcletParentStackFrame == TargetPointer.Null) + { + Debug.Assert(frameWasUnwound, "This can only happen if the funclet (and its parent) have been unwound"); + } + else + { + Debug.Assert(funcletParentStackFrame != TargetPointer.Null); + + bool isFilterFunclet = IsFilterFunclet(handle); + + if (!isFilterFunclet) + { + processNonFilterFunclet = true; + + // Set the parent frame so that the funclet skipping logic (below) can use it. + parentStackFrame = funcletParentStackFrame; + + if (!foundFirstFunclet && + pExInfo > handle.Context.StackPointer && + parentStackFrame > pExInfo) + { + Debug.Assert(pExInfo != TargetPointer.Null); + gcFrame.ShouldSaveFuncletInfo = true; + foundFirstFunclet = true; + } + + IPlatformAgnosticContext callerContext = handle.Context.Clone(); + callerContext.Unwind(_target); + if (!frameWasUnwound && IsManaged(callerContext.InstructionPointer, out _)) + { + // Initiate force reporting of references in the new managed exception handling code frames. + // These frames are still alive when we are in a finally funclet. + forceReportingWhileSkipping = ForceGcReportingStage.LookForManagedFrame; + } + + // For non-filter funclets, we will make the callback for the funclet + // but skip all the frames until we reach the parent method. When we do, + // we will make a callback for it as well and then continue to make callbacks + // for all upstack frames, until we reach another funclet or the top of the stack + // is reached. + skipFuncletCallback = false; + } + else + { + Debug.Assert(isFilterFunclet); + processNonFilterFunclet = false; + + // Nothing more to do as we have come across a filter funclet. In this case, we will: + // + // 1) Get a reference to the parent frame + // 2) Report the funclet + // 3) Continue to report the parent frame, along with a flag that funclet has been reported (see above) + // 4) Continue to report all upstack frames + } + } + } + } + } while (recheckCurrentFrame); + + if (processNonFilterFunclet || processIntermediaryNonFilterFunclet) + { + bool skipFrameDueToUnwind = false; + + if (HasFrameBeenUnwoundByAnyActiveException(handle)) + { + // This frame has been unwound by an active exception. It is not part of the live stack. + gcFrame.ShouldCrawlFrameReportGCReferences = false; + skipFrameDueToUnwind = true; + + if (IsFunclet(handle) && !skippingFunclet) + { + // we have come across a funclet that has been unwound and we haven't yet started to + // look for its parent. in such a case, the funclet will not have anything to report + // so set the corresponding flag to indicate so. + + Debug.Assert(didFuncletReportGCReferences); + didFuncletReportGCReferences = false; + } + } + + if (skipFrameDueToUnwind) + { + if (parentStackFrame != TargetPointer.Null) + { + // Check if our have reached our target method frame. + // parentStackFrame == MaxValue is a special value to indicate that we should skip one frame. + if (parentStackFrame == TargetPointer.PlatformMaxValue(_target) || + IsUnwoundToTargetParentFrame(handle, parentStackFrame)) + { + // Reset flag as we have reached target method frame so no more skipping required + skippingFunclet = false; + + // We've finished skipping as told. Now check again. + + if (processIntermediaryNonFilterFunclet || processNonFilterFunclet) + { + gcFrame.ShouldParentToFuncletSkipReportingGCReferences = true; + + didFuncletReportGCReferences = true; + + /* ResetGCRefReportingState */ + if (!processIntermediaryNonFilterFunclet) + { + funcletParentStackFrame = TargetPointer.Null; + processNonFilterFunclet = false; + } + intermediaryFuncletParentStackFrame = TargetPointer.Null; + processIntermediaryNonFilterFunclet = false; + } + + parentStackFrame = TargetPointer.Null; + + if (IsFunclet(handle)) + { + // We have reached another funclet. Reexamine this frame. + recheckCurrentFrame = true; + goto case StackWalkState.SW_FRAMELESS; + } + } + } + + if (gcFrame.ShouldCrawlFrameReportGCReferences) + { + // Skip the callback for this frame - we don't do this for unwound frames encountered + // in GC stackwalk since they may represent dynamic methods whose resolver objects + // the GC may need to keep alive. + break; + } + } + else + { + Debug.Assert(!skipFrameDueToUnwind); + + if (parentStackFrame != TargetPointer.Null) + { + // Check if our have reached our target method frame. + // parentStackFrame == MaxValue is a special value to indicate that we should skip one frame. + if (parentStackFrame == TargetPointer.PlatformMaxValue(_target) || + IsUnwoundToTargetParentFrame(handle, parentStackFrame)) + { + if (processIntermediaryNonFilterFunclet || processNonFilterFunclet) + { + bool shouldSkipReporting = true; + + if (!didFuncletReportGCReferences) + { + Debug.Assert(pExInfo != TargetPointer.Null); + Data.ExceptionInfo exInfo = _target.ProcessedData.GetOrAdd(pExInfo); + if (exInfo.CallerOfActualHandlerFrame == funcletParentStackFrame) + { + shouldSkipReporting = false; + + didFuncletReportGCReferences = true; + + gcFrame.ShouldParentFrameUseUnwindTargetPCforGCReporting = true; + + // TODO(stackref): Is this required? + // gcFrame.ehClauseForCatch = exInfo.ClauseForCatch; + } + else if (!IsFunclet(handle)) + { + if (funcletNotSeen) + { + gcFrame.ShouldParentToFuncletReportSavedFuncletSlots = true; + funcletNotSeen = false; + } + + didFuncletReportGCReferences = true; + } + } + gcFrame.ShouldParentToFuncletSkipReportingGCReferences = shouldSkipReporting; + + /* ResetGCRefReportingState */ + if (!processIntermediaryNonFilterFunclet) + { + funcletParentStackFrame = TargetPointer.Null; + processNonFilterFunclet = false; + } + intermediaryFuncletParentStackFrame = TargetPointer.Null; + processIntermediaryNonFilterFunclet = false; + } + + parentStackFrame = TargetPointer.Null; + } + } + + if (parentStackFrame == TargetPointer.Null && IsFunclet(handle)) + { + recheckCurrentFrame = true; + goto case StackWalkState.SW_FRAMELESS; + } + + if (skipFuncletCallback) + { + if (parentStackFrame != TargetPointer.Null && + forceReportingWhileSkipping == ForceGcReportingStage.Off) + { + break; + } + + if (forceReportingWhileSkipping == ForceGcReportingStage.LookForManagedFrame) + { + // State indicating that the next marker frame should turn off the reporting again. That would be the caller of the managed RhThrowEx + forceReportingWhileSkipping = ForceGcReportingStage.LookForMarkerFrame; + // TODO(stackref): need to add case to find the marker frame + } + + if (forceReportingWhileSkipping != ForceGcReportingStage.Off) + { + // TODO(stackref): add debug assert that we are in the EH code + } + } + } + } + else + { + // If we are enumerating frames for GC reporting and we determined that + // the current frame needs to be reported, ensure that it has not already + // been unwound by the active exception. If it has been, then we will + // simply skip it and not deliver a callback for it. + if (HasFrameBeenUnwoundByAnyActiveException(handle)) + { + // Invoke the GC callback for this crawlframe (to keep any dynamic methods alive) but do not report its references. + gcFrame.ShouldCrawlFrameReportGCReferences = false; + } + } + + stop = true; + break; + + case StackWalkState.SW_FRAME: + case StackWalkState.SW_SKIPPED_FRAME: + if (!skippingFunclet) + { + if (HasFrameBeenUnwoundByAnyActiveException(handle)) + { + // This frame has been unwound by an active exception. It is not part of the live stack. + gcFrame.ShouldCrawlFrameReportGCReferences = false; + } + stop = true; + } + break; + default: + stop = true; + break; + } + + if (stop) + yield return gcFrame; + } + } + + private bool IsUnwoundToTargetParentFrame(StackDataFrameHandle handle, TargetPointer targetParentFrame) + { + Debug.Assert(handle.State is StackWalkState.SW_FRAMELESS); + + IPlatformAgnosticContext callerContext = handle.Context.Clone(); + callerContext.Unwind(_target); + + return callerContext.StackPointer == targetParentFrame; + } + private bool Next(StackWalkData handle) { switch (handle.State) @@ -181,7 +661,6 @@ TargetPointer IStackWalk.GetMethodDescPtr(TargetPointer framePtr) TargetPointer IStackWalk.GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHandle) { StackDataFrameHandle handle = AssertCorrectHandle(stackDataFrameHandle); - IExecutionManager eman = _target.Contracts.ExecutionManager; // if we are at a capital F Frame, we can get the method desc from the frame TargetPointer framePtr = ((IStackWalk)this).GetFrameAddress(handle); @@ -202,9 +681,9 @@ TargetPointer IStackWalk.GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHa // 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) + if (_eman.GetCodeBlockHandle(returnAddress.Value) is CodeBlockHandle cbh) { - MethodDescHandle returnMethodDesc = rts.GetMethodDescHandle(eman.GetMethodDesc(cbh)); + MethodDescHandle returnMethodDesc = rts.GetMethodDescHandle(_eman.GetMethodDesc(cbh)); reportInteropMD = rts.HasMDContextArg(returnMethodDesc); } } @@ -230,14 +709,13 @@ TargetPointer IStackWalk.GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHa if (!IsManaged(handle.Context.InstructionPointer, out CodeBlockHandle? codeBlockHandle)) return TargetPointer.Null; - return eman.GetMethodDesc(codeBlockHandle.Value); + return _eman.GetMethodDesc(codeBlockHandle.Value); } private bool IsManaged(TargetPointer ip, [NotNullWhen(true)] out CodeBlockHandle? codeBlockHandle) { - IExecutionManager eman = _target.Contracts.ExecutionManager; TargetCodePointer codePointer = CodePointerUtils.CodePointerFromAddress(ip, _target); - if (eman.GetCodeBlockHandle(codePointer) is CodeBlockHandle cbh && cbh.Address != TargetPointer.Null) + if (_eman.GetCodeBlockHandle(codePointer) is CodeBlockHandle cbh && cbh.Address != TargetPointer.Null) { codeBlockHandle = cbh; return true; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs index 3b91d1d2bb138e..a66884d8a77e03 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs @@ -54,15 +54,15 @@ ThreadData IThread.GetThreadData(TargetPointer threadPointer) { Data.Thread thread = _target.ProcessedData.GetOrAdd(threadPointer); - TargetPointer address = _target.ReadPointer(thread.ExceptionTracker); TargetPointer firstNestedException = TargetPointer.Null; - if (address != TargetPointer.Null) + if (thread.ExceptionTracker != TargetPointer.Null) { - Data.ExceptionInfo exceptionInfo = _target.ProcessedData.GetOrAdd(address); + Data.ExceptionInfo exceptionInfo = _target.ProcessedData.GetOrAdd(thread.ExceptionTracker); firstNestedException = exceptionInfo.PreviousNestedInfo; } return new ThreadData( + threadPointer, thread.Id, thread.OSId, (ThreadState)thread.State, @@ -173,6 +173,23 @@ TargetPointer IThread.GetThreadLocalStaticBase(TargetPointer threadPointer, Targ return threadLocalStaticBase; } + bool IThread.IsInStackRegionUnwoundBySpecifiedException(TargetPointer threadAddress, TargetPointer stackPointer) + { + // See ExInfo::IsInStackRegionUnwoundBySpecifiedException for explanation + Data.Thread thread = _target.ProcessedData.GetOrAdd(threadAddress); + TargetPointer exInfo = thread.ExceptionTracker; + while (exInfo != TargetPointer.Null) + { + Data.ExceptionInfo exceptionInfo = _target.ProcessedData.GetOrAdd(exInfo); + if (exceptionInfo.StackLowBound < stackPointer && stackPointer <= exceptionInfo.StackHighBound) + { + return true; + } + exInfo = exceptionInfo.PreviousNestedInfo; + } + return false; + } + byte[] IThread.GetWatsonBuckets(TargetPointer threadPointer) { TargetPointer readFrom; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CorCompileExceptionClause.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CorCompileExceptionClause.cs new file mode 100644 index 00000000000000..0d6f83e345d9a6 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CorCompileExceptionClause.cs @@ -0,0 +1,21 @@ +// 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 sealed class CorCompileExceptionClause : IData +{ + static CorCompileExceptionClause IData.Create(Target target, TargetPointer address) + => new CorCompileExceptionClause(target, address); + + public CorCompileExceptionClause(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.CorCompileExceptionClause); + + Flags = target.Read(address + (ulong)type.Fields[nameof(Flags)].Offset); + FilterOffset = target.Read(address + (ulong)type.Fields[nameof(FilterOffset)].Offset); + } + + public uint Flags { get; } + public uint FilterOffset { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CorCompileExceptionLookupEntry.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CorCompileExceptionLookupEntry.cs new file mode 100644 index 00000000000000..6bfceb2da022f9 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CorCompileExceptionLookupEntry.cs @@ -0,0 +1,21 @@ +// 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 sealed class CorCompileExceptionLookupEntry : IData +{ + static CorCompileExceptionLookupEntry IData.Create(Target target, TargetPointer address) + => new CorCompileExceptionLookupEntry(target, address); + + public CorCompileExceptionLookupEntry(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.CorCompileExceptionLookupEntry); + + MethodStartRva = target.Read(address + (ulong)type.Fields[nameof(MethodStartRva)].Offset); + ExceptionInfoRva = target.Read(address + (ulong)type.Fields[nameof(ExceptionInfoRva)].Offset); + } + + public uint MethodStartRva { get; } + public uint ExceptionInfoRva { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEILExceptionClause.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEILExceptionClause.cs new file mode 100644 index 00000000000000..b3d40cd6e33bba --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEILExceptionClause.cs @@ -0,0 +1,21 @@ +// 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 sealed class EEILExceptionClause : IData +{ + static EEILExceptionClause IData.Create(Target target, TargetPointer address) + => new EEILExceptionClause(target, address); + + public EEILExceptionClause(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.EEILExceptionClause); + + Flags = target.Read(address + (ulong)type.Fields[nameof(Flags)].Offset); + FilterOffset = target.Read(address + (ulong)type.Fields[nameof(FilterOffset)].Offset); + } + + public uint Flags { get; } + public uint FilterOffset { get; } +} 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 287d2ac3350f6c..44465ccc9c9c48 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 @@ -14,11 +14,28 @@ public ExceptionInfo(Target target, TargetPointer address) PreviousNestedInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(PreviousNestedInfo)].Offset); ThrownObjectHandle = target.ReadPointer(address + (ulong)type.Fields[nameof(ThrownObjectHandle)].Offset); + ExceptionFlags = target.Read(address + (ulong)type.Fields[nameof(ExceptionFlags)].Offset); + StackLowBound = target.ReadPointer(address + (ulong)type.Fields[nameof(StackLowBound)].Offset); + StackHighBound = target.ReadPointer(address + (ulong)type.Fields[nameof(StackHighBound)].Offset); if (type.Fields.ContainsKey(nameof(ExceptionWatsonBucketTrackerBuckets))) ExceptionWatsonBucketTrackerBuckets = target.ReadPointer(address + (ulong)type.Fields[nameof(ExceptionWatsonBucketTrackerBuckets)].Offset); + + PassNumber = target.Read(address + (ulong)type.Fields[nameof(PassNumber)].Offset); + CSFEHClause = target.ReadPointer(address + (ulong)type.Fields[nameof(CSFEHClause)].Offset); + CSFEnclosingClause = target.ReadPointer(address + (ulong)type.Fields[nameof(CSFEnclosingClause)].Offset); + CallerOfActualHandlerFrame = target.ReadPointer(address + (ulong)type.Fields[nameof(CallerOfActualHandlerFrame)].Offset); + LastReportedFuncletInfo = target.ProcessedData.GetOrAdd(address + (ulong)type.Fields[nameof(LastReportedFuncletInfo)].Offset); } - public TargetPointer PreviousNestedInfo { get; init; } - public TargetPointer ThrownObjectHandle { get; init; } - public TargetPointer ExceptionWatsonBucketTrackerBuckets { get; init; } + public TargetPointer PreviousNestedInfo { get; } + public TargetPointer ThrownObjectHandle { get; } + public uint ExceptionFlags { get; } + public TargetPointer StackLowBound { get; } + public TargetPointer StackHighBound { get; } + public TargetPointer ExceptionWatsonBucketTrackerBuckets { get; } + public byte PassNumber { get; } + public TargetPointer CSFEHClause { get; } + public TargetPointer CSFEnclosingClause { get; } + public TargetPointer CallerOfActualHandlerFrame { get; } + public LastReportedFuncletInfo LastReportedFuncletInfo { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/LastReportedFuncletInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/LastReportedFuncletInfo.cs new file mode 100644 index 00000000000000..df04c08f874288 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/LastReportedFuncletInfo.cs @@ -0,0 +1,19 @@ +// 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 sealed class LastReportedFuncletInfo : IData +{ + static LastReportedFuncletInfo IData.Create(Target target, TargetPointer address) + => new LastReportedFuncletInfo(target, address); + + public LastReportedFuncletInfo(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.LastReportedFuncletInfo); + + IP = target.ReadCodePointer(address + (ulong)type.Fields[nameof(IP)].Offset); + } + + public TargetCodePointer IP { 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 ec460c242cb58e..843bc82f7f1328 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 @@ -31,6 +31,7 @@ public ReadyToRunInfo(Target target, TargetPointer address) DelayLoadMethodCallThunks = target.ReadPointer(address + (ulong)type.Fields[nameof(DelayLoadMethodCallThunks)].Offset); DebugInfoSection = target.ReadPointer(address + (ulong)type.Fields[nameof(DebugInfoSection)].Offset); + ExceptionInfoSection = target.ReadPointer(address + (ulong)type.Fields[nameof(ExceptionInfoSection)].Offset); // Map is from the composite info pointer (set to itself for non-multi-assembly composite images) EntryPointToMethodDescMap = CompositeInfo + (ulong)type.Fields[nameof(EntryPointToMethodDescMap)].Offset; @@ -50,6 +51,7 @@ public ReadyToRunInfo(Target target, TargetPointer address) public TargetPointer DelayLoadMethodCallThunks { get; } public TargetPointer DebugInfoSection { get; } + public TargetPointer ExceptionInfoSection { get; } public TargetPointer EntryPointToMethodDescMap { get; } public TargetPointer LoadedImageBase { get; } public TargetPointer Composite { get; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RealCodeHeader.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RealCodeHeader.cs index da639b342d4274..9718f0ab4fec6f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RealCodeHeader.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RealCodeHeader.cs @@ -13,6 +13,7 @@ public RealCodeHeader(Target target, TargetPointer address) Target.TypeInfo type = target.GetTypeInfo(DataType.RealCodeHeader); MethodDesc = target.ReadPointer(address + (ulong)type.Fields[nameof(MethodDesc)].Offset); DebugInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(DebugInfo)].Offset); + EHInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(EHInfo)].Offset); GCInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(GCInfo)].Offset); NumUnwindInfos = target.Read(address + (ulong)type.Fields[nameof(NumUnwindInfos)].Offset); UnwindInfos = address + (ulong)type.Fields[nameof(UnwindInfos)].Offset; @@ -20,10 +21,11 @@ public RealCodeHeader(Target target, TargetPointer address) JitEHInfo = jitEHInfoAddr != TargetPointer.Null ? target.ProcessedData.GetOrAdd(jitEHInfoAddr) : null; } - public TargetPointer MethodDesc { get; init; } - public TargetPointer DebugInfo { get; init; } - public TargetPointer GCInfo { get; init; } - public uint NumUnwindInfos { get; init; } - public TargetPointer UnwindInfos { get; init; } - public EEILException? JitEHInfo { get; init; } + public TargetPointer MethodDesc { get; } + public TargetPointer DebugInfo { get; } + public TargetPointer EHInfo { get; } + public TargetPointer GCInfo { get; } + public uint NumUnwindInfos { get; } + public TargetPointer UnwindInfos { get; } + public EEILException? JitEHInfo { 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 0250eaa435c2e3..fb7ed1ac4dc439 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -2408,38 +2408,39 @@ int ISOSDacInterface.GetMethodDescName(ClrDataAddress addr, uint count, char* na int ISOSDacInterface.GetMethodDescPtrFromFrame(ClrDataAddress frameAddr, ClrDataAddress* ppMD) { - int hr = HResults.S_OK; - try - { - if (frameAddr == 0 || ppMD == null) - throw new ArgumentException(); - - IStackWalk stackWalkContract = _target.Contracts.StackWalk; - TargetPointer methodDescPtr = stackWalkContract.GetMethodDescPtr(frameAddr.ToTargetPointer(_target)); - if (methodDescPtr == TargetPointer.Null) - throw new ArgumentException(); - - _target.Contracts.RuntimeTypeSystem.GetMethodDescHandle(methodDescPtr); // validation - *ppMD = methodDescPtr.ToClrDataAddress(_target); - } - catch (System.Exception ex) - { - hr = ex.HResult; - } -#if DEBUG - if (_legacyImpl is not null) - { - ClrDataAddress ppMDLocal; - int hrLocal = _legacyImpl.GetMethodDescPtrFromFrame(frameAddr, &ppMDLocal); - - Debug.ValidateHResult(hr, hrLocal); - if (hr == HResults.S_OK) - { - Debug.Assert(*ppMD == ppMDLocal); - } - } -#endif - return hr; + return HResults.E_FAIL; + // int hr = HResults.S_OK; + // try + // { + // if (frameAddr == 0 || ppMD == null) + // throw new ArgumentException(); + + // IStackWalk stackWalkContract = _target.Contracts.StackWalk; + // TargetPointer methodDescPtr = stackWalkContract.GetMethodDescPtr(frameAddr.ToTargetPointer(_target)); + // if (methodDescPtr == TargetPointer.Null) + // throw new ArgumentException(); + + // _target.Contracts.RuntimeTypeSystem.GetMethodDescHandle(methodDescPtr); // validation + // *ppMD = methodDescPtr.ToClrDataAddress(_target); + // } + // catch (System.Exception ex) + // { + // hr = ex.HResult; + // } + // #if DEBUG + // if (_legacyImpl is not null) + // { + // ClrDataAddress ppMDLocal; + // int hrLocal = _legacyImpl.GetMethodDescPtrFromFrame(frameAddr, &ppMDLocal); + + // Debug.Assert(hrLocal == hr); + // if (hr == HResults.S_OK) + // { + // Debug.Assert(*ppMD == ppMDLocal); + // } + // } + // #endif + // return hr; } int ISOSDacInterface.GetMethodDescPtrFromIP(ClrDataAddress ip, ClrDataAddress* ppMD) { From 933a96c09536d84b417e35052bafca3834531b14 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 27 Feb 2026 15:13:04 -0500 Subject: [PATCH 02/30] cdac: Implement EnumerateLiveSlots in managed GCInfoDecoder Port the native GcInfoDecoder::EnumerateLiveSlots to managed code: - Add FindSafePoint for partially-interruptible safe point lookup - Handle partially-interruptible path (1-bit-per-slot and RLE encoded) - Handle indirect live state table with pointer offset indirection - Handle fully-interruptible path with chunk-based lifetime transitions (couldBeLive bitvectors, final state bits, transition offsets) - Report untracked slots (always live unless suppressed by flags) - Add InterruptibleRanges/SlotTable decode points for lazy decoding - Save safe point and live state bit offsets during body decode - Add POINTER_SIZE_ENCBASE, LIVESTATE_RLE_*, NUM_NORM_CODE_OFFSETS_* constants to IGCInfoTraits (same across all platforms) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/GCInfo/GCInfoDecoder.cs | 326 ++++++++++++++++++ .../GCInfo/PlatformTraits/IGCInfoTraits.cs | 7 + 2 files changed, 333 insertions(+) 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 32f0671cdefad3..bb6081ada2dd75 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 @@ -22,6 +22,8 @@ private enum DecodePoints GenericInstContext, EditAndContinue, ReversePInvoke, + InterruptibleRanges, + SlotTable, Complete, } @@ -129,12 +131,14 @@ public static GcSlotDesc CreateStackSlot(int spOffset, GcStackSlotBase slotBase, private uint _numSafePoints; private uint _numInterruptibleRanges; private List _interruptibleRanges = []; + private int _safePointBitOffset; /* Slot Table Fields */ private uint _numRegisters; private uint _numUntrackedSlots; private uint _numSlots; private List _slots = []; + private int _liveStateBitOffset; public GcInfoDecoder(Target target, TargetPointer gcInfoAddress, uint gcVersion) { @@ -175,9 +179,16 @@ private IEnumerable DecodeBody() foreach (DecodePoints dp in interruptibleRanges) yield return dp; + yield return DecodePoints.InterruptibleRanges; + IEnumerable slotTable = DecodeSlotTable(); foreach (DecodePoints dp in slotTable) yield return dp; + + // Save the bit offset for EnumerateLiveSlots — the live state data follows immediately + _liveStateBitOffset = _bitOffset; + + yield return DecodePoints.SlotTable; } private IEnumerable DecodeSlotTable() @@ -319,6 +330,8 @@ private IEnumerable DecodeInterruptibleRanges() private IEnumerable DecodeSafePoints() { + // Save the position of the safe point data for FindSafePoint + _safePointBitOffset = _bitOffset; // skip over safe point data uint numBitsPerOffset = CeilOfLog2(TTraits.NormalizeCodeOffset(_codeLength)); _bitOffset += (int)(numBitsPerOffset * _numSafePoints); @@ -503,6 +516,319 @@ public IReadOnlyList GetInterruptibleRanges() return _interruptibleRanges; } + public uint NumTrackedSlots => _numSlots - _numUntrackedSlots; + + /// + /// Enumerates all GC slots that are live at the given instruction offset, invoking the callback for each. + /// This is the managed equivalent of the native GcInfoDecoder::EnumerateLiveSlots. + /// + /// The current instruction offset (relative to method start). + /// CodeManagerFlags controlling reporting behavior. + /// Called for each live slot with (slotIndex, slotDesc, gcFlags). + /// gcFlags contains GC_SLOT_INTERIOR/GC_SLOT_PINNED from the slot descriptor. + /// True if enumeration succeeded. + public bool EnumerateLiveSlots( + uint instructionOffset, + uint inputFlags, + Action reportSlot) + { + const uint ParentOfFuncletStackFrame = 0x40; + const uint NoReportUntracked = 0x80; + const uint ExecutionAborted = 0x2; + + EnsureDecodedTo(DecodePoints.SlotTable); + + bool executionAborted = (inputFlags & ExecutionAborted) != 0; + + // WantsReportOnlyLeaf is always true for non-legacy formats + if ((inputFlags & ParentOfFuncletStackFrame) != 0) + return true; + + uint numTracked = NumTrackedSlots; + if (numTracked == 0) + goto ReportUntracked; + + uint normBreakOffset = TTraits.NormalizeCodeOffset(instructionOffset); + + // Find safe point index + uint safePointIndex = _numSafePoints; + if (_numSafePoints > 0) + { + safePointIndex = FindSafePoint(instructionOffset); + } + + // Use a local bit offset starting from the saved live state position + // so we don't disturb the decoder's main _bitOffset. + int bitOffset = _liveStateBitOffset; + + if (PartiallyInterruptibleGCSupported) + { + uint pseudoBreakOffset = 0; + uint numInterruptibleLength = 0; + + if (safePointIndex < _numSafePoints && !executionAborted) + { + // We have a safe point match — skip interruptible range computation + } + else + { + // Compute pseudoBreakOffset from interruptible ranges + int countIntersections = 0; + for (int i = 0; i < _interruptibleRanges.Count; i++) + { + uint normStart = TTraits.NormalizeCodeOffset(_interruptibleRanges[i].StartOffset); + uint normStop = TTraits.NormalizeCodeOffset(_interruptibleRanges[i].EndOffset); + + if (normBreakOffset >= normStart && normBreakOffset < normStop) + { + Debug.Assert(pseudoBreakOffset == 0); + countIntersections++; + pseudoBreakOffset = numInterruptibleLength + normBreakOffset - normStart; + } + numInterruptibleLength += normStop - normStart; + } + Debug.Assert(countIntersections <= 1); + if (countIntersections == 0 && executionAborted) + goto ReportUntracked; + } + + // Read the indirect live state table header (if present) + uint numBitsPerOffset = 0; + if (_numSafePoints > 0 && _reader.ReadBits(1, ref bitOffset) != 0) + { + numBitsPerOffset = (uint)_reader.DecodeVarLengthUnsigned(TTraits.POINTER_SIZE_ENCBASE, ref bitOffset) + 1; + } + + // ---- Try partially interruptible first ---- + if (!executionAborted && safePointIndex != _numSafePoints) + { + if (numBitsPerOffset != 0) + { + int offsetTablePos = bitOffset; + bitOffset += (int)(safePointIndex * numBitsPerOffset); + uint liveStatesOffset = (uint)_reader.ReadBits((int)numBitsPerOffset, ref bitOffset); + int liveStatesStart = (int)(((uint)offsetTablePos + _numSafePoints * numBitsPerOffset + 7) & (~7u)); + bitOffset = (int)(liveStatesStart + liveStatesOffset); + + if (_reader.ReadBits(1, ref bitOffset) != 0) + { + // RLE encoded + bool fSkip = _reader.ReadBits(1, ref bitOffset) == 0; + bool fReport = true; + uint readSlots = (uint)_reader.DecodeVarLengthUnsigned( + fSkip ? TTraits.LIVESTATE_RLE_SKIP_ENCBASE : TTraits.LIVESTATE_RLE_RUN_ENCBASE, ref bitOffset); + fSkip = !fSkip; + while (readSlots < numTracked) + { + uint cnt = (uint)_reader.DecodeVarLengthUnsigned( + fSkip ? TTraits.LIVESTATE_RLE_SKIP_ENCBASE : TTraits.LIVESTATE_RLE_RUN_ENCBASE, ref bitOffset) + 1; + if (fReport) + { + for (uint slotIndex = readSlots; slotIndex < readSlots + cnt; slotIndex++) + ReportSlot(slotIndex, reportSlot); + } + readSlots += cnt; + fSkip = !fSkip; + fReport = !fReport; + } + Debug.Assert(readSlots == numTracked); + goto ReportUntracked; + } + // Normal 1-bit-per-slot encoding follows + } + else + { + bitOffset += (int)(safePointIndex * numTracked); + } + + for (uint slotIndex = 0; slotIndex < numTracked; slotIndex++) + { + if (_reader.ReadBits(1, ref bitOffset) != 0) + ReportSlot(slotIndex, reportSlot); + } + goto ReportUntracked; + } + else + { + // Skip over safe point live state data + if (numBitsPerOffset != 0) + bitOffset += (int)(_numSafePoints * numBitsPerOffset); + else + bitOffset += (int)(_numSafePoints * numTracked); + + if (_numInterruptibleRanges == 0) + goto ReportUntracked; + } + + // ---- Fully-interruptible path ---- + Debug.Assert(_numInterruptibleRanges > 0); + Debug.Assert(numInterruptibleLength > 0); + + uint numChunks = (numInterruptibleLength + TTraits.NUM_NORM_CODE_OFFSETS_PER_CHUNK - 1) / TTraits.NUM_NORM_CODE_OFFSETS_PER_CHUNK; + uint breakChunk = pseudoBreakOffset / TTraits.NUM_NORM_CODE_OFFSETS_PER_CHUNK; + Debug.Assert(breakChunk < numChunks); + + uint numBitsPerPointer = (uint)_reader.DecodeVarLengthUnsigned(TTraits.POINTER_SIZE_ENCBASE, ref bitOffset); + if (numBitsPerPointer == 0) + goto ReportUntracked; + + int pointerTablePos = bitOffset; + + // Find the chunk pointer (walk backwards if current chunk has no data) + uint chunkPointer; + uint chunk = breakChunk; + for (; ; ) + { + bitOffset = pointerTablePos + (int)(chunk * numBitsPerPointer); + chunkPointer = (uint)_reader.ReadBits((int)numBitsPerPointer, ref bitOffset); + if (chunkPointer != 0) + break; + if (chunk-- == 0) + goto ReportUntracked; + } + + int chunksStartPos = (int)(((uint)pointerTablePos + numChunks * numBitsPerPointer + 7) & (~7u)); + int chunkPos = (int)(chunksStartPos + chunkPointer - 1); + bitOffset = chunkPos; + + // Read "couldBeLive" bitvector — first pass to count + int couldBeLiveBitOffset = bitOffset; + uint numCouldBeLiveSlots = 0; + + if (_reader.ReadBits(1, ref bitOffset) != 0) + { + // RLE encoded + bool fSkipCBL = _reader.ReadBits(1, ref bitOffset) == 0; + bool fReportCBL = true; + uint readSlots = (uint)_reader.DecodeVarLengthUnsigned( + fSkipCBL ? TTraits.LIVESTATE_RLE_SKIP_ENCBASE : TTraits.LIVESTATE_RLE_RUN_ENCBASE, ref bitOffset); + fSkipCBL = !fSkipCBL; + while (readSlots < numTracked) + { + uint cnt = (uint)_reader.DecodeVarLengthUnsigned( + fSkipCBL ? TTraits.LIVESTATE_RLE_SKIP_ENCBASE : TTraits.LIVESTATE_RLE_RUN_ENCBASE, ref bitOffset) + 1; + if (fReportCBL) + numCouldBeLiveSlots += cnt; + readSlots += cnt; + fSkipCBL = !fSkipCBL; + fReportCBL = !fReportCBL; + } + Debug.Assert(readSlots == numTracked); + } + else + { + for (uint i = 0; i < numTracked; i++) + { + if (_reader.ReadBits(1, ref bitOffset) != 0) + numCouldBeLiveSlots++; + } + } + Debug.Assert(numCouldBeLiveSlots > 0); + + // "finalState" bits follow couldBeLive + int finalStateBitOffset = bitOffset; + // Transition data follows final state bits + int transitionBitOffset = bitOffset + (int)numCouldBeLiveSlots; + + // Re-read couldBeLive to iterate slot indices (second pass) + int cblOffset = couldBeLiveBitOffset; + bool cblSimple = _reader.ReadBits(1, ref cblOffset) == 0; + bool cblSkipFirst = false; + uint cblCnt = 0; + uint slotIdx = 0; + if (!cblSimple) + { + cblSkipFirst = _reader.ReadBits(1, ref cblOffset) == 0; + slotIdx = unchecked((uint)-1); + } + + for (uint i = 0; i < numCouldBeLiveSlots; i++) + { + if (cblSimple) + { + while (_reader.ReadBits(1, ref cblOffset) == 0) + slotIdx++; + } + else if (cblCnt > 0) + { + cblCnt--; + } + else if (cblSkipFirst) + { + uint tmp = (uint)_reader.DecodeVarLengthUnsigned(TTraits.LIVESTATE_RLE_SKIP_ENCBASE, ref cblOffset) + 1; + slotIdx += tmp; + cblCnt = (uint)_reader.DecodeVarLengthUnsigned(TTraits.LIVESTATE_RLE_RUN_ENCBASE, ref cblOffset); + } + else + { + uint tmp = (uint)_reader.DecodeVarLengthUnsigned(TTraits.LIVESTATE_RLE_RUN_ENCBASE, ref cblOffset) + 1; + slotIdx += tmp; + cblCnt = (uint)_reader.DecodeVarLengthUnsigned(TTraits.LIVESTATE_RLE_SKIP_ENCBASE, ref cblOffset); + } + + uint isLive = (uint)_reader.ReadBits(1, ref finalStateBitOffset); + + if (chunk == breakChunk) + { + uint normBreakOffsetDelta = pseudoBreakOffset % TTraits.NUM_NORM_CODE_OFFSETS_PER_CHUNK; + for (; ; ) + { + if (_reader.ReadBits(1, ref transitionBitOffset) == 0) + break; + + uint transitionOffset = (uint)_reader.ReadBits(TTraits.NUM_NORM_CODE_OFFSETS_PER_CHUNK_LOG2, ref transitionBitOffset); + Debug.Assert(transitionOffset > 0 && transitionOffset < TTraits.NUM_NORM_CODE_OFFSETS_PER_CHUNK); + if (transitionOffset > normBreakOffsetDelta) + isLive ^= 1; + } + } + + if (isLive != 0) + ReportSlot(slotIdx, reportSlot); + + slotIdx++; + } + } + + ReportUntracked: + if (_numUntrackedSlots > 0 && (inputFlags & (ParentOfFuncletStackFrame | NoReportUntracked)) == 0) + { + for (uint slotIndex = numTracked; slotIndex < _numSlots; slotIndex++) + ReportSlot(slotIndex, reportSlot); + } + + return true; + } + + private void ReportSlot(uint slotIndex, Action reportSlot) + { + Debug.Assert(slotIndex < _slots.Count); + GcSlotDesc slot = _slots[(int)slotIndex]; + uint gcFlags = (uint)slot.Flags & ((uint)GcSlotFlags.GC_SLOT_INTERIOR | (uint)GcSlotFlags.GC_SLOT_PINNED); + reportSlot(slotIndex, slot, gcFlags); + } + + private uint FindSafePoint(uint codeOffset) + { + EnsureDecodedTo(DecodePoints.ReversePInvoke); + + uint normBreakOffset = TTraits.NormalizeCodeOffset(codeOffset); + uint numBitsPerOffset = CeilOfLog2(TTraits.NormalizeCodeOffset(_codeLength)); + + // Linear scan through safe point offsets from the saved position + int scanOffset = _safePointBitOffset; + for (uint i = 0; i < _numSafePoints; i++) + { + uint spOffset = (uint)_reader.ReadBits((int)numBitsPerOffset, ref scanOffset); + if (spOffset == normBreakOffset) + return i; + if (spOffset > normBreakOffset) + break; + } + + return _numSafePoints; // not found + } + #endregion #region Helper Methods diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/IGCInfoTraits.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/IGCInfoTraits.cs index 51647a6a7fa600..c8db92b7b65cc4 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/IGCInfoTraits.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/IGCInfoTraits.cs @@ -47,4 +47,11 @@ internal interface IGCInfoTraits static abstract int NUM_INTERRUPTIBLE_RANGES_ENCBASE { get; } static abstract bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA { get; } + + // These are the same across all platforms + static virtual int POINTER_SIZE_ENCBASE { get; } = 3; + static virtual int LIVESTATE_RLE_RUN_ENCBASE { get; } = 2; + static virtual int LIVESTATE_RLE_SKIP_ENCBASE { get; } = 4; + static virtual uint NUM_NORM_CODE_OFFSETS_PER_CHUNK { get; } = 64; + static virtual int NUM_NORM_CODE_OFFSETS_PER_CHUNK_LOG2 { get; } = 6; } From 57b16fbcc2d623cde9cfe760a3465613f4eb739c Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 27 Feb 2026 15:19:35 -0500 Subject: [PATCH 03/30] cdac: Fix build errors in stack walk and execution manager - Fix IsFrameless: use StackWalkState.SW_FRAMELESS check - Fix EnumGcRefs call: pass CodeManagerFlags parameter (was missing) - Add public access modifier to GetMethodRegionInfo in ExecutionManager_1/2 - Fix redundant equality (== false) in ExecutionManagerCore - Suppress unused parameter/variable analyzer errors in GcScanner stub Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ExecutionManager/ExecutionManagerCore.cs | 2 +- .../ExecutionManager/ExecutionManager_1.cs | 2 +- .../ExecutionManager/ExecutionManager_2.cs | 2 +- .../Contracts/StackWalk/GC/GcScanner.cs | 14 +++++++++----- .../Contracts/StackWalk/StackWalk_1.cs | 5 ++--- 5 files changed, 14 insertions(+), 11 deletions(-) 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 4514f59f8fce70..c4d300619c9f6f 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 @@ -337,7 +337,7 @@ bool IExecutionManager.IsFilterFunclet(CodeBlockHandle codeInfoHandle) IExecutionManager eman = this; - if (eman.IsFunclet(codeInfoHandle) == false) + if (!eman.IsFunclet(codeInfoHandle)) return false; TargetPointer codeAddress = info.StartAddress.Value + info.RelativeOffset.Value; 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 e12ee5c7ee3c7f..a474ed46f7ee8e 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 @@ -19,7 +19,7 @@ internal ExecutionManager_1(Target target, Data.RangeSectionMap topRangeSectionM public TargetPointer GetMethodDesc(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetMethodDesc(codeInfoHandle); public TargetCodePointer GetStartAddress(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetStartAddress(codeInfoHandle); public TargetCodePointer GetFuncletStartAddress(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetFuncletStartAddress(codeInfoHandle); - void GetMethodRegionInfo(CodeBlockHandle codeInfoHandle, out uint hotSize, out TargetPointer coldStart, out uint coldSize) => _executionManagerCore.GetMethodRegionInfo(codeInfoHandle, out hotSize, out coldStart, out coldSize); + public void GetMethodRegionInfo(CodeBlockHandle codeInfoHandle, out uint hotSize, out TargetPointer coldStart, out uint coldSize) => _executionManagerCore.GetMethodRegionInfo(codeInfoHandle, out hotSize, out coldStart, out coldSize); public uint GetJITType(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetJITType(codeInfoHandle); public TargetPointer NonVirtualEntry2MethodDesc(TargetCodePointer entrypoint) => _executionManagerCore.NonVirtualEntry2MethodDesc(entrypoint); public bool IsFunclet(CodeBlockHandle codeInfoHandle) => _executionManagerCore.IsFunclet(codeInfoHandle); 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 d5bfce8739456b..5b04824441ee46 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 @@ -19,7 +19,7 @@ internal ExecutionManager_2(Target target, Data.RangeSectionMap topRangeSectionM public TargetPointer GetMethodDesc(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetMethodDesc(codeInfoHandle); public TargetCodePointer GetStartAddress(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetStartAddress(codeInfoHandle); public TargetCodePointer GetFuncletStartAddress(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetFuncletStartAddress(codeInfoHandle); - void GetMethodRegionInfo(CodeBlockHandle codeInfoHandle, out uint hotSize, out TargetPointer coldStart, out uint coldSize) => _executionManagerCore.GetMethodRegionInfo(codeInfoHandle, out hotSize, out coldStart, out coldSize); + public void GetMethodRegionInfo(CodeBlockHandle codeInfoHandle, out uint hotSize, out TargetPointer coldStart, out uint coldSize) => _executionManagerCore.GetMethodRegionInfo(codeInfoHandle, out hotSize, out coldStart, out coldSize); public uint GetJITType(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetJITType(codeInfoHandle); public TargetPointer NonVirtualEntry2MethodDesc(TargetCodePointer entrypoint) => _executionManagerCore.NonVirtualEntry2MethodDesc(entrypoint); public bool IsFunclet(CodeBlockHandle codeInfoHandle) => _executionManagerCore.IsFunclet(codeInfoHandle); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs index 45cd547b8e5313..658e2e85395d71 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -33,17 +33,21 @@ public bool EnumGcRefs( CodeManagerFlags flags, GcScanContext scanContext) { - TargetNUInt curOffs = _eman.GetRelativeOffset(cbh); + _ = context; + _ = scanContext; + _ = _eman.GetRelativeOffset(cbh); - _eman.GetGCInfo(cbh, out TargetPointer pGcInfo, out uint gcVersion); + _eman.GetGCInfo(cbh, out _, out _); if (_eman.IsFilterFunclet(cbh)) { - // Filters are the only funclet that run during the 1st pass, and must have - // both the leaf and the parent frame reported. In order to avoid double - // reporting of the untracked variables, do not report them for the filter. flags |= CodeManagerFlags.NoReportUntracked; } + _ = flags; + + // TODO(stackref): Use GCInfoDecoder.EnumerateLiveSlots to enumerate live slots, + // translate slot descriptors into target addresses using the context, + // and report them via scanContext.GCEnumCallback / scanContext.GCReportCallback. return false; } 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 c7a3bc929e2ad4..565f69908ec71f 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 @@ -106,13 +106,12 @@ void IStackWalk.WalkStackReferences(ThreadData threadData) if (reportGcReferences) { - if (IsFrameless(gcFrame.Frame)) + if (gcFrame.Frame.State == StackWalkState.SW_FRAMELESS) { - // TODO(stackref): are the "GetCodeManagerFlags" flags relevant? if (!IsManaged(gcFrame.Frame.Context.InstructionPointer, out CodeBlockHandle? cbh)) throw new InvalidOperationException("Expected managed code"); GcScanner gcScanner = new(_target); - gcScanner.EnumGcRefs(gcFrame.Frame.Context, cbh.Value, scanContext); + gcScanner.EnumGcRefs(gcFrame.Frame.Context, cbh.Value, 0, scanContext); } else { From 7d56297388ff957fb45bfeca482021a14d895ade Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 27 Feb 2026 15:27:04 -0500 Subject: [PATCH 04/30] cdac: Complete GcScanner.EnumGcRefs with live slot enumeration - Wire GcScanner to use IGCInfoDecoder.EnumerateLiveSlots - Add LiveSlotCallback delegate and EnumerateLiveSlots to IGCInfoDecoder - Add interface implementation in GcInfoDecoder that wraps the generic method - Translate register slots to values via IPlatformAgnosticContext - Translate stack slots using SP/FP base + offset addressing - Add StackBaseRegister accessor to GcInfoDecoder - Report live slots to GcScanContext.GCEnumCallback with proper flags - Add GcScanFlags.None value Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/GCInfo/GCInfoDecoder.cs | 21 ++++++ .../Contracts/GCInfo/IGCInfoDecoder.cs | 16 ++++ .../Contracts/StackWalk/GC/GcScanFlags.cs | 1 + .../Contracts/StackWalk/GC/GcScanner.cs | 74 ++++++++++++++++--- 4 files changed, 100 insertions(+), 12 deletions(-) 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 bb6081ada2dd75..f3bba268b62333 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 @@ -516,8 +516,29 @@ public IReadOnlyList GetInterruptibleRanges() return _interruptibleRanges; } + public uint StackBaseRegister + { + get + { + EnsureDecodedTo(DecodePoints.ReversePInvoke); + return _stackBaseRegister; + } + } + public uint NumTrackedSlots => _numSlots - _numUntrackedSlots; + bool IGCInfoDecoder.EnumerateLiveSlots( + uint instructionOffset, + uint inputFlags, + LiveSlotCallback reportSlot) + { + return EnumerateLiveSlots(instructionOffset, inputFlags, + (uint slotIndex, GcSlotDesc slot, uint gcFlags) => + { + reportSlot(slot.IsRegister, slot.RegisterNumber, slot.SpOffset, (uint)slot.Base, gcFlags); + }); + } + /// /// Enumerates all GC slots that are live at the given instruction offset, invoking the callback for each. /// This is the managed equivalent of the native GcInfoDecoder::EnumerateLiveSlots. 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 41bd8bdb3ea989..df640c205d9332 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 @@ -1,9 +1,25 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; + namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; internal interface IGCInfoDecoder : IGCInfoHandle { uint GetCodeLength(); + uint StackBaseRegister { get; } + + /// + /// Enumerates all live GC slots at the given instruction offset. + /// + /// Relative offset from method start. + /// CodeManagerFlags controlling reporting. + /// Callback: (isRegister, registerNumber, spOffset, spBase, gcFlags). + bool EnumerateLiveSlots( + uint instructionOffset, + uint inputFlags, + LiveSlotCallback reportSlot) => throw new NotImplementedException(); } + +internal delegate void LiveSlotCallback(bool isRegister, uint registerNumber, int spOffset, uint spBase, uint gcFlags); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanFlags.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanFlags.cs index 85f7b666f1ef9e..0575b625d5b9d4 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanFlags.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanFlags.cs @@ -8,6 +8,7 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; [Flags] internal enum GcScanFlags { + None = 0x0, GC_CALL_INTERIOR = 0x1, GC_CALL_PINNED = 0x2, } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs index 658e2e85395d71..27b7f5d1d73e03 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; @@ -33,22 +34,71 @@ public bool EnumGcRefs( CodeManagerFlags flags, GcScanContext scanContext) { - _ = context; - _ = scanContext; - _ = _eman.GetRelativeOffset(cbh); - - _eman.GetGCInfo(cbh, out _, out _); + TargetNUInt relativeOffset = _eman.GetRelativeOffset(cbh); + _eman.GetGCInfo(cbh, out TargetPointer gcInfoAddr, out uint gcVersion); if (_eman.IsFilterFunclet(cbh)) - { flags |= CodeManagerFlags.NoReportUntracked; - } - _ = flags; - // TODO(stackref): Use GCInfoDecoder.EnumerateLiveSlots to enumerate live slots, - // translate slot descriptors into target addresses using the context, - // and report them via scanContext.GCEnumCallback / scanContext.GCReportCallback. + IGCInfoHandle handle = _gcInfo.DecodePlatformSpecificGCInfo(gcInfoAddr, gcVersion); + if (handle is not IGCInfoDecoder decoder) + return false; + + uint stackBaseRegister = decoder.StackBaseRegister; + + return decoder.EnumerateLiveSlots( + (uint)relativeOffset.Value, + (uint)flags, + (bool isRegister, uint registerNumber, int spOffset, uint spBase, uint gcFlags) => + { + GcScanFlags scanFlags = GcScanFlags.None; + if ((gcFlags & 0x1) != 0) // GC_SLOT_INTERIOR + scanFlags |= GcScanFlags.GC_CALL_INTERIOR; + if ((gcFlags & 0x2) != 0) // GC_SLOT_PINNED + scanFlags |= GcScanFlags.GC_CALL_PINNED; + + if (isRegister) + { + TargetPointer regValue = GetRegisterValue(context, registerNumber); + GcScanSlotLocation loc = new((int)registerNumber, 0, false); + scanContext.GCEnumCallback(regValue, scanFlags, loc); + } + else + { + TargetPointer baseAddr = spBase switch + { + 1 => context.StackPointer, // GC_SP_REL + 2 => GetRegisterValue(context, stackBaseRegister), // GC_FRAMEREG_REL + 0 => context.StackPointer, // GC_CALLER_SP_REL (TODO: use actual caller SP) + _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), + }; + + TargetPointer addr = new(baseAddr.Value + (ulong)(long)spOffset); + GcScanSlotLocation loc = new(0, spOffset, true); + scanContext.GCEnumCallback(addr, scanFlags, loc); + } + }); + } + + private static TargetPointer GetRegisterValue(IPlatformAgnosticContext context, uint registerNumber) + { + if (registerNumber == 4) return context.StackPointer; + if (registerNumber == 5) return context.FramePointer; + + // Map register number to context field name (AMD64 ordering) + // TODO: Support ARM64 and other architectures + string? fieldName = registerNumber switch + { + 0 => "Rax", 1 => "Rcx", 2 => "Rdx", 3 => "Rbx", + 6 => "Rsi", 7 => "Rdi", + 8 => "R8", 9 => "R9", 10 => "R10", 11 => "R11", + 12 => "R12", 13 => "R13", 14 => "R14", 15 => "R15", + _ => null, + }; + + if (fieldName is not null && context.TryReadRegister(null!, fieldName, out TargetNUInt value)) + return new TargetPointer(value.Value); - return false; + throw new InvalidOperationException($"Failed to read register #{registerNumber} from context"); } } From 8cc1e70543114a3f34998ec7452b113edcab94ad Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 27 Feb 2026 15:37:43 -0500 Subject: [PATCH 05/30] cdac: Implement GetStackReferences end-to-end - Add StackReferenceData public data class in Abstractions - Change IStackWalk.WalkStackReferences to return IReadOnlyList - Update StackWalk_1.WalkStackReferences to convert and return results - Add ISOSStackRefEnum, ISOSStackRefErrorEnum COM interfaces with GUIDs - Add SOSStackRefData, SOSStackRefError structs, SOSStackSourceType enum - Add SOSStackRefEnum class implementing ISOSStackRefEnum (follows SOSHandleEnum pattern) - Wire up SOSDacImpl.GetStackReferences: find thread by OS ID, walk stack references, convert to SOSStackRefData[], return via COM enumerator - Remove Console.WriteLine debug output from WalkStackReferences Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/IStackWalk.cs | 2 +- .../Contracts/StackReferenceData.cs | 17 +++ .../Contracts/StackWalk/StackWalk_1.cs | 17 ++- .../ISOSDacInterface.cs | 2 +- .../SOSDacImpl.cs | 113 +++++++++++++++++- 5 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/StackReferenceData.cs diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs index 14110b2a6d7ad6..d5f4fd3763a183 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs @@ -13,7 +13,7 @@ public interface IStackWalk : IContract static string IContract.Name => nameof(StackWalk); public virtual IEnumerable CreateStackWalk(ThreadData threadData) => throw new NotImplementedException(); - void WalkStackReferences(ThreadData threadData) => throw new NotImplementedException(); + IReadOnlyList WalkStackReferences(ThreadData threadData) => throw new NotImplementedException(); byte[] GetRawContext(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException(); TargetPointer GetFrameAddress(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException(); string GetFrameName(TargetPointer frameIdentifier) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/StackReferenceData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/StackReferenceData.cs new file mode 100644 index 00000000000000..fb4dd3c351e8e0 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/StackReferenceData.cs @@ -0,0 +1,17 @@ +// 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; + +public class StackReferenceData +{ + public bool HasRegisterInformation { get; init; } + public int Register { get; init; } + public int Offset { get; init; } + public TargetPointer Address { get; init; } + public TargetPointer Object { get; init; } + public uint Flags { get; init; } + public bool IsStackSourceFrame { get; init; } + public TargetPointer Source { get; init; } + public TargetPointer StackPointer { get; init; } +} 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 565f69908ec71f..eeac3bfd8fa5ae 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 @@ -78,7 +78,7 @@ IEnumerable IStackWalk.CreateStackWalk(ThreadData threadD } } - void IStackWalk.WalkStackReferences(ThreadData threadData) + IReadOnlyList IStackWalk.WalkStackReferences(ThreadData threadData) { // TODO(stackref): This isn't quite right. We need to check if the FilterContext or ProfilerFilterContext // is set and prefer that if either is not null. @@ -90,8 +90,6 @@ void IStackWalk.WalkStackReferences(ThreadData threadData) foreach (GCFrameData gcFrame in gcFrames) { - Console.WriteLine(gcFrame); - TargetPointer pMethodDesc = ((IStackWalk)this).GetMethodDescPtr(gcFrame.Frame); bool reportGcReferences = gcFrame.ShouldCrawlFrameReportGCReferences; @@ -125,6 +123,19 @@ void IStackWalk.WalkStackReferences(ThreadData threadData) // TODO(stackref): Handle exceptions properly } } + + return scanContext.StackRefs.Select(r => new StackReferenceData + { + HasRegisterInformation = r.HasRegisterInformation, + Register = r.Register, + Offset = r.Offset, + Address = r.Address, + Object = r.Object, + Flags = (uint)r.Flags, + IsStackSourceFrame = r.SourceType == StackRefData.SourceTypes.StackSourceFrame, + Source = r.Source, + StackPointer = r.StackPointer, + }).ToList(); } private record GCFrameData diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs index e88180432fe8d0..382ab0819f22e1 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs @@ -526,7 +526,7 @@ public enum EHClauseType : uint public enum SOSStackSourceType : uint { SOS_StackSourceIP = 0, - SOS_StackSourceFrame = 1 + SOS_StackSourceFrame = 1, } public struct SOSStackRefData 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 fb7ed1ac4dc439..c1d54879753bd3 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -3637,8 +3637,119 @@ int ISOSDacInterface.GetStackLimits(ClrDataAddress threadPtr, ClrDataAddress* lo return hr; } + [GeneratedComClass] + internal sealed unsafe partial class SOSStackRefEnum : ISOSStackRefEnum + { + private readonly SOSStackRefData[] _refs; + private uint _index; + + public SOSStackRefEnum(SOSStackRefData[] refs) + { + _refs = refs; + } + + int ISOSStackRefEnum.Next(uint count, SOSStackRefData[] refs, uint* pFetched) + { + int hr = HResults.S_OK; + try + { + if (pFetched is null || refs is null) + throw new NullReferenceException(); + + uint written = 0; + while (written < count && _index < _refs.Length) + refs[written++] = _refs[(int)_index++]; + + *pFetched = written; + hr = _index < _refs.Length ? HResults.S_FALSE : HResults.S_OK; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + return hr; + } + + int ISOSStackRefEnum.EnumerateErrors(DacComNullableByRef ppEnum) + { + return HResults.E_NOTIMPL; + } + + int ISOSEnum.Skip(uint count) + { + _index += count; + return HResults.S_OK; + } + + int ISOSEnum.Reset() + { + _index = 0; + return HResults.S_OK; + } + + int ISOSEnum.GetCount(uint* pCount) + { + if (pCount is null) return HResults.E_POINTER; + *pCount = (uint)_refs.Length; + return HResults.S_OK; + } + } + int ISOSDacInterface.GetStackReferences(int osThreadID, DacComNullableByRef ppEnum) - => _legacyImpl is not null ? _legacyImpl.GetStackReferences(osThreadID, ppEnum) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; + try + { + Contracts.IThread threadContract = _target.Contracts.Thread; + Contracts.ThreadStoreData threadStoreData = threadContract.GetThreadStoreData(); + + TargetPointer threadPtr = threadStoreData.FirstThread; + Contracts.ThreadData? matchingThread = null; + while (threadPtr != TargetPointer.Null) + { + Contracts.ThreadData td = threadContract.GetThreadData(threadPtr); + if ((int)td.OSId.Value == osThreadID) + { + matchingThread = td; + break; + } + threadPtr = td.NextThread; + } + + if (matchingThread is null) + throw new ArgumentException($"Thread with OS ID {osThreadID} not found"); + + Contracts.IStackWalk stackWalk = _target.Contracts.StackWalk; + IReadOnlyList refs = stackWalk.WalkStackReferences(matchingThread.Value); + + SOSStackRefData[] sosRefs = new SOSStackRefData[refs.Count]; + for (int i = 0; i < refs.Count; i++) + { + Contracts.StackReferenceData r = refs[i]; + sosRefs[i] = new SOSStackRefData + { + HasRegisterInformation = r.HasRegisterInformation ? 1 : 0, + Register = r.Register, + Offset = r.Offset, + Address = r.Address.ToClrDataAddress(_target), + Object = r.Object.ToClrDataAddress(_target), + Flags = r.Flags, + SourceType = r.IsStackSourceFrame ? SOSStackSourceType.SOS_StackSourceFrame : SOSStackSourceType.SOS_StackSourceIP, + Source = r.Source.ToClrDataAddress(_target), + StackPointer = r.StackPointer.ToClrDataAddress(_target), + }; + } + + ppEnum.Interface = new SOSStackRefEnum(sosRefs); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + return hr; + } int ISOSDacInterface.GetStressLogAddress(ClrDataAddress* stressLog) { From 316d0dd93150bc9c482a7d99d88ecfb99e9b3595 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 2 Mar 2026 12:57:25 -0500 Subject: [PATCH 06/30] cdac: Add dump tests for GetStackReferences Add three test classes for stack reference enumeration: - StackReferenceDumpTests: Basic tests using StackWalk debuggee (WalkStackReferences returns without throwing, refs have valid source info) - GCRootsStackReferenceDumpTests: Tests using GCRoots debuggee which keeps objects alive on stack via GC.KeepAlive (finds refs, refs point to valid objects) - PInvokeFrameStackReferenceDumpTests: Tests using PInvokeStub debuggee which has InlinedCallFrame on the stack (non-frameless Frame path) The PInvokeStub tests exercise the Frame::GcScanRoots path which is not yet implemented (empty else block in WalkStackReferences). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Debuggees/GCRoots/GCRoots.csproj | 3 + .../DumpTests/StackReferenceDumpTests.cs | 140 ++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/GCRoots/GCRoots.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/GCRoots/GCRoots.csproj index 35e3d8428b7cfc..b5bf84aa517d8a 100644 --- a/src/native/managed/cdac/tests/DumpTests/Debuggees/GCRoots/GCRoots.csproj +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/GCRoots/GCRoots.csproj @@ -1,2 +1,5 @@ + + Heap;Full + diff --git a/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs new file mode 100644 index 00000000000000..b47fa18f889ca1 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Dump-based integration tests for GetStackReferences / WalkStackReferences. +/// Verifies that the cDAC can enumerate GC references on the managed stack. +/// +public class StackReferenceDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "StackWalk"; + protected override string DumpType => "full"; + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void WalkStackReferences_ReturnsWithoutThrowing(TestConfiguration config) + { + InitializeDumpTest(config); + IStackWalk stackWalk = Target.Contracts.StackWalk; + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + Assert.NotNull(refs); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void WalkStackReferences_RefsHaveValidSourceInfo(TestConfiguration config) + { + InitializeDumpTest(config); + IStackWalk stackWalk = Target.Contracts.StackWalk; + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + foreach (StackReferenceData r in refs) + { + Assert.True(r.Source != TargetPointer.Null, "Stack reference should have a non-null Source (IP or Frame address)"); + Assert.True(r.StackPointer != TargetPointer.Null, "Stack reference should have a non-null StackPointer"); + } + } +} + +/// +/// Tests using the GCRoots debuggee, which keeps objects alive on the stack +/// via GC.KeepAlive before crashing. Should produce stack references to those objects. +/// +public class GCRootsStackReferenceDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "GCRoots"; + protected override string DumpType => "full"; + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void WalkStackReferences_FindsRefsOnMainThread(TestConfiguration config) + { + InitializeDumpTest(config); + IStackWalk stackWalk = Target.Contracts.StackWalk; + + ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "Main"); + + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + Assert.NotNull(refs); + Assert.True(refs.Count > 0, + "Expected GCRoots Main thread to have at least one stack reference (objects kept alive via GC.KeepAlive)"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void WalkStackReferences_RefsPointToValidObjects(TestConfiguration config) + { + InitializeDumpTest(config); + IStackWalk stackWalk = Target.Contracts.StackWalk; + + ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "Main"); + + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + + int validObjectCount = 0; + foreach (StackReferenceData r in refs) + { + if (r.Object == TargetPointer.Null) + continue; + + // Each non-null object reference should point to a valid managed object. + // The object's method table pointer (first pointer-sized field) should be non-null. + try + { + TargetPointer methodTable = Target.ReadPointer(r.Object); + if (methodTable != TargetPointer.Null) + validObjectCount++; + } + catch + { + // Some refs may be interior pointers or otherwise unreadable + } + } + + Assert.True(validObjectCount > 0, + $"Expected at least one stack ref pointing to a valid object (total refs: {refs.Count})"); + } +} + +/// +/// Tests using the PInvokeStub debuggee, which crashes inside native code +/// during a P/Invoke. The managed stack has an InlinedCallFrame (non-frameless). +/// Frame::GcScanRoots needs to be implemented for these refs to be reported. +/// These tests are expected to fail until frame-gc-scan-roots is implemented. +/// +public class PInvokeFrameStackReferenceDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "PInvokeStub"; + protected override string DumpType => "full"; + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + [SkipOnOS(IncludeOnly = "windows", Reason = "PInvokeStub debuggee uses msvcrt.dll (Windows only)")] + public void WalkStackReferences_PInvokeThread_ReturnsWithoutThrowing(TestConfiguration config) + { + InitializeDumpTest(config); + IStackWalk stackWalk = Target.Contracts.StackWalk; + + ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "Main"); + + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + Assert.NotNull(refs); + } +} From 82816bd7f02c9fb410d8b95c33f0bfd1b2773c03 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 2 Mar 2026 13:49:57 -0500 Subject: [PATCH 07/30] cdac: Restore datadescriptor entries with proper native support Add native C++ changes needed for the data descriptor entries: - Add friend cdac_data to ExceptionFlags for m_flags access - Add LastReportedFuncletInfo struct and field to ExInfo - Add cdac_data specialization for LocalCount - Use cdac_data::ExceptionFlagsValue for ExceptionFlags offset Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/inc/patchpointinfo.h | 4 ++++ src/coreclr/vm/datadescriptor/datadescriptor.h | 6 ++++++ src/coreclr/vm/datadescriptor/datadescriptor.inc | 2 +- src/coreclr/vm/exinfo.h | 4 ++++ src/coreclr/vm/exstatecommon.h | 1 + 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/coreclr/inc/patchpointinfo.h b/src/coreclr/inc/patchpointinfo.h index 9700f998bf7988..483c5fb83d90f3 100644 --- a/src/coreclr/inc/patchpointinfo.h +++ b/src/coreclr/inc/patchpointinfo.h @@ -10,6 +10,8 @@ #ifndef _PATCHPOINTINFO_H_ #define _PATCHPOINTINFO_H_ +template struct cdac_data; + // -------------------------------------------------------------------------------- // Describes information needed to make an OSR transition // - location of IL-visible locals and other important state on the @@ -217,6 +219,8 @@ struct PatchpointInfo } private: + template friend struct cdac_data; + enum { OFFSET_SHIFT = 0x1, diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.h b/src/coreclr/vm/datadescriptor/datadescriptor.h index 36c62393091e66..0f3f5161b4020f 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.h +++ b/src/coreclr/vm/datadescriptor/datadescriptor.h @@ -28,6 +28,12 @@ #include "../debug/ee/debugger.h" #include "patchpointinfo.h" +template<> +struct cdac_data +{ + static constexpr size_t LocalCount = offsetof(PatchpointInfo, m_numberOfLocals); +}; + #ifdef HAVE_GCCOVER #include "gccover.h" #endif // HAVE_GCCOVER diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 2ee065576f42c9..2d9ae26c1d532a 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -134,7 +134,7 @@ CDAC_TYPE_BEGIN(ExceptionInfo) CDAC_TYPE_INDETERMINATE(ExceptionInfo) CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, PreviousNestedInfo, offsetof(ExInfo, m_pPrevNestedInfo)) CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, ThrownObjectHandle, offsetof(ExInfo, m_hThrowable)) -CDAC_TYPE_FIELD(ExceptionInfo, /*uint32*/, ExceptionFlags, offsetof(ExInfo, m_ExceptionFlags.m_flags)) +CDAC_TYPE_FIELD(ExceptionInfo, /*uint32*/, ExceptionFlags, cdac_data::ExceptionFlagsValue) CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, StackLowBound, cdac_data::StackLowBound) CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, StackHighBound, cdac_data::StackHighBound) #ifndef TARGET_UNIX diff --git a/src/coreclr/vm/exinfo.h b/src/coreclr/vm/exinfo.h index a409fef070407b..29551cd4e8e2f0 100644 --- a/src/coreclr/vm/exinfo.h +++ b/src/coreclr/vm/exinfo.h @@ -194,6 +194,9 @@ struct ExInfo int m_longJmpReturnValue; #endif + // Last reported funclet info for cDAC stack walking + LastReportedFuncletInfo m_lastReportedFunclet; + #if defined(TARGET_UNIX) void TakeExceptionPointersOwnership(PAL_SEHException* ex); #endif // TARGET_UNIX @@ -370,6 +373,7 @@ struct cdac_data + offsetof(ExInfo::StackRange, m_sfLowBound); static constexpr size_t StackHighBound = offsetof(ExInfo, m_ScannedStackRange) + offsetof(ExInfo::StackRange, m_sfHighBound); + static constexpr size_t ExceptionFlagsValue = offsetof(ExInfo, m_ExceptionFlags.m_flags); }; #endif // TARGET_UNIX diff --git a/src/coreclr/vm/exstatecommon.h b/src/coreclr/vm/exstatecommon.h index 5dfefafac1214d..a9897e51d14066 100644 --- a/src/coreclr/vm/exstatecommon.h +++ b/src/coreclr/vm/exstatecommon.h @@ -255,6 +255,7 @@ class EHClauseInfo class ExceptionFlags { + friend struct ::cdac_data; public: ExceptionFlags() { From 521b94bb7485d150c64b7740e724e4229c331cbe Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 2 Mar 2026 15:39:43 -0500 Subject: [PATCH 08/30] update --- docs/design/datacontracts/StackWalk.md | 13 ++++ src/coreclr/vm/exstatecommon.h | 2 +- .../ExecutionManagerCore.EEJitManager.cs | 3 + .../ExecutionManager/ExecutionManagerCore.cs | 2 +- .../Contracts/StackWalk/ExceptionHandling.cs | 69 +++++++++++-------- .../Contracts/StackWalk/StackWalk_1.cs | 12 ++-- 6 files changed, 63 insertions(+), 38 deletions(-) diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 6795924f2006b2..c33ccf3acab61b 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -74,12 +74,25 @@ This contract depends on the following descriptors: | `CalleeSavedRegisters` | For each callee saved register `r`, `r` | Register names associated with stored register values | | `TailCallFrame` (x86 Windows) | `CalleeSavedRegisters` | CalleeSavedRegisters data structure | | `TailCallFrame` (x86 Windows) | `ReturnAddress` | Frame's stored instruction pointer | +| `ExceptionInfo` | `ExceptionFlags` | Bit flags from `ExceptionFlags` class (`exstatecommon.h`). Used for GC reference reporting during stack walks with funclet handling. | +| `ExceptionInfo` | `StackLowBound` | Low bound of the stack range unwound by this exception | +| `ExceptionInfo` | `StackHighBound` | High bound of the stack range unwound by this exception | +| `ExceptionInfo` | `CSFEHClause` | Caller stack frame of the current EH clause | +| `ExceptionInfo` | `CSFEnclosingClause` | Caller stack frame of the enclosing clause | +| `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) | Global variables used: | Global Name | Type | Purpose | | --- | --- | --- | | For each FrameType ``, `##Identifier` | `FrameIdentifier` enum value | Identifier used to determine concrete type of Frames | +Constants used: +| Source | Name | Value | Purpose | +| --- | --- | --- | --- | +| `ExceptionFlags` (`exstatecommon.h`) | `Ex_UnwindHasStarted` | `0x00000004` | Bit flag in `ExceptionInfo.ExceptionFlags` indicating exception unwinding (2nd pass) has started. Used by `IsInStackRegionUnwoundBySpecifiedException` to skip ExInfo trackers still in the 1st pass. | + Contracts used: | Contract Name | | --- | diff --git a/src/coreclr/vm/exstatecommon.h b/src/coreclr/vm/exstatecommon.h index a9897e51d14066..39444d3cbeb2ca 100644 --- a/src/coreclr/vm/exstatecommon.h +++ b/src/coreclr/vm/exstatecommon.h @@ -347,7 +347,7 @@ class ExceptionFlags { // Unused = 0x00000001, Ex_UnwindingToFindResumeFrame = 0x00000002, - Ex_UnwindHasStarted = 0x00000004, + Ex_UnwindHasStarted = 0x00000004, // [cDAC] [StackWalk]: Contract depends on this value Ex_UseExInfoForStackwalk = 0x00000008, // Use this ExInfo to unwind a fault (AV, zerodiv) back to managed code? #ifdef DEBUGGING_SUPPORTED diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs index 70875032b3a12d..e670a2449f66d5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs @@ -158,6 +158,9 @@ public override IEnumerable GetEHClauses(RangeSection rangeSection, Ta if (!GetRealCodeHeader(rangeSection, codeStart, out Data.RealCodeHeader? realCodeHeader)) yield break; + if (realCodeHeader.EHInfo == TargetPointer.Null) + yield break; + // number of EH clauses is stored in a pointer sized integer just before the EHInfo array TargetNUInt ehClauseCount = Target.ReadNUInt(realCodeHeader.EHInfo - (uint)Target.PointerSize); uint ehClauseSize = Target.GetTypeInfo(DataType.EEILExceptionClause).Size ?? throw new InvalidOperationException("EEILExceptionClause size is not known"); 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 c4d300619c9f6f..d47b53114ab4f6 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 @@ -321,7 +321,7 @@ TargetPointer IExecutionManager.NonVirtualEntry2MethodDesc(TargetCodePointer ent bool IExecutionManager.IsFunclet(CodeBlockHandle codeInfoHandle) { - return ((IExecutionManager)this).GetStartAddress(codeInfoHandle) == + return ((IExecutionManager)this).GetStartAddress(codeInfoHandle) != ((IExecutionManager)this).GetFuncletStartAddress(codeInfoHandle); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/ExceptionHandling.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/ExceptionHandling.cs index f950bfe321741e..751558be08c1a5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/ExceptionHandling.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/ExceptionHandling.cs @@ -10,6 +10,17 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts; internal partial class StackWalk_1 : IStackWalk { + /// + /// Flags from the ExceptionFlags class (exstatecommon.h). + /// These are bit flags stored in ExInfo.m_ExceptionFlags.m_flags. + /// + [Flags] + private enum ExceptionFlagsEnum : uint + { + // See Ex_UnwindHasStarted in src/coreclr/vm/exstatecommon.h + UnwindHasStarted = 0x00000004, + } + /// /// Given the CrawlFrame for a funclet frame, return the frame pointer of the enclosing funclet frame. /// For filter funclet frames and normal method frames, this function returns a NULL StackFrame. @@ -126,46 +137,44 @@ private bool HasFrameBeenUnwoundByAnyActiveException(IStackDataFrameHandle stack { StackDataFrameHandle handle = AssertCorrectHandle(stackDataFrameHandle); - TargetPointer exInfo = GetCurrentExceptionTracker(handle); - while (exInfo != TargetPointer.Null) + TargetPointer callerStackPointer; + if (handle.State is StackWalkState.SW_FRAMELESS) + { + IPlatformAgnosticContext callerContext = handle.Context.Clone(); + callerContext.Unwind(_target); + callerStackPointer = callerContext.StackPointer; + } + else { - Data.ExceptionInfo exceptionInfo = _target.ProcessedData.GetOrAdd(exInfo); - exInfo = exceptionInfo.PreviousNestedInfo; + callerStackPointer = handle.FrameAddress; + } - TargetPointer stackPointer; - if (handle.State is StackWalkState.SW_FRAMELESS) - { - IPlatformAgnosticContext callerContext = handle.Context.Clone(); - callerContext.Unwind(_target); - stackPointer = callerContext.StackPointer; - } - else - { - stackPointer = handle.Context.FramePointer; - } - if (IsInStackRegionUnwoundBySpecifiedException(handle.ThreadData, stackPointer)) - { + TargetPointer pExInfo = GetCurrentExceptionTracker(handle); + while (pExInfo != TargetPointer.Null) + { + Data.ExceptionInfo exceptionInfo = _target.ProcessedData.GetOrAdd(pExInfo); + pExInfo = exceptionInfo.PreviousNestedInfo; + + if (IsInStackRegionUnwoundBySpecifiedException(callerStackPointer, exceptionInfo)) return true; - } } return false; } - private bool IsInStackRegionUnwoundBySpecifiedException(ThreadData threadData, TargetPointer stackPointer) + private bool IsInStackRegionUnwoundBySpecifiedException(TargetPointer callerStackPointer, Data.ExceptionInfo exceptionInfo) { - // See ExInfo::IsInStackRegionUnwoundBySpecifiedException for explanation - Data.Thread thread = _target.ProcessedData.GetOrAdd(threadData.ThreadAddress); - TargetPointer exInfo = thread.ExceptionTracker; - while (exInfo != TargetPointer.Null) + // The tracker must be in the second pass (unwind has started), and its stack range must not be empty. + if ((exceptionInfo.ExceptionFlags & (uint)ExceptionFlagsEnum.UnwindHasStarted) == 0) + return false; + + // Check for empty range + if (exceptionInfo.StackLowBound == TargetPointer.PlatformMaxValue(_target) + && exceptionInfo.StackHighBound == TargetPointer.Null) { - Data.ExceptionInfo exceptionInfo = _target.ProcessedData.GetOrAdd(exInfo); - if (exceptionInfo.StackLowBound < stackPointer && stackPointer <= exceptionInfo.StackHighBound) - { - return true; - } - exInfo = exceptionInfo.PreviousNestedInfo; + return false; } - return false; + + return (exceptionInfo.StackLowBound < callerStackPointer) && (callerStackPointer <= exceptionInfo.StackHighBound); } } 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 eeac3bfd8fa5ae..0c1cc9fcb781fa 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 @@ -90,12 +90,12 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre foreach (GCFrameData gcFrame in gcFrames) { - TargetPointer pMethodDesc = ((IStackWalk)this).GetMethodDescPtr(gcFrame.Frame); - - bool reportGcReferences = gcFrame.ShouldCrawlFrameReportGCReferences; - try { + TargetPointer pMethodDesc = ((IStackWalk)this).GetMethodDescPtr(gcFrame.Frame); + + bool reportGcReferences = gcFrame.ShouldCrawlFrameReportGCReferences; + TargetPointer pFrame = ((IStackWalk)this).GetFrameAddress(gcFrame.Frame); scanContext.UpdateScanContext( gcFrame.Frame.Context.StackPointer, @@ -113,14 +113,14 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre } else { - + // TODO(stackref): Implement Frame::GcScanRoots for non-frameless frames } } } catch (System.Exception ex) { Debug.WriteLine($"Exception during WalkStackReferences: {ex}"); - // TODO(stackref): Handle exceptions properly + // Matching native DAC behavior: capture errors, don't propagate } } From f30563a8ae629f74c4302b3eac42d9b02a66d0bb Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 2 Mar 2026 15:40:45 -0500 Subject: [PATCH 09/30] add tests --- .../DumpTests/Debuggees/StackRefs/Program.cs | 42 +++++ .../Debuggees/StackRefs/StackRefs.csproj | 5 + .../DumpTests/StackReferenceDumpTests.cs | 163 ++++++++++++++---- 3 files changed, 178 insertions(+), 32 deletions(-) create mode 100644 src/native/managed/cdac/tests/DumpTests/Debuggees/StackRefs/Program.cs create mode 100644 src/native/managed/cdac/tests/DumpTests/Debuggees/StackRefs/StackRefs.csproj diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/StackRefs/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/StackRefs/Program.cs new file mode 100644 index 00000000000000..b6fe4395d5bef9 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/StackRefs/Program.cs @@ -0,0 +1,42 @@ +// 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.Runtime.CompilerServices; + +/// +/// Debuggee for cDAC dump tests — exercises stack reference enumeration. +/// Creates objects on the stack that should be reported as GC references, +/// then crashes with them still live. The test walks the stack and verifies +/// the expected references are found. +/// +internal static class Program +{ + /// + /// Marker string that tests can search for in the reported GC references + /// to verify that stack refs are being enumerated correctly. + /// + public const string MarkerValue = "cDAC-StackRefs-Marker-12345"; + + private static void Main() + { + MethodWithStackRefs(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void MethodWithStackRefs() + { + // These locals will be GC-tracked in the JIT's GCInfo. + // The string has a known value we can find in the dump. + string marker = MarkerValue; + int[] array = [1, 2, 3, 4, 5]; + object boxed = 42; + + // Force the JIT to keep them alive at the FailFast call site. + GC.KeepAlive(marker); + GC.KeepAlive(array); + GC.KeepAlive(boxed); + + Environment.FailFast("cDAC dump test: StackRefs debuggee intentional crash"); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/StackRefs/StackRefs.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/StackRefs/StackRefs.csproj new file mode 100644 index 00000000000000..bb776824769fe6 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/StackRefs/StackRefs.csproj @@ -0,0 +1,5 @@ + + + Full + + diff --git a/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs index b47fa18f889ca1..c5ee32e78782ff 100644 --- a/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs @@ -1,28 +1,32 @@ // 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.Collections.Generic; using System.Linq; +using System.Text; using Microsoft.Diagnostics.DataContractReader.Contracts; using Xunit; namespace Microsoft.Diagnostics.DataContractReader.DumpTests; /// -/// Dump-based integration tests for GetStackReferences / WalkStackReferences. -/// Verifies that the cDAC can enumerate GC references on the managed stack. +/// Dump-based integration tests for WalkStackReferences. +/// Uses the InitializeDumpTest overload to target different debuggees per test. /// public class StackReferenceDumpTests : DumpTestBase { protected override string DebuggeeName => "StackWalk"; protected override string DumpType => "full"; + // --- StackWalk debuggee: basic stack walk --- + [ConditionalTheory] [MemberData(nameof(TestConfigurations))] [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] public void WalkStackReferences_ReturnsWithoutThrowing(TestConfiguration config) { - InitializeDumpTest(config); + InitializeDumpTest(config, "StackWalk", "full"); IStackWalk stackWalk = Target.Contracts.StackWalk; ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); @@ -36,7 +40,7 @@ public void WalkStackReferences_ReturnsWithoutThrowing(TestConfiguration config) [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] public void WalkStackReferences_RefsHaveValidSourceInfo(TestConfiguration config) { - InitializeDumpTest(config); + InitializeDumpTest(config, "StackWalk", "full"); IStackWalk stackWalk = Target.Contracts.StackWalk; ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); @@ -48,23 +52,15 @@ public void WalkStackReferences_RefsHaveValidSourceInfo(TestConfiguration config Assert.True(r.StackPointer != TargetPointer.Null, "Stack reference should have a non-null StackPointer"); } } -} -/// -/// Tests using the GCRoots debuggee, which keeps objects alive on the stack -/// via GC.KeepAlive before crashing. Should produce stack references to those objects. -/// -public class GCRootsStackReferenceDumpTests : DumpTestBase -{ - protected override string DebuggeeName => "GCRoots"; - protected override string DumpType => "full"; + // --- GCRoots debuggee: objects kept alive on stack --- [ConditionalTheory] [MemberData(nameof(TestConfigurations))] [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] - public void WalkStackReferences_FindsRefsOnMainThread(TestConfiguration config) + public void GCRoots_WalkStackReferences_FindsRefs(TestConfiguration config) { - InitializeDumpTest(config); + InitializeDumpTest(config, "GCRoots", "full"); IStackWalk stackWalk = Target.Contracts.StackWalk; ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "Main"); @@ -78,9 +74,9 @@ public void WalkStackReferences_FindsRefsOnMainThread(TestConfiguration config) [ConditionalTheory] [MemberData(nameof(TestConfigurations))] [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] - public void WalkStackReferences_RefsPointToValidObjects(TestConfiguration config) + public void GCRoots_RefsPointToValidObjects(TestConfiguration config) { - InitializeDumpTest(config); + InitializeDumpTest(config, "GCRoots", "full"); IStackWalk stackWalk = Target.Contracts.StackWalk; ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "Main"); @@ -93,8 +89,6 @@ public void WalkStackReferences_RefsPointToValidObjects(TestConfiguration config if (r.Object == TargetPointer.Null) continue; - // Each non-null object reference should point to a valid managed object. - // The object's method table pointer (first pointer-sized field) should be non-null. try { TargetPointer methodTable = Target.ReadPointer(r.Object); @@ -110,26 +104,131 @@ public void WalkStackReferences_RefsPointToValidObjects(TestConfiguration config Assert.True(validObjectCount > 0, $"Expected at least one stack ref pointing to a valid object (total refs: {refs.Count})"); } -} -/// -/// Tests using the PInvokeStub debuggee, which crashes inside native code -/// during a P/Invoke. The managed stack has an InlinedCallFrame (non-frameless). -/// Frame::GcScanRoots needs to be implemented for these refs to be reported. -/// These tests are expected to fail until frame-gc-scan-roots is implemented. -/// -public class PInvokeFrameStackReferenceDumpTests : DumpTestBase -{ - protected override string DebuggeeName => "PInvokeStub"; - protected override string DumpType => "full"; + // --- StackRefs debuggee: known objects on stack with verifiable content --- + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void StackRefs_FindsMarkerString(TestConfiguration config) + { + InitializeDumpTest(config, "StackRefs", "full"); + IStackWalk stackWalk = Target.Contracts.StackWalk; + + ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "MethodWithStackRefs"); + + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + Assert.True(refs.Count > 0, "Expected at least one stack reference from MethodWithStackRefs"); + + // Search for the marker string "cDAC-StackRefs-Marker-12345" among the object references. + // A System.String in the CLR has: [MethodTable*][length:int32][chars...] + // The chars start at offset (pointerSize + 4) from the object start. + bool foundMarker = false; + string expectedMarker = "cDAC-StackRefs-Marker-12345"; + + foreach (StackReferenceData r in refs) + { + if (r.Object == TargetPointer.Null) + continue; + + try + { + // Read the method table pointer to verify it's a valid object + TargetPointer mt = Target.ReadPointer(r.Object); + if (mt == TargetPointer.Null) + continue; + + // Read string length (int32 at offset pointerSize) + int strLength = Target.Read(r.Object + (ulong)Target.PointerSize); + if (strLength <= 0 || strLength > 1024) + continue; + + // Read chars (UTF-16, starting at offset pointerSize + 4) + byte[] charBytes = new byte[strLength * 2]; + Target.ReadBuffer(r.Object + (ulong)Target.PointerSize + 4, charBytes); + string value = Encoding.Unicode.GetString(charBytes); + + if (value == expectedMarker) + { + foundMarker = true; + break; + } + } + catch + { + // Not a string or not readable — skip + } + } + + Assert.True(foundMarker, + $"Expected to find marker string '{expectedMarker}' among {refs.Count} stack references"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void StackRefs_FindsArrayReference(TestConfiguration config) + { + InitializeDumpTest(config, "StackRefs", "full"); + IStackWalk stackWalk = Target.Contracts.StackWalk; + + ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "MethodWithStackRefs"); + + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + Assert.True(refs.Count > 0, "Expected at least one stack reference from MethodWithStackRefs"); + + // Look for the int[] { 1, 2, 3, 4, 5 } array. + // An array in the CLR has: [MethodTable*][length:pointer-sized][elements...] + // For int[], elements start at offset (pointerSize + pointerSize). + bool foundArray = false; + + foreach (StackReferenceData r in refs) + { + if (r.Object == TargetPointer.Null) + continue; + + try + { + TargetPointer mt = Target.ReadPointer(r.Object); + if (mt == TargetPointer.Null) + continue; + + // Read array length + ulong arrayLength = Target.ReadNUInt(r.Object + (ulong)Target.PointerSize).Value; + if (arrayLength != 5) + continue; + + // Read int elements + ulong elementsOffset = (ulong)Target.PointerSize + (ulong)Target.PointerSize; + int elem0 = Target.Read(r.Object + elementsOffset); + int elem1 = Target.Read(r.Object + elementsOffset + 4); + int elem2 = Target.Read(r.Object + elementsOffset + 8); + + if (elem0 == 1 && elem1 == 2 && elem2 == 3) + { + foundArray = true; + break; + } + } + catch + { + // Not an array or not readable — skip + } + } + + Assert.True(foundArray, + $"Expected to find int[]{{1,2,3,4,5}} among {refs.Count} stack references"); + } + + // --- PInvokeStub debuggee: Frame-based path --- [ConditionalTheory] [MemberData(nameof(TestConfigurations))] [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] [SkipOnOS(IncludeOnly = "windows", Reason = "PInvokeStub debuggee uses msvcrt.dll (Windows only)")] - public void WalkStackReferences_PInvokeThread_ReturnsWithoutThrowing(TestConfiguration config) + public void PInvoke_WalkStackReferences_ReturnsWithoutThrowing(TestConfiguration config) { - InitializeDumpTest(config); + InitializeDumpTest(config, "PInvokeStub", "full"); IStackWalk stackWalk = Target.Contracts.StackWalk; ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "Main"); From 95f6b6596d9da5561c0daea6091911a37cda3930 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 2 Mar 2026 16:05:38 -0500 Subject: [PATCH 10/30] update test script --- .../DumpTests/StackReferenceDumpTests.cs | 43 ++++--------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs index c5ee32e78782ff..80d27a1c4f2ab2 100644 --- a/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using Microsoft.Diagnostics.DataContractReader.Contracts; using Xunit; @@ -114,15 +113,13 @@ public void StackRefs_FindsMarkerString(TestConfiguration config) { InitializeDumpTest(config, "StackRefs", "full"); IStackWalk stackWalk = Target.Contracts.StackWalk; + IObject objectContract = Target.Contracts.Object; ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "MethodWithStackRefs"); IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); Assert.True(refs.Count > 0, "Expected at least one stack reference from MethodWithStackRefs"); - // Search for the marker string "cDAC-StackRefs-Marker-12345" among the object references. - // A System.String in the CLR has: [MethodTable*][length:int32][chars...] - // The chars start at offset (pointerSize + 4) from the object start. bool foundMarker = false; string expectedMarker = "cDAC-StackRefs-Marker-12345"; @@ -133,21 +130,7 @@ public void StackRefs_FindsMarkerString(TestConfiguration config) try { - // Read the method table pointer to verify it's a valid object - TargetPointer mt = Target.ReadPointer(r.Object); - if (mt == TargetPointer.Null) - continue; - - // Read string length (int32 at offset pointerSize) - int strLength = Target.Read(r.Object + (ulong)Target.PointerSize); - if (strLength <= 0 || strLength > 1024) - continue; - - // Read chars (UTF-16, starting at offset pointerSize + 4) - byte[] charBytes = new byte[strLength * 2]; - Target.ReadBuffer(r.Object + (ulong)Target.PointerSize + 4, charBytes); - string value = Encoding.Unicode.GetString(charBytes); - + string value = objectContract.GetStringValue(r.Object); if (value == expectedMarker) { foundMarker = true; @@ -171,15 +154,14 @@ public void StackRefs_FindsArrayReference(TestConfiguration config) { InitializeDumpTest(config, "StackRefs", "full"); IStackWalk stackWalk = Target.Contracts.StackWalk; + IObject objectContract = Target.Contracts.Object; ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "MethodWithStackRefs"); IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); Assert.True(refs.Count > 0, "Expected at least one stack reference from MethodWithStackRefs"); - // Look for the int[] { 1, 2, 3, 4, 5 } array. - // An array in the CLR has: [MethodTable*][length:pointer-sized][elements...] - // For int[], elements start at offset (pointerSize + pointerSize). + // Look for the int[] { 1, 2, 3, 4, 5 } array using the Object contract. bool foundArray = false; foreach (StackReferenceData r in refs) @@ -189,20 +171,13 @@ public void StackRefs_FindsArrayReference(TestConfiguration config) try { - TargetPointer mt = Target.ReadPointer(r.Object); - if (mt == TargetPointer.Null) - continue; - - // Read array length - ulong arrayLength = Target.ReadNUInt(r.Object + (ulong)Target.PointerSize).Value; - if (arrayLength != 5) + TargetPointer dataStart = objectContract.GetArrayData(r.Object, out uint count, out _, out _); + if (count != 5) continue; - // Read int elements - ulong elementsOffset = (ulong)Target.PointerSize + (ulong)Target.PointerSize; - int elem0 = Target.Read(r.Object + elementsOffset); - int elem1 = Target.Read(r.Object + elementsOffset + 4); - int elem2 = Target.Read(r.Object + elementsOffset + 8); + int elem0 = Target.Read(dataStart + sizeof(int) * 0); + int elem1 = Target.Read(dataStart + sizeof(int) * 1); + int elem2 = Target.Read(dataStart + sizeof(int) * 2); if (elem0 == 1 && elem1 == 2 && elem2 == 3) { From 42644282bed19193db12c48e32bea7eb7054be24 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 2 Mar 2026 16:33:36 -0500 Subject: [PATCH 11/30] clean up --- .../Contracts/IThread.cs | 1 - .../Contracts/Thread_1.cs | 17 ----------------- 2 files changed, 18 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs index 671b9bf49d74c5..4206b23d3b9f97 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs @@ -54,7 +54,6 @@ void GetStackLimitData(TargetPointer threadPointer, out TargetPointer stackBase, out TargetPointer stackLimit, out TargetPointer frameAddress) => throw new NotImplementedException(); TargetPointer IdToThread(uint id) => throw new NotImplementedException(); TargetPointer GetThreadLocalStaticBase(TargetPointer threadPointer, TargetPointer tlsIndexPtr) => throw new NotImplementedException(); - bool IsInStackRegionUnwoundBySpecifiedException(TargetPointer threadAddress, TargetPointer stackPointer) => throw new NotImplementedException(); TargetPointer GetThrowableObject(TargetPointer threadPointer) => throw new NotImplementedException(); byte[] GetWatsonBuckets(TargetPointer threadPointer) => throw new NotImplementedException(); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs index a66884d8a77e03..b5ff9b71e8e0bf 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs @@ -173,23 +173,6 @@ TargetPointer IThread.GetThreadLocalStaticBase(TargetPointer threadPointer, Targ return threadLocalStaticBase; } - bool IThread.IsInStackRegionUnwoundBySpecifiedException(TargetPointer threadAddress, TargetPointer stackPointer) - { - // See ExInfo::IsInStackRegionUnwoundBySpecifiedException for explanation - Data.Thread thread = _target.ProcessedData.GetOrAdd(threadAddress); - TargetPointer exInfo = thread.ExceptionTracker; - while (exInfo != TargetPointer.Null) - { - Data.ExceptionInfo exceptionInfo = _target.ProcessedData.GetOrAdd(exInfo); - if (exceptionInfo.StackLowBound < stackPointer && stackPointer <= exceptionInfo.StackHighBound) - { - return true; - } - exInfo = exceptionInfo.PreviousNestedInfo; - } - return false; - } - byte[] IThread.GetWatsonBuckets(TargetPointer threadPointer) { TargetPointer readFrom; From c08e71ac3a640f6fadc098048b4458e7c16e617b Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 2 Mar 2026 16:35:51 -0500 Subject: [PATCH 12/30] add debug verification --- .../SOSDacImpl.cs | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) 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 c1d54879753bd3..f9f0374841003a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -3699,6 +3699,7 @@ int ISOSEnum.GetCount(uint* pCount) int ISOSDacInterface.GetStackReferences(int osThreadID, DacComNullableByRef ppEnum) { int hr = HResults.S_OK; + SOSStackRefData[]? sosRefs = null; try { Contracts.IThread threadContract = _target.Contracts.Thread; @@ -3723,7 +3724,7 @@ int ISOSDacInterface.GetStackReferences(int osThreadID, DacComNullableByRef refs = stackWalk.WalkStackReferences(matchingThread.Value); - SOSStackRefData[] sosRefs = new SOSStackRefData[refs.Count]; + sosRefs = new SOSStackRefData[refs.Count]; for (int i = 0; i < refs.Count; i++) { Contracts.StackReferenceData r = refs[i]; @@ -3742,12 +3743,45 @@ int ISOSDacInterface.GetStackReferences(int osThreadID, DacComNullableByRef.ConvertToUnmanaged(ppEnum.Interface); } catch (System.Exception ex) { hr = ex.HResult; } +#if DEBUG + if (_legacyImpl is not null) + { + DacComNullableByRef legacyOut = new(isNullRef: false); + int hrLocal = _legacyImpl.GetStackReferences(osThreadID, legacyOut); + Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + + if (hrLocal == HResults.S_OK && legacyOut.Interface is not null) + { + ISOSStackRefEnum legacyRefEnum = legacyOut.Interface; + + uint legacyCount; + legacyRefEnum.GetCount(&legacyCount); + + SOSStackRefData[] legacyRefs = new SOSStackRefData[legacyCount]; + uint legacyFetched; + legacyRefEnum.Next(legacyCount, legacyRefs, &legacyFetched); + + if (hr == HResults.S_OK && sosRefs is not null) + { + Debug.WriteLine($"GetStackReferences debug: cDAC={sosRefs.Length} refs, DAC={legacyFetched} refs"); + + // Don't assert count equality yet — cDAC may miss Frame-based refs. + // Once frame-gc-scan-roots is implemented, enable this: + // Debug.Assert(sosRefs.Length == legacyFetched, $"cDAC: {sosRefs.Length} refs, DAC: {legacyFetched} refs"); + } + } + } +#endif + return hr; } From e1f3c739aa9e3279161ac03a4eaa6ea9d49a14f7 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 2 Mar 2026 17:02:33 -0500 Subject: [PATCH 13/30] cdac: Add Frame GcScanRoots dispatch for non-frameless frames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ScanFrameRoots method that dispatches based on frame type name. Most frame types use the base Frame::GcScanRoots_Impl which is a no-op. Key findings documented in the code: - GCFrame is NOT part of the Frame chain and the DAC does not scan it - Stub frames (StubDispatch, External, CallCounting, Dynamic, CLRToCOM) call PromoteCallerStack to report method arguments — not yet implemented - InlinedCallFrame, SoftwareExceptionFrame, etc. use the base no-op Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/StackWalk/StackWalk_1.cs | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) 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 0c1cc9fcb781fa..a4c45733bfda2a 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 @@ -113,7 +113,21 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre } else { - // TODO(stackref): Implement Frame::GcScanRoots for non-frameless frames + // Non-frameless: capital "F" Frame GcScanRoots dispatch. + // The base Frame::GcScanRoots_Impl is a no-op for most frame types. + // Frame types that override it (StubDispatchFrame, ExternalMethodFrame, + // CallCountingHelperFrame, DynamicHelperFrame, CLRToCOMMethodFrame, + // HijackFrame, ProtectValueClassFrame) call PromoteCallerStack to + // report method arguments from the transition block. + // + // GCFrame is NOT part of the Frame chain — it has its own linked list + // that the GC scans separately. The DAC's DacStackReferenceWalker + // does not scan GCFrame roots. + // + // For now, this is a no-op matching the base Frame behavior. + // TODO(stackref): Implement PromoteCallerStack for stub frames that + // report caller arguments (StubDispatchFrame, ExternalMethodFrame, etc.) + ScanFrameRoots(gcFrame.Frame, scanContext); } } } @@ -760,4 +774,58 @@ private static StackDataFrameHandle AssertCorrectHandle(IStackDataFrameHandle st return handle; } + + /// + /// Scans GC roots for a non-frameless (capital "F" Frame) stack frame. + /// Dispatches based on frame type identifier. Most frame types have a no-op + /// GcScanRoots (the base Frame implementation does nothing). + /// + /// Frame types with meaningful GcScanRoots that call PromoteCallerStack: + /// StubDispatchFrame, ExternalMethodFrame, CallCountingHelperFrame, + /// DynamicHelperFrame, CLRToCOMMethodFrame, HijackFrame, ProtectValueClassFrame. + /// + private void ScanFrameRoots(StackDataFrameHandle frame, GcScanContext scanContext) + { + _ = scanContext; // Will be used when stub frame scanning is implemented + // Read the frame type identifier + TargetPointer frameAddress = frame.FrameAddress; + if (frameAddress == TargetPointer.Null) + return; + + // Get the frame name to identify the type + string frameName = ((IStackWalk)this).GetFrameName(frameAddress); + + // Most frame types use the base no-op GcScanRoots_Impl. + // The ones that do work (stub frames) need PromoteCallerStack which + // requires reading the transition block and decoding method signatures. + // This is not yet implemented. + switch (frameName) + { + case "StubDispatchFrame": + case "ExternalMethodFrame": + case "CallCountingHelperFrame": + case "DynamicHelperFrame": + case "CLRToCOMMethodFrame": + case "ComPrestubMethodFrame": + // These frames call PromoteCallerStack to report method arguments. + // TODO(stackref): Implement PromoteCallerStack / PromoteCallerStackUsingGCRefMap + break; + + case "HijackFrame": + // Reports return value registers (X86 only with FEATURE_HIJACK) + // TODO(stackref): Implement HijackFrame scanning + break; + + case "ProtectValueClassFrame": + // Scans value types in linked list + // TODO(stackref): Implement ProtectValueClassFrame scanning + break; + + default: + // Base Frame::GcScanRoots_Impl is a no-op — nothing to report. + // This covers: InlinedCallFrame, SoftwareExceptionFrame, FaultingExceptionFrame, + // ResumableFrame, FuncEvalFrame, PrestubMethodFrame, PInvokeCalliFrame, etc. + break; + } + } }; From 76be992c8be05cb394728fe1d1ad00baab745050 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 3 Mar 2026 10:38:59 -0500 Subject: [PATCH 14/30] cdac: Fix stack slot register in GcScanner, add set-based debug validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix GcScanSlotLocation register for stack slots: was hardcoded to 0, now correctly maps GC_SP_REL→RSP(4), GC_FRAMEREG_REL→stackBaseRegister - Update GetStackReferences debug block to use set-based comparison (match by Address) instead of index-based, since ref ordering may differ - Validate Object, SourceType, Source, and Flags for each matched ref Known issue: Some refs have different computed addresses between cDAC and legacy DAC due to stack slot address computation differences. Needs further investigation of SP/FP handling during stack walk context management. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/StackWalk/GC/GcScanner.cs | 9 ++++++- .../SOSDacImpl.cs | 25 ++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs index 27b7f5d1d73e03..ce4ad31e68cde1 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -74,7 +74,14 @@ public bool EnumGcRefs( }; TargetPointer addr = new(baseAddr.Value + (ulong)(long)spOffset); - GcScanSlotLocation loc = new(0, spOffset, true); + int regForBase = spBase switch + { + 1 => 4, // GC_SP_REL → RSP (reg 4 on AMD64) + 2 => (int)stackBaseRegister, // GC_FRAMEREG_REL → stack base register (e.g., RBP=5) + 0 => 4, // GC_CALLER_SP_REL → RSP + _ => 0, + }; + GcScanSlotLocation loc = new(regForBase, spOffset, true); scanContext.GCEnumCallback(addr, scanFlags, loc); } }); 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 f9f0374841003a..e427c4d30f209c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -3774,9 +3774,28 @@ int ISOSDacInterface.GetStackReferences(int osThreadID, DacComNullableByRef Date: Tue, 3 Mar 2026 11:28:23 -0500 Subject: [PATCH 15/30] cdac: Fix GCInfo slot table decoding bugs Fix two bugs in the GCInfoDecoder slot table decoder that caused wrong slots to be reported as live: 1. When previous slot had non-zero flags, subsequent slots use a FULL offset (STACK_SLOT_ENCBASE) not a delta. The managed code incorrectly used STACK_SLOT_DELTA_ENCBASE for this case. 2. When previous slot had zero flags, subsequent slots use an unsigned delta (DecodeVarLengthUnsigned) with no +1 adjustment. The managed code incorrectly used DecodeVarLengthSigned with +1. Both bugs affected tracked and untracked stack slot sections. Verified with DOTNET_ENABLE_CDAC=1 and cdb against three debuggee dumps: all refs now match the legacy DAC exactly (count, Address, Object, Source, SourceType, Flags, Register, Offset for every ref). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/GCInfo/GCInfoDecoder.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) 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 f3bba268b62333..5ebe0391f9be17 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 @@ -248,13 +248,15 @@ private IEnumerable DecodeSlotTable() if (flags != 0) { - normSpOffset = _reader.DecodeVarLengthSigned(TTraits.STACK_SLOT_DELTA_ENCBASE, ref _bitOffset); + // When previous flags were non-zero, the next slot uses a FULL offset (not delta) + normSpOffset = _reader.DecodeVarLengthSigned(TTraits.STACK_SLOT_ENCBASE, ref _bitOffset); spOffset = TTraits.DenormalizeStackSlot(normSpOffset); flags = (GcSlotFlags)_reader.ReadBits(2, ref _bitOffset); } else { - normSpOffset += _reader.DecodeVarLengthSigned(TTraits.STACK_SLOT_DELTA_ENCBASE, ref _bitOffset) + 1; + int normSpOffsetDelta = (int)_reader.DecodeVarLengthUnsigned(TTraits.STACK_SLOT_DELTA_ENCBASE, ref _bitOffset); + normSpOffset += normSpOffsetDelta; spOffset = TTraits.DenormalizeStackSlot(normSpOffset); } @@ -278,13 +280,15 @@ private IEnumerable DecodeSlotTable() if (flags != 0) { - normSpOffset = _reader.DecodeVarLengthSigned(TTraits.STACK_SLOT_DELTA_ENCBASE, ref _bitOffset); + // When previous flags were non-zero, the next slot uses a FULL offset (not delta) + normSpOffset = _reader.DecodeVarLengthSigned(TTraits.STACK_SLOT_ENCBASE, ref _bitOffset); spOffset = TTraits.DenormalizeStackSlot(normSpOffset); flags = (GcSlotFlags)_reader.ReadBits(2, ref _bitOffset); } else { - normSpOffset += _reader.DecodeVarLengthSigned(TTraits.STACK_SLOT_DELTA_ENCBASE, ref _bitOffset) + 1; + int normSpOffsetDelta = (int)_reader.DecodeVarLengthUnsigned(TTraits.STACK_SLOT_DELTA_ENCBASE, ref _bitOffset); + normSpOffset += normSpOffsetDelta; spOffset = TTraits.DenormalizeStackSlot(normSpOffset); } @@ -826,6 +830,7 @@ private void ReportSlot(uint slotIndex, Action reportSlo Debug.Assert(slotIndex < _slots.Count); GcSlotDesc slot = _slots[(int)slotIndex]; uint gcFlags = (uint)slot.Flags & ((uint)GcSlotFlags.GC_SLOT_INTERIOR | (uint)GcSlotFlags.GC_SLOT_PINNED); + reportSlot(slotIndex, slot, gcFlags); } From c6fcf5721cbc5dcc07d3805300a4267915b75628 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 3 Mar 2026 11:53:16 -0500 Subject: [PATCH 16/30] cdac: Fix ARM64 stack base register and ExecutionAborted exit path Fix two bugs found via deep comparison with native GCInfoDecoder: 1. ARM64GCInfoTraits.DenormalizeStackBaseRegister used 0x29 (41 decimal) instead of 29 decimal. ARM64's frame pointer is X29, so the native XORs with 29. This would produce wrong addresses for all ARM64 stack-base-relative GC slots. 2. When ExecutionAborted and instruction offset is not in any interruptible range, the native code jumps to ExitSuccess (skips all reporting). The managed code incorrectly jumped to ReportUntracked, which would over-report untracked slots for aborted frames. Also documented the missing scratch register/slot filtering as a known gap (TODO in ReportSlot). The native ReportSlotToGC checks IsScratchRegister/IsScratchStackSlot for non-leaf frames; the cDAC currently reports all slots unconditionally. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/GCInfo/GCInfoDecoder.cs | 6 +++++- .../Contracts/GCInfo/PlatformTraits/ARM64GCInfoTraits.cs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) 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 5ebe0391f9be17..010d07a7d49f44 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 @@ -614,7 +614,7 @@ public bool EnumerateLiveSlots( } Debug.Assert(countIntersections <= 1); if (countIntersections == 0 && executionAborted) - goto ReportUntracked; + return true; // Native: goto ExitSuccess (skip all reporting including untracked) } // Read the indirect live state table header (if present) @@ -827,6 +827,10 @@ public bool EnumerateLiveSlots( private void ReportSlot(uint slotIndex, Action reportSlot) { + // TODO(stackref): The native ReportSlotToGC filters out scratch registers/stack slots + // for non-leaf frames (when reportScratchSlots is false) and respects ReportFPBasedSlotsOnly. + // The cDAC currently reports all slots unconditionally, which over-reports for non-leaf frames. + // This is safe (extra roots won't cause crashes) but imprecise. Debug.Assert(slotIndex < _slots.Count); GcSlotDesc slot = _slots[(int)slotIndex]; uint gcFlags = (uint)slot.Flags & ((uint)GcSlotFlags.GC_SLOT_INTERIOR | (uint)GcSlotFlags.GC_SLOT_PINNED); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARM64GCInfoTraits.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARM64GCInfoTraits.cs index 549cb48cbe8608..06c0317b3c36dc 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARM64GCInfoTraits.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARM64GCInfoTraits.cs @@ -7,7 +7,7 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; internal class ARM64GCInfoTraits : IGCInfoTraits { - public static uint DenormalizeStackBaseRegister(uint reg) => reg ^ 0x29u; + public static uint DenormalizeStackBaseRegister(uint reg) => reg ^ 29u; public static uint DenormalizeCodeLength(uint len) => len << 2; public static uint NormalizeCodeLength(uint len) => len >> 2; public static uint DenormalizeCodeOffset(uint offset) => offset << 2; From b8c9d73322442e17bec1530ba0879e2dff1ccc5a Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 3 Mar 2026 12:21:49 -0500 Subject: [PATCH 17/30] cdac: Match native skip behavior and add FindSafePoint TODO - Match native safe point skip: always skip numSafePoints * numTracked bits in the else branch, matching the native behavior. The indirect table case (numBitsPerOffset != 0) combined with interruptible ranges is unreachable in practice. - Add TODO for FindSafePoint binary search optimization (perf only, no correctness impact). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/GCInfo/GCInfoDecoder.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) 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 010d07a7d49f44..7b1fd9b8deea27 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 @@ -675,11 +675,13 @@ public bool EnumerateLiveSlots( } else { - // Skip over safe point live state data - if (numBitsPerOffset != 0) - bitOffset += (int)(_numSafePoints * numBitsPerOffset); - else - bitOffset += (int)(_numSafePoints * numTracked); + // Skip over safe point live state data. + // NOTE: The native code always skips numSafePoints * numTracked here, + // even when numBitsPerOffset != 0 (indirect table). This is technically + // wrong for the indirect case, but the encoder never produces both + // indirect safe points AND interruptible ranges, so it's unreachable. + // Match the native behavior for consistency. + bitOffset += (int)(_numSafePoints * numTracked); if (_numInterruptibleRanges == 0) goto ReportUntracked; @@ -845,6 +847,8 @@ private uint FindSafePoint(uint codeOffset) uint normBreakOffset = TTraits.NormalizeCodeOffset(codeOffset); uint numBitsPerOffset = CeilOfLog2(TTraits.NormalizeCodeOffset(_codeLength)); + // TODO(stackref): The native FindSafePoint uses binary search (NarrowSafePointSearch) + // when numSafePoints > 32. This is a performance optimization only — no correctness impact. // Linear scan through safe point offsets from the saved position int scanOffset = _safePointBitOffset; for (uint i = 0; i < _numSafePoints; i++) From 96d29584b98619030385b6164d1b8108cc45e837 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 3 Mar 2026 13:14:20 -0500 Subject: [PATCH 18/30] cdac: Implement scratch register/slot filtering in GCInfo decoder Add scratch register filtering to match native ReportSlotToGC behavior: - Add IsScratchRegister to IGCInfoTraits with per-platform implementations: - AMD64: preserved = rbx, rbp, rsi, rdi, r12-r15 (Windows ABI) - ARM64: preserved = x19-x28; scratch = x0-x17, x29-x30 - ARM: preserved = r4-r11; scratch = r0-r3, r12, r14 - Interpreter: no scratch registers - Add scratch filtering in ReportSlot: skip scratch registers for non-leaf frames (when ActiveStackFrame is not set) - Add ReportFPBasedSlotsOnly filtering: skip register slots and non-FP-relative stack slots when flag is set - Add IsScratchStackSlot check: skip SP-relative slots in the outgoing/scratch area for non-leaf frames - Set ActiveStackFrame flag for the first frameless frame in WalkStackReferences (matching native GetCodeManagerFlags behavior) Verified with DOTNET_ENABLE_CDAC=1 against three debuggee dumps: all refs match the legacy DAC exactly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/GCInfo/GCInfoDecoder.cs | 39 +++++++++++++++++-- .../PlatformTraits/AMD64GCInfoTraits.cs | 17 ++++++++ .../PlatformTraits/ARM64GCInfoTraits.cs | 4 ++ .../GCInfo/PlatformTraits/ARMGCInfoTraits.cs | 4 ++ .../GCInfo/PlatformTraits/IGCInfoTraits.cs | 6 +++ .../PlatformTraits/InterpreterGCInfoTraits.cs | 3 ++ .../Contracts/StackWalk/StackWalk_1.cs | 10 ++++- 7 files changed, 78 insertions(+), 5 deletions(-) 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 7b1fd9b8deea27..83c86194616403 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 @@ -140,6 +140,10 @@ public static GcSlotDesc CreateStackSlot(int spOffset, GcStackSlotBase slotBase, private List _slots = []; private int _liveStateBitOffset; + /* EnumerateLiveSlots state (set per-call) */ + private bool _reportScratchSlots; + private bool _reportFpBasedSlotsOnly; + public GcInfoDecoder(Target target, TargetPointer gcInfoAddress, uint gcVersion) { _target = target; @@ -557,13 +561,20 @@ public bool EnumerateLiveSlots( uint inputFlags, Action reportSlot) { + const uint ActiveStackFrame = 0x1; const uint ParentOfFuncletStackFrame = 0x40; const uint NoReportUntracked = 0x80; const uint ExecutionAborted = 0x2; + const uint ReportFPBasedSlotsOnly = 0x200; EnsureDecodedTo(DecodePoints.SlotTable); bool executionAborted = (inputFlags & ExecutionAborted) != 0; + bool reportScratchSlots = (inputFlags & ActiveStackFrame) != 0; + bool reportFpBasedSlotsOnly = (inputFlags & ReportFPBasedSlotsOnly) != 0; + + _reportScratchSlots = reportScratchSlots; + _reportFpBasedSlotsOnly = reportFpBasedSlotsOnly; // WantsReportOnlyLeaf is always true for non-legacy formats if ((inputFlags & ParentOfFuncletStackFrame) != 0) @@ -829,14 +840,34 @@ public bool EnumerateLiveSlots( private void ReportSlot(uint slotIndex, Action reportSlot) { - // TODO(stackref): The native ReportSlotToGC filters out scratch registers/stack slots - // for non-leaf frames (when reportScratchSlots is false) and respects ReportFPBasedSlotsOnly. - // The cDAC currently reports all slots unconditionally, which over-reports for non-leaf frames. - // This is safe (extra roots won't cause crashes) but imprecise. Debug.Assert(slotIndex < _slots.Count); GcSlotDesc slot = _slots[(int)slotIndex]; uint gcFlags = (uint)slot.Flags & ((uint)GcSlotFlags.GC_SLOT_INTERIOR | (uint)GcSlotFlags.GC_SLOT_PINNED); + if (slot.IsRegister) + { + // Skip scratch registers for non-leaf frames + if (!_reportScratchSlots && TTraits.IsScratchRegister(slot.RegisterNumber)) + return; + // FP-based-only mode skips all register slots + if (_reportFpBasedSlotsOnly) + return; + } + else + { + // Skip scratch stack slots for non-leaf frames (slots in the outgoing/scratch area) + if (!_reportScratchSlots && TTraits.HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA + && slot.Base == GcStackSlotBase.GC_SP_REL + && slot.SpOffset >= 0 + && (uint)slot.SpOffset < _fixedStackParameterScratchArea) + { + return; + } + // FP-based-only mode: only report GC_FRAMEREG_REL slots + if (_reportFpBasedSlotsOnly && slot.Base != GcStackSlotBase.GC_FRAMEREG_REL) + return; + } + reportSlot(slotIndex, slot, gcFlags); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/AMD64GCInfoTraits.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/AMD64GCInfoTraits.cs index a3c35bd9458791..7899f98082a1e4 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/AMD64GCInfoTraits.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/AMD64GCInfoTraits.cs @@ -40,4 +40,21 @@ internal class AMD64GCInfoTraits : IGCInfoTraits public static int NUM_INTERRUPTIBLE_RANGES_ENCBASE => 1; public static bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA => true; + + // Preserved (non-scratch): rbx(3), rbp(5), rsi(6), rdi(7), r12(12)-r15(15) + // On Unix ABI, rsi(6) and rdi(7) are scratch, but the GCInfo encoder + // uses the Windows ABI register numbering for all platforms. + public static bool IsScratchRegister(uint regNum) + { + const uint preservedMask = + (1u << 3) // rbx + | (1u << 5) // rbp + | (1u << 6) // rsi (Windows ABI) + | (1u << 7) // rdi (Windows ABI) + | (1u << 12) // r12 + | (1u << 13) // r13 + | (1u << 14) // r14 + | (1u << 15); // r15 + return (preservedMask & (1u << (int)regNum)) == 0; + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARM64GCInfoTraits.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARM64GCInfoTraits.cs index 06c0317b3c36dc..730447950b2fb7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARM64GCInfoTraits.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARM64GCInfoTraits.cs @@ -40,4 +40,8 @@ internal class ARM64GCInfoTraits : IGCInfoTraits public static int NUM_INTERRUPTIBLE_RANGES_ENCBASE => 1; public static bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA => true; + + // Preserved (non-scratch): x19-x28 + // Scratch: x0-x17, x29(FP), x30(LR) + public static bool IsScratchRegister(uint regNum) => regNum <= 17 || regNum >= 29; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARMGCInfoTraits.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARMGCInfoTraits.cs index 486c3c5bc4348b..c26261f7e4fb6e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARMGCInfoTraits.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARMGCInfoTraits.cs @@ -40,4 +40,8 @@ internal class ARMGCInfoTraits : IGCInfoTraits public static int NUM_INTERRUPTIBLE_RANGES_ENCBASE => 2; public static bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA => true; + + // Preserved (non-scratch): r4-r11 (and r14/LR is special) + // Scratch: r0-r3, r12, r14 + public static bool IsScratchRegister(uint regNum) => regNum <= 3 || regNum == 12 || regNum == 14; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/IGCInfoTraits.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/IGCInfoTraits.cs index c8db92b7b65cc4..24c8f80f4ba703 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/IGCInfoTraits.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/IGCInfoTraits.cs @@ -48,6 +48,12 @@ internal interface IGCInfoTraits static abstract bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA { get; } + /// + /// Returns true if the given register is a scratch (volatile) register. + /// Scratch register slots should only be reported for the active (leaf) stack frame. + /// + static abstract bool IsScratchRegister(uint regNum); + // These are the same across all platforms static virtual int POINTER_SIZE_ENCBASE { get; } = 3; static virtual int LIVESTATE_RLE_RUN_ENCBASE { get; } = 2; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/InterpreterGCInfoTraits.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/InterpreterGCInfoTraits.cs index 66819ea6508bf4..0ff7607b8a127e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/InterpreterGCInfoTraits.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/InterpreterGCInfoTraits.cs @@ -40,4 +40,7 @@ internal class InterpreterGCInfoTraits : IGCInfoTraits public static int NUM_INTERRUPTIBLE_RANGES_ENCBASE => 1; public static bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA => false; + + // Interpreter doesn't use physical registers for GC slots + public static bool IsScratchRegister(uint regNum) => false; } 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 a4c45733bfda2a..b210ab3b514388 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 @@ -87,6 +87,7 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre IEnumerable gcFrames = Filter(frames); GcScanContext scanContext = new(_target, resolveInteriorPointers: false); + bool isFirstFramelessFrame = true; foreach (GCFrameData gcFrame in gcFrames) { @@ -108,8 +109,15 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre { if (!IsManaged(gcFrame.Frame.Context.InstructionPointer, out CodeBlockHandle? cbh)) throw new InvalidOperationException("Expected managed code"); + + // The leaf (active) frame reports scratch registers; parent frames don't. + GcScanner.CodeManagerFlags codeManagerFlags = isFirstFramelessFrame + ? GcScanner.CodeManagerFlags.ActiveStackFrame + : 0; + isFirstFramelessFrame = false; + GcScanner gcScanner = new(_target); - gcScanner.EnumGcRefs(gcFrame.Frame.Context, cbh.Value, 0, scanContext); + gcScanner.EnumGcRefs(gcFrame.Frame.Context, cbh.Value, codeManagerFlags, scanContext); } else { From a7bb140492a1d14c344afba5a7434dfb4e743d54 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 3 Mar 2026 16:10:17 -0500 Subject: [PATCH 19/30] Implement context register fetching by number --- .../StackWalk/Context/AMD64Context.cs | 34 +++++----- .../StackWalk/Context/ARM64Context.cs | 66 +++++++++---------- .../Contracts/StackWalk/Context/ARMContext.cs | 32 ++++----- .../StackWalk/Context/ContextHolder.cs | 45 +++++++++++++ .../Context/IPlatformAgnosticContext.cs | 31 +++++---- .../StackWalk/Context/RegisterAttribute.cs | 6 ++ .../Contracts/StackWalk/GC/GcScanner.cs | 31 ++------- 7 files changed, 139 insertions(+), 106 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64Context.cs index a4be57139660a6..65c66318b6bcee 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64Context.cs @@ -141,71 +141,71 @@ public void Unwind(Target target) #region General and control registers - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 0)] [FieldOffset(0x78)] public ulong Rax; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 1)] [FieldOffset(0x80)] public ulong Rcx; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 2)] [FieldOffset(0x88)] public ulong Rdx; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 3)] [FieldOffset(0x90)] public ulong Rbx; - [Register(RegisterType.Control | RegisterType.StackPointer)] + [Register(RegisterType.Control | RegisterType.StackPointer, RegisterNumber = 4)] [FieldOffset(0x98)] public ulong Rsp; - [Register(RegisterType.Control | RegisterType.FramePointer)] + [Register(RegisterType.Control | RegisterType.FramePointer, RegisterNumber = 5)] [FieldOffset(0xa0)] public ulong Rbp; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 6)] [FieldOffset(0xa8)] public ulong Rsi; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 7)] [FieldOffset(0xb0)] public ulong Rdi; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 8)] [FieldOffset(0xb8)] public ulong R8; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 9)] [FieldOffset(0xc0)] public ulong R9; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 10)] [FieldOffset(0xc8)] public ulong R10; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 11)] [FieldOffset(0xd0)] public ulong R11; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 12)] [FieldOffset(0xd8)] public ulong R12; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 13)] [FieldOffset(0xe0)] public ulong R13; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 14)] [FieldOffset(0xe8)] public ulong R14; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 15)] [FieldOffset(0xf0)] public ulong R15; - [Register(RegisterType.Control | RegisterType.ProgramCounter)] + [Register(RegisterType.Control | RegisterType.ProgramCounter, RegisterNumber = 16)] [FieldOffset(0xf8)] public ulong Rip; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64Context.cs index 423c91415c4f9a..59031edce2fab6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64Context.cs @@ -76,119 +76,119 @@ public void Unwind(Target target) [FieldOffset(0x4)] public uint Cpsr; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 0)] [FieldOffset(0x8)] public ulong X0; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 1)] [FieldOffset(0x10)] public ulong X1; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 2)] [FieldOffset(0x18)] public ulong X2; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 3)] [FieldOffset(0x20)] public ulong X3; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 4)] [FieldOffset(0x28)] public ulong X4; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 5)] [FieldOffset(0x30)] public ulong X5; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 6)] [FieldOffset(0x38)] public ulong X6; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 7)] [FieldOffset(0x40)] public ulong X7; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 8)] [FieldOffset(0x48)] public ulong X8; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 9)] [FieldOffset(0x50)] public ulong X9; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 10)] [FieldOffset(0x58)] public ulong X10; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 11)] [FieldOffset(0x60)] public ulong X11; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 12)] [FieldOffset(0x68)] public ulong X12; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 13)] [FieldOffset(0x70)] public ulong X13; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 14)] [FieldOffset(0x78)] public ulong X14; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 15)] [FieldOffset(0x80)] public ulong X15; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 16)] [FieldOffset(0x88)] public ulong X16; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 17)] [FieldOffset(0x90)] public ulong X17; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 18)] [FieldOffset(0x98)] public ulong X18; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 19)] [FieldOffset(0xa0)] public ulong X19; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 20)] [FieldOffset(0xa8)] public ulong X20; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 21)] [FieldOffset(0xb0)] public ulong X21; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 22)] [FieldOffset(0xb8)] public ulong X22; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 23)] [FieldOffset(0xc0)] public ulong X23; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 24)] [FieldOffset(0xc8)] public ulong X24; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 25)] [FieldOffset(0xd0)] public ulong X25; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 26)] [FieldOffset(0xd8)] public ulong X26; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 27)] [FieldOffset(0xe0)] public ulong X27; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 28)] [FieldOffset(0xe8)] public ulong X28; @@ -196,19 +196,19 @@ public void Unwind(Target target) #region Control Registers - [Register(RegisterType.Control | RegisterType.FramePointer)] + [Register(RegisterType.Control | RegisterType.FramePointer, RegisterNumber = 29)] [FieldOffset(0xf0)] public ulong Fp; - [Register(RegisterType.Control)] + [Register(RegisterType.Control, RegisterNumber = 30)] [FieldOffset(0xf8)] public ulong Lr; - [Register(RegisterType.Control | RegisterType.StackPointer)] + [Register(RegisterType.Control | RegisterType.StackPointer, RegisterNumber = 31)] [FieldOffset(0x100)] public ulong Sp; - [Register(RegisterType.Control | RegisterType.ProgramCounter)] + [Register(RegisterType.Control | RegisterType.ProgramCounter, RegisterNumber = 32)] [FieldOffset(0x108)] public ulong Pc; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARMContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARMContext.cs index c8aeb154a0e373..4172f6f5b728c4 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARMContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARMContext.cs @@ -62,55 +62,55 @@ public void Unwind(Target target) #region General registers - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 0)] [FieldOffset(0x4)] public uint R0; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 1)] [FieldOffset(0x8)] public uint R1; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 2)] [FieldOffset(0xc)] public uint R2; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 3)] [FieldOffset(0x10)] public uint R3; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 4)] [FieldOffset(0x14)] public uint R4; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 5)] [FieldOffset(0x18)] public uint R5; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 6)] [FieldOffset(0x1c)] public uint R6; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 7)] [FieldOffset(0x20)] public uint R7; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 8)] [FieldOffset(0x24)] public uint R8; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 9)] [FieldOffset(0x28)] public uint R9; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 10)] [FieldOffset(0x2c)] public uint R10; - [Register(RegisterType.General | RegisterType.FramePointer)] + [Register(RegisterType.General | RegisterType.FramePointer, RegisterNumber = 11)] [FieldOffset(0x30)] public uint R11; - [Register(RegisterType.General)] + [Register(RegisterType.General, RegisterNumber = 12)] [FieldOffset(0x34)] public uint R12; @@ -118,15 +118,15 @@ public void Unwind(Target target) #region Control Registers - [Register(RegisterType.Control | RegisterType.StackPointer)] + [Register(RegisterType.Control | RegisterType.StackPointer, RegisterNumber = 13)] [FieldOffset(0x38)] public uint Sp; - [Register(RegisterType.Control)] + [Register(RegisterType.Control, RegisterNumber = 14)] [FieldOffset(0x3c)] public uint Lr; - [Register(RegisterType.Control | RegisterType.ProgramCounter)] + [Register(RegisterType.Control | RegisterType.ProgramCounter, RegisterNumber = 15)] [FieldOffset(0x40)] public uint Pc; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ContextHolder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ContextHolder.cs index 9bb6eca934f1ff..567dcbdca2c4f7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ContextHolder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ContextHolder.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.InteropServices; @@ -11,6 +12,34 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; public class ContextHolder<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T> : IPlatformAgnosticContext, IEquatable> where T : unmanaged, IPlatformContext { + private static readonly Dictionary s_registerNumberToField = BuildRegisterLookup(); + private static readonly uint s_spRegisterNumber = FindSPRegisterNumber(); + + private static Dictionary BuildRegisterLookup() + { + var lookup = new Dictionary(); + foreach (FieldInfo field in typeof(T).GetFields(BindingFlags.Public | BindingFlags.Instance)) + { + RegisterAttribute? attr = field.GetCustomAttribute(); + if (attr is not null && attr.RegisterNumber >= 0) + lookup[attr.RegisterNumber] = field; + } + + return lookup; + } + + private static uint FindSPRegisterNumber() + { + foreach (FieldInfo field in typeof(T).GetFields(BindingFlags.Public | BindingFlags.Instance)) + { + RegisterAttribute? attr = field.GetCustomAttribute(); + if (attr is not null && attr.RegisterType.HasFlag(RegisterType.StackPointer) && attr.RegisterNumber >= 0) + return (uint)attr.RegisterNumber; + } + + return uint.MaxValue; + } + public T Context; public uint Size => Context.Size; @@ -20,6 +49,22 @@ public class ContextHolder<[DynamicallyAccessedMembers(DynamicallyAccessedMember public TargetPointer InstructionPointer { get => Context.InstructionPointer; set => Context.InstructionPointer = value; } public TargetPointer FramePointer { get => Context.FramePointer; set => Context.FramePointer = value; } + public uint SPRegisterNumber => s_spRegisterNumber; + + public TargetPointer GetRegisterValue(uint registerNumber) + { + if (!s_registerNumberToField.TryGetValue((int)registerNumber, out FieldInfo? field)) + throw new ArgumentOutOfRangeException(nameof(registerNumber), $"Register number {registerNumber} not found in {typeof(T).Name}"); + + object? value = field.GetValue(Context); + return value switch + { + ulong ul => new TargetPointer(ul), + uint ui => new TargetPointer(ui), + _ => throw new InvalidOperationException($"Unexpected register field type {field.FieldType} for register {registerNumber}"), + }; + } + public unsafe void ReadFromAddress(Target target, TargetPointer address) { Span buffer = new byte[Size]; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs index 5783028f84cf55..0f69b0913a4e5c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs @@ -7,23 +7,26 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; public interface IPlatformAgnosticContext { - public abstract uint Size { get; } - public abstract uint DefaultContextFlags { get; } + abstract uint Size { get; } + abstract uint DefaultContextFlags { get; } - public TargetPointer StackPointer { get; set; } - public TargetPointer InstructionPointer { get; set; } - public TargetPointer FramePointer { get; set; } + TargetPointer StackPointer { get; set; } + TargetPointer InstructionPointer { get; set; } + TargetPointer FramePointer { get; set; } - public abstract void Clear(); - public abstract void ReadFromAddress(Target target, TargetPointer address); - public abstract void FillFromBuffer(Span buffer); - public abstract byte[] GetBytes(); - public abstract IPlatformAgnosticContext Clone(); - public abstract bool TrySetRegister(Target target, string fieldName, TargetNUInt value); - public abstract bool TryReadRegister(Target target, string fieldName, out TargetNUInt value); - public abstract void Unwind(Target target); + uint SPRegisterNumber { get; } + TargetPointer GetRegisterValue(uint registerNumber); - public static IPlatformAgnosticContext GetContextForPlatform(Target target) + abstract void Clear(); + abstract void ReadFromAddress(Target target, TargetPointer address); + abstract void FillFromBuffer(Span buffer); + abstract byte[] GetBytes(); + abstract IPlatformAgnosticContext Clone(); + abstract bool TrySetRegister(Target target, string fieldName, TargetNUInt value); + abstract bool TryReadRegister(Target target, string fieldName, out TargetNUInt value); + abstract void Unwind(Target target); + + static IPlatformAgnosticContext GetContextForPlatform(Target target) { IRuntimeInfo runtimeInfo = target.Contracts.RuntimeInfo; return runtimeInfo.GetTargetArchitecture() switch diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RegisterAttribute.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RegisterAttribute.cs index 1ae0c32bf7ffa4..2535a80e036acc 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RegisterAttribute.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RegisterAttribute.cs @@ -34,6 +34,12 @@ public sealed class RegisterAttribute : Attribute /// public RegisterType RegisterType { get; } + /// + /// Gets or sets the ISA register number (processor encoding). + /// -1 indicates no register number is assigned (e.g., segment registers, debug registers). + /// + public int RegisterNumber { get; set; } = -1; + public RegisterAttribute(RegisterType registerType) { RegisterType = registerType; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs index ce4ad31e68cde1..4e8f21dc8a75f3 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -59,7 +59,7 @@ public bool EnumGcRefs( if (isRegister) { - TargetPointer regValue = GetRegisterValue(context, registerNumber); + TargetPointer regValue = context.GetRegisterValue(registerNumber); GcScanSlotLocation loc = new((int)registerNumber, 0, false); scanContext.GCEnumCallback(regValue, scanFlags, loc); } @@ -68,7 +68,7 @@ public bool EnumGcRefs( TargetPointer baseAddr = spBase switch { 1 => context.StackPointer, // GC_SP_REL - 2 => GetRegisterValue(context, stackBaseRegister), // GC_FRAMEREG_REL + 2 => context.GetRegisterValue(stackBaseRegister), // GC_FRAMEREG_REL 0 => context.StackPointer, // GC_CALLER_SP_REL (TODO: use actual caller SP) _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), }; @@ -76,9 +76,9 @@ public bool EnumGcRefs( TargetPointer addr = new(baseAddr.Value + (ulong)(long)spOffset); int regForBase = spBase switch { - 1 => 4, // GC_SP_REL → RSP (reg 4 on AMD64) - 2 => (int)stackBaseRegister, // GC_FRAMEREG_REL → stack base register (e.g., RBP=5) - 0 => 4, // GC_CALLER_SP_REL → RSP + 1 => (int)context.SPRegisterNumber, // GC_SP_REL + 2 => (int)stackBaseRegister, // GC_FRAMEREG_REL + 0 => (int)context.SPRegisterNumber, // GC_CALLER_SP_REL _ => 0, }; GcScanSlotLocation loc = new(regForBase, spOffset, true); @@ -87,25 +87,4 @@ public bool EnumGcRefs( }); } - private static TargetPointer GetRegisterValue(IPlatformAgnosticContext context, uint registerNumber) - { - if (registerNumber == 4) return context.StackPointer; - if (registerNumber == 5) return context.FramePointer; - - // Map register number to context field name (AMD64 ordering) - // TODO: Support ARM64 and other architectures - string? fieldName = registerNumber switch - { - 0 => "Rax", 1 => "Rcx", 2 => "Rdx", 3 => "Rbx", - 6 => "Rsi", 7 => "Rdi", - 8 => "R8", 9 => "R9", 10 => "R10", 11 => "R11", - 12 => "R12", 13 => "R13", 14 => "R14", 15 => "R15", - _ => null, - }; - - if (fieldName is not null && context.TryReadRegister(null!, fieldName, out TargetNUInt value)) - return new TargetPointer(value.Value); - - throw new InvalidOperationException($"Failed to read register #{registerNumber} from context"); - } } From 59b637a58f586bfada74b9951338ff73cfee7d39 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 3 Mar 2026 16:25:26 -0500 Subject: [PATCH 20/30] cdac: Address PR review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix 5 issues from PR #125075 review: 1. datadescriptor.inc: Fix EHInfo type annotation from /*uint16*/ to /*pointer*/ — phdrJitEHInfo is PTR_EE_ILEXCEPTION, not uint16. 2. StackWalk.md: Update GetMethodDescPtr(IStackDataFrameHandle) docs to describe InlinedCallFrame special case for interop MethodDesc reporting at SW_SKIPPED_FRAME positions. 3. BitStreamReader: Replace static host-dependent BitsPerSize (IntPtr.Size * 8) with instance-based _bitsPerSize (target.PointerSize * 8) for correct cross-architecture analysis. 4. SOSDacImpl: Restore GetMethodDescPtrFromFrame implementation that was incorrectly stubbed with E_FAIL. Restores the cDAC implementation with debug validation against legacy DAC. 5. ReadyToRunJitManager: Fix GetEHClauses clause address computation to include entry.ExceptionInfoRva — was computing from imageBase directly, missing the RVA offset to the exception info section. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/StackWalk.md | 5 +- .../vm/datadescriptor/datadescriptor.inc | 2 +- ...ecutionManagerCore.ReadyToRunJitManager.cs | 3 +- .../Contracts/GCInfo/BitStreamReader.cs | 38 ++++++----- .../SOSDacImpl.cs | 65 +++++++++---------- 5 files changed, 58 insertions(+), 55 deletions(-) diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index c33ccf3acab61b..021594ab5b1d3f 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -373,9 +373,10 @@ TargetPointer GetMethodDescPtr(TargetPointer framePtr) ``` `GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHandle)` returns the method desc pointer associated with a `IStackDataFrameHandle`. Note this can either be at a capital 'F' frame or a managed frame unlike the above API which works only at capital 'F' frames. -This API is implemeted as follows: +This API is implemented as follows: 1. Try to get the current frame address with `GetFrameAddress`. If the address is not null, return `GetMethodDescPtr()`. -2. Check if the current context IP is a managed context using the ExecutionManager contract. If it is a managed contet, use the ExecutionManager context to find the related MethodDesc and return the pointer to it. + - Special case: For `InlinedCallFrame` at a `SW_SKIPPED_FRAME` position, if the frame's MethodDesc is an IL stub (`DynamicMethodDesc`), report the interop target MethodDesc instead. This ensures P/Invoke transitions show the target method rather than the internal stub. +2. Check if the current context IP is a managed context using the ExecutionManager contract. If it is a managed context, use the ExecutionManager contract to find the related MethodDesc and return the pointer to it. ```csharp TargetPointer GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHandle) ``` diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 2d9ae26c1d532a..d493020e42953b 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -782,7 +782,7 @@ CDAC_TYPE_BEGIN(RealCodeHeader) CDAC_TYPE_INDETERMINATE(RealCodeHeader) CDAC_TYPE_FIELD(RealCodeHeader, /*pointer*/, MethodDesc, offsetof(RealCodeHeader, phdrMDesc)) CDAC_TYPE_FIELD(RealCodeHeader, /*pointer*/, DebugInfo, offsetof(RealCodeHeader, phdrDebugInfo)) -CDAC_TYPE_FIELD(RealCodeHeader, /*uint16*/, EHInfo, offsetof(RealCodeHeader, phdrJitEHInfo)) +CDAC_TYPE_FIELD(RealCodeHeader, /*pointer*/, EHInfo, offsetof(RealCodeHeader, phdrJitEHInfo)) CDAC_TYPE_FIELD(RealCodeHeader, /*pointer*/, GCInfo, offsetof(RealCodeHeader, phdrJitGCInfo)) CDAC_TYPE_FIELD(RealCodeHeader, /*uint32*/, NumUnwindInfos, offsetof(RealCodeHeader, nUnwindInfos)) CDAC_TYPE_FIELD(RealCodeHeader, /* T_RUNTIME_FUNCTION */, UnwindInfos, offsetof(RealCodeHeader, unwindInfos)) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs index 021a92bb805426..69441acdc49494 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs @@ -239,11 +239,12 @@ bool Match(uint index) uint exceptionInfoSize = nextEntry.ExceptionInfoRva - entry.ExceptionInfoRva; uint clauseSize = Target.GetTypeInfo(DataType.CorCompileExceptionClause).Size ?? throw new InvalidOperationException("CorCompileExceptionClause size is not known"); + Debug.Assert(exceptionInfoSize % clauseSize == 0); uint numClauses = exceptionInfoSize / clauseSize; for (uint i = 0; i < numClauses; i++) { - TargetPointer clauseAddress = imageBase + (i * clauseSize); + TargetPointer clauseAddress = imageBase + entry.ExceptionInfoRva + (i * clauseSize); Data.CorCompileExceptionClause clause = Target.ProcessedData.GetOrAdd(clauseAddress); yield return new EHClause() { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/BitStreamReader.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/BitStreamReader.cs index 64b5ffa35f8415..02ef9ff7f12cf7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/BitStreamReader.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/BitStreamReader.cs @@ -14,7 +14,7 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts; /// internal struct BitStreamReader { - private static readonly int BitsPerSize = IntPtr.Size * 8; + private readonly int _bitsPerSize; private readonly Target _target; private readonly TargetPointer _buffer; @@ -37,6 +37,7 @@ public BitStreamReader(Target target, TargetPointer buffer) throw new ArgumentException("Buffer pointer cannot be null", nameof(buffer)); _target = target; + _bitsPerSize = target.PointerSize * 8; // Align buffer to pointer size boundary (similar to native implementation) nuint pointerMask = (nuint)target.PointerSize - 1; @@ -59,6 +60,7 @@ public BitStreamReader(Target target, TargetPointer buffer) public BitStreamReader(BitStreamReader other) { _target = other._target; + _bitsPerSize = other._bitsPerSize; _buffer = other._buffer; _initialRelPos = other._initialRelPos; _current = other._current; @@ -74,18 +76,18 @@ public BitStreamReader(BitStreamReader other) [MethodImpl(MethodImplOptions.AggressiveInlining)] public nuint Read(int numBits) { - Debug.Assert(numBits > 0 && numBits <= BitsPerSize); + Debug.Assert(numBits > 0 && numBits <= _bitsPerSize); nuint result = _currentValue; _currentValue >>= numBits; int newRelPos = _relPos + numBits; - if (newRelPos > BitsPerSize) + if (newRelPos > _bitsPerSize) { // Need to read from next word _current = new TargetPointer(_current.Value + (ulong)_target.PointerSize); nuint nextValue = ReadPointerSizedValue(_current); - newRelPos -= BitsPerSize; + newRelPos -= _bitsPerSize; nuint extraBits = nextValue << (numBits - newRelPos); result |= extraBits; _currentValue = nextValue >> newRelPos; @@ -94,7 +96,7 @@ public nuint Read(int numBits) _relPos = newRelPos; // Mask to get only the requested bits - nuint mask = (nuint.MaxValue >> (BitsPerSize - numBits)); + nuint mask = (nuint.MaxValue >> (_bitsPerSize - numBits)); result &= mask; return result; @@ -108,7 +110,7 @@ public nuint Read(int numBits) public nuint ReadOneFast() { // Check if we need to fetch the next word - if (_relPos == BitsPerSize) + if (_relPos == _bitsPerSize) { _current = new TargetPointer(_current.Value + (ulong)_target.PointerSize); _currentValue = ReadPointerSizedValue(_current); @@ -130,7 +132,7 @@ public nuint ReadOneFast() public nuint GetCurrentPos() { long wordOffset = ((long)_current.Value - (long)_buffer.Value) / _target.PointerSize; - return (nuint)(wordOffset * BitsPerSize + _relPos - _initialRelPos); + return (nuint)(wordOffset * _bitsPerSize + _relPos - _initialRelPos); } /// @@ -141,8 +143,8 @@ public nuint GetCurrentPos() public void SetCurrentPos(nuint pos) { nuint adjPos = pos + (nuint)_initialRelPos; - nuint wordOffset = adjPos / (nuint)BitsPerSize; - int newRelPos = (int)(adjPos % (nuint)BitsPerSize); + nuint wordOffset = adjPos / (nuint)_bitsPerSize; + int newRelPos = (int)(adjPos % (nuint)_bitsPerSize); _current = new TargetPointer(_buffer.Value + wordOffset * (ulong)_target.PointerSize); _relPos = newRelPos; @@ -160,8 +162,8 @@ public void Skip(nint numBitsToSkip) nuint newPos = (nuint)((nint)GetCurrentPos() + numBitsToSkip); nuint adjPos = newPos + (nuint)_initialRelPos; - nuint wordOffset = adjPos / (nuint)BitsPerSize; - int newRelPos = (int)(adjPos % (nuint)BitsPerSize); + nuint wordOffset = adjPos / (nuint)_bitsPerSize; + int newRelPos = (int)(adjPos % (nuint)_bitsPerSize); _current = new TargetPointer(_buffer.Value + wordOffset * (ulong)_target.PointerSize); _relPos = newRelPos; @@ -173,7 +175,7 @@ public void Skip(nint numBitsToSkip) if (_relPos == 0) { _current = new TargetPointer(_current.Value - (ulong)_target.PointerSize); - _relPos = BitsPerSize; + _relPos = _bitsPerSize; _currentValue = 0; } else @@ -190,7 +192,7 @@ public void Skip(nint numBitsToSkip) [MethodImpl(MethodImplOptions.AggressiveInlining)] public nuint DecodeVarLengthUnsigned(int baseValue) { - Debug.Assert(baseValue > 0 && baseValue < BitsPerSize); + Debug.Assert(baseValue > 0 && baseValue < _bitsPerSize); nuint result = Read(baseValue + 1); if ((result & ((nuint)1 << baseValue)) != 0) @@ -208,14 +210,14 @@ public nuint DecodeVarLengthUnsigned(int baseValue) /// The additional bits for the decoded value private nuint DecodeVarLengthUnsignedMore(int baseValue) { - Debug.Assert(baseValue > 0 && baseValue < BitsPerSize); + Debug.Assert(baseValue > 0 && baseValue < _bitsPerSize); nuint numEncodings = (nuint)1 << baseValue; nuint result = numEncodings; for (int shift = baseValue; ; shift += baseValue) { - Debug.Assert(shift + baseValue <= BitsPerSize); + Debug.Assert(shift + baseValue <= _bitsPerSize); nuint currentChunk = Read(baseValue + 1); result ^= (currentChunk & (numEncodings - 1)) << shift; @@ -235,14 +237,14 @@ private nuint DecodeVarLengthUnsignedMore(int baseValue) /// The decoded signed integer public nint DecodeVarLengthSigned(int baseValue) { - Debug.Assert(baseValue > 0 && baseValue < BitsPerSize); + Debug.Assert(baseValue > 0 && baseValue < _bitsPerSize); nuint numEncodings = (nuint)1 << baseValue; nint result = 0; for (int shift = 0; ; shift += baseValue) { - Debug.Assert(shift + baseValue <= BitsPerSize); + Debug.Assert(shift + baseValue <= _bitsPerSize); nuint currentChunk = Read(baseValue + 1); result |= (nint)(currentChunk & (numEncodings - 1)) << shift; @@ -250,7 +252,7 @@ public nint DecodeVarLengthSigned(int baseValue) if ((currentChunk & numEncodings) == 0) { // Extension bit is not set, sign-extend and we're done - int signBits = BitsPerSize - (shift + baseValue); + int signBits = _bitsPerSize - (shift + baseValue); result <<= signBits; result >>= signBits; // Arithmetic right shift for sign extension return result; 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 e427c4d30f209c..9cd170fb25e3a3 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -2408,39 +2408,38 @@ int ISOSDacInterface.GetMethodDescName(ClrDataAddress addr, uint count, char* na int ISOSDacInterface.GetMethodDescPtrFromFrame(ClrDataAddress frameAddr, ClrDataAddress* ppMD) { - return HResults.E_FAIL; - // int hr = HResults.S_OK; - // try - // { - // if (frameAddr == 0 || ppMD == null) - // throw new ArgumentException(); - - // IStackWalk stackWalkContract = _target.Contracts.StackWalk; - // TargetPointer methodDescPtr = stackWalkContract.GetMethodDescPtr(frameAddr.ToTargetPointer(_target)); - // if (methodDescPtr == TargetPointer.Null) - // throw new ArgumentException(); - - // _target.Contracts.RuntimeTypeSystem.GetMethodDescHandle(methodDescPtr); // validation - // *ppMD = methodDescPtr.ToClrDataAddress(_target); - // } - // catch (System.Exception ex) - // { - // hr = ex.HResult; - // } - // #if DEBUG - // if (_legacyImpl is not null) - // { - // ClrDataAddress ppMDLocal; - // int hrLocal = _legacyImpl.GetMethodDescPtrFromFrame(frameAddr, &ppMDLocal); - - // Debug.Assert(hrLocal == hr); - // if (hr == HResults.S_OK) - // { - // Debug.Assert(*ppMD == ppMDLocal); - // } - // } - // #endif - // return hr; + int hr = HResults.S_OK; + try + { + if (frameAddr == 0 || ppMD is null) + throw new ArgumentException(); + + Contracts.IStackWalk stackWalkContract = _target.Contracts.StackWalk; + TargetPointer methodDescPtr = stackWalkContract.GetMethodDescPtr(frameAddr.ToTargetPointer(_target)); + if (methodDescPtr == TargetPointer.Null) + throw new ArgumentException(); + + _target.Contracts.RuntimeTypeSystem.GetMethodDescHandle(methodDescPtr); // validation + *ppMD = methodDescPtr.ToClrDataAddress(_target); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacyImpl is not null) + { + ClrDataAddress ppMDLocal; + int hrLocal = _legacyImpl.GetMethodDescPtrFromFrame(frameAddr, &ppMDLocal); + + Debug.Assert(hrLocal == hr); + if (hr == HResults.S_OK) + { + Debug.Assert(*ppMD == ppMDLocal); + } + } +#endif + return hr; } int ISOSDacInterface.GetMethodDescPtrFromIP(ClrDataAddress ip, ClrDataAddress* ppMD) { From 8b5e023590d8531632a32104d58a61c6f81ef33c Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 6 Mar 2026 14:45:24 -0500 Subject: [PATCH 21/30] GCReportCallback missing object dereference --- .../Contracts/StackWalk/GC/GcScanContext.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs index ccde7a11b99612..aa5f6e22614ae0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs @@ -90,13 +90,17 @@ public void GCReportCallback(TargetPointer ppObj, GcScanFlags flags) throw new NotImplementedException(); } + // Read the object pointer from the stack slot, matching legacy DAC behavior + // (DacStackReferenceWalker::GCReportCallback in daccess.cpp) + _target.TryReadPointer(ppObj, out TargetPointer obj); + StackRefData data = new() { HasRegisterInformation = false, Register = 0, Offset = 0, Address = ppObj, - Object = TargetPointer.Null, + Object = obj, Flags = flags, StackPointer = StackPointer, }; From 2a40690888ead7f03f6d2575a5171606d7e7f051 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 9 Mar 2026 16:27:55 -0400 Subject: [PATCH 22/30] Fix cDAC stack walking bugs found via GC stress verification Fix several bugs in the cDAC's stack reference walking that caused mismatches against the legacy DAC during GC stress testing: - Fix GC_CALLER_SP_REL using wrong base address: GcScanner used the current context's StackPointer for GC_CALLER_SP_REL slots instead of the actual caller SP. Fixed by computing the caller SP via clone+unwind, with lazy caching to avoid repeated unwinds. - Fix IsFirst/ActiveStackFrame tracking: The cDAC used a simple isFirstFramelessFrame boolean to determine active frame status. Replaced with an IsFirst state machine in StackWalkData matching native CrawlFrame::isFirst semantics - starts true, set false after frameless frames, restored to true after FRAME_ATTR_RESUMABLE frames (ResumableFrame, RedirectedThreadFrame, HijackFrame). - Fix FaultingExceptionFrame incorrectly treated as resumable: FaultingExceptionFrame has FRAME_ATTR_FAULTED but NOT FRAME_ATTR_RESUMABLE. Including it in the resumable check caused IsFirst=true on the wrong managed frame, producing spurious scratch register refs. - Skip Frames below initial context SP in CreateStackWalk: Matches the native DAC behavior where StackWalkFrames with a profiler filter context skips Frames at lower SP (pushed more recently). Without this, RedirectedThreadFrame from GC stress redirect incorrectly set IsFirst=true for non-leaf managed frames. - Refactor scratch stack slot detection into IsScratchStackSlot on platform traits (AMD64, ARM64, ARM), matching the native GcInfoDecoder per-platform IsScratchStackSlot pattern. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/GCInfo/GCInfoDecoder.cs | 7 +- .../PlatformTraits/AMD64GCInfoTraits.cs | 12 ++++ .../PlatformTraits/ARM64GCInfoTraits.cs | 10 +++ .../GCInfo/PlatformTraits/ARMGCInfoTraits.cs | 10 +++ .../GCInfo/PlatformTraits/IGCInfoTraits.cs | 8 +++ .../Contracts/StackWalk/GC/GcScanner.cs | 21 +++++- .../Contracts/StackWalk/StackWalk_1.cs | 69 +++++++++++++++++-- 7 files changed, 124 insertions(+), 13 deletions(-) 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 83c86194616403..e94eb65b49031b 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 @@ -856,13 +856,8 @@ private void ReportSlot(uint slotIndex, Action reportSlo else { // Skip scratch stack slots for non-leaf frames (slots in the outgoing/scratch area) - if (!_reportScratchSlots && TTraits.HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA - && slot.Base == GcStackSlotBase.GC_SP_REL - && slot.SpOffset >= 0 - && (uint)slot.SpOffset < _fixedStackParameterScratchArea) - { + if (!_reportScratchSlots && TTraits.IsScratchStackSlot(slot.SpOffset, (uint)slot.Base, _fixedStackParameterScratchArea)) return; - } // FP-based-only mode: only report GC_FRAMEREG_REL slots if (_reportFpBasedSlotsOnly && slot.Base != GcStackSlotBase.GC_FRAMEREG_REL) return; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/AMD64GCInfoTraits.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/AMD64GCInfoTraits.cs index 7899f98082a1e4..023a5d4b191bdc 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/AMD64GCInfoTraits.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/AMD64GCInfoTraits.cs @@ -57,4 +57,16 @@ public static bool IsScratchRegister(uint regNum) | (1u << 15); // r15 return (preservedMask & (1u << (int)regNum)) == 0; } + + // AMD64 has a fixed stack parameter scratch area (shadow space + outgoing args). + // Stack slots with GC_SP_REL base and offset in [0, scratchAreaSize) are scratch slots. + // This matches the native IsScratchStackSlot which computes GetStackSlot and checks + // pSlot < pRD->SP + m_SizeOfStackOutgoingAndScratchArea. + public static bool IsScratchStackSlot(int spOffset, uint spBase, uint fixedStackParameterScratchArea) + { + // GC_SP_REL = 1 + return spBase == 1 + && spOffset >= 0 + && (uint)spOffset < fixedStackParameterScratchArea; + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARM64GCInfoTraits.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARM64GCInfoTraits.cs index 730447950b2fb7..6381e22861124b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARM64GCInfoTraits.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARM64GCInfoTraits.cs @@ -44,4 +44,14 @@ internal class ARM64GCInfoTraits : IGCInfoTraits // Preserved (non-scratch): x19-x28 // Scratch: x0-x17, x29(FP), x30(LR) public static bool IsScratchRegister(uint regNum) => regNum <= 17 || regNum >= 29; + + // ARM64 has a fixed stack parameter scratch area. + // Stack slots with GC_SP_REL base in [0, scratchAreaSize) are scratch slots. + public static bool IsScratchStackSlot(int spOffset, uint spBase, uint fixedStackParameterScratchArea) + { + // GC_SP_REL = 1 + return spBase == 1 + && spOffset >= 0 + && (uint)spOffset < fixedStackParameterScratchArea; + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARMGCInfoTraits.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARMGCInfoTraits.cs index c26261f7e4fb6e..ebbb953d0c2d7d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARMGCInfoTraits.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/ARMGCInfoTraits.cs @@ -44,4 +44,14 @@ internal class ARMGCInfoTraits : IGCInfoTraits // Preserved (non-scratch): r4-r11 (and r14/LR is special) // Scratch: r0-r3, r12, r14 public static bool IsScratchRegister(uint regNum) => regNum <= 3 || regNum == 12 || regNum == 14; + + // ARM has a fixed stack parameter scratch area. + // Stack slots with GC_SP_REL base in [0, scratchAreaSize) are scratch slots. + public static bool IsScratchStackSlot(int spOffset, uint spBase, uint fixedStackParameterScratchArea) + { + // GC_SP_REL = 1 + return spBase == 1 + && spOffset >= 0 + && (uint)spOffset < fixedStackParameterScratchArea; + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/IGCInfoTraits.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/IGCInfoTraits.cs index 24c8f80f4ba703..2d3c5faf59579a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/IGCInfoTraits.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/IGCInfoTraits.cs @@ -54,6 +54,14 @@ internal interface IGCInfoTraits /// static abstract bool IsScratchRegister(uint regNum); + /// + /// Returns true if a stack slot at the given offset and base is in the scratch/outgoing area. + /// Scratch stack slots should only be reported for the active (leaf) stack frame. + /// spBase uses the GcStackSlotBase encoding: 0=CALLER_SP_REL, 1=SP_REL, 2=FRAMEREG_REL. + /// + static virtual bool IsScratchStackSlot(int spOffset, uint spBase, uint fixedStackParameterScratchArea) + => false; + // These are the same across all platforms static virtual int POINTER_SIZE_ENCBASE { get; } = 3; static virtual int LIVESTATE_RLE_RUN_ENCBASE { get; } = 2; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs index 4e8f21dc8a75f3..eda68ea277fda9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -46,6 +46,10 @@ public bool EnumGcRefs( uint stackBaseRegister = decoder.StackBaseRegister; + // Lazily compute the caller SP for GC_CALLER_SP_REL slots. + // The native code uses GET_CALLER_SP(pRD) which comes from EnsureCallerContextIsValid. + TargetPointer? callerSP = null; + return decoder.EnumerateLiveSlots( (uint)relativeOffset.Value, (uint)flags, @@ -69,7 +73,7 @@ public bool EnumGcRefs( { 1 => context.StackPointer, // GC_SP_REL 2 => context.GetRegisterValue(stackBaseRegister), // GC_FRAMEREG_REL - 0 => context.StackPointer, // GC_CALLER_SP_REL (TODO: use actual caller SP) + 0 => GetCallerSP(context, ref callerSP), // GC_CALLER_SP_REL _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), }; @@ -87,4 +91,19 @@ public bool EnumGcRefs( }); } + /// + /// Compute the caller's SP by unwinding the current context one frame. + /// Cached in to avoid repeated unwinds for the same frame. + /// + private TargetPointer GetCallerSP(IPlatformAgnosticContext context, ref TargetPointer? cached) + { + if (cached is null) + { + IPlatformAgnosticContext callerContext = context.Clone(); + callerContext.Unwind(_target); + cached = callerContext.StackPointer; + } + return cached.Value; + } + } 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 b210ab3b514388..b73cfdc4db9c59 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 @@ -42,7 +42,9 @@ private record StackDataFrameHandle( IPlatformAgnosticContext Context, StackWalkState State, TargetPointer FrameAddress, - ThreadData ThreadData) : IStackDataFrameHandle + ThreadData ThreadData, + bool IsResumableFrame = false, + bool IsActiveFrame = false) : IStackDataFrameHandle { } private class StackWalkData(IPlatformAgnosticContext context, StackWalkState state, FrameIterator frameIter, ThreadData threadData) @@ -52,7 +54,48 @@ private class StackWalkData(IPlatformAgnosticContext context, StackWalkState sta public FrameIterator FrameIter { get; set; } = frameIter; public ThreadData ThreadData { get; set; } = threadData; - public StackDataFrameHandle ToDataFrame() => new(Context.Clone(), State, FrameIter.CurrentFrameAddress, ThreadData); + // Track isFirst exactly like native CrawlFrame::isFirst in StackFrameIterator. + // Starts true, set false after processing a managed (frameless) frame, + // set back to true when encountering a ResumableFrame (FRAME_ATTR_RESUMABLE). + public bool IsFirst { get; set; } = true; + + public bool IsCurrentFrameResumable() + { + if (State is not (StackWalkState.SW_FRAME or StackWalkState.SW_SKIPPED_FRAME)) + return false; + + var ft = FrameIter.GetCurrentFrameType(); + // 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. + return ft is FrameIterator.FrameType.ResumableFrame + or FrameIterator.FrameType.RedirectedThreadFrame + or FrameIterator.FrameType.HijackFrame; + } + + /// + /// 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 + /// + public void AdvanceIsFirst() + { + if (State == StackWalkState.SW_FRAMELESS) + IsFirst = false; + else + IsFirst = IsCurrentFrameResumable(); + } + + public StackDataFrameHandle ToDataFrame() + { + bool isResumable = IsCurrentFrameResumable(); + bool isActiveFrame = IsFirst && State == StackWalkState.SW_FRAMELESS; + return new(Context.Clone(), State, FrameIter.CurrentFrameAddress, ThreadData, isResumable, isActiveFrame); + } } IEnumerable IStackWalk.CreateStackWalk(ThreadData threadData) @@ -62,6 +105,18 @@ IEnumerable IStackWalk.CreateStackWalk(ThreadData threadD StackWalkState state = IsManaged(context.InstructionPointer, out _) ? StackWalkState.SW_FRAMELESS : StackWalkState.SW_FRAME; FrameIterator frameIterator = new(_target, threadData); + // Skip Frames whose address is below the initial context's SP. + // This matches the native DAC behavior: when StackWalkFrames starts from a + // profiler filter context, Frames at a lower SP (pushed more recently) are + // not encountered during the walk. Without this, Frames like + // RedirectedThreadFrame (pushed during GC stress redirect) would incorrectly + // set IsFirst=true for the wrong managed frame. + TargetPointer initialSP = context.StackPointer; + while (frameIterator.IsValid() && frameIterator.CurrentFrameAddress.Value < initialSP.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()) { @@ -71,10 +126,12 @@ IEnumerable IStackWalk.CreateStackWalk(ThreadData threadD StackWalkData stackWalkData = new(context, state, frameIterator, threadData); yield return stackWalkData.ToDataFrame(); + stackWalkData.AdvanceIsFirst(); while (Next(stackWalkData)) { yield return stackWalkData.ToDataFrame(); + stackWalkData.AdvanceIsFirst(); } } @@ -87,7 +144,6 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre IEnumerable gcFrames = Filter(frames); GcScanContext scanContext = new(_target, resolveInteriorPointers: false); - bool isFirstFramelessFrame = true; foreach (GCFrameData gcFrame in gcFrames) { @@ -110,11 +166,12 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre if (!IsManaged(gcFrame.Frame.Context.InstructionPointer, out CodeBlockHandle? cbh)) throw new InvalidOperationException("Expected managed code"); - // The leaf (active) frame reports scratch registers; parent frames don't. - GcScanner.CodeManagerFlags codeManagerFlags = isFirstFramelessFrame + // IsActiveFrame was computed during CreateStackWalk, matching native + // CrawlFrame::IsActiveFunc() semantics. Active frames report scratch + // registers; non-active frames skip them. + GcScanner.CodeManagerFlags codeManagerFlags = gcFrame.Frame.IsActiveFrame ? GcScanner.CodeManagerFlags.ActiveStackFrame : 0; - isFirstFramelessFrame = false; GcScanner gcScanner = new(_target); gcScanner.EnumGcRefs(gcFrame.Frame.Context, cbh.Value, codeManagerFlags, scanContext); From db6d9b38bf5525db29d9f8d0f7609b1f65f2fa18 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 11 Mar 2026 13:21:23 -0400 Subject: [PATCH 23/30] Fix Frame skip to use caller SP instead of initial SP The initial Frame skip used the leaf's SP as the threshold, which missed active InlinedCallFrames whose address was above the leaf SP but below the caller SP. These Frames would be processed as SW_FRAME, causing UpdateContextFromFrame to restore the IP to the P/Invoke return address within the same method and producing duplicate GC refs. Use the caller SP (computed by unwinding the initial managed frame) as the skip threshold, matching the native CheckForSkippedFrames which uses EnsureCallerContextIsValid + GetSP(pCallerContext). This correctly skips all Frames between the managed frame and its caller, including both RedirectedThreadFrame and active InlinedCallFrames. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/StackWalk/StackWalk_1.cs | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) 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 b73cfdc4db9c59..f4f3af6fb3dac7 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 @@ -105,14 +105,26 @@ IEnumerable IStackWalk.CreateStackWalk(ThreadData threadD StackWalkState state = IsManaged(context.InstructionPointer, out _) ? StackWalkState.SW_FRAMELESS : StackWalkState.SW_FRAME; FrameIterator frameIterator = new(_target, threadData); - // Skip Frames whose address is below the initial context's SP. - // This matches the native DAC behavior: when StackWalkFrames starts from a - // profiler filter context, Frames at a lower SP (pushed more recently) are - // not encountered during the walk. Without this, Frames like - // RedirectedThreadFrame (pushed during GC stress redirect) would incorrectly - // set IsFirst=true for the wrong managed frame. - TargetPointer initialSP = context.StackPointer; - while (frameIterator.IsValid() && frameIterator.CurrentFrameAddress.Value < initialSP.Value) + // Skip Frames below the initial managed frame's caller SP, matching the + // native DAC behavior. The native's CheckForSkippedFrames uses + // EnsureCallerContextIsValid + GetSP(pCallerContext) to determine which + // Frames are "skipped" (between the managed frame and its caller). + // All Frames below this SP belong to the current managed frame or + // frames pushed more recently (e.g., RedirectedThreadFrame from GC stress, + // active InlinedCallFrames from P/Invoke calls within the method). + TargetPointer skipBelowSP; + if (state == StackWalkState.SW_FRAMELESS) + { + // Compute the caller SP by unwinding the initial managed frame. + IPlatformAgnosticContext callerCtx = context.Clone(); + callerCtx.Unwind(_target); + skipBelowSP = callerCtx.StackPointer; + } + else + { + skipBelowSP = context.StackPointer; + } + while (frameIterator.IsValid() && frameIterator.CurrentFrameAddress.Value < skipBelowSP.Value) { frameIterator.Next(); } From b567fbcb84607f369263ff2774a0b1b0046a26c3 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 11 Mar 2026 15:23:08 -0400 Subject: [PATCH 24/30] Guard new Data fields with ContainsKey for backward compatibility Newer fields added to RealCodeHeader (EHInfo), ReadyToRunInfo (ExceptionInfoSection), and ExceptionInfo (ExceptionFlags, StackLowBound, StackHighBound, PassNumber, CSFEHClause, CSFEnclosingClause, CallerOfActualHandlerFrame, LastReportedFuncletInfo) may not exist in older contract versions. Guard each with type.Fields.ContainsKey and default to safe values to prevent KeyNotFoundException when analyzing older dumps. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/StackWalk/StackWalk_1.cs | 1 + .../Data/ExceptionInfo.cs | 26 ++++++++++++------- .../Data/ReadyToRunInfo.cs | 3 ++- .../Data/RealCodeHeader.cs | 3 ++- 4 files changed, 22 insertions(+), 11 deletions(-) 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 f4f3af6fb3dac7..63471fb9392eef 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 @@ -290,6 +290,7 @@ private IEnumerable Filter(IEnumerable handle if (exInfo.PassNumber == 2 && exInfo.CSFEnclosingClause != TargetPointer.Null && funcletParentStackFrame == TargetPointer.Null && + exInfo.LastReportedFuncletInfo is not null && exInfo.LastReportedFuncletInfo.IP != TargetCodePointer.Null) { // We are in the 2nd pass and we have already called an exceptionally called 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 44465ccc9c9c48..9865c2dd6fc011 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 @@ -14,17 +14,25 @@ public ExceptionInfo(Target target, TargetPointer address) PreviousNestedInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(PreviousNestedInfo)].Offset); ThrownObjectHandle = target.ReadPointer(address + (ulong)type.Fields[nameof(ThrownObjectHandle)].Offset); - ExceptionFlags = target.Read(address + (ulong)type.Fields[nameof(ExceptionFlags)].Offset); - StackLowBound = target.ReadPointer(address + (ulong)type.Fields[nameof(StackLowBound)].Offset); - StackHighBound = target.ReadPointer(address + (ulong)type.Fields[nameof(StackHighBound)].Offset); + if (type.Fields.ContainsKey(nameof(ExceptionFlags))) + ExceptionFlags = target.Read(address + (ulong)type.Fields[nameof(ExceptionFlags)].Offset); + if (type.Fields.ContainsKey(nameof(StackLowBound))) + StackLowBound = target.ReadPointer(address + (ulong)type.Fields[nameof(StackLowBound)].Offset); + if (type.Fields.ContainsKey(nameof(StackHighBound))) + StackHighBound = target.ReadPointer(address + (ulong)type.Fields[nameof(StackHighBound)].Offset); if (type.Fields.ContainsKey(nameof(ExceptionWatsonBucketTrackerBuckets))) ExceptionWatsonBucketTrackerBuckets = target.ReadPointer(address + (ulong)type.Fields[nameof(ExceptionWatsonBucketTrackerBuckets)].Offset); - PassNumber = target.Read(address + (ulong)type.Fields[nameof(PassNumber)].Offset); - CSFEHClause = target.ReadPointer(address + (ulong)type.Fields[nameof(CSFEHClause)].Offset); - CSFEnclosingClause = target.ReadPointer(address + (ulong)type.Fields[nameof(CSFEnclosingClause)].Offset); - CallerOfActualHandlerFrame = target.ReadPointer(address + (ulong)type.Fields[nameof(CallerOfActualHandlerFrame)].Offset); - LastReportedFuncletInfo = target.ProcessedData.GetOrAdd(address + (ulong)type.Fields[nameof(LastReportedFuncletInfo)].Offset); + if (type.Fields.ContainsKey(nameof(PassNumber))) + PassNumber = target.Read(address + (ulong)type.Fields[nameof(PassNumber)].Offset); + if (type.Fields.ContainsKey(nameof(CSFEHClause))) + CSFEHClause = target.ReadPointer(address + (ulong)type.Fields[nameof(CSFEHClause)].Offset); + if (type.Fields.ContainsKey(nameof(CSFEnclosingClause))) + CSFEnclosingClause = target.ReadPointer(address + (ulong)type.Fields[nameof(CSFEnclosingClause)].Offset); + if (type.Fields.ContainsKey(nameof(CallerOfActualHandlerFrame))) + CallerOfActualHandlerFrame = target.ReadPointer(address + (ulong)type.Fields[nameof(CallerOfActualHandlerFrame)].Offset); + if (type.Fields.ContainsKey(nameof(LastReportedFuncletInfo))) + LastReportedFuncletInfo = target.ProcessedData.GetOrAdd(address + (ulong)type.Fields[nameof(LastReportedFuncletInfo)].Offset); } public TargetPointer PreviousNestedInfo { get; } @@ -37,5 +45,5 @@ public ExceptionInfo(Target target, TargetPointer address) public TargetPointer CSFEHClause { get; } public TargetPointer CSFEnclosingClause { get; } public TargetPointer CallerOfActualHandlerFrame { get; } - public LastReportedFuncletInfo LastReportedFuncletInfo { get; } + public LastReportedFuncletInfo? LastReportedFuncletInfo { 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 843bc82f7f1328..74c6525d06832f 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 @@ -31,7 +31,8 @@ public ReadyToRunInfo(Target target, TargetPointer address) DelayLoadMethodCallThunks = target.ReadPointer(address + (ulong)type.Fields[nameof(DelayLoadMethodCallThunks)].Offset); DebugInfoSection = target.ReadPointer(address + (ulong)type.Fields[nameof(DebugInfoSection)].Offset); - ExceptionInfoSection = target.ReadPointer(address + (ulong)type.Fields[nameof(ExceptionInfoSection)].Offset); + if (type.Fields.ContainsKey(nameof(ExceptionInfoSection))) + ExceptionInfoSection = target.ReadPointer(address + (ulong)type.Fields[nameof(ExceptionInfoSection)].Offset); // Map is from the composite info pointer (set to itself for non-multi-assembly composite images) EntryPointToMethodDescMap = CompositeInfo + (ulong)type.Fields[nameof(EntryPointToMethodDescMap)].Offset; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RealCodeHeader.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RealCodeHeader.cs index 9718f0ab4fec6f..5258f81dd44dea 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RealCodeHeader.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RealCodeHeader.cs @@ -13,7 +13,8 @@ public RealCodeHeader(Target target, TargetPointer address) Target.TypeInfo type = target.GetTypeInfo(DataType.RealCodeHeader); MethodDesc = target.ReadPointer(address + (ulong)type.Fields[nameof(MethodDesc)].Offset); DebugInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(DebugInfo)].Offset); - EHInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(EHInfo)].Offset); + if (type.Fields.ContainsKey(nameof(EHInfo))) + EHInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(EHInfo)].Offset); GCInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(GCInfo)].Offset); NumUnwindInfos = target.Read(address + (ulong)type.Fields[nameof(NumUnwindInfos)].Offset); UnwindInfos = address + (ulong)type.Fields[nameof(UnwindInfos)].Offset; From 5b66392849dd33fc46147dfb8f453670e5cc3d41 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 11 Mar 2026 15:40:39 -0400 Subject: [PATCH 25/30] Address PR review comments: cleanup and safety fixes - Remove unused usings in GcScanContext.cs (Data namespace, StackWalk_1 static) - Fix trailing semicolon on class closing brace in StackWalk_1.cs - Discard unused pMethodDesc assignment in StackWalk_1.cs - Add buffer length validation in SOSStackRefEnum.Next to prevent IndexOutOfRangeException - Use Debug.ValidateHResult in GetMethodDescPtrFromFrame to match codebase pattern Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/StackWalk/GC/GcScanContext.cs | 2 -- .../Contracts/StackWalk/StackWalk_1.cs | 4 ++-- .../SOSDacImpl.cs | 3 ++- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs index aa5f6e22614ae0..2da36e0827710c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; -using Microsoft.Diagnostics.DataContractReader.Data; -using static Microsoft.Diagnostics.DataContractReader.Contracts.StackWalk_1; namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; 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 63471fb9392eef..2aada861b150b7 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 @@ -161,7 +161,7 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre { try { - TargetPointer pMethodDesc = ((IStackWalk)this).GetMethodDescPtr(gcFrame.Frame); + _ = ((IStackWalk)this).GetMethodDescPtr(gcFrame.Frame); bool reportGcReferences = gcFrame.ShouldCrawlFrameReportGCReferences; @@ -906,4 +906,4 @@ private void ScanFrameRoots(StackDataFrameHandle frame, GcScanContext scanContex break; } } -}; +} 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 9cd170fb25e3a3..997bfb95ba422e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -2432,7 +2432,7 @@ int ISOSDacInterface.GetMethodDescPtrFromFrame(ClrDataAddress frameAddr, ClrData ClrDataAddress ppMDLocal; int hrLocal = _legacyImpl.GetMethodDescPtrFromFrame(frameAddr, &ppMDLocal); - Debug.Assert(hrLocal == hr); + Debug.ValidateHResult(hr, hrLocal); if (hr == HResults.S_OK) { Debug.Assert(*ppMD == ppMDLocal); @@ -3655,6 +3655,7 @@ int ISOSStackRefEnum.Next(uint count, SOSStackRefData[] refs, uint* pFetched) if (pFetched is null || refs is null) throw new NullReferenceException(); + count = Math.Min(count, (uint)refs.Length); uint written = 0; while (written < count && _index < _refs.Length) refs[written++] = _refs[(int)_index++]; From 865647317f3c13d684369254201ae2084e46d1ee Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 11 Mar 2026 16:13:05 -0400 Subject: [PATCH 26/30] Address remaining PR review comments - Remove unused 'using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions' from StackWalk_1.cs - Remove unused 'using System.Linq' and 'using System' from StackReferenceDumpTests.cs - Remove unused 'using System' from StackRefData.cs and GcScanSlotLocation.cs - Clear ppEnum.Interface on failure paths in SOSDacImpl.GetStackReferences Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/StackWalk/GC/GcScanSlotLocation.cs | 2 -- .../Contracts/StackWalk/GC/StackRefData.cs | 2 -- .../Contracts/StackWalk/StackWalk_1.cs | 1 - .../SOSDacImpl.cs | 2 ++ .../managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs | 2 -- 5 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanSlotLocation.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanSlotLocation.cs index 7e45bba9d19ce1..e9829ab4bceba0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanSlotLocation.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanSlotLocation.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; - namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; internal readonly record struct GcScanSlotLocation(int Reg, int RegOffset, bool TargetPtr); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs index f7670e68a9f21c..46e5bac46f6431 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; - namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; internal class StackRefData 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 2aada861b150b7..bc7d0cf05f2c89 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 @@ -5,7 +5,6 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics; using System.Collections.Generic; -using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; using Microsoft.Diagnostics.DataContractReader.Data; using System.Linq; 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 997bfb95ba422e..fc77974b3dd5ab 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -3750,6 +3750,8 @@ int ISOSDacInterface.GetStackReferences(int osThreadID, DacComNullableByRef Date: Wed, 11 Mar 2026 16:30:36 -0400 Subject: [PATCH 27/30] Restore GetMethodDescPtr docs and fix patchpointinfo friend - Restore the full GetMethodDescPtr(IStackDataFrameHandle) documentation in StackWalk.md that describes the ReportInteropMD special case. The docs were incorrectly simplified but the implementation was unchanged. - Use specific friend declaration in patchpointinfo.h instead of generic template friend, matching the codebase convention. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/ExecutionManager.md | 5 + docs/design/datacontracts/StackWalk.md | 19 +- src/coreclr/inc/patchpointinfo.h | 2 +- .../Contracts/IThread.cs | 12 +- .../Contracts/GCInfo/BitStreamReader.cs | 281 ------------------ 5 files changed, 27 insertions(+), 292 deletions(-) delete mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/BitStreamReader.cs diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index 12869acd36f828..fdc7f032f21b9c 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -50,7 +50,10 @@ struct CodeBlockHandle List GetExceptionClauses(CodeBlockHandle codeInfoHandle); // Extension Methods (implemented in terms of other APIs) + // Returns true if the code block is a funclet (exception handler, filter, or finally) bool IsFunclet(CodeBlockHandle codeInfoHandle); + // Returns true if the code block is specifically a filter funclet + bool IsFilterFunclet(CodeBlockHandle codeInfoHandle); ``` ```csharp @@ -450,6 +453,8 @@ There are two distinct clause data types. JIT-compiled code uses `EEExceptionCla After obtaining the clause array bounds, the common iteration logic classifies each clause by its flags. The native `COR_ILEXCEPTION_CLAUSE` flags are bit flags: `Filter` (0x1), `Finally` (0x2), `Fault` (0x4). If none are set, the clause is `Typed`. For typed clauses, if the `CachedClass` flag (0x10000000) is set (JIT-only, used for dynamic methods), the union field contains a resolved `TypeHandle` pointer; the clause is a catch-all if this pointer equals the `ObjectMethodTable` global. Otherwise, the union field is a metadata `ClassToken`. To determine whether a typed clause is a catch-all handler, the `ClassToken` (which may be a `TypeDef` or `TypeRef`) is resolved to a `MethodTable` via the `Loader` contract's module lookup maps (`TypeDefToMethodTable` or `TypeRefToMethodTable`) and compared against the `ObjectMethodTable` global. For typed clauses without a cached type handle, the module address is resolved by walking `CodeBlockHandle` → `MethodDesc` → `MethodTable` → `TypeHandle` → `Module` via the `RuntimeTypeSystem` contract. +`IsFilterFunclet` first checks `IsFunclet`. If the code block is a funclet, it retrieves the EH clauses for the method and checks whether any filter clause's handler offset matches the funclet's relative offset. If a match is found, the funclet is a filter funclet. + ### RangeSectionMap The range section map logically partitions the entire 32-bit or 64-bit addressable space into chunks. diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 021594ab5b1d3f..c77d5f296736f3 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -98,6 +98,7 @@ Contracts used: | --- | | `ExecutionManager` | | `Thread` | +| `RuntimeTypeSystem` | ### Stackwalk Algorithm @@ -372,11 +373,21 @@ string GetFrameName(TargetPointer frameIdentifier); TargetPointer GetMethodDescPtr(TargetPointer framePtr) ``` -`GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHandle)` returns the method desc pointer associated with a `IStackDataFrameHandle`. Note this can either be at a capital 'F' frame or a managed frame unlike the above API which works only at capital 'F' frames. +`GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHandle)` returns the method desc pointer associated with a `IStackDataFrameHandle`. Note there are two major differences between this API and the one above that operates on a TargetPointer. +* This API can either be at a capital 'F' frame or a managed frame unlike the TargetPointer overload which only works at capital 'F' frames. +* This API handles the special ReportInteropMD case which happens under the following conditions + 1. The dataFrame is at an `InlinedCallFrame` + 2. The dataFrame is in a `SW_SKIPPED_FRAME` state + 3. The InlinedCallFrame's return address is managed code + 4. The InlinedCallFrame's return address method has a MDContext arg + + In this case, we report the actual interop MethodDesc. A pointer to the MethodDesc immediately follows the InlinedCallFrame in memory. This API is implemented as follows: -1. Try to get the current frame address with `GetFrameAddress`. If the address is not null, return `GetMethodDescPtr()`. - - Special case: For `InlinedCallFrame` at a `SW_SKIPPED_FRAME` position, if the frame's MethodDesc is an IL stub (`DynamicMethodDesc`), report the interop target MethodDesc instead. This ensures P/Invoke transitions show the target method rather than the internal stub. -2. Check if the current context IP is a managed context using the ExecutionManager contract. If it is a managed context, use the ExecutionManager contract to find the related MethodDesc and return the pointer to it. +1. Try to get the current frame address `framePtr` with `GetFrameAddress`. +2. If the address is not null, compute `reportInteropMD` as listed above. Otherwise skip to step 5. +3. If `reportInteropMD`, dereference the pointer immediately following the InlinedCallFrame and return that value. +4. If `!reportInteropMD`, return `GetMethodDescPtr(framePtr)`. +5. Check if the current context IP is a managed context using the ExecutionManager contract. If it is a managed context, use the ExecutionManager context to find the related MethodDesc and return the pointer to it. ```csharp TargetPointer GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHandle) ``` diff --git a/src/coreclr/inc/patchpointinfo.h b/src/coreclr/inc/patchpointinfo.h index 483c5fb83d90f3..6f030e714c39dc 100644 --- a/src/coreclr/inc/patchpointinfo.h +++ b/src/coreclr/inc/patchpointinfo.h @@ -219,7 +219,7 @@ struct PatchpointInfo } private: - template friend struct cdac_data; + friend struct cdac_data; enum { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs index 4206b23d3b9f97..0b33f1b932da7b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs @@ -20,12 +20,12 @@ public record struct ThreadStoreCounts( [Flags] public enum ThreadState { - Unknown = 0x00000000, - Hijacked = 0x00000080, // Return address has been hijacked - Background = 0x00000200, // Thread is a background thread - Unstarted = 0x00000400, // Thread has never been started - Dead = 0x00000800, // Thread is dead - ThreadPoolWorker = 0x01000000, // Thread is a thread pool worker thread + Unknown = 0x00000000, + Hijacked = 0x00000080, // Return address has been hijacked + Background = 0x00000200, // Thread is a background thread + Unstarted = 0x00000400, // Thread has never been started + Dead = 0x00000800, // Thread is dead + ThreadPoolWorker = 0x01000000, // Thread is a thread pool worker thread } public record struct ThreadData( diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/BitStreamReader.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/BitStreamReader.cs deleted file mode 100644 index 02ef9ff7f12cf7..00000000000000 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/BitStreamReader.cs +++ /dev/null @@ -1,281 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace Microsoft.Diagnostics.DataContractReader.Contracts; - -/// -/// Managed implementation of the native BitStreamReader class for reading compressed GC info. -/// This class provides methods to read variable-length bit sequences from a memory buffer -/// accessed through the Target abstraction. -/// -internal struct BitStreamReader -{ - private readonly int _bitsPerSize; - - private readonly Target _target; - private readonly TargetPointer _buffer; - private readonly int _initialRelPos; - - private TargetPointer _current; - private int _relPos; - private nuint _currentValue; - - /// - /// Initializes a new BitStreamReader starting at the specified buffer address. - /// - /// The target process to read from - /// Pointer to the start of the bit stream data - public BitStreamReader(Target target, TargetPointer buffer) - { - ArgumentNullException.ThrowIfNull(target); - - if (buffer == TargetPointer.Null) - throw new ArgumentException("Buffer pointer cannot be null", nameof(buffer)); - - _target = target; - _bitsPerSize = target.PointerSize * 8; - - // Align buffer to pointer size boundary (similar to native implementation) - nuint pointerMask = (nuint)target.PointerSize - 1; - TargetPointer alignedBuffer = new(buffer.Value & ~(ulong)pointerMask); - - _buffer = alignedBuffer; - _current = alignedBuffer; - _initialRelPos = (int)((buffer.Value % (ulong)target.PointerSize) * 8); - _relPos = _initialRelPos; - - // Prefetch the first word and position it correctly - _currentValue = ReadPointerSizedValue(_current); - _currentValue >>= _relPos; - } - - /// - /// Copy constructor - /// - /// The BitStreamReader to copy from - public BitStreamReader(BitStreamReader other) - { - _target = other._target; - _bitsPerSize = other._bitsPerSize; - _buffer = other._buffer; - _initialRelPos = other._initialRelPos; - _current = other._current; - _relPos = other._relPos; - _currentValue = other._currentValue; - } - - /// - /// Reads the specified number of bits from the stream. - /// - /// Number of bits to read (1 to pointer size in bits) - /// The value read from the stream - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public nuint Read(int numBits) - { - Debug.Assert(numBits > 0 && numBits <= _bitsPerSize); - - nuint result = _currentValue; - _currentValue >>= numBits; - int newRelPos = _relPos + numBits; - - if (newRelPos > _bitsPerSize) - { - // Need to read from next word - _current = new TargetPointer(_current.Value + (ulong)_target.PointerSize); - nuint nextValue = ReadPointerSizedValue(_current); - newRelPos -= _bitsPerSize; - nuint extraBits = nextValue << (numBits - newRelPos); - result |= extraBits; - _currentValue = nextValue >> newRelPos; - } - - _relPos = newRelPos; - - // Mask to get only the requested bits - nuint mask = (nuint.MaxValue >> (_bitsPerSize - numBits)); - result &= mask; - - return result; - } - - /// - /// Reads a single bit from the stream (optimized version). - /// - /// The bit value (0 or 1) - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public nuint ReadOneFast() - { - // Check if we need to fetch the next word - if (_relPos == _bitsPerSize) - { - _current = new TargetPointer(_current.Value + (ulong)_target.PointerSize); - _currentValue = ReadPointerSizedValue(_current); - _relPos = 0; - } - - _relPos++; - nuint result = _currentValue & 1; - _currentValue >>= 1; - - return result; - } - - /// - /// Gets the current position in bits from the start of the stream. - /// - /// Current bit position - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public nuint GetCurrentPos() - { - long wordOffset = ((long)_current.Value - (long)_buffer.Value) / _target.PointerSize; - return (nuint)(wordOffset * _bitsPerSize + _relPos - _initialRelPos); - } - - /// - /// Sets the current position in the stream to the specified bit offset. - /// - /// Target bit position from the start of the stream - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetCurrentPos(nuint pos) - { - nuint adjPos = pos + (nuint)_initialRelPos; - nuint wordOffset = adjPos / (nuint)_bitsPerSize; - int newRelPos = (int)(adjPos % (nuint)_bitsPerSize); - - _current = new TargetPointer(_buffer.Value + wordOffset * (ulong)_target.PointerSize); - _relPos = newRelPos; - - // Prefetch the new word and position it correctly - _currentValue = ReadPointerSizedValue(_current) >> newRelPos; - } - - /// - /// Skips the specified number of bits in the stream. - /// - /// Number of bits to skip (can be negative) - public void Skip(nint numBitsToSkip) - { - nuint newPos = (nuint)((nint)GetCurrentPos() + numBitsToSkip); - - nuint adjPos = newPos + (nuint)_initialRelPos; - nuint wordOffset = adjPos / (nuint)_bitsPerSize; - int newRelPos = (int)(adjPos % (nuint)_bitsPerSize); - - _current = new TargetPointer(_buffer.Value + wordOffset * (ulong)_target.PointerSize); - _relPos = newRelPos; - - // Skipping ahead may go to a position at the edge-exclusive - // end of the stream. The location may have no more data. - // We will not prefetch on word boundary - in case - // the next word is in an unreadable page. - if (_relPos == 0) - { - _current = new TargetPointer(_current.Value - (ulong)_target.PointerSize); - _relPos = _bitsPerSize; - _currentValue = 0; - } - else - { - _currentValue = ReadPointerSizedValue(_current) >> _relPos; - } - } - - /// - /// Decodes a variable-length unsigned integer. - /// - /// Base value for encoding (number of bits per chunk) - /// The decoded unsigned integer - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public nuint DecodeVarLengthUnsigned(int baseValue) - { - Debug.Assert(baseValue > 0 && baseValue < _bitsPerSize); - - nuint result = Read(baseValue + 1); - if ((result & ((nuint)1 << baseValue)) != 0) - { - result ^= DecodeVarLengthUnsignedMore(baseValue); - } - - return result; - } - - /// - /// Helper method for decoding variable-length unsigned integers with extension bits. - /// - /// Base value for encoding - /// The additional bits for the decoded value - private nuint DecodeVarLengthUnsignedMore(int baseValue) - { - Debug.Assert(baseValue > 0 && baseValue < _bitsPerSize); - - nuint numEncodings = (nuint)1 << baseValue; - nuint result = numEncodings; - - for (int shift = baseValue; ; shift += baseValue) - { - Debug.Assert(shift + baseValue <= _bitsPerSize); - - nuint currentChunk = Read(baseValue + 1); - result ^= (currentChunk & (numEncodings - 1)) << shift; - - if ((currentChunk & numEncodings) == 0) - { - // Extension bit is not set, we're done - return result; - } - } - } - - /// - /// Decodes a variable-length signed integer. - /// - /// Base value for encoding (number of bits per chunk) - /// The decoded signed integer - public nint DecodeVarLengthSigned(int baseValue) - { - Debug.Assert(baseValue > 0 && baseValue < _bitsPerSize); - - nuint numEncodings = (nuint)1 << baseValue; - nint result = 0; - - for (int shift = 0; ; shift += baseValue) - { - Debug.Assert(shift + baseValue <= _bitsPerSize); - - nuint currentChunk = Read(baseValue + 1); - result |= (nint)(currentChunk & (numEncodings - 1)) << shift; - - if ((currentChunk & numEncodings) == 0) - { - // Extension bit is not set, sign-extend and we're done - int signBits = _bitsPerSize - (shift + baseValue); - result <<= signBits; - result >>= signBits; // Arithmetic right shift for sign extension - return result; - } - } - } - - /// - /// Reads a pointer-sized value from the target at the specified address. - /// - /// Address to read from - /// The value read as nuint - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private nuint ReadPointerSizedValue(TargetPointer address) - { - if (_target.PointerSize == 4) - { - return _target.Read(address); - } - else - { - Debug.Assert(_target.PointerSize == 8); - return (nuint)_target.Read(address); - } - } -} From f71402c312deb2943c526c8188032e90e87ccdd6 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 11 Mar 2026 17:52:20 -0400 Subject: [PATCH 28/30] Remove unused m_lastReportedFunclet from ExInfo The m_lastReportedFunclet field was added to ExInfo but is never written by the runtime, making it always zero-initialized. The cDAC code that reads it can never trigger. Remove the field from ExInfo, the data descriptor entry, and the managed LastReportedFuncletInfo data class. Mark the Filter code path as explicitly unreachable with a TODO for when runtime support is added. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../vm/datadescriptor/datadescriptor.inc | 6 ----- src/coreclr/vm/exinfo.h | 3 --- .../DataType.cs | 1 - ...ecutionManagerCore.ReadyToRunJitManager.cs | 10 -------- .../ExecutionManager/ExecutionManagerCore.cs | 11 ++------- .../Contracts/GCInfo/GCInfoDecoder.cs | 24 +++++++------------ .../Contracts/GCInfo/GCInfo_1.cs | 1 - .../Contracts/GCInfo/IGCInfoDecoder.cs | 18 ++++++++++++-- .../Contracts/StackWalk/GC/GcScanner.cs | 11 +-------- .../Contracts/StackWalk/StackWalk_1.cs | 18 +++++++------- .../Data/ExceptionInfo.cs | 3 --- .../Data/LastReportedFuncletInfo.cs | 19 --------------- 12 files changed, 36 insertions(+), 89 deletions(-) delete mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/LastReportedFuncletInfo.cs diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index d493020e42953b..1b8a0b3d7a5364 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -144,14 +144,8 @@ CDAC_TYPE_FIELD(ExceptionInfo, /*uint8*/, PassNumber, offsetof(ExInfo, m_passNum CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, CSFEHClause, offsetof(ExInfo, m_csfEHClause)) CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, CSFEnclosingClause, offsetof(ExInfo, m_csfEnclosingClause)) CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, CallerOfActualHandlerFrame, offsetof(ExInfo, m_sfCallerOfActualHandlerFrame)) -CDAC_TYPE_FIELD(ExceptionInfo, /*LastReportedFuncletInfo*/, LastReportedFuncletInfo, offsetof(ExInfo, m_lastReportedFunclet)) CDAC_TYPE_END(ExceptionInfo) -CDAC_TYPE_BEGIN(LastReportedFuncletInfo) -CDAC_TYPE_INDETERMINATE(LastReportedFuncletInfo) -CDAC_TYPE_FIELD(LastReportedFuncletInfo, /*PCODE*/, IP, offsetof(LastReportedFuncletInfo, IP)) -CDAC_TYPE_END(LastReportedFuncletInfo) - CDAC_TYPE_BEGIN(GCHandle) CDAC_TYPE_SIZE(sizeof(OBJECTHANDLE)) CDAC_TYPE_END(GCHandle) diff --git a/src/coreclr/vm/exinfo.h b/src/coreclr/vm/exinfo.h index 29551cd4e8e2f0..3b5fb4904f376c 100644 --- a/src/coreclr/vm/exinfo.h +++ b/src/coreclr/vm/exinfo.h @@ -194,9 +194,6 @@ struct ExInfo int m_longJmpReturnValue; #endif - // Last reported funclet info for cDAC stack walking - LastReportedFuncletInfo m_lastReportedFunclet; - #if defined(TARGET_UNIX) void TakeExceptionPointersOwnership(PAL_SEHException* ex); #endif // TARGET_UNIX 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 a9a975aa1f48db..b00806b0d88883 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -37,7 +37,6 @@ public enum DataType ExceptionLookupTableEntry, EEILException, R2RExceptionClause, - LastReportedFuncletInfo, RuntimeThreadLocals, IdDispenser, Module, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs index 69441acdc49494..c7f19c670db345 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.ReadyToRunJitManager.cs @@ -180,16 +180,6 @@ private uint GetR2RGCInfoVersion(Data.ReadyToRunInfo r2rInfo) }; } - private uint GetUnwindDataSize() - { - RuntimeInfoArchitecture arch = Target.Contracts.RuntimeInfo.GetTargetArchitecture(); - return arch switch - { - RuntimeInfoArchitecture.X86 => sizeof(uint), - _ => throw new NotSupportedException($"GetUnwindDataSize not supported for architecture: {arch}") - }; - } - public override IEnumerable GetEHClauses(RangeSection rangeSection, TargetCodePointer jittedCodeAddress) { // ReadyToRunJitManager::GetEHClauses 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 d47b53114ab4f6..7f75e11dd8b5e7 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 @@ -149,6 +149,7 @@ internal static RangeSection Find(Target target, Data.RangeSectionMap topRangeSe private sealed class EHClause { + // ECMA-335 Partition II, Section 25.4.6 — Exception handling clause flags. public enum CorExceptionFlag : uint { COR_ILEXCEPTION_CLAUSE_NONE = 0x0, @@ -340,16 +341,8 @@ bool IExecutionManager.IsFilterFunclet(CodeBlockHandle codeInfoHandle) if (!eman.IsFunclet(codeInfoHandle)) return false; - TargetPointer codeAddress = info.StartAddress.Value + info.RelativeOffset.Value; TargetPointer funcletStartAddress = eman.GetFuncletStartAddress(codeInfoHandle).AsTargetPointer; - - uint relativeOffsetInFunclet = (uint)(codeAddress - funcletStartAddress); - Debug.Assert(eman.GetRelativeOffset(codeInfoHandle).Value >= relativeOffsetInFunclet); - - uint funcletStartOffset = (uint)(eman.GetRelativeOffset(codeInfoHandle).Value - relativeOffsetInFunclet); - // can we calculate this much more simply?? - uint funcletStartOffset2 = (uint)(funcletStartAddress - info.StartAddress); - Debug.Assert(funcletStartOffset == funcletStartOffset2); + uint funcletStartOffset = (uint)(funcletStartAddress - info.StartAddress); IEnumerable ehClauses = jitManager.GetEHClauses(range, codeInfoHandle.Address.Value); foreach (EHClause ehClause in ehClauses) 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 e94eb65b49031b..56d530321ec151 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 @@ -537,10 +537,10 @@ public uint StackBaseRegister bool IGCInfoDecoder.EnumerateLiveSlots( uint instructionOffset, - uint inputFlags, + CodeManagerFlags flags, LiveSlotCallback reportSlot) { - return EnumerateLiveSlots(instructionOffset, inputFlags, + return EnumerateLiveSlots(instructionOffset, flags, (uint slotIndex, GcSlotDesc slot, uint gcFlags) => { reportSlot(slot.IsRegister, slot.RegisterNumber, slot.SpOffset, (uint)slot.Base, gcFlags); @@ -552,32 +552,26 @@ bool IGCInfoDecoder.EnumerateLiveSlots( /// This is the managed equivalent of the native GcInfoDecoder::EnumerateLiveSlots. /// /// The current instruction offset (relative to method start). - /// CodeManagerFlags controlling reporting behavior. + /// CodeManagerFlags controlling reporting behavior. /// Called for each live slot with (slotIndex, slotDesc, gcFlags). /// gcFlags contains GC_SLOT_INTERIOR/GC_SLOT_PINNED from the slot descriptor. /// True if enumeration succeeded. public bool EnumerateLiveSlots( uint instructionOffset, - uint inputFlags, + CodeManagerFlags flags, Action reportSlot) { - const uint ActiveStackFrame = 0x1; - const uint ParentOfFuncletStackFrame = 0x40; - const uint NoReportUntracked = 0x80; - const uint ExecutionAborted = 0x2; - const uint ReportFPBasedSlotsOnly = 0x200; - EnsureDecodedTo(DecodePoints.SlotTable); - bool executionAborted = (inputFlags & ExecutionAborted) != 0; - bool reportScratchSlots = (inputFlags & ActiveStackFrame) != 0; - bool reportFpBasedSlotsOnly = (inputFlags & ReportFPBasedSlotsOnly) != 0; + bool executionAborted = flags.HasFlag(CodeManagerFlags.ExecutionAborted); + bool reportScratchSlots = flags.HasFlag(CodeManagerFlags.ActiveStackFrame); + bool reportFpBasedSlotsOnly = flags.HasFlag(CodeManagerFlags.ReportFPBasedSlotsOnly); _reportScratchSlots = reportScratchSlots; _reportFpBasedSlotsOnly = reportFpBasedSlotsOnly; // WantsReportOnlyLeaf is always true for non-legacy formats - if ((inputFlags & ParentOfFuncletStackFrame) != 0) + if (flags.HasFlag(CodeManagerFlags.ParentOfFuncletStackFrame)) return true; uint numTracked = NumTrackedSlots; @@ -829,7 +823,7 @@ public bool EnumerateLiveSlots( } ReportUntracked: - if (_numUntrackedSlots > 0 && (inputFlags & (ParentOfFuncletStackFrame | NoReportUntracked)) == 0) + if (_numUntrackedSlots > 0 && (flags & (CodeManagerFlags.ParentOfFuncletStackFrame | CodeManagerFlags.NoReportUntracked)) == 0) { for (uint slotIndex = numTracked; slotIndex < _numSlots; slotIndex++) ReportSlot(slotIndex, 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 397fba29665955..f34292572a936e 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 @@ -6,7 +6,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts; - internal class GCInfo_1 : IGCInfo where TTraits : IGCInfoTraits { private readonly Target _target; 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 df640c205d9332..046ab59dd2ee18 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 @@ -5,6 +5,20 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; +/// +/// Flags controlling GC reference reporting behavior. +/// These match the native ICodeManager flags in eetwain.h. +/// +[Flags] +internal enum CodeManagerFlags : uint +{ + ActiveStackFrame = 0x1, + ExecutionAborted = 0x2, + ParentOfFuncletStackFrame = 0x40, + NoReportUntracked = 0x80, + ReportFPBasedSlotsOnly = 0x200, +} + internal interface IGCInfoDecoder : IGCInfoHandle { uint GetCodeLength(); @@ -14,11 +28,11 @@ internal interface IGCInfoDecoder : IGCInfoHandle /// Enumerates all live GC slots at the given instruction offset. /// /// Relative offset from method start. - /// CodeManagerFlags controlling reporting. + /// CodeManagerFlags controlling reporting. /// Callback: (isRegister, registerNumber, spOffset, spBase, gcFlags). bool EnumerateLiveSlots( uint instructionOffset, - uint inputFlags, + CodeManagerFlags flags, LiveSlotCallback reportSlot) => throw new NotImplementedException(); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs index eda68ea277fda9..1db50b383fd977 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -8,15 +8,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; internal class GcScanner { - public enum CodeManagerFlags : uint - { - ActiveStackFrame = 0x1, - ExecutionAborted = 0x2, - ParentOfFuncletStackFrame = 0x40, - NoReportUntracked = 0x80, - ReportFPBasedSlotsOnly = 0x200, - } - private readonly Target _target; private readonly IExecutionManager _eman; private readonly IGCInfo _gcInfo; @@ -52,7 +43,7 @@ public bool EnumGcRefs( return decoder.EnumerateLiveSlots( (uint)relativeOffset.Value, - (uint)flags, + flags, (bool isRegister, uint registerNumber, int spOffset, uint spBase, uint gcFlags) => { GcScanFlags scanFlags = GcScanFlags.None; 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 bc7d0cf05f2c89..3ebadf6a44c4ef 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 @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Collections.Generic; using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; +using Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; using Microsoft.Diagnostics.DataContractReader.Data; using System.Linq; @@ -180,8 +181,8 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre // IsActiveFrame was computed during CreateStackWalk, matching native // CrawlFrame::IsActiveFunc() semantics. Active frames report scratch // registers; non-active frames skip them. - GcScanner.CodeManagerFlags codeManagerFlags = gcFrame.Frame.IsActiveFrame - ? GcScanner.CodeManagerFlags.ActiveStackFrame + CodeManagerFlags codeManagerFlags = gcFrame.Frame.IsActiveFrame + ? CodeManagerFlags.ActiveStackFrame : 0; GcScanner gcScanner = new(_target); @@ -286,18 +287,15 @@ private IEnumerable Filter(IEnumerable handle if (!movedPastFirstExInfo) { Data.ExceptionInfo exInfo = _target.ProcessedData.GetOrAdd(pExInfo); + // TODO: The native StackFrameIterator::Filter checks pExInfo->m_lastReportedFunclet.IP + // to handle the case where a finally funclet was reported in a previous GC run. + // This requires runtime support to persist LastReportedFuncletInfo on ExInfo, + // which is not yet implemented. Until then this block is unreachable. if (exInfo.PassNumber == 2 && exInfo.CSFEnclosingClause != TargetPointer.Null && funcletParentStackFrame == TargetPointer.Null && - exInfo.LastReportedFuncletInfo is not null && - exInfo.LastReportedFuncletInfo.IP != TargetCodePointer.Null) + false) // TODO: check lastReportedFunclet.IP != 0 when runtime support is added { - // We are in the 2nd pass and we have already called an exceptionally called - // finally funclet and reported that to GC in a previous GC run. But we have - // not seen any funclet on the call stack yet. - // Simulate that we have actualy seen a finally funclet during this pass and - // that it didn't report GC references to ensure that the references will be - // reported by the parent correctly. funcletParentStackFrame = exInfo.CSFEnclosingClause; parentStackFrame = exInfo.CSFEnclosingClause; processNonFilterFunclet = true; 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 9865c2dd6fc011..7be59deadf7a59 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 @@ -31,8 +31,6 @@ public ExceptionInfo(Target target, TargetPointer address) CSFEnclosingClause = target.ReadPointer(address + (ulong)type.Fields[nameof(CSFEnclosingClause)].Offset); if (type.Fields.ContainsKey(nameof(CallerOfActualHandlerFrame))) CallerOfActualHandlerFrame = target.ReadPointer(address + (ulong)type.Fields[nameof(CallerOfActualHandlerFrame)].Offset); - if (type.Fields.ContainsKey(nameof(LastReportedFuncletInfo))) - LastReportedFuncletInfo = target.ProcessedData.GetOrAdd(address + (ulong)type.Fields[nameof(LastReportedFuncletInfo)].Offset); } public TargetPointer PreviousNestedInfo { get; } @@ -45,5 +43,4 @@ public ExceptionInfo(Target target, TargetPointer address) public TargetPointer CSFEHClause { get; } public TargetPointer CSFEnclosingClause { get; } public TargetPointer CallerOfActualHandlerFrame { get; } - public LastReportedFuncletInfo? LastReportedFuncletInfo { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/LastReportedFuncletInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/LastReportedFuncletInfo.cs deleted file mode 100644 index df04c08f874288..00000000000000 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/LastReportedFuncletInfo.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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 sealed class LastReportedFuncletInfo : IData -{ - static LastReportedFuncletInfo IData.Create(Target target, TargetPointer address) - => new LastReportedFuncletInfo(target, address); - - public LastReportedFuncletInfo(Target target, TargetPointer address) - { - Target.TypeInfo type = target.GetTypeInfo(DataType.LastReportedFuncletInfo); - - IP = target.ReadCodePointer(address + (ulong)type.Fields[nameof(IP)].Offset); - } - - public TargetCodePointer IP { get; } -} From 6fc9f4eb59ad4c85b97fccdbf5ef0e30130abae7 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Thu, 12 Mar 2026 14:27:35 -0400 Subject: [PATCH 29/30] remove guards that were not required --- .../Data/ExceptionInfo.cs | 22 ++++++------------- .../Data/ReadyToRunInfo.cs | 3 +-- .../Data/RealCodeHeader.cs | 3 +-- 3 files changed, 9 insertions(+), 19 deletions(-) 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 7be59deadf7a59..8f2470d6e71996 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 @@ -14,23 +14,15 @@ public ExceptionInfo(Target target, TargetPointer address) PreviousNestedInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(PreviousNestedInfo)].Offset); ThrownObjectHandle = target.ReadPointer(address + (ulong)type.Fields[nameof(ThrownObjectHandle)].Offset); - if (type.Fields.ContainsKey(nameof(ExceptionFlags))) - ExceptionFlags = target.Read(address + (ulong)type.Fields[nameof(ExceptionFlags)].Offset); - if (type.Fields.ContainsKey(nameof(StackLowBound))) - StackLowBound = target.ReadPointer(address + (ulong)type.Fields[nameof(StackLowBound)].Offset); - if (type.Fields.ContainsKey(nameof(StackHighBound))) - StackHighBound = target.ReadPointer(address + (ulong)type.Fields[nameof(StackHighBound)].Offset); + ExceptionFlags = target.Read(address + (ulong)type.Fields[nameof(ExceptionFlags)].Offset); + StackLowBound = target.ReadPointer(address + (ulong)type.Fields[nameof(StackLowBound)].Offset); + StackHighBound = target.ReadPointer(address + (ulong)type.Fields[nameof(StackHighBound)].Offset); if (type.Fields.ContainsKey(nameof(ExceptionWatsonBucketTrackerBuckets))) ExceptionWatsonBucketTrackerBuckets = target.ReadPointer(address + (ulong)type.Fields[nameof(ExceptionWatsonBucketTrackerBuckets)].Offset); - - if (type.Fields.ContainsKey(nameof(PassNumber))) - PassNumber = target.Read(address + (ulong)type.Fields[nameof(PassNumber)].Offset); - if (type.Fields.ContainsKey(nameof(CSFEHClause))) - CSFEHClause = target.ReadPointer(address + (ulong)type.Fields[nameof(CSFEHClause)].Offset); - if (type.Fields.ContainsKey(nameof(CSFEnclosingClause))) - CSFEnclosingClause = target.ReadPointer(address + (ulong)type.Fields[nameof(CSFEnclosingClause)].Offset); - if (type.Fields.ContainsKey(nameof(CallerOfActualHandlerFrame))) - CallerOfActualHandlerFrame = target.ReadPointer(address + (ulong)type.Fields[nameof(CallerOfActualHandlerFrame)].Offset); + PassNumber = target.Read(address + (ulong)type.Fields[nameof(PassNumber)].Offset); + CSFEHClause = target.ReadPointer(address + (ulong)type.Fields[nameof(CSFEHClause)].Offset); + CSFEnclosingClause = target.ReadPointer(address + (ulong)type.Fields[nameof(CSFEnclosingClause)].Offset); + CallerOfActualHandlerFrame = target.ReadPointer(address + (ulong)type.Fields[nameof(CallerOfActualHandlerFrame)].Offset); } public TargetPointer PreviousNestedInfo { 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 74c6525d06832f..843bc82f7f1328 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 @@ -31,8 +31,7 @@ public ReadyToRunInfo(Target target, TargetPointer address) DelayLoadMethodCallThunks = target.ReadPointer(address + (ulong)type.Fields[nameof(DelayLoadMethodCallThunks)].Offset); DebugInfoSection = target.ReadPointer(address + (ulong)type.Fields[nameof(DebugInfoSection)].Offset); - if (type.Fields.ContainsKey(nameof(ExceptionInfoSection))) - ExceptionInfoSection = target.ReadPointer(address + (ulong)type.Fields[nameof(ExceptionInfoSection)].Offset); + ExceptionInfoSection = target.ReadPointer(address + (ulong)type.Fields[nameof(ExceptionInfoSection)].Offset); // Map is from the composite info pointer (set to itself for non-multi-assembly composite images) EntryPointToMethodDescMap = CompositeInfo + (ulong)type.Fields[nameof(EntryPointToMethodDescMap)].Offset; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RealCodeHeader.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RealCodeHeader.cs index 5258f81dd44dea..9718f0ab4fec6f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RealCodeHeader.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RealCodeHeader.cs @@ -13,8 +13,7 @@ public RealCodeHeader(Target target, TargetPointer address) Target.TypeInfo type = target.GetTypeInfo(DataType.RealCodeHeader); MethodDesc = target.ReadPointer(address + (ulong)type.Fields[nameof(MethodDesc)].Offset); DebugInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(DebugInfo)].Offset); - if (type.Fields.ContainsKey(nameof(EHInfo))) - EHInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(EHInfo)].Offset); + EHInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(EHInfo)].Offset); GCInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(GCInfo)].Offset); NumUnwindInfos = target.Read(address + (ulong)type.Fields[nameof(NumUnwindInfos)].Offset); UnwindInfos = address + (ulong)type.Fields[nameof(UnwindInfos)].Offset; From c92125898248c18cbd42cd7c80f6f01f1893f995 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Thu, 12 Mar 2026 14:27:45 -0400 Subject: [PATCH 30/30] remove stale comment --- .../Contracts/StackWalk/GC/GcScanContext.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs index 2da36e0827710c..4c6b157ba51d3e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs @@ -32,9 +32,6 @@ public void UpdateScanContext(TargetPointer sp, TargetPointer ip, TargetPointer public void GCEnumCallback(TargetPointer pObject, GcScanFlags flags, GcScanSlotLocation loc) { - // Yuck. The GcInfoDecoder reports a local pointer for registers (as it's reading out of the REGDISPLAY - // in the stack walk), and it reports a TADDR for stack locations. This is architecturally difficulty - // to fix, so we are leaving it for now. TargetPointer addr; TargetPointer obj;