diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index 12869acd36f828..a7bd29440c7ad7 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 @@ -111,7 +114,7 @@ Data descriptors used: | `RealCodeHeader` | `UnwindInfos` | Start address of Unwind Infos | | `RealCodeHeader` | `DebugInfo` | Pointer to the DebugInfo | | `RealCodeHeader` | `GCInfo` | Pointer to the GCInfo encoding | -| `RealCodeHeader` | `JitEHInfo` | Pointer to the `EE_ILEXCEPTION` containing exception clauses | +| `RealCodeHeader` | `EHInfo` | Pointer to the `EE_ILEXCEPTION` containing exception clauses | | `Module` | `ReadyToRunInfo` | Pointer to the `ReadyToRunInfo` for the module | | `ReadyToRunInfo` | `ReadyToRunHeader` | Pointer to the ReadyToRunHeader | | `ReadyToRunInfo` | `CompositeInfo` | Pointer to composite R2R info - or itself for non-composite | @@ -124,6 +127,7 @@ Data descriptors used: | `ReadyToRunInfo` | `EntryPointToMethodDescMap` | `HashMap` of entry point addresses to `MethodDesc` pointers | | `ReadyToRunInfo` | `LoadedImageBase` | Base address of the loaded R2R image | | `ReadyToRunInfo` | `Composite` | Pointer to the `ReadyToRunCoreInfo` used for section lookup | +| `ReadyToRunInfo` | `ExceptionInfoSection` | Pointer to the `ImageDataDirectory` for R2R exception info section | | `ReadyToRunHeader` | `MajorVersion` | ReadyToRun major version | | `ReadyToRunHeader` | `MinorVersion` | ReadyToRun minor version | | `ImageDataDirectory` | `VirtualAddress` | Virtual address of the image data directory | @@ -444,12 +448,14 @@ For R2R images, `hasFlagByte` is always `false`. There are two distinct clause data types. JIT-compiled code uses `EEExceptionClause` (corresponding to `EE_ILEXCEPTION_CLAUSE`), which has a pointer-sized union field that can hold a `TypeHandle`, `ClassToken`, or `FilterOffset`. ReadyToRun code uses `R2RExceptionClause` (corresponding to `CORCOMPILE_EXCEPTION_CLAUSE`), which has a 4-byte union field containing only `ClassToken` or `FilterOffset`. Both types share the same common fields: `Flags`, `TryStartPC`, `TryEndPC`, `HandlerStartPC`, and `HandlerEndPC`. -* For jitted code (`EEJitManager`), the exception clauses are stored in an `EE_ILEXCEPTION` structure pointed to by the `JitEHInfo` field of the `RealCodeHeader`. The `EEILException` data type wraps this structure: its `Clauses` field gives the address of the first clause (at `offsetof(EE_ILEXCEPTION, Clauses)`, skipping the 4-byte `COR_ILMETHOD_SECT_FAT` header). The number of clauses is stored as a pointer-sized integer immediately before the `EE_ILEXCEPTION` structure (at `JitEHInfo.Address - sizeof(pointer)`). The clause array is strided using the size of `EEExceptionClause`. +* For jitted code (`EEJitManager`), the exception clauses are stored in an `EE_ILEXCEPTION` structure pointed to by the `EHInfo` field of the `RealCodeHeader`. The `EEILException` data type wraps this structure: its `Clauses` field gives the address of the first clause (at `offsetof(EE_ILEXCEPTION, Clauses)`, skipping the 4-byte `COR_ILMETHOD_SECT_FAT` header). The number of clauses is stored as a pointer-sized integer immediately before the `EE_ILEXCEPTION` structure (at `EHInfo.Address - sizeof(pointer)`). The clause array is strided using the size of `EEExceptionClause`. * For R2R code (`ReadyToRunJitManager`), exception clause data is found via the `ExceptionInfo` section (section type 104) of the R2R image. The section is located by traversing `ReadyToRunInfo::Composite` to reach the `ReadyToRunCoreInfo`, then reading its `Header` pointer to the `ReadyToRunCoreHeader`, and iterating through the inline `ReadyToRunSection` array that immediately follows the header. The `ExceptionInfo` section contains an `ExceptionLookupTableEntry` array, where each entry maps a `MethodStartRVA` to an `ExceptionInfoRVA`. A binary search (falling back to linear scan for small ranges) finds the entry matching the method's RVA. The exception clauses span from that entry's `ExceptionInfoRVA` to the next entry's `ExceptionInfoRVA`, both offset from the image base. The clause array is strided using the size of `R2RExceptionClause`. 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 bc83127e4df2c8..51a55df2b63165 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -57,6 +57,9 @@ This contract depends on the following descriptors: | `StubDispatchFrame` | `MethodDescPtr` | Pointer to Frame's method desc | | `StubDispatchFrame` | `RepresentativeMTPtr` | Pointer to Frame's method table pointer | | `StubDispatchFrame` | `RepresentativeSlot` | Frame's method table slot | +| `StubDispatchFrame` | `GCRefMap` | Cached pointer to GC reference map blob for caller stack promotion | +| `ExternalMethodFrame` | `GCRefMap` | Cached pointer to GC reference map blob for caller stack promotion | +| `DynamicHelperFrame` | `DynamicHelperFrameFlags` | Flags indicating which argument registers contain GC references | | `TransitionBlock` | `ReturnAddress` | Return address associated with the TransitionBlock | | `TransitionBlock` | `CalleeSavedRegisters` | Platform specific CalleeSavedRegisters struct associated with the TransitionBlock | | `TransitionBlock` (arm) | `ArgumentRegisters` | ARM specific `ArgumentRegisters` struct | @@ -74,11 +77,27 @@ 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 | +| `TransitionBlockOffsetOfFirstGCRefMapSlot` | `uint32` | Byte offset within TransitionBlock where GCRefMap slot enumeration begins. ARM64: RetBuffArgReg offset; others: ArgumentRegisters offset. | +| `TransitionBlockOffsetOfArgumentRegisters` | `uint32` | Byte offset of the ArgumentRegisters within the TransitionBlock | +| `TransitionBlockOffsetOfArgs` | `uint32` | Byte offset of stack arguments (first arg after registers) = `sizeof(TransitionBlock)` | + +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 | @@ -369,11 +388,11 @@ TargetPointer GetMethodDescPtr(TargetPointer framePtr) 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 implemeted as follows: +This API is implemented 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)`. +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/eng/Subsets.props b/eng/Subsets.props index 21db6bbdcb8609..f4ea63b49be08f 100644 --- a/eng/Subsets.props +++ b/eng/Subsets.props @@ -254,6 +254,7 @@ + @@ -528,6 +529,10 @@ + + + + diff --git a/src/coreclr/inc/clrconfigvalues.h b/src/coreclr/inc/clrconfigvalues.h index fd64be3df1b59f..e6419460df6167 100644 --- a/src/coreclr/inc/clrconfigvalues.h +++ b/src/coreclr/inc/clrconfigvalues.h @@ -286,7 +286,7 @@ RETAIL_CONFIG_DWORD_INFO(INTERNAL_JitEnableNoWayAssert, W("JitEnableNoWayAssert" RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_JitFramed, W("JitFramed"), 0, "Forces EBP frames") CONFIG_DWORD_INFO(INTERNAL_JitThrowOnAssertionFailure, W("JitThrowOnAssertionFailure"), 0, "Throw managed exception on assertion failures during JIT instead of failfast") -CONFIG_DWORD_INFO(INTERNAL_JitGCStress, W("JitGCStress"), 0, "GC stress mode for jit") +CONFIG_DWORD_INFO(INTERNAL_JitGCStress, W("JitGCStress"), 0, "cDAC stress mode for jit") CONFIG_DWORD_INFO(INTERNAL_JitHeartbeat, W("JitHeartbeat"), 0, "") RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_JITMinOpts, W("JITMinOpts"), 0, "Forces MinOpts") @@ -747,6 +747,10 @@ CONFIG_STRING_INFO(INTERNAL_PerfTypesToLog, W("PerfTypesToLog"), "Log facility L CONFIG_STRING_INFO(INTERNAL_PrestubGC, W("PrestubGC"), "") CONFIG_STRING_INFO(INTERNAL_PrestubHalt, W("PrestubHalt"), "") RETAIL_CONFIG_STRING_INFO(EXTERNAL_RestrictedGCStressExe, W("RestrictedGCStressExe"), "") +RETAIL_CONFIG_DWORD_INFO(INTERNAL_CdacStressFailFast, W("CdacStressFailFast"), 0, "If nonzero, assert on cDAC/runtime GC ref mismatch during cDAC stress (GCSTRESS_CDAC mode).") +RETAIL_CONFIG_STRING_INFO(INTERNAL_CdacStressLogFile, W("CdacStressLogFile"), "Log file path for cDAC cDAC stress verification results.") +RETAIL_CONFIG_DWORD_INFO(INTERNAL_CdacStressStep, W("CdacStressStep"), 1, "Verify every Nth cDAC stress point (1=every point, 100=every 100th). Reduces overhead while maintaining code path diversity.") +RETAIL_CONFIG_DWORD_INFO(INTERNAL_CdacStress, W("CdacStress"), 0, "Enable cDAC stack reference verification. Bit flags: 0x1=alloc points, 0x2=GC trigger points, 0x4=unique stacks only, 0x8=instruction points.") CONFIG_DWORD_INFO(INTERNAL_ReturnSourceTypeForTesting, W("ReturnSourceTypeForTesting"), 0, "Allows returning the (internal only) source type of an IL to Native mapping for debugging purposes") RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_RSStressLog, W("RSStressLog"), 0, "Allows turning on logging for RS startup") CONFIG_DWORD_INFO(INTERNAL_SBDumpOnNewIndex, W("SBDumpOnNewIndex"), 0, "Used for Syncblock debugging. It's been a while since any of those have been used.") diff --git a/src/coreclr/inc/gcinfotypes.h b/src/coreclr/inc/gcinfotypes.h index d67de3b4d87549..dc56c94477db0e 100644 --- a/src/coreclr/inc/gcinfotypes.h +++ b/src/coreclr/inc/gcinfotypes.h @@ -53,6 +53,7 @@ inline UINT32 CeilOfLog2(size_t x) #endif } +// [cDAC] [StackWalk]: GCInfo decoder depends on these values. enum GcSlotFlags { GC_SLOT_BASE = 0x0, @@ -65,6 +66,7 @@ enum GcSlotFlags GC_SLOT_IS_DELETED = 0x10, }; +// [cDAC] [StackWalk]: GCInfo decoder depends on these values. enum GcStackSlotBase { GC_CALLER_SP_REL = 0x0, @@ -131,6 +133,7 @@ struct GcStackSlot // //-------------------------------------------------------------------------------- +// [cDAC] [StackWalk]: GCInfo decoder depends on these values. enum ReturnKind { // Cases for Return in one register diff --git a/src/coreclr/inc/patchpointinfo.h b/src/coreclr/inc/patchpointinfo.h index 9700f998bf7988..6e8d32938b27ab 100644 --- a/src/coreclr/inc/patchpointinfo.h +++ b/src/coreclr/inc/patchpointinfo.h @@ -7,6 +7,8 @@ #include +#include "../vm/cdacdata.h" + #ifndef _PATCHPOINTINFO_H_ #define _PATCHPOINTINFO_H_ @@ -217,6 +219,8 @@ struct PatchpointInfo } private: + friend struct ::cdac_data; + enum { OFFSET_SHIFT = 0x1, @@ -238,4 +242,10 @@ struct PatchpointInfo typedef DPTR(struct PatchpointInfo) PTR_PatchpointInfo; +template<> +struct cdac_data +{ + static constexpr size_t LocalCount = offsetof(PatchpointInfo, m_numberOfLocals); +}; + #endif // _PATCHPOINTINFO_H_ diff --git a/src/coreclr/vm/CMakeLists.txt b/src/coreclr/vm/CMakeLists.txt index 3a4c0babdab259..24f26110acd238 100644 --- a/src/coreclr/vm/CMakeLists.txt +++ b/src/coreclr/vm/CMakeLists.txt @@ -329,6 +329,7 @@ set(VM_SOURCES_WKS finalizerthread.cpp floatdouble.cpp floatsingle.cpp + cdacstress.cpp frozenobjectheap.cpp gccover.cpp gcenv.ee.cpp diff --git a/src/coreclr/vm/cdacstress.cpp b/src/coreclr/vm/cdacstress.cpp new file mode 100644 index 00000000000000..3c35e9006f37f6 --- /dev/null +++ b/src/coreclr/vm/cdacstress.cpp @@ -0,0 +1,1192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// +// CdacStress.cpp +// +// Implements in-process cDAC loading and stack reference verification. +// Enabled via DOTNET_CdacStress (bit flags) or legacy DOTNET_GCStress=0x20. +// At each enabled stress point we: +// 1. Ask the cDAC to enumerate stack GC references via ISOSDacInterface::GetStackReferences +// 2. Ask the runtime to enumerate stack GC references via StackWalkFrames + GcInfoDecoder +// 3. Compare the two sets and report any mismatches +// + +#include "common.h" + +#ifdef HAVE_GCCOVER + +#include "CdacStress.h" +#include "../../native/managed/cdac/inc/cdac_reader.h" +#include "../../debug/datadescriptor-shared/inc/contract-descriptor.h" +#include +#include +#include "threads.h" +#include "eeconfig.h" +#include "gccover.h" +#include "sstring.h" +#include "exinfo.h" + +// Forward-declare the 3-param GcEnumObject used as a GCEnumCallback. +// Defined in gcenv.ee.common.cpp; not exposed in any header. +extern void GcEnumObject(LPVOID pData, OBJECTREF *pObj, uint32_t flags); + +#define CDAC_LIB_NAME MAKEDLLNAME_W(W("mscordaccore_universal")) + +// Represents a single GC stack reference for comparison purposes. +struct StackRef +{ + CLRDATA_ADDRESS Address; // Location on stack holding the ref + CLRDATA_ADDRESS Object; // The object pointer value + unsigned int Flags; // SOSRefFlags (interior, pinned) + CLRDATA_ADDRESS Source; // IP or Frame that owns this ref + int SourceType; // SOS_StackSourceIP or SOS_StackSourceFrame + int Register; // Register number (cDAC only) + int Offset; // Register offset (cDAC only) + CLRDATA_ADDRESS StackPointer; // Stack pointer at this ref (cDAC only) +}; + +// Fixed-size buffer for collecting refs during stack walk. +// No heap allocation inside the promote callback — we're under NOTHROW contracts. +static const int MAX_COLLECTED_REFS = 4096; + +// Static state — cDAC +static HMODULE s_cdacModule = NULL; +static intptr_t s_cdacHandle = 0; +static IUnknown* s_cdacSosInterface = nullptr; +static IXCLRDataProcess* s_cdacProcess = nullptr; // Cached QI result for Flush() +static ISOSDacInterface* s_cdacSosDac = nullptr; // Cached QI result for GetStackReferences() + +// Static state — legacy DAC (for three-way comparison) +static HMODULE s_dacModule = NULL; +static ISOSDacInterface* s_dacSosDac = nullptr; +static IXCLRDataProcess* s_dacProcess = nullptr; + +// Static state — common +static bool s_initialized = false; +static bool s_failFast = true; +static DWORD s_step = 1; // Verify every Nth stress point (1=every point) +static DWORD s_cdacStressLevel = 0; // Resolved CdacStressFlags +static FILE* s_logFile = nullptr; +static CrstStatic s_cdacLock; // Serializes cDAC access from concurrent GC stress threads + +// Unique-stack filtering: hash set of previously seen stack traces. +// Protected by s_cdacLock (already held during VerifyAtStressPoint). +static const int UNIQUE_STACK_DEPTH = 8; // Number of return addresses to hash +static SHash>>* s_seenStacks = nullptr; + +// Thread-local reentrancy guard — prevents infinite recursion when +// allocations inside VerifyAtStressPoint trigger VerifyAtAllocPoint. +thread_local bool t_inVerification = false; + +// Verification counters (reported at shutdown) +static volatile LONG s_verifyCount = 0; +static volatile LONG s_verifyPass = 0; +static volatile LONG s_verifyFail = 0; +static volatile LONG s_verifySkip = 0; + +// Thread-local storage for the current thread context at the stress point. +static thread_local PCONTEXT s_currentContext = nullptr; +static thread_local DWORD s_currentThreadId = 0; + +// Extern declaration for the contract descriptor symbol exported from coreclr. +extern "C" struct ContractDescriptor DotNetRuntimeContractDescriptor; + +//----------------------------------------------------------------------------- +// In-process callbacks for the cDAC reader. +// These allow the cDAC to read memory from the current process. +//----------------------------------------------------------------------------- + +// Helper for ReadFromTargetCallback — AVInRuntimeImplOkayHolder cannot be +// directly inside PAL_TRY scope (see controller.cpp:109). +static void ReadFromTargetHelper(void* src, uint8_t* dest, uint32_t count) +{ + AVInRuntimeImplOkayHolder AVOkay; + memcpy(dest, src, count); +} + +static int ReadFromTargetCallback(uint64_t addr, uint8_t* dest, uint32_t count, void* context) +{ + void* src = reinterpret_cast(static_cast(addr)); + struct Param { void* src; uint8_t* dest; uint32_t count; } param; + param.src = src; param.dest = dest; param.count = count; + PAL_TRY(Param *, pParam, ¶m) + { + ReadFromTargetHelper(pParam->src, pParam->dest, pParam->count); + } + PAL_EXCEPT(EXCEPTION_EXECUTE_HANDLER) + { + return E_FAIL; + } + PAL_ENDTRY + return S_OK; +} + +static int WriteToTargetCallback(uint64_t addr, const uint8_t* buff, uint32_t count, void* context) +{ + return E_NOTIMPL; +} + +static int ReadThreadContextCallback(uint32_t threadId, uint32_t contextFlags, uint32_t contextBufferSize, uint8_t* contextBuffer, void* context) +{ + // Return the thread context that was stored by VerifyAtStressPoint. + if (s_currentContext != nullptr && s_currentThreadId == threadId) + { + DWORD copySize = min(contextBufferSize, (uint32_t)sizeof(CONTEXT)); + memcpy(contextBuffer, s_currentContext, copySize); + return S_OK; + } + + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: ReadThreadContext mismatch: requested=%u stored=%u\n", + threadId, s_currentThreadId)); + return E_FAIL; +} + +//----------------------------------------------------------------------------- +// Minimal ICLRDataTarget implementation for loading the legacy DAC in-process. +// Routes ReadVirtual/GetThreadContext to the same callbacks as the cDAC. +//----------------------------------------------------------------------------- +class InProcessDataTarget : public ICLRDataTarget +{ + volatile LONG m_refCount; +public: + InProcessDataTarget() : m_refCount(1) {} + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppObj) override + { + if (riid == IID_IUnknown || riid == __uuidof(ICLRDataTarget)) + { + *ppObj = static_cast(this); + AddRef(); + return S_OK; + } + *ppObj = nullptr; + return E_NOINTERFACE; + } + ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&m_refCount); } + ULONG STDMETHODCALLTYPE Release() override + { + ULONG c = InterlockedDecrement(&m_refCount); + if (c == 0) delete this; + return c; + } + + HRESULT STDMETHODCALLTYPE GetMachineType(ULONG32* machineType) override + { +#ifdef TARGET_AMD64 + *machineType = IMAGE_FILE_MACHINE_AMD64; +#elif defined(TARGET_ARM64) + *machineType = IMAGE_FILE_MACHINE_ARM64; +#elif defined(TARGET_X86) + *machineType = IMAGE_FILE_MACHINE_I386; +#else + return E_NOTIMPL; +#endif + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetPointerSize(ULONG32* pointerSize) override + { + *pointerSize = sizeof(void*); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetImageBase(LPCWSTR imagePath, CLRDATA_ADDRESS* baseAddress) override + { + HMODULE hMod = ::GetModuleHandleW(imagePath); + if (hMod == NULL) return E_FAIL; + *baseAddress = (CLRDATA_ADDRESS)hMod; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE ReadVirtual(CLRDATA_ADDRESS address, BYTE* buffer, ULONG32 bytesRequested, ULONG32* bytesRead) override + { + int hr = ReadFromTargetCallback((uint64_t)address, buffer, bytesRequested, nullptr); + if (hr == S_OK && bytesRead != nullptr) + *bytesRead = bytesRequested; + return hr; + } + + HRESULT STDMETHODCALLTYPE WriteVirtual(CLRDATA_ADDRESS, BYTE*, ULONG32, ULONG32*) override { return E_NOTIMPL; } + + HRESULT STDMETHODCALLTYPE GetTLSValue(ULONG32 threadId, ULONG32 index, CLRDATA_ADDRESS* value) override { return E_NOTIMPL; } + HRESULT STDMETHODCALLTYPE SetTLSValue(ULONG32 threadId, ULONG32 index, CLRDATA_ADDRESS value) override { return E_NOTIMPL; } + HRESULT STDMETHODCALLTYPE GetCurrentThreadID(ULONG32* threadId) override + { + *threadId = ::GetCurrentThreadId(); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetThreadContext(ULONG32 threadId, ULONG32 contextFlags, ULONG32 contextSize, BYTE* contextBuffer) override + { + return ReadThreadContextCallback(threadId, contextFlags, contextSize, contextBuffer, nullptr); + } + + HRESULT STDMETHODCALLTYPE SetThreadContext(ULONG32, ULONG32, BYTE*) override { return E_NOTIMPL; } + HRESULT STDMETHODCALLTYPE Request(ULONG32, ULONG32, BYTE*, ULONG32, BYTE*) override { return E_NOTIMPL; } +}; + +//----------------------------------------------------------------------------- +// Initialization / Shutdown +//----------------------------------------------------------------------------- + +bool CdacStress::IsEnabled() +{ + // Check DOTNET_CdacStress first (new config) + DWORD cdacStress = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_CdacStress); + if (cdacStress != 0) + return true; + + // Fall back to legacy DOTNET_GCStress=0x20 + return (g_pConfig->GetGCStressLevel() & EEConfig::GCSTRESS_CDAC) != 0; +} + +bool CdacStress::IsInitialized() +{ + return s_initialized; +} + +DWORD GetCdacStressLevel() +{ + return s_cdacStressLevel; +} + +bool CdacStress::IsUniqueEnabled() +{ + return (s_cdacStressLevel & CDACSTRESS_UNIQUE) != 0; +} + +bool CdacStress::Initialize() +{ + if (!IsEnabled()) + return false; + + // Resolve the stress level from DOTNET_CdacStress or legacy GCSTRESS_CDAC + DWORD cdacStress = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_CdacStress); + if (cdacStress != 0) + { + s_cdacStressLevel = cdacStress; + } + else + { + // Legacy: GCSTRESS_CDAC maps to allocation-point verification + s_cdacStressLevel = CDACSTRESS_ALLOC; + } + + // Load mscordaccore_universal from next to coreclr + PathString path; + if (WszGetModuleFileName(reinterpret_cast(GetCurrentModuleBase()), path) == 0) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to get module file name\n")); + return false; + } + + SString::Iterator iter = path.End(); + if (!path.FindBack(iter, DIRECTORY_SEPARATOR_CHAR_W)) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to find directory separator\n")); + return false; + } + + iter++; + path.Truncate(iter); + path.Append(CDAC_LIB_NAME); + + s_cdacModule = CLRLoadLibrary(path.GetUnicode()); + if (s_cdacModule == NULL) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to load %S\n", path.GetUnicode())); + return false; + } + + // Resolve cdac_reader_init + auto init = reinterpret_cast(::GetProcAddress(s_cdacModule, "cdac_reader_init")); + if (init == nullptr) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to resolve cdac_reader_init\n")); + ::FreeLibrary(s_cdacModule); + s_cdacModule = NULL; + return false; + } + + // Get the address of the contract descriptor in our own process + uint64_t descriptorAddr = reinterpret_cast(&DotNetRuntimeContractDescriptor); + + // Initialize the cDAC reader with in-process callbacks + if (init(descriptorAddr, &ReadFromTargetCallback, &WriteToTargetCallback, &ReadThreadContextCallback, nullptr, &s_cdacHandle) != 0) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: cdac_reader_init failed\n")); + ::FreeLibrary(s_cdacModule); + s_cdacModule = NULL; + return false; + } + + // Create the SOS interface + auto createSos = reinterpret_cast( + ::GetProcAddress(s_cdacModule, "cdac_reader_create_sos_interface")); + if (createSos == nullptr) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to resolve cdac_reader_create_sos_interface\n")); + auto freeFn = reinterpret_cast(::GetProcAddress(s_cdacModule, "cdac_reader_free")); + if (freeFn != nullptr) + freeFn(s_cdacHandle); + ::FreeLibrary(s_cdacModule); + s_cdacModule = NULL; + s_cdacHandle = 0; + return false; + } + + if (createSos(s_cdacHandle, nullptr, &s_cdacSosInterface) != 0) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: cdac_reader_create_sos_interface failed\n")); + auto freeFn = reinterpret_cast(::GetProcAddress(s_cdacModule, "cdac_reader_free")); + if (freeFn != nullptr) + freeFn(s_cdacHandle); + ::FreeLibrary(s_cdacModule); + s_cdacModule = NULL; + s_cdacHandle = 0; + return false; + } + + // Read configuration for fail-fast behavior + s_failFast = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_CdacStressFailFast) != 0; + + // Read step interval for throttling verifications + s_step = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_CdacStressStep); + if (s_step == 0) + s_step = 1; + + // Cache QI results so we don't QI on every stress point + { + HRESULT hr = s_cdacSosInterface->QueryInterface(__uuidof(IXCLRDataProcess), reinterpret_cast(&s_cdacProcess)); + if (FAILED(hr) || s_cdacProcess == nullptr) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to QI for IXCLRDataProcess (hr=0x%08x)\n", hr)); + } + + hr = s_cdacSosInterface->QueryInterface(__uuidof(ISOSDacInterface), reinterpret_cast(&s_cdacSosDac)); + if (FAILED(hr) || s_cdacSosDac == nullptr) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to QI for ISOSDacInterface (hr=0x%08x) - cannot verify\n", hr)); + if (s_cdacProcess != nullptr) + { + s_cdacProcess->Release(); + s_cdacProcess = nullptr; + } + auto freeFn = reinterpret_cast(::GetProcAddress(s_cdacModule, "cdac_reader_free")); + if (freeFn != nullptr) + freeFn(s_cdacHandle); + ::FreeLibrary(s_cdacModule); + s_cdacModule = NULL; + s_cdacHandle = 0; + return false; + } + } + + // Open log file if configured + CLRConfigStringHolder logFilePath(CLRConfig::GetConfigValue(CLRConfig::INTERNAL_CdacStressLogFile)); + if (logFilePath != nullptr) + { + SString sLogPath(logFilePath); + fopen_s(&s_logFile, sLogPath.GetUTF8(), "w"); + if (s_logFile != nullptr) + { + fprintf(s_logFile, "=== cDAC GC Stress Verification Log ===\n"); + fprintf(s_logFile, "FailFast: %s\n", s_failFast ? "true" : "false"); + fprintf(s_logFile, "Step: %u (verify every %u stress points)\n\n", s_step, s_step); + } + } + + s_cdacLock.Init(CrstGCCover, CRST_DEFAULT); + + if (IsUniqueEnabled()) + { + s_seenStacks = new SHash>>(); + } + + // Load the legacy DAC for three-way comparison (optional — non-fatal if it fails). + { + PathString dacPath; + if (WszGetModuleFileName(reinterpret_cast(GetCurrentModuleBase()), dacPath) != 0) + { + SString::Iterator dacIter = dacPath.End(); + if (dacPath.FindBack(dacIter, DIRECTORY_SEPARATOR_CHAR_W)) + { + dacIter++; + dacPath.Truncate(dacIter); + dacPath.Append(W("mscordaccore.dll")); + + s_dacModule = CLRLoadLibrary(dacPath.GetUnicode()); + if (s_dacModule != NULL) + { + typedef HRESULT (STDAPICALLTYPE *PFN_CLRDataCreateInstance)(REFIID, ICLRDataTarget*, void**); + auto pfnCreate = reinterpret_cast( + ::GetProcAddress(s_dacModule, "CLRDataCreateInstance")); + if (pfnCreate != nullptr) + { + InProcessDataTarget* pTarget = new (nothrow) InProcessDataTarget(); + if (pTarget != nullptr) + { + IUnknown* pDacUnk = nullptr; + HRESULT hr = pfnCreate(__uuidof(IUnknown), pTarget, (void**)&pDacUnk); + pTarget->Release(); + if (SUCCEEDED(hr) && pDacUnk != nullptr) + { + pDacUnk->QueryInterface(__uuidof(ISOSDacInterface), (void**)&s_dacSosDac); + pDacUnk->QueryInterface(__uuidof(IXCLRDataProcess), (void**)&s_dacProcess); + pDacUnk->Release(); + } + } + } + if (s_dacSosDac == nullptr) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Legacy DAC loaded but QI for ISOSDacInterface failed\n")); + } + } + else + { + LOG((LF_GCROOTS, LL_INFO10, "CDAC GC Stress: Legacy DAC not found (three-way comparison disabled)\n")); + } + } + } + } + + s_initialized = true; + LOG((LF_GCROOTS, LL_INFO10, "CDAC GC Stress: Initialized successfully (failFast=%d, logFile=%s)\n", + s_failFast, s_logFile != nullptr ? "yes" : "no")); + return true; +} + +void CdacStress::Shutdown() +{ + if (!s_initialized) + return; + + // Print summary to stderr so results are always visible + LONG actualVerifications = s_verifyPass + s_verifyFail + s_verifySkip; + fprintf(stderr, "CDAC GC Stress: %ld stress points, %ld verifications (%ld pass / %ld fail, %ld skipped)\n", + (long)s_verifyCount, (long)actualVerifications, (long)s_verifyPass, (long)s_verifyFail, (long)s_verifySkip); + STRESS_LOG3(LF_GCROOTS, LL_ALWAYS, + "CDAC GC Stress shutdown: %d verifications (%d pass / %d fail)\n", + (int)actualVerifications, (int)s_verifyPass, (int)s_verifyFail); + + if (s_logFile != nullptr) + { + fprintf(s_logFile, "\n=== Summary ===\n"); + fprintf(s_logFile, "Total stress points: %ld\n", (long)s_verifyCount); + fprintf(s_logFile, "Total verifications: %ld\n", (long)actualVerifications); + fprintf(s_logFile, " Passed: %ld\n", (long)s_verifyPass); + fprintf(s_logFile, " Failed: %ld\n", (long)s_verifyFail); + fprintf(s_logFile, " Skipped: %ld\n", (long)s_verifySkip); + fclose(s_logFile); + s_logFile = nullptr; + } + + if (s_cdacSosDac != nullptr) + { + s_cdacSosDac->Release(); + s_cdacSosDac = nullptr; + } + + if (s_cdacProcess != nullptr) + { + s_cdacProcess->Release(); + s_cdacProcess = nullptr; + } + + if (s_cdacSosInterface != nullptr) + { + s_cdacSosInterface->Release(); + s_cdacSosInterface = nullptr; + } + + if (s_cdacHandle != 0) + { + auto freeFn = reinterpret_cast(::GetProcAddress(s_cdacModule, "cdac_reader_free")); + if (freeFn != nullptr) + freeFn(s_cdacHandle); + s_cdacHandle = 0; + } + + // Legacy DAC cleanup + if (s_dacSosDac != nullptr) { s_dacSosDac->Release(); s_dacSosDac = nullptr; } + if (s_dacProcess != nullptr) { s_dacProcess->Release(); s_dacProcess = nullptr; } + + if (s_seenStacks != nullptr) + { + delete s_seenStacks; + s_seenStacks = nullptr; + } + + s_initialized = false; + LOG((LF_GCROOTS, LL_INFO10, "CDAC GC Stress: Shutdown complete\n")); +} + +//----------------------------------------------------------------------------- +// Collect stack refs from the cDAC +//----------------------------------------------------------------------------- + +static bool CollectStackRefs(ISOSDacInterface* pSosDac, DWORD osThreadId, SArray* pRefs) +{ + if (pSosDac == nullptr) + return false; + + ISOSStackRefEnum* pEnum = nullptr; + HRESULT hr = pSosDac->GetStackReferences(osThreadId, &pEnum); + + if (FAILED(hr) || pEnum == nullptr) + return false; + + SOSStackRefData refData; + unsigned int fetched = 0; + while (true) + { + hr = pEnum->Next(1, &refData, &fetched); + if (FAILED(hr) || fetched == 0) + break; + + StackRef ref; + ref.Address = refData.Address; + ref.Object = refData.Object; + ref.Flags = refData.Flags; + ref.Source = refData.Source; + ref.SourceType = refData.SourceType; + ref.Register = refData.Register; + ref.Offset = refData.Offset; + ref.StackPointer = refData.StackPointer; + pRefs->Append(ref); + } + + // Release twice: once for the normal ref, and once for the extra ref-count + // leaked by SOSDacImpl.GetStackReferences for COM compat (see ConvertToUnmanaged call). + pEnum->Release(); + pEnum->Release(); + return true; +} + +//----------------------------------------------------------------------------- +// Collect stack refs from the runtime's own GC scanning +//----------------------------------------------------------------------------- + +struct RuntimeRefCollectionContext +{ + StackRef refs[MAX_COLLECTED_REFS]; + int count; + bool overflow; +}; + +static void CollectRuntimeRefsPromoteFunc(PTR_PTR_Object ppObj, ScanContext* sc, uint32_t flags) +{ + RuntimeRefCollectionContext* ctx = reinterpret_cast(sc->_unused1); + if (ctx == nullptr) + return; + if (ctx->count >= MAX_COLLECTED_REFS) + { + ctx->overflow = true; + return; + } + + StackRef& ref = ctx->refs[ctx->count++]; + + // Always report the real ppObj address. For register-based refs, ppObj points + // into the REGDISPLAY/CONTEXT on the native stack — we can't reliably distinguish + // these from managed stack slots on the runtime side. The comparison logic handles + // this by matching register refs (cDAC Address=0) by (Object, Flags) only. + ref.Address = reinterpret_cast(ppObj); + ref.Object = reinterpret_cast(*ppObj); + + ref.Flags = 0; + if (flags & GC_CALL_INTERIOR) + ref.Flags |= SOSRefInterior; + if (flags & GC_CALL_PINNED) + ref.Flags |= SOSRefPinned; + ref.Source = 0; + ref.SourceType = 0; +} + +static bool CollectRuntimeStackRefs(Thread* pThread, PCONTEXT regs, StackRef* outRefs, int* outCount) +{ + RuntimeRefCollectionContext collectCtx; + collectCtx.count = 0; + collectCtx.overflow = false; + + GCCONTEXT gcctx = {}; + + // Set up ScanContext the same way ScanStackRoots does — the stack_limit and + // thread_under_crawl fields are required for PromoteCarefully/IsAddressInStack. + ScanContext sc; + sc.promotion = TRUE; + sc.thread_under_crawl = pThread; + sc._unused1 = &collectCtx; + + Frame* pTopFrame = pThread->GetFrame(); + Object** topStack = (Object**)pTopFrame; + if (InlinedCallFrame::FrameHasActiveCall(pTopFrame)) + { + InlinedCallFrame* pInlinedFrame = dac_cast(pTopFrame); + topStack = (Object**)pInlinedFrame->GetCallSiteSP(); + } + sc.stack_limit = (uintptr_t)topStack; + + gcctx.f = CollectRuntimeRefsPromoteFunc; + gcctx.sc = ≻ + gcctx.cf = NULL; + + // Set FORBIDGC_LOADER_USE_ENABLED so MethodDesc::GetName uses NOTHROW + // instead of THROWS inside EECodeManager::EnumGcRefs. + GCForbidLoaderUseHolder forbidLoaderUse; + + unsigned flagsStackWalk = ALLOW_ASYNC_STACK_WALK | ALLOW_INVALID_OBJECTS; + flagsStackWalk |= GC_FUNCLET_REFERENCE_REPORTING; + + // Use a callback that matches DAC behavior (DacStackReferenceWalker::Callback): + // Only call EnumGcRefs for frameless frames and GcScanRoots for explicit frames. + // Deliberately skip the post-scan logic (LCG resolver promotion, + // GcReportLoaderAllocator, generic param context) that GcStackCrawlCallBack + // includes — the DAC's callback has that logic disabled (#if 0). + struct DiagContext { GCCONTEXT* gcctx; RuntimeRefCollectionContext* collectCtx; }; + DiagContext diagCtx = { &gcctx, &collectCtx }; + + auto dacLikeCallback = [](CrawlFrame* pCF, VOID* pData) -> StackWalkAction + { + DiagContext* dCtx = (DiagContext*)pData; + GCCONTEXT* gcctx = dCtx->gcctx; + + ResetPointerHolder rph(&gcctx->cf); + gcctx->cf = pCF; + + bool fReportGCReferences = pCF->ShouldCrawlframeReportGCReferences(); + + if (fReportGCReferences) + { + if (pCF->IsFrameless()) + { + ICodeManager* pCM = pCF->GetCodeManager(); + _ASSERTE(pCM != NULL); + unsigned flags = pCF->GetCodeManagerFlags(); + pCM->EnumGcRefs(pCF->GetRegisterSet(), + pCF->GetCodeInfo(), + flags, + GcEnumObject, + gcctx); + } + else + { + Frame* pFrame = pCF->GetFrame(); + pFrame->GcScanRoots(gcctx->f, gcctx->sc); + } + } + + return SWA_CONTINUE; + }; + + pThread->StackWalkFrames(dacLikeCallback, &diagCtx, flagsStackWalk); + + // NOTE: ScanStackRoots also scans the separate GCFrame linked list + // (Thread::GetGCFrame), but the DAC's GetStackReferences / DacStackReferenceWalker + // does NOT include those. We intentionally omit GCFrame scanning here so our + // runtime-side collection matches what the cDAC is expected to produce. + + // Copy results out + *outCount = collectCtx.count; + memcpy(outRefs, collectCtx.refs, collectCtx.count * sizeof(StackRef)); + return !collectCtx.overflow; +} + +//----------------------------------------------------------------------------- +// Filter cDAC refs to match runtime PromoteCarefully behavior. +// The runtime's PromoteCarefully (siginfo.cpp) skips interior pointers whose +// object value is a stack address. The cDAC reports all GcInfo slots without +// this filter, so we apply it here before comparing against runtime refs. +//----------------------------------------------------------------------------- + +static int FilterInteriorStackRefs(StackRef* refs, int count, Thread* pThread, uintptr_t stackLimit) +{ + int writeIdx = 0; + for (int i = 0; i < count; i++) + { + bool isInterior = (refs[i].Flags & SOSRefInterior) != 0; + if (isInterior && + pThread->IsAddressInStack((void*)(size_t)refs[i].Object) && + (size_t)refs[i].Object >= stackLimit) + { + continue; + } + refs[writeIdx++] = refs[i]; + } + return writeIdx; +} + +//----------------------------------------------------------------------------- +// Deduplicate cDAC refs that have the same (Address, Object, Flags). +// The cDAC may walk the same managed frame at two different offsets due to +// Frames restoring context (e.g. InlinedCallFrame). The same stack slots +// get reported from both offsets. The runtime only walks each frame once, +// so we deduplicate to match. +//----------------------------------------------------------------------------- + +static int CompareStackRefKey(const void* a, const void* b) +{ + const StackRef* refA = static_cast(a); + const StackRef* refB = static_cast(b); + if (refA->Address != refB->Address) + return (refA->Address < refB->Address) ? -1 : 1; + if (refA->Object != refB->Object) + return (refA->Object < refB->Object) ? -1 : 1; + if (refA->Flags != refB->Flags) + return (refA->Flags < refB->Flags) ? -1 : 1; + return 0; +} + +static int DeduplicateRefs(StackRef* refs, int count) +{ + if (count <= 1) + return count; + qsort(refs, count, sizeof(StackRef), CompareStackRefKey); + int writeIdx = 1; + for (int i = 1; i < count; i++) + { + // Only dedup stack-based refs (Address != 0). + // Register refs (Address == 0) are legitimately different entries + // even when Address/Object/Flags match (different registers). + if (refs[i].Address != 0 && + refs[i].Address == refs[i-1].Address && + refs[i].Object == refs[i-1].Object && + refs[i].Flags == refs[i-1].Flags) + { + continue; + } + refs[writeIdx++] = refs[i]; + } + return writeIdx; +} + +//----------------------------------------------------------------------------- +// Report mismatch +//----------------------------------------------------------------------------- + +static void ReportMismatch(const char* message, Thread* pThread, PCONTEXT regs) +{ + LOG((LF_GCROOTS, LL_ERROR, "CDAC GC Stress: %s (Thread=0x%x, IP=0x%p)\n", + message, pThread->GetOSThreadId(), (void*)GetIP(regs))); + + if (s_failFast) + { + _ASSERTE_MSG(false, message); + } +} + +//----------------------------------------------------------------------------- +// Compare IXCLRDataStackWalk frame-by-frame between cDAC and legacy DAC. +// Creates a stack walk on each, advances in lockstep, and compares +// GetContext + Request(FRAME_DATA) at each step. +//----------------------------------------------------------------------------- + +static void CompareStackWalks(Thread* pThread, PCONTEXT regs) +{ + if (s_cdacProcess == nullptr || s_dacProcess == nullptr) + return; + + DWORD osThreadId = pThread->GetOSThreadId(); + + // Get IXCLRDataTask for the thread from both processes + IXCLRDataTask* cdacTask = nullptr; + IXCLRDataTask* dacTask = nullptr; + + HRESULT hr1 = s_cdacProcess->GetTaskByOSThreadID(osThreadId, &cdacTask); + HRESULT hr2 = s_dacProcess->GetTaskByOSThreadID(osThreadId, &dacTask); + + if (FAILED(hr1) || cdacTask == nullptr || FAILED(hr2) || dacTask == nullptr) + { + if (cdacTask) cdacTask->Release(); + if (dacTask) dacTask->Release(); + return; + } + + // Create stack walks + IXCLRDataStackWalk* cdacWalk = nullptr; + IXCLRDataStackWalk* dacWalk = nullptr; + + hr1 = cdacTask->CreateStackWalk(0xF /* CLRDATA_SIMPFRAME_MANAGED_METHOD | ... */, &cdacWalk); + hr2 = dacTask->CreateStackWalk(0xF, &dacWalk); + + cdacTask->Release(); + dacTask->Release(); + + if (FAILED(hr1) || cdacWalk == nullptr || FAILED(hr2) || dacWalk == nullptr) + { + if (cdacWalk) cdacWalk->Release(); + if (dacWalk) dacWalk->Release(); + return; + } + + // Walk in lockstep comparing each frame + int frameIdx = 0; + bool mismatch = false; + while (frameIdx < 200) // safety limit + { + // Compare GetContext + BYTE cdacCtx[4096] = {}; + BYTE dacCtx[4096] = {}; + ULONG32 cdacCtxSize = 0, dacCtxSize = 0; + + hr1 = cdacWalk->GetContext(0, sizeof(cdacCtx), &cdacCtxSize, cdacCtx); + hr2 = dacWalk->GetContext(0, sizeof(dacCtx), &dacCtxSize, dacCtx); + + if (hr1 != hr2) + { + if (s_logFile) + fprintf(s_logFile, " [WALK_MISMATCH] Frame %d: GetContext hr mismatch cDAC=0x%x DAC=0x%x\n", + frameIdx, hr1, hr2); + mismatch = true; + break; + } + if (hr1 != S_OK) + break; // both finished + + if (cdacCtxSize != dacCtxSize) + { + if (s_logFile) + fprintf(s_logFile, " [WALK_MISMATCH] Frame %d: Context size differs cDAC=%u DAC=%u\n", + frameIdx, cdacCtxSize, dacCtxSize); + mismatch = true; + } + else if (cdacCtxSize >= sizeof(CONTEXT)) + { + // Compare IP and SP — these are what matter for stack walk parity. + // Other CONTEXT fields (floating-point, debug registers, xstate) may + // differ between cDAC and DAC without affecting the walk. + PCODE cdacIP = GetIP((CONTEXT*)cdacCtx); + PCODE dacIP = GetIP((CONTEXT*)dacCtx); + TADDR cdacSP = GetSP((CONTEXT*)cdacCtx); + TADDR dacSP = GetSP((CONTEXT*)dacCtx); + + if (cdacIP != dacIP || cdacSP != dacSP) + { + fprintf(s_logFile, " [WALK_MISMATCH] Frame %d: Context differs cDAC_IP=0x%llx cDAC_SP=0x%llx DAC_IP=0x%llx DAC_SP=0x%llx\n", + frameIdx, + (unsigned long long)cdacIP, (unsigned long long)cdacSP, + (unsigned long long)dacIP, (unsigned long long)dacSP); + mismatch = true; + } + } + + // Compare Request(FRAME_DATA) + ULONG64 cdacFrameAddr = 0, dacFrameAddr = 0; + hr1 = cdacWalk->Request(0xf0000000, 0, nullptr, sizeof(cdacFrameAddr), (BYTE*)&cdacFrameAddr); + hr2 = dacWalk->Request(0xf0000000, 0, nullptr, sizeof(dacFrameAddr), (BYTE*)&dacFrameAddr); + + if (hr1 == S_OK && hr2 == S_OK && cdacFrameAddr != dacFrameAddr) + { + if (s_logFile) + { + PCODE cdacIP = 0, dacIP = 0; + if (cdacCtxSize >= sizeof(CONTEXT)) + cdacIP = GetIP((CONTEXT*)cdacCtx); + if (dacCtxSize >= sizeof(CONTEXT)) + dacIP = GetIP((CONTEXT*)dacCtx); + fprintf(s_logFile, " [WALK_MISMATCH] Frame %d: FrameAddr cDAC=0x%llx DAC=0x%llx (cDAC_IP=0x%llx DAC_IP=0x%llx)\n", + frameIdx, (unsigned long long)cdacFrameAddr, (unsigned long long)dacFrameAddr, + (unsigned long long)cdacIP, (unsigned long long)dacIP); + } + mismatch = true; + } + + // Advance both + hr1 = cdacWalk->Next(); + hr2 = dacWalk->Next(); + + if (hr1 != hr2) + { + if (s_logFile) + fprintf(s_logFile, " [WALK_MISMATCH] Frame %d: Next hr mismatch cDAC=0x%x DAC=0x%x\n", + frameIdx, hr1, hr2); + mismatch = true; + break; + } + if (hr1 != S_OK) + break; // both finished + + frameIdx++; + } + + if (!mismatch && s_logFile) + fprintf(s_logFile, " [WALK_OK] %d frames matched between cDAC and DAC\n", frameIdx); + + cdacWalk->Release(); + dacWalk->Release(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// Compare two ref sets using two-phase matching. +// Phase 1: Match stack refs (Address != 0) by exact (Address, Object, Flags). +// Phase 2: Match register refs (Address == 0) by (Object, Flags) only. +// Returns true if all refs in setA have a match in setB and counts are equal. +//----------------------------------------------------------------------------- + +static bool CompareRefSets(StackRef* refsA, int countA, StackRef* refsB, int countB) +{ + if (countA != countB) + return false; + if (countA == 0) + return true; + + bool matched[MAX_COLLECTED_REFS] = {}; + + for (int i = 0; i < countA; i++) + { + if (refsA[i].Address == 0) + continue; + bool found = false; + for (int j = 0; j < countB; j++) + { + if (matched[j]) continue; + if (refsA[i].Address == refsB[j].Address && + refsA[i].Object == refsB[j].Object && + refsA[i].Flags == refsB[j].Flags) + { + matched[j] = true; + found = true; + break; + } + } + if (!found) return false; + } + + for (int i = 0; i < countA; i++) + { + if (refsA[i].Address != 0) + continue; + bool found = false; + for (int j = 0; j < countB; j++) + { + if (matched[j]) continue; + if (refsA[i].Object == refsB[j].Object && + refsA[i].Flags == refsB[j].Flags) + { + matched[j] = true; + found = true; + break; + } + } + if (!found) return false; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Filter interior stack pointers and deduplicate a ref set in place. +//----------------------------------------------------------------------------- + +static int FilterAndDedup(StackRef* refs, int count, Thread* pThread, uintptr_t stackLimit) +{ + count = FilterInteriorStackRefs(refs, count, pThread, stackLimit); + count = DeduplicateRefs(refs, count); + return count; +} + +//----------------------------------------------------------------------------- +// Main entry point: verify at a GC stress point +//----------------------------------------------------------------------------- + +bool CdacStress::ShouldSkipStressPoint() +{ + LONG count = InterlockedIncrement(&s_verifyCount); + + if (s_step <= 1) + return false; + + return (count % s_step) != 0; +} + +void CdacStress::VerifyAtAllocPoint() +{ + if (!s_initialized) + return; + + // Reentrancy guard: allocations inside VerifyAtStressPoint (e.g., SArray) + // would trigger this function again, causing deadlock on s_cdacLock. + if (t_inVerification) + return; + + if (ShouldSkipStressPoint()) + return; + + Thread* pThread = GetThreadNULLOk(); + if (pThread == nullptr || !pThread->PreemptiveGCDisabled()) + return; + + CONTEXT ctx; + RtlCaptureContext(&ctx); + VerifyAtStressPoint(pThread, &ctx); +} + +void CdacStress::VerifyAtStressPoint(Thread* pThread, PCONTEXT regs) +{ + _ASSERTE(s_initialized); + _ASSERTE(pThread != nullptr); + _ASSERTE(regs != nullptr); + + // RAII guard: set t_inVerification=true on entry, false on exit. + // Prevents infinite recursion when allocations inside this function + // trigger VerifyAtAllocPoint again (which would deadlock on s_cdacLock). + struct ReentrancyGuard { + ReentrancyGuard() { t_inVerification = true; } + ~ReentrancyGuard() { t_inVerification = false; } + } reentrancyGuard; + + // Serialize cDAC access — the cDAC's ProcessedData cache and COM interfaces + // are not thread-safe, and GC stress can fire on multiple threads. + CrstHolder cdacLock(&s_cdacLock); + + // Unique-stack filtering: use IP + SP as a stack identity. + // This skips re-verification at the same code location with the same stack depth. + if (IsUniqueEnabled() && s_seenStacks != nullptr) + { + SIZE_T stackHash = GetIP(regs) ^ (GetSP(regs) * 2654435761u); + if (s_seenStacks->LookupPtr(stackHash) != nullptr) + return; + s_seenStacks->Add(stackHash); + } + + // Set the thread context for the cDAC's ReadThreadContext callback. + s_currentContext = regs; + s_currentThreadId = pThread->GetOSThreadId(); + + // Flush the cDAC's ProcessedData cache so it re-reads from the live process. + if (s_cdacProcess != nullptr) + { + s_cdacProcess->Flush(); + } + + // Flush the legacy DAC cache too. + if (s_dacProcess != nullptr) + { + s_dacProcess->Flush(); + } + + // Compare IXCLRDataStackWalk frame-by-frame between cDAC and legacy DAC. + if (s_cdacStressLevel & CDACSTRESS_WALK) + { + CompareStackWalks(pThread, regs); + } + + // Compare GC stack references. + if (!(s_cdacStressLevel & CDACSTRESS_REFS)) + { + s_currentContext = nullptr; + s_currentThreadId = 0; + return; + } + + // Step 1: Collect raw refs from cDAC (always) and DAC (if USE_DAC). + DWORD osThreadId = pThread->GetOSThreadId(); + + SArray cdacRefs; + bool haveCdac = CollectStackRefs(s_cdacSosDac, osThreadId, &cdacRefs); + + SArray dacRefs; + bool haveDac = false; + if (s_cdacStressLevel & CDACSTRESS_USE_DAC) + { + haveDac = (s_dacSosDac != nullptr) && CollectStackRefs(s_dacSosDac, osThreadId, &dacRefs); + } + + s_currentContext = nullptr; + s_currentThreadId = 0; + + StackRef runtimeRefsBuf[MAX_COLLECTED_REFS]; + int runtimeCount = 0; + CollectRuntimeStackRefs(pThread, regs, runtimeRefsBuf, &runtimeCount); + + if (!haveCdac) + { + InterlockedIncrement(&s_verifySkip); + if (s_logFile != nullptr) + fprintf(s_logFile, "[SKIP] Thread=0x%x IP=0x%p - cDAC GetStackReferences failed\n", + osThreadId, (void*)GetIP(regs)); + return; + } + + // Step 2: Compare cDAC vs DAC raw (before any filtering). + int rawCdacCount = (int)cdacRefs.GetCount(); + int rawDacCount = haveDac ? (int)dacRefs.GetCount() : -1; + bool dacMatch = true; + if (haveDac) + { + StackRef* cdacBuf = cdacRefs.OpenRawBuffer(); + StackRef* dacBuf = dacRefs.OpenRawBuffer(); + dacMatch = CompareRefSets(cdacBuf, rawCdacCount, dacBuf, rawDacCount); + cdacRefs.CloseRawBuffer(); + dacRefs.CloseRawBuffer(); + } + + // Step 3: Filter cDAC refs and compare vs RT (always). + Frame* pTopFrame = pThread->GetFrame(); + Object** topStack = (Object**)pTopFrame; + if (InlinedCallFrame::FrameHasActiveCall(pTopFrame)) + { + InlinedCallFrame* pInlinedFrame = dac_cast(pTopFrame); + topStack = (Object**)pInlinedFrame->GetCallSiteSP(); + } + uintptr_t stackLimit = (uintptr_t)topStack; + + int filteredCdacCount = rawCdacCount; + if (filteredCdacCount > 0) + { + StackRef* cdacBuf = cdacRefs.OpenRawBuffer(); + filteredCdacCount = FilterAndDedup(cdacBuf, filteredCdacCount, pThread, stackLimit); + cdacRefs.CloseRawBuffer(); + } + runtimeCount = DeduplicateRefs(runtimeRefsBuf, runtimeCount); + + StackRef* cdacBuf = cdacRefs.OpenRawBuffer(); + bool rtMatch = CompareRefSets(cdacBuf, filteredCdacCount, runtimeRefsBuf, runtimeCount); + cdacRefs.CloseRawBuffer(); + + // Step 4: Pass requires cDAC vs RT match. + // DAC mismatch is logged separately but doesn't affect pass/fail. + bool pass = rtMatch; + + if (pass) + InterlockedIncrement(&s_verifyPass); + else + InterlockedIncrement(&s_verifyFail); + + // Step 5: Log results. + if (s_logFile != nullptr) + { + const char* label = pass ? "PASS" : "FAIL"; + if (pass && !dacMatch) + label = "DAC_MISMATCH"; + fprintf(s_logFile, "[%s] Thread=0x%x IP=0x%p cDAC=%d DAC=%d RT=%d\n", + label, osThreadId, (void*)GetIP(regs), + rawCdacCount, rawDacCount, runtimeCount); + + if (!pass || !dacMatch) + { + for (int i = 0; i < rawCdacCount; i++) + fprintf(s_logFile, " cDAC [%d]: Address=0x%llx Object=0x%llx Flags=0x%x Source=0x%llx SourceType=%d SP=0x%llx\n", + i, (unsigned long long)cdacRefs[i].Address, (unsigned long long)cdacRefs[i].Object, + cdacRefs[i].Flags, (unsigned long long)cdacRefs[i].Source, cdacRefs[i].SourceType, + (unsigned long long)cdacRefs[i].StackPointer); + if (haveDac) + { + for (int i = 0; i < rawDacCount; i++) + fprintf(s_logFile, " DAC [%d]: Address=0x%llx Object=0x%llx Flags=0x%x Source=0x%llx\n", + i, (unsigned long long)dacRefs[i].Address, (unsigned long long)dacRefs[i].Object, + dacRefs[i].Flags, (unsigned long long)dacRefs[i].Source); + } + for (int i = 0; i < runtimeCount; i++) + fprintf(s_logFile, " RT [%d]: Address=0x%llx Object=0x%llx Flags=0x%x\n", + i, (unsigned long long)runtimeRefsBuf[i].Address, (unsigned long long)runtimeRefsBuf[i].Object, + runtimeRefsBuf[i].Flags); + + fflush(s_logFile); + } + } +} + +#endif // HAVE_GCCOVER diff --git a/src/coreclr/vm/cdacstress.h b/src/coreclr/vm/cdacstress.h new file mode 100644 index 00000000000000..b151155559e9c5 --- /dev/null +++ b/src/coreclr/vm/cdacstress.h @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// +// CdacStress.h +// +// Infrastructure for verifying cDAC stack reference reporting against the +// runtime's own GC root enumeration at stress trigger points. +// +// Enabled via DOTNET_CdacStress (bit flags) or legacy DOTNET_GCStress=0x20. +// + +#ifndef _CDAC_STRESS_H_ +#define _CDAC_STRESS_H_ + +// Trigger points for cDAC stress verification. +enum cdac_trigger_points +{ + cdac_on_alloc, // Verify at allocation points + cdac_on_gc, // Verify at GC trigger points + cdac_on_instr, // Verify at instruction-level stress points (needs GCStress=0x4) +}; + +#ifdef HAVE_GCCOVER + +// Bit flags for DOTNET_CdacStress configuration. +// +// Low nibble: WHERE to trigger verification +// High nibble: WHAT to validate +// Modifier: HOW to filter +enum CdacStressFlags : DWORD +{ + // Trigger points (low nibble — where stress fires) + CDACSTRESS_ALLOC = 0x1, // Verify at allocation points + CDACSTRESS_GC = 0x2, // Verify at GC trigger points (future) + CDACSTRESS_INSTR = 0x4, // Verify at instruction stress points (needs GCStress=0x4) + + // Validation types (high nibble — what to check) + CDACSTRESS_REFS = 0x10, // Compare GC stack references + CDACSTRESS_WALK = 0x20, // Compare IXCLRDataStackWalk frame-by-frame + CDACSTRESS_USE_DAC = 0x40, // Also load legacy DAC and compare cDAC against it + + // Modifiers + CDACSTRESS_UNIQUE = 0x100, // Only verify on unique (IP, SP) pairs +}; + +// Forward declarations +class Thread; + +// Accessor for the resolved stress level — called by template specializations. +DWORD GetCdacStressLevel(); + +class CdacStress +{ +public: + static bool Initialize(); + static void Shutdown(); + static bool IsInitialized(); + + // Returns true if cDAC stress is enabled via DOTNET_CdacStress or legacy GCSTRESS_CDAC. + static bool IsEnabled(); + + // Template-based trigger point check, following the GCStress pattern. + template + static bool IsEnabled(); + + // Returns true if unique-stack filtering is active. + static bool IsUniqueEnabled(); + + // Verify at a stress point if the given trigger is enabled and not skipped. + // Follows the GCStress::MaybeTrigger pattern — call sites are one-liners. + template + FORCEINLINE static void MaybeVerify(Thread* pThread, PCONTEXT regs) + { + if (IsEnabled() && !ShouldSkipStressPoint()) + VerifyAtStressPoint(pThread, regs); + } + + // Allocation-point variant: captures thread context automatically. + template + FORCEINLINE static void MaybeVerify() + { + if (IsEnabled() && !ShouldSkipStressPoint()) + VerifyAtAllocPoint(); + } + + // Main entry point: verify cDAC stack refs match runtime stack refs. + static void VerifyAtStressPoint(Thread* pThread, PCONTEXT regs); + + // Verify at an allocation point. Captures current thread context. + static void VerifyAtAllocPoint(); + + // Returns true if this stress point should be skipped (step throttling). + static bool ShouldSkipStressPoint(); +}; + +template<> FORCEINLINE bool CdacStress::IsEnabled() +{ + return IsInitialized() && (GetCdacStressLevel() & CDACSTRESS_ALLOC) != 0; +} + +template<> FORCEINLINE bool CdacStress::IsEnabled() +{ + return IsInitialized() && (GetCdacStressLevel() & CDACSTRESS_GC) != 0; +} + +template<> FORCEINLINE bool CdacStress::IsEnabled() +{ + return IsInitialized() && (GetCdacStressLevel() & CDACSTRESS_INSTR) != 0; +} + +#else // !HAVE_GCCOVER + +// Stub when HAVE_GCCOVER is not defined — all calls compile to nothing. +class CdacStress +{ +public: + template + FORCEINLINE static void MaybeVerify(Thread* pThread, PCONTEXT regs) { } + template + FORCEINLINE static void MaybeVerify() { } +}; + +#endif // HAVE_GCCOVER +#endif // _CDAC_STRESS_H_ diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 6ce92ecfff8e6a..0d903c3bb52205 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -209,6 +209,10 @@ #include "genanalysis.h" +#ifdef HAVE_GCCOVER +#include "CdacStress.h" +#endif + HRESULT EEStartup(); @@ -963,6 +967,10 @@ void EEStartupHelper() #ifdef HAVE_GCCOVER MethodDesc::Init(); + if (CdacStress::IsEnabled()) + { + CdacStress::Initialize(); + } #endif Assembly::Initialize(); @@ -1244,6 +1252,10 @@ void STDMETHODCALLTYPE EEShutDownHelper(BOOL fIsDllUnloading) // Indicate the EE is the shut down phase. InterlockedOr((LONG*)&g_fEEShutDown, ShutDown_Start); +#ifdef HAVE_GCCOVER + CdacStress::Shutdown(); +#endif + if (!IsAtProcessExit() && !g_fFastExitProcess) { // Wait for the finalizer thread to deliver process exit event 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 b6cd256d09dc32..2ce60c9e78ab2a 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -44,6 +44,8 @@ CDAC_TYPE_FIELD(Thread, /*pointer*/, Frame, cdac_data::Frame) CDAC_TYPE_FIELD(Thread, /*pointer*/, CachedStackBase, cdac_data::CachedStackBase) CDAC_TYPE_FIELD(Thread, /*pointer*/, CachedStackLimit, cdac_data::CachedStackLimit) CDAC_TYPE_FIELD(Thread, /*pointer*/, ExceptionTracker, cdac_data::ExceptionTracker) +CDAC_TYPE_FIELD(Thread, /*pointer*/, DebuggerFilterContext, cdac_data::DebuggerFilterContext) +CDAC_TYPE_FIELD(Thread, /*pointer*/, ProfilerFilterContext, cdac_data::ProfilerFilterContext) CDAC_TYPE_FIELD(Thread, GCHandle, GCHandle, cdac_data::ExposedObject) CDAC_TYPE_FIELD(Thread, GCHandle, LastThrownObject, cdac_data::LastThrownObject) CDAC_TYPE_FIELD(Thread, pointer, LinkNext, cdac_data::Link) @@ -132,14 +134,22 @@ 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, cdac_data::ExceptionFlagsValue) +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, /*uint32*/, ClauseForCatchHandlerStartPC, offsetof(ExInfo, m_ClauseForCatch) + offsetof(EE_ILEXCEPTION_CLAUSE, HandlerStartPC)) +CDAC_TYPE_FIELD(ExceptionInfo, /*uint32*/, ClauseForCatchHandlerEndPC, offsetof(ExInfo, m_ClauseForCatch) + offsetof(EE_ILEXCEPTION_CLAUSE, HandlerEndPC)) CDAC_TYPE_END(ExceptionInfo) - CDAC_TYPE_BEGIN(GCHandle) CDAC_TYPE_SIZE(sizeof(OBJECTHANDLE)) CDAC_TYPE_END(GCHandle) @@ -663,6 +673,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) @@ -764,10 +775,10 @@ 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, /*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)) -CDAC_TYPE_FIELD(RealCodeHeader, /*pointer*/, JitEHInfo, offsetof(RealCodeHeader, phdrJitEHInfo)) CDAC_TYPE_END(RealCodeHeader) CDAC_TYPE_BEGIN(EEExceptionClause) @@ -795,6 +806,11 @@ CDAC_TYPE_INDETERMINATE(EEILException) CDAC_TYPE_FIELD(EEILException, /* EE_ILEXCEPTION_CLAUSE */, Clauses, offsetof(EE_ILEXCEPTION, Clauses)) CDAC_TYPE_END(EEILException) +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)) @@ -901,8 +917,19 @@ CDAC_TYPE_SIZE(sizeof(StubDispatchFrame)) CDAC_TYPE_FIELD(StubDispatchFrame, /*pointer*/, RepresentativeMTPtr, cdac_data::RepresentativeMTPtr) CDAC_TYPE_FIELD(StubDispatchFrame, /*pointer*/, MethodDescPtr, cdac_data::MethodDescPtr) CDAC_TYPE_FIELD(StubDispatchFrame, /*uint32*/, RepresentativeSlot, cdac_data::RepresentativeSlot) +CDAC_TYPE_FIELD(StubDispatchFrame, /*pointer*/, GCRefMap, cdac_data::GCRefMap) CDAC_TYPE_END(StubDispatchFrame) +CDAC_TYPE_BEGIN(ExternalMethodFrame) +CDAC_TYPE_SIZE(sizeof(ExternalMethodFrame)) +CDAC_TYPE_FIELD(ExternalMethodFrame, /*pointer*/, GCRefMap, cdac_data::GCRefMap) +CDAC_TYPE_END(ExternalMethodFrame) + +CDAC_TYPE_BEGIN(DynamicHelperFrame) +CDAC_TYPE_SIZE(sizeof(DynamicHelperFrame)) +CDAC_TYPE_FIELD(DynamicHelperFrame, /*int32*/, DynamicHelperFrameFlags, cdac_data::DynamicHelperFrameFlags) +CDAC_TYPE_END(DynamicHelperFrame) + #ifdef FEATURE_HIJACK CDAC_TYPE_BEGIN(ResumableFrame) CDAC_TYPE_SIZE(sizeof(ResumableFrame)) @@ -1275,6 +1302,18 @@ CDAC_GLOBAL_POINTER(GCThread, &::g_pSuspensionThread) #undef FRAME_TYPE_NAME CDAC_GLOBAL(MethodDescTokenRemainderBitCount, uint8, METHOD_TOKEN_REMAINDER_BIT_COUNT) + +CDAC_GLOBAL(TransitionBlockOffsetOfArgs, uint32, sizeof(TransitionBlock)) +#if (defined(TARGET_AMD64) && !defined(UNIX_AMD64_ABI)) || defined(TARGET_WASM) +CDAC_GLOBAL(TransitionBlockOffsetOfArgumentRegisters, uint32, sizeof(TransitionBlock)) +CDAC_GLOBAL(TransitionBlockOffsetOfFirstGCRefMapSlot, uint32, sizeof(TransitionBlock)) +#elif defined(TARGET_ARM64) +CDAC_GLOBAL(TransitionBlockOffsetOfArgumentRegisters, uint32, offsetof(TransitionBlock, m_argumentRegisters)) +CDAC_GLOBAL(TransitionBlockOffsetOfFirstGCRefMapSlot, uint32, offsetof(TransitionBlock, m_x8RetBuffReg)) +#else +CDAC_GLOBAL(TransitionBlockOffsetOfArgumentRegisters, uint32, offsetof(TransitionBlock, m_argumentRegisters)) +CDAC_GLOBAL(TransitionBlockOffsetOfFirstGCRefMapSlot, uint32, offsetof(TransitionBlock, m_argumentRegisters)) +#endif #if FEATURE_COMINTEROP CDAC_GLOBAL(FeatureCOMInterop, uint8, 1) #else diff --git a/src/coreclr/vm/eeconfig.h b/src/coreclr/vm/eeconfig.h index fecb76eb69fb41..141ab06c19e9b7 100644 --- a/src/coreclr/vm/eeconfig.h +++ b/src/coreclr/vm/eeconfig.h @@ -370,6 +370,10 @@ class EEConfig GCSTRESS_INSTR_JIT = 4, // GC on every allowable JITed instr GCSTRESS_INSTR_NGEN = 8, // GC on every allowable NGEN instr GCSTRESS_UNIQUE = 16, // GC only on a unique stack trace + GCSTRESS_CDAC = 32, // Verify cDAC GC references at stress points + + // Excludes cDAC stress as it is fundamentally different from the other stress modes + GCSTRESS_ALLSTRESS = GCSTRESS_ALLOC | GCSTRESS_TRANSITION | GCSTRESS_INSTR_JIT | GCSTRESS_INSTR_NGEN, }; GCStressFlags GetGCStressLevel() const { WRAPPER_NO_CONTRACT; SUPPORTS_DAC; return GCStressFlags(iGCStress); } diff --git a/src/coreclr/vm/exinfo.h b/src/coreclr/vm/exinfo.h index 302975c5d7ec04..fc223da926cc0d 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(); @@ -358,13 +360,18 @@ struct ExInfo static StackWalkAction RareFindParentStackFrameCallback(CrawlFrame* pCF, LPVOID pData); }; -#ifndef TARGET_UNIX template<> struct cdac_data { + 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); + static constexpr size_t ExceptionFlagsValue = offsetof(ExInfo, m_ExceptionFlags.m_flags); +#ifndef TARGET_UNIX static constexpr size_t ExceptionWatsonBucketTrackerBuckets = offsetof(ExInfo, m_WatsonBucketTracker) + offsetof(EHWatsonBucketTracker, m_WatsonUnhandledInfo.m_pUnhandledBuckets); -}; #endif // TARGET_UNIX +}; #endif // __ExInfo_h__ diff --git a/src/coreclr/vm/exstatecommon.h b/src/coreclr/vm/exstatecommon.h index 5aab62086f8c46..231a0ce05e4962 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() { @@ -346,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/coreclr/vm/frames.h b/src/coreclr/vm/frames.h index 55072025229b0f..eb3fa240ee9148 100644 --- a/src/coreclr/vm/frames.h +++ b/src/coreclr/vm/frames.h @@ -1728,6 +1728,7 @@ struct cdac_data { static constexpr size_t RepresentativeMTPtr = offsetof(StubDispatchFrame, m_pRepresentativeMT); static constexpr uint32_t RepresentativeSlot = offsetof(StubDispatchFrame, m_representativeSlot); + static constexpr size_t GCRefMap = offsetof(StubDispatchFrame, m_pGCRefMap); }; typedef DPTR(class StubDispatchFrame) PTR_StubDispatchFrame; @@ -1763,6 +1764,8 @@ class CallCountingHelperFrame : public FramedMethodFrame class ExternalMethodFrame : public FramedMethodFrame { + friend struct ::cdac_data; + // Indirection and containing module. Used to compute pGCRefMap lazily. PTR_Module m_pZapModule; TADDR m_pIndirection; @@ -1803,8 +1806,16 @@ class ExternalMethodFrame : public FramedMethodFrame typedef DPTR(class ExternalMethodFrame) PTR_ExternalMethodFrame; +template <> +struct cdac_data +{ + static constexpr size_t GCRefMap = offsetof(ExternalMethodFrame, m_pGCRefMap); +}; + class DynamicHelperFrame : public FramedMethodFrame { + friend struct ::cdac_data; + int m_dynamicHelperFrameFlags; public: @@ -1825,6 +1836,12 @@ class DynamicHelperFrame : public FramedMethodFrame typedef DPTR(class DynamicHelperFrame) PTR_DynamicHelperFrame; +template <> +struct cdac_data +{ + static constexpr size_t DynamicHelperFrameFlags = offsetof(DynamicHelperFrame, m_dynamicHelperFrameFlags); +}; + #ifdef FEATURE_COMINTEROP //------------------------------------------------------------------------ diff --git a/src/coreclr/vm/gccover.cpp b/src/coreclr/vm/gccover.cpp index 26d0f0b78efa8b..5e516a13ad4246 100644 --- a/src/coreclr/vm/gccover.cpp +++ b/src/coreclr/vm/gccover.cpp @@ -24,6 +24,7 @@ #include "gccover.h" #include "virtualcallstub.h" #include "threadsuspend.h" +#include "CdacStress.h" #if defined(TARGET_AMD64) || defined(TARGET_ARM) #include "gcinfodecoder.h" @@ -887,6 +888,8 @@ void DoGcStress (PCONTEXT regs, NativeCodeVersion nativeCodeVersion) // Do the actual stress work // + CdacStress::MaybeVerify(pThread, regs); + // BUG(github #10318) - when not using allocation contexts, the alloc lock // must be acquired here. Until fixed, this assert prevents random heap corruption. assert(GCHeapUtilities::UseThreadAllocationContexts()); @@ -1195,7 +1198,9 @@ void DoGcStress (PCONTEXT regs, NativeCodeVersion nativeCodeVersion) // Do the actual stress work // - // BUG(github #10318) - when not using allocation contexts, the alloc lock + CdacStress::MaybeVerify(pThread, regs); + + // BUG(github #10318)- when not using allocation contexts, the alloc lock // must be acquired here. Until fixed, this assert prevents random heap corruption. assert(GCHeapUtilities::UseThreadAllocationContexts()); GCHeapUtilities::GetGCHeap()->StressHeap(&t_runtime_thread_locals.alloc_context.m_GCAllocContext); diff --git a/src/coreclr/vm/gchelpers.cpp b/src/coreclr/vm/gchelpers.cpp index 7eb08201edd85e..21a22e19677ce6 100644 --- a/src/coreclr/vm/gchelpers.cpp +++ b/src/coreclr/vm/gchelpers.cpp @@ -30,6 +30,10 @@ #include "eeprofinterfaces.inl" #include "frozenobjectheap.h" +#ifdef HAVE_GCCOVER +#include "CdacStress.h" +#endif + #ifdef FEATURE_COMINTEROP #include "runtimecallablewrapper.h" #endif // FEATURE_COMINTEROP @@ -411,6 +415,9 @@ inline Object* Alloc(ee_alloc_context* pEEAllocContext, size_t size, GC_ALLOC_FL } } + // Verify cDAC stack references before the allocation-triggered GC (while refs haven't moved). + CdacStress::MaybeVerify(); + GCStress::MaybeTrigger(pAllocContext); // for SOH, if there is enough space in the current allocation context, then @@ -477,6 +484,7 @@ inline Object* Alloc(size_t size, GC_ALLOC_FLAGS flags) if (GCHeapUtilities::UseThreadAllocationContexts()) { ee_alloc_context *threadContext = GetThreadEEAllocContext(); + CdacStress::MaybeVerify(); GCStress::MaybeTrigger(&threadContext->m_GCAllocContext); retVal = Alloc(threadContext, size, flags); } @@ -484,6 +492,7 @@ inline Object* Alloc(size_t size, GC_ALLOC_FLAGS flags) { GlobalAllocLockHolder holder(&g_global_alloc_lock); ee_alloc_context *globalContext = &g_global_alloc_context; + CdacStress::MaybeVerify(); GCStress::MaybeTrigger(&globalContext->m_GCAllocContext); retVal = Alloc(globalContext, size, flags); } diff --git a/src/coreclr/vm/readytoruninfo.cpp b/src/coreclr/vm/readytoruninfo.cpp index 78dd7e82152ef7..06ce66933bf176 100644 --- a/src/coreclr/vm/readytoruninfo.cpp +++ b/src/coreclr/vm/readytoruninfo.cpp @@ -898,6 +898,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 a084354bba6dc7..6963a5000311e7 100644 --- a/src/coreclr/vm/readytoruninfo.h +++ b/src/coreclr/vm/readytoruninfo.h @@ -151,6 +151,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; @@ -198,6 +199,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; } @@ -403,6 +405,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/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index f4eaa99d79e484..7e99ecba7bdb3a 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -3773,6 +3773,8 @@ struct cdac_data static_assert(std::is_same().m_ExceptionState), ThreadExceptionState>::value, "Thread::m_ExceptionState is of type ThreadExceptionState"); static constexpr size_t ExceptionTracker = offsetof(Thread, m_ExceptionState) + offsetof(ThreadExceptionState, m_pCurrentTracker); + static constexpr size_t DebuggerFilterContext = offsetof(Thread, m_debuggerFilterContext); + static constexpr size_t ProfilerFilterContext = offsetof(Thread, m_pProfilerFilterContext); #ifndef TARGET_UNIX static constexpr size_t TEB = offsetof(Thread, m_pTEB); static constexpr size_t UEWatsonBucketTrackerBuckets = offsetof(Thread, m_ExceptionState) + offsetof(ThreadExceptionState, m_UEWatsonBucketTracker) 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..85fab9ac72bbe8 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 @@ -8,11 +8,25 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts; public interface IStackDataFrameHandle { }; +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; } +} + public interface IStackWalk : IContract { static string IContract.Name => nameof(StackWalk); public virtual IEnumerable CreateStackWalk(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/IThread.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs index e0ce63691a5313..4a1b3969548903 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 @@ -29,6 +29,7 @@ public enum ThreadState } public record struct ThreadData( + TargetPointer ThreadAddress, uint Id, TargetNUInt OSId, ThreadState State, 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 2a49c5a0d11569..579472df9193ce 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -151,6 +151,8 @@ public enum DataType HijackFrame, TailCallFrame, StubDispatchFrame, + ExternalMethodFrame, + DynamicHelperFrame, ComCallWrapper, SimpleComCallWrapper, ComMethodTable, 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/Constants.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs index 2585e72902f7ac..185e3ef8486841 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs @@ -75,6 +75,10 @@ public static class Globals public const string MethodDescTokenRemainderBitCount = nameof(MethodDescTokenRemainderBitCount); public const string DirectorySeparator = nameof(DirectorySeparator); + public const string TransitionBlockOffsetOfFirstGCRefMapSlot = nameof(TransitionBlockOffsetOfFirstGCRefMapSlot); + public const string TransitionBlockOffsetOfArgumentRegisters = nameof(TransitionBlockOffsetOfArgumentRegisters); + public const string TransitionBlockOffsetOfArgs = nameof(TransitionBlockOffsetOfArgs); + public const string ExecutionManagerCodeRangeMapAddress = nameof(ExecutionManagerCodeRangeMapAddress); public const string EEJitManagerAddress = nameof(EEJitManagerAddress); public const string StubCodeBlockLast = nameof(StubCodeBlockLast); 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..ada2d0a4a45459 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 @@ -65,13 +65,17 @@ public override void GetMethodRegionInfo( public override TargetPointer GetUnwindInfo(RangeSection rangeSection, TargetCodePointer jittedCodeAddress) { if (rangeSection.IsRangeList) + { return TargetPointer.Null; + } if (rangeSection.Data == null) throw new ArgumentException(nameof(rangeSection)); TargetPointer codeStart = FindMethodCode(rangeSection, jittedCodeAddress); if (codeStart == TargetPointer.Null) + { return TargetPointer.Null; + } Debug.Assert(codeStart.Value <= jittedCodeAddress.Value); if (!GetRealCodeHeader(rangeSection, codeStart, out Data.RealCodeHeader? realCodeHeader)) @@ -188,14 +192,18 @@ public override void GetExceptionClauses(RangeSection rangeSection, CodeBlockHan throw new ArgumentException(nameof(rangeSection)); Data.RealCodeHeader? realCodeHeader; - if (!GetRealCodeHeader(rangeSection, codeInfoHandle.Address, out realCodeHeader) || realCodeHeader == null) + // codeInfoHandle.Address is the IP, not the code start. We need to find the actual + // method start via the nibble map so GetRealCodeHeader reads at the correct offset. + TargetPointer codeStart = FindMethodCode(rangeSection, new TargetCodePointer(codeInfoHandle.Address.Value)); + if (!GetRealCodeHeader(rangeSection, codeStart, out realCodeHeader) || realCodeHeader == null) return; - if (realCodeHeader.JitEHInfo == null) + if (realCodeHeader.EHInfo == TargetPointer.Null) return; - TargetNUInt numEHInfos = Target.ReadNUInt(realCodeHeader.JitEHInfo.Address - (ulong)Target.PointerSize); - startAddr = realCodeHeader.JitEHInfo.Clauses; + Data.EEILException ehInfo = Target.ProcessedData.GetOrAdd(realCodeHeader.EHInfo); + TargetNUInt numEHInfos = Target.ReadNUInt(ehInfo.Address - (ulong)Target.PointerSize); + startAddr = ehInfo.Clauses; endAddr = startAddr + numEHInfos.Value * Target.GetTypeInfo(DataType.EEExceptionClause).Size!.Value; } } 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 e7a980a7b0f639..de5ed98934de7f 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 @@ -306,6 +306,36 @@ 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}"); + + IExecutionManager eman = this; + + if (!eman.IsFunclet(codeInfoHandle)) + return false; + + TargetPointer funcletStartAddress = eman.GetFuncletStartAddress(codeInfoHandle).AsTargetPointer; + uint funcletStartOffset = (uint)(funcletStartAddress - info.StartAddress); + + List clauses = eman.GetExceptionClauses(codeInfoHandle); + foreach (ExceptionClauseInfo clause in clauses) + { + if (clause.ClauseType == ExceptionClauseInfo.ExceptionClauseFlags.Filter && clause.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 236e017ac6b5c2..93f609a1d49780 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 @@ -22,6 +22,8 @@ internal ExecutionManager_1(Target target, Data.RangeSectionMap topRangeSectionM 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); + 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 ab5dea03e96066..4a8efb3c280039 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 @@ -22,6 +22,8 @@ internal ExecutionManager_2(Target target, Data.RangeSectionMap topRangeSectionM 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); + 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/GCInfoDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs index 3b51810689bdac..219dbaf1fa68c0 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() @@ -237,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); } @@ -267,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); } @@ -319,6 +334,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); @@ -497,6 +514,388 @@ public uint GetCodeLength() return _codeLength; } + public IReadOnlyList GetInterruptibleRanges() + { + EnsureDecodedTo(DecodePoints.InterruptibleRanges); + return _interruptibleRanges; + } + + /// + public uint? FindFirstInterruptiblePoint(uint startOffset, uint endOffset) + { + EnsureDecodedTo(DecodePoints.InterruptibleRanges); + + foreach (InterruptibleRange range in _interruptibleRanges) + { + if (range.EndOffset <= startOffset) + continue; + + if (startOffset >= range.StartOffset && startOffset < range.EndOffset) + return startOffset; + + if (range.StartOffset < endOffset) + return range.StartOffset; + } + + return null; + } + + public uint StackBaseRegister + { + get + { + EnsureDecodedTo(DecodePoints.ReversePInvoke); + return _stackBaseRegister; + } + } + + public uint NumTrackedSlots => _numSlots - _numUntrackedSlots; + + bool IGCInfoDecoder.EnumerateLiveSlots( + uint instructionOffset, + CodeManagerFlags flags, + LiveSlotCallback reportSlot) + { + return EnumerateLiveSlots(instructionOffset, flags, + (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. + /// + /// 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, + CodeManagerFlags flags, + Action reportSlot) + { + EnsureDecodedTo(DecodePoints.SlotTable); + + bool executionAborted = flags.HasFlag(CodeManagerFlags.ExecutionAborted); + bool reportScratchSlots = flags.HasFlag(CodeManagerFlags.ActiveStackFrame); + bool reportFpBasedSlotsOnly = flags.HasFlag(CodeManagerFlags.ReportFPBasedSlotsOnly); + + // WantsReportOnlyLeaf is always true for non-legacy formats + if (flags.HasFlag(CodeManagerFlags.ParentOfFuncletStackFrame)) + 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) + return true; // Native: goto ExitSuccess (skip all reporting including untracked) + } + + // 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, reportScratchSlots, reportFpBasedSlotsOnly, 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, reportScratchSlots, reportFpBasedSlotsOnly, reportSlot); + } + goto ReportUntracked; + } + else + { + // 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; + } + + // ---- 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, reportScratchSlots, reportFpBasedSlotsOnly, reportSlot); + + slotIdx++; + } + } + + ReportUntracked: + if (_numUntrackedSlots > 0 && (flags & (CodeManagerFlags.ParentOfFuncletStackFrame | CodeManagerFlags.NoReportUntracked)) == 0) + { + for (uint slotIndex = numTracked; slotIndex < _numSlots; slotIndex++) + ReportSlot(slotIndex, reportScratchSlots, reportFpBasedSlotsOnly, reportSlot); + } + + return true; + } + + private void ReportSlot(uint slotIndex, bool reportScratchSlots, bool reportFpBasedSlotsOnly, 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); + + 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.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; + } + + reportSlot(slotIndex, slot, gcFlags); + } + + private uint FindSafePoint(uint codeOffset) + { + EnsureDecodedTo(DecodePoints.InterruptibleRanges); + + 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++) + { + 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/IGCInfoDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs index 41bd8bdb3ea989..7c25381f31fb38 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,45 @@ // 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; +/// +/// Flags controlling GC reference reporting behavior. +/// These match the native ICodeManager flags in eetwain.h. +/// +[Flags] +internal enum CodeManagerFlags : uint +{ + ActiveStackFrame = 0x1, + ExecutionAborted = 0x2, + ParentOfFuncletStackFrame = 0x40, + NoReportUntracked = 0x80, + ReportFPBasedSlotsOnly = 0x200, +} + internal interface IGCInfoDecoder : IGCInfoHandle { uint GetCodeLength(); + uint StackBaseRegister { get; } + + /// + /// Finds the first interruptible point within the given handler range [startOffset, endOffset). + /// Returns null if no interruptible point exists in the range. + /// + uint? FindFirstInterruptiblePoint(uint startOffset, uint endOffset) => null; + + /// + /// Enumerates all live GC slots at the given instruction offset. + /// + /// Relative offset from method start. + /// CodeManagerFlags controlling reporting. + /// Callback: (isRegister, registerNumber, spOffset, spBase, gcFlags). + bool EnumerateLiveSlots( + uint instructionOffset, + CodeManagerFlags flags, + LiveSlotCallback reportSlot); } + +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/GCInfo/PlatformTraits/AMD64GCInfoTraits.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/AMD64GCInfoTraits.cs index a3c35bd9458791..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 @@ -40,4 +40,33 @@ 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; + } + + // 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 549cb48cbe8608..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 @@ -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; @@ -40,4 +40,18 @@ 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; + + // 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 486c3c5bc4348b..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 @@ -40,4 +40,18 @@ 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; + + // 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 51647a6a7fa600..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 @@ -47,4 +47,25 @@ internal interface IGCInfoTraits static abstract int NUM_INTERRUPTIBLE_RANGES_ENCBASE { get; } 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); + + /// + /// 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; + 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; } 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/GCInfo/PlatformTraits/LoongArch64GCInfoTraits.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/LoongArch64GCInfoTraits.cs index 39e0f13bfcc90d..489739a5ade160 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/LoongArch64GCInfoTraits.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/LoongArch64GCInfoTraits.cs @@ -40,4 +40,8 @@ internal class LoongArch64GCInfoTraits : IGCInfoTraits public static int NUM_INTERRUPTIBLE_RANGES_ENCBASE => 1; public static bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA => true; + + // LoongArch64 scratch registers: RA (reg 1), A0-A7 (4-11), T0-T8 (12-21) + // See gcinfodecoder.cpp IsScratchRegister for TARGET_LOONGARCH64 + public static bool IsScratchRegister(uint regNum) => regNum == 1 || (regNum >= 4 && regNum <= 21); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/RISCV64GCInfoTraits.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/RISCV64GCInfoTraits.cs index ab64543814230e..7bf3a1421e100c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/RISCV64GCInfoTraits.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/RISCV64GCInfoTraits.cs @@ -40,4 +40,9 @@ internal class RISCV64GCInfoTraits : IGCInfoTraits public static int NUM_INTERRUPTIBLE_RANGES_ENCBASE => 1; public static bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA => true; + + // RISCV64 scratch registers: RA (1), T0-T2 (5-7), A0-A7 (10-17), T3-T6 (28-31) + // See gcinfodecoder.cpp IsScratchRegister for TARGET_RISCV64 + public static bool IsScratchRegister(uint regNum) + => regNum == 1 || (regNum >= 5 && regNum <= 7) || (regNum >= 10 && regNum <= 17) || regNum >= 28; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64/AMD64Unwinder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64/AMD64Unwinder.cs index 6f4253dbfff624..7c4666a88c1104 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64/AMD64Unwinder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64/AMD64Unwinder.cs @@ -46,12 +46,20 @@ public bool Unwind(ref AMD64Context context) UnwindCode unwindOp; if (_eman.GetCodeBlockHandle(context.InstructionPointer.Value) is not CodeBlockHandle cbh) + { return false; + } TargetPointer controlPC = context.InstructionPointer; TargetPointer imageBase = _eman.GetUnwindInfoBaseAddress(cbh); - Data.RuntimeFunction functionEntry = _target.ProcessedData.GetOrAdd(_eman.GetUnwindInfo(cbh)); + TargetPointer unwindInfoAddr = _eman.GetUnwindInfo(cbh); + + if (unwindInfoAddr == TargetPointer.Null) + { + return false; + } + Data.RuntimeFunction functionEntry = _target.ProcessedData.GetOrAdd(unwindInfoAddr); if (functionEntry.EndAddress is null) return false; if (GetUnwindInfoHeader(imageBase + functionEntry.UnwindData) is not UnwindInfoHeader unwindInfo) 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 5136c9ce637b49..8ab2e2468da526 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 @@ -33,6 +33,8 @@ public enum ContextFlagsValues : uint public readonly uint Size => 0x4d0; public readonly uint DefaultContextFlags => (uint)ContextFlagsValues.CONTEXT_ALL; + public readonly int StackPointerRegister => 4; + public TargetPointer StackPointer { readonly get => new(Rsp); 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 4c5765adf05d29..f88f9a9ecfb62b 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 @@ -43,6 +43,8 @@ public enum ContextFlagsValues : uint ContextFlagsValues.CONTEXT_FLOATING_POINT | ContextFlagsValues.CONTEXT_DEBUG_REGISTERS); + public readonly int StackPointerRegister => 31; + public TargetPointer StackPointer { readonly get => new(Sp); 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 e785d35d4c9692..abbcd8805257a8 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 @@ -31,6 +31,8 @@ public enum ContextFlagsValues : uint public readonly uint Size => 0x1a0; public readonly uint DefaultContextFlags => (uint)ContextFlagsValues.CONTEXT_ALL; + public readonly int StackPointerRegister => 13; + public TargetPointer StackPointer { readonly get => new(Sp); 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 a4c3394437431c..c2bffd2ad361b8 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 @@ -14,6 +14,8 @@ public sealed class ContextHolder : IPlatformAgnosticContext, IEquatable Context.Size; public uint DefaultContextFlags => Context.DefaultContextFlags; + public int StackPointerRegister => Context.StackPointerRegister; + public TargetPointer StackPointer { get => Context.StackPointer; set => Context.StackPointer = value; } public TargetPointer InstructionPointer { get => Context.InstructionPointer; set => Context.InstructionPointer = value; } public TargetPointer FramePointer { get => Context.FramePointer; set => Context.FramePointer = value; } 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 46c2d6c16affaa..c95012b12b74a8 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 @@ -10,6 +10,8 @@ public interface IPlatformAgnosticContext public abstract uint Size { get; } public abstract uint DefaultContextFlags { get; } + public int StackPointerRegister { get; } + public TargetPointer StackPointer { get; set; } public TargetPointer InstructionPointer { get; set; } public TargetPointer FramePointer { get; set; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformContext.cs index fec353c5e3400a..df26023ee54a4f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformContext.cs @@ -8,6 +8,8 @@ public interface IPlatformContext uint Size { get; } uint DefaultContextFlags { get; } + int StackPointerRegister { get; } + TargetPointer StackPointer { get; set; } TargetPointer InstructionPointer { get; set; } TargetPointer FramePointer { get; set; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/LoongArch64Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/LoongArch64Context.cs index 520e6f3c3d0fcb..6acf124a0d11c1 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/LoongArch64Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/LoongArch64Context.cs @@ -41,6 +41,8 @@ public enum ContextFlagsValues : uint ContextFlagsValues.CONTEXT_FLOATING_POINT | ContextFlagsValues.CONTEXT_DEBUG_REGISTERS); + public readonly int StackPointerRegister => 3; + public TargetPointer StackPointer { readonly get => new(Sp); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RISCV64Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RISCV64Context.cs index ef275ac9c2f6dd..d401d90d89cda3 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RISCV64Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RISCV64Context.cs @@ -38,6 +38,8 @@ public enum ContextFlagsValues : uint public readonly uint DefaultContextFlags => (uint)ContextFlagsValues.CONTEXT_ALL; + public readonly int StackPointerRegister => 2; + public TargetPointer StackPointer { readonly get => new(Sp); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86Context.cs index fab6cfbc06d9f1..505da9a8d52889 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86Context.cs @@ -40,6 +40,8 @@ public enum ContextFlagsValues : uint public readonly uint Size => 0x2cc; public readonly uint DefaultContextFlags => (uint)ContextFlagsValues.CONTEXT_ALL; + public readonly int StackPointerRegister => 4; + public TargetPointer StackPointer { readonly get => new(Esp); 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..767f8527418e07 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/ExceptionHandling.cs @@ -0,0 +1,219 @@ +// 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 +{ + /// + /// 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. + /// + /// + /// 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); + // ExceptionTracker is the address of the field on the Thread object. + // Dereference to get the actual ExInfo pointer. + return _target.ReadPointer(thread.ExceptionTracker); + } + + private bool HasFrameBeenUnwoundByAnyActiveException(IStackDataFrameHandle stackDataFrameHandle) + { + StackDataFrameHandle handle = AssertCorrectHandle(stackDataFrameHandle); + + TargetPointer callerStackPointer; + if (handle.State is StackWalkState.SW_FRAMELESS) + { + IPlatformAgnosticContext callerContext = handle.Context.Clone(); + callerContext.Unwind(_target); + callerStackPointer = callerContext.StackPointer; + } + else + { + callerStackPointer = handle.FrameAddress; + } + + 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(TargetPointer callerStackPointer, Data.ExceptionInfo exceptionInfo) + { + // 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) + { + return false; + } + + return (exceptionInfo.StackLowBound < callerStackPointer) && (callerStackPointer <= exceptionInfo.StackHighBound); + } + + /// + /// Checks if the current frame is the throw-site frame during exception first-pass. + /// During first pass (UnwindHasStarted=0), the ExInfo's StackLowBound is set to the + /// SP of the frame that threw the exception. The legacy DAC does not report GC refs + /// from this frame during first pass. + /// + private bool IsAtFirstPassExceptionThrowSite(IStackDataFrameHandle stackDataFrameHandle) + { + StackDataFrameHandle handle = AssertCorrectHandle(stackDataFrameHandle); + if (handle.State is not StackWalkState.SW_FRAMELESS) + return false; + + TargetPointer frameSP = handle.Context.StackPointer; + + TargetPointer pExInfo = GetCurrentExceptionTracker(handle); + while (pExInfo != TargetPointer.Null) + { + Data.ExceptionInfo exInfo = _target.ProcessedData.GetOrAdd(pExInfo); + pExInfo = exInfo.PreviousNestedInfo; + + // First pass only (unwind has NOT started) + if ((exInfo.ExceptionFlags & (uint)ExceptionFlagsEnum.UnwindHasStarted) != 0) + continue; + + // Check for empty range (ExInfo just created) + if (exInfo.StackLowBound == TargetPointer.PlatformMaxValue(_target) + && exInfo.StackHighBound == TargetPointer.Null) + continue; + + // The throw-site frame's SP matches the ExInfo's StackLowBound + if (frameSP == exInfo.StackLowBound) + return true; + } + + return false; + } + +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/CorSigParser.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/CorSigParser.cs new file mode 100644 index 00000000000000..b8c6a0a173552b --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/CorSigParser.cs @@ -0,0 +1,212 @@ +// 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; + +/// +/// Minimal signature parser for GC reference classification of method parameters. +/// Parses the ECMA-335 II.23.2.1 MethodDefSig format, classifying each parameter +/// type as a GC reference, interior pointer, value type, or non-GC primitive. +/// +internal ref struct CorSigParser +{ + private ReadOnlySpan _sig; + private int _index; + private readonly int _pointerSize; + + public CorSigParser(ReadOnlySpan signature, int pointerSize) + { + _sig = signature; + _index = 0; + _pointerSize = pointerSize; + } + + public bool AtEnd => _index >= _sig.Length; + + public byte ReadByte() + { + if (_index >= _sig.Length) + throw new InvalidOperationException("Unexpected end of signature."); + return _sig[_index++]; + } + + /// + /// Reads a compressed unsigned integer (ECMA-335 II.23.2). + /// + public uint ReadCompressedUInt() + { + byte b = ReadByte(); + if ((b & 0x80) == 0) + return b; + if ((b & 0xC0) == 0x80) + { + byte b2 = ReadByte(); + return (uint)(((b & 0x3F) << 8) | b2); + } + if ((b & 0xE0) == 0xC0) + { + byte b2 = ReadByte(); + byte b3 = ReadByte(); + byte b4 = ReadByte(); + return (uint)(((b & 0x1F) << 24) | (b2 << 16) | (b3 << 8) | b4); + } + throw new InvalidOperationException("Invalid compressed integer encoding."); + } + + /// + /// Reads the next type from the signature and classifies it for GC scanning. + /// Advances past the full type encoding. + /// + public GcTypeKind ReadTypeAndClassify() + { + CorElementType elemType = (CorElementType)ReadCompressedUInt(); + + switch (elemType) + { + case CorElementType.Void: + case CorElementType.Boolean: + case CorElementType.Char: + case CorElementType.I1: + case CorElementType.U1: + case CorElementType.I2: + case CorElementType.U2: + case CorElementType.I4: + case CorElementType.U4: + case CorElementType.I8: + case CorElementType.U8: + case CorElementType.R4: + case CorElementType.R8: + case CorElementType.I: + case CorElementType.U: + return GcTypeKind.None; + + case CorElementType.String: + case CorElementType.Object: + return GcTypeKind.Ref; + + case CorElementType.Class: + ReadCompressedUInt(); // TypeDefOrRefOrSpecEncoded + return GcTypeKind.Ref; + + case CorElementType.ValueType: + ReadCompressedUInt(); // TypeDefOrRefOrSpecEncoded + return GcTypeKind.Other; + + case CorElementType.SzArray: + SkipType(); // element type + return GcTypeKind.Ref; + + case CorElementType.Array: + SkipType(); // element type + SkipArrayShape(); + return GcTypeKind.Ref; + + case CorElementType.GenericInst: + { + byte baseType = ReadByte(); // CLASS, VALUETYPE, or INTERNAL + if (baseType == (byte)CorElementType.Internal) + { + // ELEMENT_TYPE_INTERNAL embeds a raw pointer to a TypeHandle + _index += _pointerSize; + } + else + { + ReadCompressedUInt(); // TypeDefOrRefOrSpecEncoded + } + uint argCount = ReadCompressedUInt(); + for (uint i = 0; i < argCount; i++) + SkipType(); + // Conservative: treat INTERNAL base as Ref (could be either class or valuetype). + // CLASS-based generics are Ref; VALUETYPE-based and unknown are Other. + return baseType == (byte)CorElementType.Class ? GcTypeKind.Ref : GcTypeKind.Other; + } + + case CorElementType.Byref: + SkipType(); // inner type + return GcTypeKind.Interior; + + case CorElementType.Ptr: + SkipType(); // pointee type + return GcTypeKind.None; + + case CorElementType.FnPtr: + SkipMethodSignature(); + return GcTypeKind.None; + + case CorElementType.TypedByRef: + return GcTypeKind.Other; + + case CorElementType.Var: + case CorElementType.MVar: + ReadCompressedUInt(); // type parameter index + // Conservative: generic type params could be GC refs. + // The runtime resolves these via the generic context. + // For now, treat as potential GC ref to avoid missing references. + return GcTypeKind.Ref; + + case CorElementType.CModReqd: + case CorElementType.CModOpt: + ReadCompressedUInt(); // TypeDefOrRefOrSpecEncoded + return ReadTypeAndClassify(); // recurse past the modifier + + case CorElementType.Sentinel: + return ReadTypeAndClassify(); // skip sentinel, read next type + + case CorElementType.Internal: + // Runtime-internal type: raw pointer to TypeHandle follows. + // Skip the pointer bytes. Conservative: treat as potential GC ref. + _index += _pointerSize; + return GcTypeKind.Ref; + + default: + return GcTypeKind.None; + } + } + + /// + /// Skips over a complete type encoding in the signature. + /// + public void SkipType() + { + ReadTypeAndClassify(); // Same traversal, just discard the result + } + + private void SkipArrayShape() + { + _ = ReadCompressedUInt(); // rank + uint numSizes = ReadCompressedUInt(); + for (uint i = 0; i < numSizes; i++) + ReadCompressedUInt(); + uint numLoBounds = ReadCompressedUInt(); + for (uint i = 0; i < numLoBounds; i++) + ReadCompressedUInt(); // lo bounds are signed but encoded as unsigned + } + + private void SkipMethodSignature() + { + byte callingConv = ReadByte(); + if ((callingConv & 0x10) != 0) // GENERIC + ReadCompressedUInt(); // generic param count + uint paramCount = ReadCompressedUInt(); + SkipType(); // return type + for (uint i = 0; i < paramCount; i++) + SkipType(); + } +} + +/// +/// Classification of a signature type for GC scanning purposes. +/// +internal enum GcTypeKind +{ + /// Not a GC reference (primitives, pointers). + None, + /// Object reference (class, string, array). + Ref, + /// Interior pointer (byref). + Interior, + /// Value type that may contain embedded GC references. + Other, +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs new file mode 100644 index 00000000000000..c384a1394431db --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +/// +/// Token values from CORCOMPILE_GCREFMAP_TOKENS (corcompile.h). +/// These indicate the type of GC reference at each transition block slot. +/// +internal enum GCRefMapToken +{ + Skip = 0, + Ref = 1, + Interior = 2, + MethodParam = 3, + TypeParam = 4, + VASigCookie = 5, +} + +/// +/// Managed port of the native GCRefMapDecoder (gcrefmap.h:158-246). +/// Decodes a compact bitstream describing which transition block slots +/// contain GC references for a given call site. +/// +internal ref struct GCRefMapDecoder +{ + private readonly Target _target; + private TargetPointer _currentByte; + private int _pendingByte; + private int _pos; + + public GCRefMapDecoder(Target target, TargetPointer blob) + { + _target = target; + _currentByte = blob; + _pendingByte = 0x80; // Forces first byte read + _pos = 0; + } + + public bool AtEnd => _pendingByte == 0; + + public int CurrentPos => _pos; + + private int GetBit() + { + int x = _pendingByte; + if ((x & 0x80) != 0) + { + x = _target.Read(_currentByte); + _currentByte = new TargetPointer(_currentByte.Value + 1); + x |= ((x & 0x80) << 7); + } + _pendingByte = x >> 1; + return x & 1; + } + + private int GetTwoBit() + { + int result = GetBit(); + result |= GetBit() << 1; + return result; + } + + private int GetInt() + { + int result = 0; + int bit = 0; + do + { + result |= GetBit() << (bit++); + result |= GetBit() << (bit++); + result |= GetBit() << (bit++); + } + while (GetBit() != 0); + return result; + } + + /// + /// x86 only: Read the stack pop count from the stream. + /// + public uint ReadStackPop() + { + int x = GetTwoBit(); + if (x == 3) + x = GetInt() + 3; + return (uint)x; + } + + /// + /// Read the next GC reference token from the stream. + /// Advances CurrentPos as appropriate. + /// + public GCRefMapToken ReadToken() + { + int val = GetTwoBit(); + if (val == 3) + { + int ext = GetInt(); + if ((ext & 1) == 0) + { + _pos += (ext >> 1) + 4; + return GCRefMapToken.Skip; + } + else + { + _pos++; + return (GCRefMapToken)((ext >> 1) + 3); + } + } + _pos++; + return (GCRefMapToken)val; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs new file mode 100644 index 00000000000000..184a875c908980 --- /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; + +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) + { + 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 + // https://github.com/dotnet/runtime/issues/125728 + 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 + // https://github.com/dotnet/runtime/issues/125728 + throw new NotImplementedException(); + } + + // Read the object pointer from the stack slot, matching legacy DAC behavior + // (DacStackReferenceWalker::GCReportCallback in daccess.cpp) + TargetPointer obj = _target.ReadPointer(ppObj); + + StackRefData data = new() + { + HasRegisterInformation = false, + Register = 0, + Offset = 0, + Address = ppObj, + 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); + } +} 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..0575b625d5b9d4 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanFlags.cs @@ -0,0 +1,14 @@ +// 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 +{ + None = 0x0, + 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..e9829ab4bceba0 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanSlotLocation.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +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..72063a93fa6c01 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -0,0 +1,112 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +internal class GcScanner +{ + private readonly Target _target; + private readonly IExecutionManager _eman; + private readonly IGCInfo _gcInfo; + + internal GcScanner(Target target) + { + _target = target; + _eman = target.Contracts.ExecutionManager; + _gcInfo = target.Contracts.GCInfo; + } + + public bool EnumGcRefs( + IPlatformAgnosticContext context, + CodeBlockHandle cbh, + CodeManagerFlags flags, + GcScanContext scanContext, + uint? relOffsetOverride = null) + { + TargetNUInt relativeOffset = _eman.GetRelativeOffset(cbh); + _eman.GetGCInfo(cbh, out TargetPointer gcInfoAddr, out uint gcVersion); + + if (_eman.IsFilterFunclet(cbh)) + flags |= CodeManagerFlags.NoReportUntracked; + + IGCInfoHandle handle = _gcInfo.DecodePlatformSpecificGCInfo(gcInfoAddr, gcVersion); + if (handle is not IGCInfoDecoder decoder) + return false; + + uint stackBaseRegister = decoder.StackBaseRegister; + + // Lazily compute the caller SP for GC_CALLER_SP_REL slots. + // The native code uses GET_CALLER_SP(pRD) which comes from EnsureCallerContextIsValid. + TargetPointer? callerSP = null; + + uint offsetToUse = relOffsetOverride ?? (uint)relativeOffset.Value; + + return decoder.EnumerateLiveSlots( + offsetToUse, + flags, + (bool isRegister, uint registerNumber, int spOffset, uint spBase, uint gcFlags) => + { + GcScanFlags scanFlags = GcScanFlags.None; + if ((gcFlags & 0x1) != 0) // GC_SLOT_INTERIOR + scanFlags |= GcScanFlags.GC_CALL_INTERIOR; + if ((gcFlags & 0x2) != 0) // GC_SLOT_PINNED + scanFlags |= GcScanFlags.GC_CALL_PINNED; + + if (isRegister) + { + TargetPointer regValue = ReadRegisterValue(context, (int)registerNumber); + GcScanSlotLocation loc = new((int)registerNumber, 0, false); + scanContext.GCEnumCallback(regValue, scanFlags, loc); + } + else + { + int spReg = context.StackPointerRegister; + int reg = spBase switch + { + 1 => spReg, // GC_SP_REL → SP register number + 2 => (int)stackBaseRegister, // GC_FRAMEREG_REL → frame base register + 0 => -(spReg + 1), // GC_CALLER_SP_REL → -(SP + 1) + _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), + }; + TargetPointer baseAddr = spBase switch + { + 1 => context.StackPointer, // GC_SP_REL + 2 => ReadRegisterValue(context, (int)stackBaseRegister), // GC_FRAMEREG_REL + 0 => GetCallerSP(context, ref callerSP), // GC_CALLER_SP_REL + _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), + }; + + TargetPointer addr = new(baseAddr.Value + (ulong)(long)spOffset); + GcScanSlotLocation loc = new(reg, spOffset, true); + scanContext.GCEnumCallback(addr, scanFlags, loc); + } + }); + } + + /// + /// Compute the caller's SP by unwinding the current context one frame. + /// Cached in to avoid repeated unwinds for the same frame. + /// + private TargetPointer GetCallerSP(IPlatformAgnosticContext context, ref TargetPointer? cached) + { + if (cached is null) + { + IPlatformAgnosticContext callerContext = context.Clone(); + callerContext.Unwind(_target); + cached = callerContext.StackPointer; + } + return cached.Value; + } + + private static TargetPointer ReadRegisterValue(IPlatformAgnosticContext context, int registerNumber) + { + if (!context.TryReadRegister(registerNumber, out TargetNUInt value)) + throw new ArgumentOutOfRangeException(nameof(registerNumber), $"Register number {registerNumber} not found"); + + return new TargetPointer(value.Value); + } + +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs new file mode 100644 index 00000000000000..46e5bac46f6431 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.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..1e1e271a5d9635 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,19 +5,24 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics; using System.Collections.Generic; -using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; +using Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; using Microsoft.Diagnostics.DataContractReader.Data; +using System.Linq; 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,41 +43,573 @@ public enum StackWalkState private record StackDataFrameHandle( IPlatformAgnosticContext Context, StackWalkState State, - TargetPointer FrameAddress) : IStackDataFrameHandle + TargetPointer FrameAddress, + ThreadData ThreadData, + bool IsResumableFrame = false, + bool IsActiveFrame = false) : IStackDataFrameHandle { } - private class StackWalkData(IPlatformAgnosticContext context, StackWalkState state, FrameIterator frameIter) + private class StackWalkData(IPlatformAgnosticContext context, StackWalkState state, FrameIterator frameIter, ThreadData threadData, bool skipActiveICFOnce = false) { 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); + // When an active InlinedCallFrame is processed as SW_FRAME without advancing + // the FrameIterator, the same Frame would be re-encountered by + // CheckForSkippedFrames. This one-shot flag tells CheckForSkippedFrames to + // advance past it once, preventing a duplicate SW_SKIPPED_FRAME yield. + // + // Must be false for ClrDataStackWalk (which needs exact DAC frame parity) + // and true for WalkStackReferences (which matches native DacStackReferenceWalker + // behavior of not re-enumerating the same InlinedCallFrame). + public bool SkipActiveICFOnce { get; } = skipActiveICFOnce; + public bool SkipCurrentFrameInCheck { get; set; } + + + // 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 (implicit in line 2235 assignment) + /// + 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) + => CreateStackWalkCore(threadData, skipInitialFrames: false); + + /// + /// Core stack walk implementation. + /// + /// Thread to walk. + /// + /// When true, pre-advances the FrameIterator past explicit Frames below the initial + /// managed frame's caller SP. This matches the native DacStackReferenceWalker behavior + /// for GC reference enumeration, where these frames are within the current managed + /// frame's stack range and don't contribute additional GC roots. + /// + /// Must be false for ClrDataStackWalk, which advances the cDAC and legacy DAC in + /// lockstep and must yield the same frame sequence (including initial skipped frames). + /// + private IEnumerable CreateStackWalkCore(ThreadData threadData, bool skipInitialFrames) { IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); FillContextFromThread(context, threadData); StackWalkState state = IsManaged(context.InstructionPointer, out _) ? StackWalkState.SW_FRAMELESS : StackWalkState.SW_FRAME; FrameIterator frameIterator = new(_target, threadData); - // if the next Frame is not valid and we are not in managed code, there is nothing to return + if (skipInitialFrames) + { + TargetPointer skipBelowSP; + if (state == StackWalkState.SW_FRAMELESS) + { + IPlatformAgnosticContext callerCtx = context.Clone(); + callerCtx.Unwind(_target); + skipBelowSP = callerCtx.StackPointer; + } + else + { + skipBelowSP = context.StackPointer; + } + while (frameIterator.IsValid() && frameIterator.CurrentFrameAddress.Value < skipBelowSP.Value) + { + frameIterator.Next(); + } + } + if (state == StackWalkState.SW_FRAME && !frameIterator.IsValid()) { yield break; } - StackWalkData stackWalkData = new(context, state, frameIterator); + StackWalkData stackWalkData = new(context, state, frameIterator, threadData, skipActiveICFOnce: skipInitialFrames); yield return stackWalkData.ToDataFrame(); + stackWalkData.AdvanceIsFirst(); while (Next(stackWalkData)) { yield return stackWalkData.ToDataFrame(); + stackWalkData.AdvanceIsFirst(); + } + } + + 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. + IEnumerable stackFrames = CreateStackWalkCore(threadData, skipInitialFrames: true); + IEnumerable frames = stackFrames.Select(AssertCorrectHandle); + IEnumerable gcFrames = Filter(frames); + + GcScanContext scanContext = new(_target, resolveInteriorPointers: false); + + foreach (GCFrameData gcFrame in gcFrames) + { + try + { + _ = ((IStackWalk)this).GetMethodDescPtr(gcFrame.Frame); + + bool reportGcReferences = gcFrame.ShouldCrawlFrameReportGCReferences; + + + TargetPointer pFrame = ((IStackWalk)this).GetFrameAddress(gcFrame.Frame); + scanContext.UpdateScanContext( + gcFrame.Frame.Context.StackPointer, + gcFrame.Frame.Context.InstructionPointer, + pFrame); + + if (reportGcReferences) + { + if (gcFrame.Frame.State == StackWalkState.SW_FRAMELESS) + { + if (!IsManaged(gcFrame.Frame.Context.InstructionPointer, out CodeBlockHandle? cbh)) + throw new InvalidOperationException("Expected managed code"); + + CodeManagerFlags codeManagerFlags = gcFrame.Frame.IsActiveFrame + ? CodeManagerFlags.ActiveStackFrame + : 0; + + if (gcFrame.ShouldParentToFuncletSkipReportingGCReferences) + codeManagerFlags |= CodeManagerFlags.ParentOfFuncletStackFrame; + + uint? relOffsetOverride = null; + if (gcFrame.ShouldParentFrameUseUnwindTargetPCforGCReporting) + { + _eman.GetGCInfo(cbh.Value, out TargetPointer gcInfoAddr, out uint gcVersion); + IGCInfoHandle gcHandle = _target.Contracts.GCInfo.DecodePlatformSpecificGCInfo(gcInfoAddr, gcVersion); + if (gcHandle is IGCInfoDecoder decoder) + { + relOffsetOverride = decoder.FindFirstInterruptiblePoint( + gcFrame.ClauseForCatchHandlerStartPC, + gcFrame.ClauseForCatchHandlerEndPC); + } + } + + GcScanner gcScanner = new(_target); + gcScanner.EnumGcRefs(gcFrame.Frame.Context, cbh.Value, codeManagerFlags, scanContext, relOffsetOverride); + } + else + { + ScanFrameRoots(gcFrame.Frame, scanContext); + } + } + } + catch (System.Exception ex) + { + Debug.WriteLine($"Exception during WalkStackReferences at IP=0x{gcFrame.Frame.Context.InstructionPointer:X}: {ex.GetType().Name}: {ex.Message}"); + } + } + + 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 + { + public GCFrameData(StackDataFrameHandle frame) + { + Frame = frame; + } + + public StackDataFrameHandle Frame { get; } + public bool ShouldParentToFuncletSkipReportingGCReferences { get; set; } + public bool ShouldCrawlFrameReportGCReferences { get; set; } // required + public bool ShouldParentFrameUseUnwindTargetPCforGCReporting { get; set; } + public uint ClauseForCatchHandlerStartPC { get; set; } + public uint ClauseForCatchHandlerEndPC { get; set; } + } + + 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; + TargetPointer parentStackFrame = TargetPointer.Null; + TargetPointer funcletParentStackFrame = TargetPointer.Null; + TargetPointer intermediaryFuncletParentStackFrame; + + 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) + { + movedPastFirstExInfo = true; + } + } + + // 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; + + // 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; + } + } + } + } + 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; + + // 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; + + gcFrame.ClauseForCatchHandlerStartPC = exInfo.ClauseForCatchHandlerStartPC; + gcFrame.ClauseForCatchHandlerEndPC = exInfo.ClauseForCatchHandlerEndPC; + } + else if (!IsFunclet(handle)) + { + 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) + { + // Skip intermediate frames between funclet and parent. + // The native runtime unconditionally skips these frames. + break; + } + } + } + } + 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; + } + else if (IsAtFirstPassExceptionThrowSite(handle)) + { + // During first-pass exception handling, the throw-site frame is + // being dispatched. The legacy DAC does not report GC refs from + // this frame during first pass. Suppress to match DAC behavior. + 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) @@ -89,6 +626,10 @@ private bool Next(StackWalkData handle) } break; case StackWalkState.SW_SKIPPED_FRAME: + // Skipped Frames still need UpdateContextFromFrame if they restore + // a context (e.g., SoftwareExceptionFrame, ResumableFrame). The native + // StackFrameIterator always calls UpdateRegDisplay for these frames. + handle.FrameIter.UpdateContextFromFrame(handle.Context); handle.FrameIter.Next(); break; case StackWalkState.SW_FRAME: @@ -97,6 +638,16 @@ private bool Next(StackWalkData handle) { handle.FrameIter.Next(); } + else + { + // Active InlinedCallFrame: FrameIter was NOT advanced. The next + // CheckForSkippedFrames would re-encounter this same Frame and + // create a spurious SW_SKIPPED_FRAME -> SW_FRAMELESS duplicate. + // Only applies to WalkStackReferences path — ClrDataStackWalk + // must yield Frames in the same order as the legacy DAC. + if (handle.SkipActiveICFOnce) + handle.SkipCurrentFrameInCheck = true; + } break; case StackWalkState.SW_ERROR: case StackWalkState.SW_COMPLETE: @@ -149,6 +700,19 @@ private bool CheckForSkippedFrames(StackWalkData handle) return false; } + // If the current Frame was already processed as SW_FRAME (e.g., an active + // InlinedCallFrame that wasn't advanced), skip it once to avoid a duplicate + // SW_SKIPPED_FRAME -> SW_FRAMELESS yield for the same managed IP. + if (handle.SkipCurrentFrameInCheck) + { + handle.SkipCurrentFrameInCheck = false; + handle.FrameIter.Next(); + if (!handle.FrameIter.IsValid()) + { + return false; + } + } + // get the caller context IPlatformAgnosticContext parentContext = handle.Context.Clone(); parentContext.Unwind(_target); @@ -181,7 +745,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 +765,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 +793,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; @@ -246,10 +808,28 @@ private bool IsManaged(TargetPointer ip, [NotNullWhen(true)] out CodeBlockHandle return false; } - private unsafe void FillContextFromThread(IPlatformAgnosticContext context, ThreadData threadData) + private void FillContextFromThread(IPlatformAgnosticContext context, ThreadData threadData) { byte[] bytes = new byte[context.Size]; Span buffer = new Span(bytes); + + // Match the native DacStackReferenceWalker behavior: if the thread has a + // FilterContext or ProfilerFilterContext set, use that instead of calling + // GetThreadContext. During debugger breaks, GC stress redirection, or + // profiler stack walks, these contexts hold the correct managed frame state. + Data.Thread thread = _target.ProcessedData.GetOrAdd(threadData.ThreadAddress); + + TargetPointer filterContext = thread.DebuggerFilterContext; + if (filterContext == TargetPointer.Null) + filterContext = thread.ProfilerFilterContext; + + if (filterContext != TargetPointer.Null) + { + _target.ReadBuffer(filterContext.Value, buffer); + context.FillFromBuffer(buffer); + return; + } + // The underlying ICLRDataTarget.GetThreadContext has some variance depending on the host. // SOS's managed implementation sets the ContextFlags to platform specific values defined in ThreadService.cs (diagnostics repo) // SOS's native implementation keeps the ContextFlags passed into this function. @@ -272,4 +852,329 @@ 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) + { + TargetPointer frameAddress = frame.FrameAddress; + if (frameAddress == TargetPointer.Null) + return; + + // Read the frame's VTable pointer (Identifier) to determine its type. + // GetFrameName expects a VTable identifier, not a frame address. + Data.Frame frameData = _target.ProcessedData.GetOrAdd(frameAddress); + string frameName = ((IStackWalk)this).GetFrameName(frameData.Identifier); + + switch (frameName) + { + case "StubDispatchFrame": + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + Data.StubDispatchFrame sdf = _target.ProcessedData.GetOrAdd(frameAddress); + if (sdf.GCRefMap != TargetPointer.Null) + { + PromoteCallerStackUsingGCRefMap(fmf.TransitionBlockPtr, sdf.GCRefMap, scanContext); + } + else + { + PromoteCallerStackUsingMetaSig(frameAddress, fmf.TransitionBlockPtr, scanContext); + } + break; + } + + case "ExternalMethodFrame": + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + Data.ExternalMethodFrame emf = _target.ProcessedData.GetOrAdd(frameAddress); + if (emf.GCRefMap != TargetPointer.Null) + { + PromoteCallerStackUsingGCRefMap(fmf.TransitionBlockPtr, emf.GCRefMap, scanContext); + } + break; + } + + case "DynamicHelperFrame": + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + Data.DynamicHelperFrame dhf = _target.ProcessedData.GetOrAdd(frameAddress); + ScanDynamicHelperFrame(fmf.TransitionBlockPtr, dhf.DynamicHelperFrameFlags, scanContext); + break; + } + + case "CallCountingHelperFrame": + case "PrestubMethodFrame": + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + PromoteCallerStackUsingMetaSig(frameAddress, fmf.TransitionBlockPtr, scanContext); + break; + } + + case "CLRToCOMMethodFrame": + case "ComPrestubMethodFrame": + // These frames call PromoteCallerStack to report method arguments. + // TODO(stackref): Implement PromoteCallerStack for COM interop frames + 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. + break; + } + } + + /// + /// Decodes a GCRefMap bitstream and reports GC references in the transition block. + /// Port of native TransitionFrame::PromoteCallerStackUsingGCRefMap (frames.cpp). + /// + private void PromoteCallerStackUsingGCRefMap( + TargetPointer transitionBlock, + TargetPointer gcRefMapBlob, + GcScanContext scanContext) + { + GCRefMapDecoder decoder = new(_target, gcRefMapBlob); + + // x86: skip stack pop count + if (_target.PointerSize == 4) + decoder.ReadStackPop(); + + while (!decoder.AtEnd) + { + int pos = decoder.CurrentPos; + GCRefMapToken token = decoder.ReadToken(); + uint offset = OffsetFromGCRefMapPos(pos); + TargetPointer slotAddress = new(transitionBlock.Value + offset); + + switch (token) + { + case GCRefMapToken.Skip: + break; + + case GCRefMapToken.Ref: + scanContext.GCReportCallback(slotAddress, GcScanFlags.None); + break; + + case GCRefMapToken.Interior: + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + break; + + case GCRefMapToken.MethodParam: + case GCRefMapToken.TypeParam: + // The DAC skips these (guarded by #ifndef DACCESS_COMPILE in native). + // They represent loader allocator references, not managed GC refs. + break; + + case GCRefMapToken.VASigCookie: + // VASigCookie requires MetaSig parsing — not yet implemented. + // TODO(stackref): Implement VASIG_COOKIE handling + break; + } + } + } + + /// + /// Converts a GCRefMap position to a byte offset within the transition block. + /// Port of native OffsetFromGCRefMapPos (frames.cpp:1624-1633). + /// + private uint OffsetFromGCRefMapPos(int pos) + { + uint firstSlotOffset = _target.ReadGlobal(Constants.Globals.TransitionBlockOffsetOfFirstGCRefMapSlot); + + return firstSlotOffset + (uint)(pos * _target.PointerSize); + } + + /// + /// Scans GC roots for a DynamicHelperFrame based on its flags. + /// Port of native DynamicHelperFrame::GcScanRoots_Impl (frames.cpp:1071-1105). + /// + private void ScanDynamicHelperFrame( + TargetPointer transitionBlock, + int dynamicHelperFrameFlags, + GcScanContext scanContext) + { + const int DynamicHelperFrameFlags_ObjectArg = 1; + const int DynamicHelperFrameFlags_ObjectArg2 = 2; + + uint argRegOffset = _target.ReadGlobal(Constants.Globals.TransitionBlockOffsetOfArgumentRegisters); + + if ((dynamicHelperFrameFlags & DynamicHelperFrameFlags_ObjectArg) != 0) + { + TargetPointer argAddr = new(transitionBlock.Value + argRegOffset); + // On x86, this would need offsetof(ArgumentRegisters, ECX) adjustment. + // For AMD64/ARM64, the first argument register is at the base offset. + scanContext.GCReportCallback(argAddr, GcScanFlags.None); + } + + if ((dynamicHelperFrameFlags & DynamicHelperFrameFlags_ObjectArg2) != 0) + { + TargetPointer argAddr = new(transitionBlock.Value + argRegOffset + (uint)_target.PointerSize); + // On x86, this would need offsetof(ArgumentRegisters, EDX) adjustment. + // For AMD64/ARM64, the second argument is pointer-size after the first. + scanContext.GCReportCallback(argAddr, GcScanFlags.None); + } + } + + /// + /// Promotes caller stack GC references by parsing the method signature via MetaSig. + /// Used when a frame has no precomputed GCRefMap (e.g., dynamic/LCG methods). + /// Port of native TransitionFrame::PromoteCallerStack + PromoteCallerStackHelper (frames.cpp). + /// + private void PromoteCallerStackUsingMetaSig( + TargetPointer frameAddress, + TargetPointer transitionBlock, + GcScanContext scanContext) + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + TargetPointer methodDescPtr = fmf.MethodDescPtr; + if (methodDescPtr == TargetPointer.Null) + return; + + ReadOnlySpan signature; + try + { + signature = GetMethodSignatureBytes(methodDescPtr); + } + catch (System.Exception) + { + return; + } + + if (signature.IsEmpty) + return; + + CorSigParser parser = new(signature, _target.PointerSize); + + // Parse calling convention + byte callingConvByte = parser.ReadByte(); + bool hasThis = (callingConvByte & 0x20) != 0; // IMAGE_CEE_CS_CALLCONV_HASTHIS + bool isGeneric = (callingConvByte & 0x10) != 0; + + if (isGeneric) + parser.ReadCompressedUInt(); // skip generic param count + + uint paramCount = parser.ReadCompressedUInt(); + + // Skip return type + parser.SkipType(); + + // Walk through GCRefMap positions. + // The position numbering matches how GCRefMap encodes slots: + // ARM64: pos 0 = RetBuf (x8), pos 1+ = argument registers (x0-x7), then stack + // Others: pos 0 = first argument register/slot, etc. + int pos = 0; + + // On ARM64, position 0 is the return buffer register (x8). + // Methods without a return buffer skip this slot. + // TODO: detect HasRetBuf from the signature's return type when needed. + // For now, we skip the retbuf slot on ARM64 since the common case + // (dynamic invoke stubs) doesn't use return buffers. + bool isArm64 = IsTargetArm64(); + if (isArm64) + pos++; + + // Promote 'this' if present + if (hasThis) + { + uint offset = OffsetFromGCRefMapPos(pos); + TargetPointer slotAddress = new(transitionBlock.Value + offset); + scanContext.GCReportCallback(slotAddress, GcScanFlags.None); + pos++; + } + + // Walk each parameter + for (uint i = 0; i < paramCount; i++) + { + uint offset = OffsetFromGCRefMapPos(pos); + TargetPointer slotAddress = new(transitionBlock.Value + offset); + + GcTypeKind kind = parser.ReadTypeAndClassify(); + + switch (kind) + { + case GcTypeKind.Ref: + scanContext.GCReportCallback(slotAddress, GcScanFlags.None); + break; + + case GcTypeKind.Interior: + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + break; + + case GcTypeKind.Other: + // Value types may contain embedded GC references. + // Full scanning requires reading the MethodTable's GCDesc. + // TODO(stackref): Implement value type GCDesc scanning for MetaSig path. + break; + + case GcTypeKind.None: + break; + } + + pos++; + } + } + + /// + /// Gets the raw signature bytes for a MethodDesc. + /// For StoredSigMethodDesc (dynamic, array, EEImpl methods), reads the embedded signature. + /// For normal IL methods, reads from module metadata. + /// + private ReadOnlySpan GetMethodSignatureBytes(TargetPointer methodDescPtr) + { + IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + MethodDescHandle mdh = rts.GetMethodDescHandle(methodDescPtr); + + // Try StoredSigMethodDesc first (dynamic/LCG/array methods) + if (rts.IsStoredSigMethodDesc(mdh, out ReadOnlySpan storedSig)) + return storedSig; + + // Normal IL methods: get signature from metadata + uint methodToken = rts.GetMethodToken(mdh); + if (methodToken == 0x06000000) // mdtMethodDef with RID 0 = no token + return default; + + TargetPointer methodTablePtr = rts.GetMethodTable(mdh); + TypeHandle typeHandle = rts.GetTypeHandle(methodTablePtr); + TargetPointer modulePtr = rts.GetModule(typeHandle); + + ILoader loader = _target.Contracts.Loader; + ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(modulePtr); + + IEcmaMetadata ecmaMetadata = _target.Contracts.EcmaMetadata; + MetadataReader? mdReader = ecmaMetadata.GetMetadata(moduleHandle); + if (mdReader is null) + return default; + + MethodDefinitionHandle methodDefHandle = MetadataTokens.MethodDefinitionHandle((int)(methodToken & 0x00FFFFFF)); + MethodDefinition methodDef = mdReader.GetMethodDefinition(methodDefHandle); + BlobReader blobReader = mdReader.GetBlobReader(methodDef.Signature); + return blobReader.ReadBytes(blobReader.Length); + } + + /// + /// Detects if the target architecture is ARM64 based on TransitionBlock layout. + /// On ARM64, GetOffsetOfFirstGCRefMapSlot != GetOffsetOfArgumentRegisters + /// (because the first GCRefMap slot is the x8 RetBuf register, not x0). + /// + private bool IsTargetArm64() + { + uint firstGCRefMapSlot = _target.ReadGlobal(Constants.Globals.TransitionBlockOffsetOfFirstGCRefMapSlot); + uint argRegsOffset = _target.ReadGlobal(Constants.Globals.TransitionBlockOffsetOfArgumentRegisters); + return firstGCRefMapSlot != argRegsOffset; + } +} 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 42b52d034231f8..5f346dabad4701 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 @@ -63,6 +63,7 @@ ThreadData IThread.GetThreadData(TargetPointer threadPointer) } return new ThreadData( + threadPointer, thread.Id, thread.OSId, (ThreadState)thread.State, 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..c5d5eaffaf43fd 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,29 @@ 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); + ClauseForCatchHandlerStartPC = target.Read(address + (ulong)type.Fields[nameof(ClauseForCatchHandlerStartPC)].Offset); + ClauseForCatchHandlerEndPC = target.Read(address + (ulong)type.Fields[nameof(ClauseForCatchHandlerEndPC)].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 uint ClauseForCatchHandlerStartPC { get; } + public uint ClauseForCatchHandlerEndPC { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs new file mode 100644 index 00000000000000..652b60fb7bb49d --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal class DynamicHelperFrame : IData +{ + static DynamicHelperFrame IData.Create(Target target, TargetPointer address) + => new DynamicHelperFrame(target, address); + + public DynamicHelperFrame(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.DynamicHelperFrame); + DynamicHelperFrameFlags = target.Read(address + (ulong)type.Fields[nameof(DynamicHelperFrameFlags)].Offset); + } + + public int DynamicHelperFrameFlags { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs new file mode 100644 index 00000000000000..1a07c91757f705 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal class ExternalMethodFrame : IData +{ + static ExternalMethodFrame IData.Create(Target target, TargetPointer address) + => new ExternalMethodFrame(target, address); + + public ExternalMethodFrame(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.ExternalMethodFrame); + GCRefMap = target.ReadPointer(address + (ulong)type.Fields[nameof(GCRefMap)].Offset); + } + + public TargetPointer GCRefMap { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs index f4e677dafddaa9..07d9f199523eb5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs @@ -14,6 +14,7 @@ public StubDispatchFrame(Target target, TargetPointer address) MethodDescPtr = target.ReadPointer(address + (ulong)type.Fields[nameof(MethodDescPtr)].Offset); RepresentativeMTPtr = target.ReadPointer(address + (ulong)type.Fields[nameof(RepresentativeMTPtr)].Offset); RepresentativeSlot = target.Read(address + (ulong)type.Fields[nameof(RepresentativeSlot)].Offset); + GCRefMap = target.ReadPointer(address + (ulong)type.Fields[nameof(GCRefMap)].Offset); Address = address; } @@ -21,4 +22,5 @@ public StubDispatchFrame(Target target, TargetPointer address) public TargetPointer MethodDescPtr { get; } public TargetPointer RepresentativeMTPtr { get; } public uint RepresentativeSlot { get; } + public TargetPointer GCRefMap { 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..9111838126ae78 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,17 +13,16 @@ 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; - TargetPointer jitEHInfoAddr = target.ReadPointer(address + (ulong)type.Fields[nameof(JitEHInfo)].Offset); - 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; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs index 9e78142e7c97af..f66e4a7dc85a9b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs @@ -40,6 +40,9 @@ public Thread(Target target, TargetPointer address) ? target.ReadPointer(address + (ulong)watsonFieldInfo.Offset) : TargetPointer.Null; ThreadLocalDataPtr = target.ReadPointer(address + (ulong)type.Fields[nameof(ThreadLocalDataPtr)].Offset); + + DebuggerFilterContext = target.ReadPointer(address + (ulong)type.Fields[nameof(DebuggerFilterContext)].Offset); + ProfilerFilterContext = target.ReadPointer(address + (ulong)type.Fields[nameof(ProfilerFilterContext)].Offset); } public uint Id { get; init; } @@ -56,4 +59,6 @@ public Thread(Target target, TargetPointer address) public TargetPointer ExceptionTracker { get; init; } public TargetPointer UEWatsonBucketTrackerBuckets { get; init; } public TargetPointer ThreadLocalDataPtr { get; init; } + public TargetPointer DebuggerFilterContext { get; init; } + public TargetPointer ProfilerFilterContext { get; init; } } 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 c85871c709a009..551fd774caad7b 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 41f2f5ec085aa0..4ab5a8f7c4ca6e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -2411,10 +2411,10 @@ int ISOSDacInterface.GetMethodDescPtrFromFrame(ClrDataAddress frameAddr, ClrData int hr = HResults.S_OK; try { - if (frameAddr == 0 || ppMD == null) + if (frameAddr == 0 || ppMD is null) throw new ArgumentException(); - IStackWalk stackWalkContract = _target.Contracts.StackWalk; + Contracts.IStackWalk stackWalkContract = _target.Contracts.StackWalk; TargetPointer methodDescPtr = stackWalkContract.GetMethodDescPtr(frameAddr.ToTargetPointer(_target)); if (methodDescPtr == TargetPointer.Null) throw new ArgumentException(); @@ -3692,8 +3692,178 @@ 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(); + + count = Math.Min(count, (uint)refs.Length); + uint written = 0; + while (written < count && _index < _refs.Length) + refs[written++] = _refs[(int)_index++]; + + *pFetched = written; + // COMPAT: S_FALSE means more items remain, S_OK means enumeration is complete. + // This is the inverse of the standard COM IEnumXxx convention, but matches + // the legacy DAC behavior (see SOSHandleEnum.Next). + 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 = Math.Min(_index + count, (uint)_refs.Length); + 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; + SOSStackRefData[]? sosRefs = null; + 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); + + 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); + // COMPAT: In the legacy DAC, this API leaks a ref-count of the returned enumerator. + // Leak a refcount here to match previous behavior and avoid breaking customer code. + ComInterfaceMarshaller.ConvertToUnmanaged(ppEnum.Interface); + } + catch (System.Exception ex) + { + hr = ex.HResult; + if (!ppEnum.IsNullRef) + ppEnum.Interface = default; + } + +#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"); + + Debug.Assert((uint)sosRefs.Length == legacyFetched, $"cDAC: {sosRefs.Length} refs, DAC: {legacyFetched} refs"); + + // Verify every DAC ref exists in the cDAC set (by Address which is unique per slot) + for (uint i = 0; i < legacyFetched; i++) + { + SOSStackRefData dac = legacyRefs[i]; + bool found = false; + for (int j = 0; j < sosRefs.Length; j++) + { + if (sosRefs[j].Address == dac.Address) + { + SOSStackRefData cdac = sosRefs[j]; + Debug.Assert(cdac.Object == dac.Object, $"Address {dac.Address:x}: Object cDAC: {cdac.Object:x}, DAC: {dac.Object:x}"); + Debug.Assert(cdac.SourceType == dac.SourceType, $"Address {dac.Address:x}: SourceType cDAC: {cdac.SourceType}, DAC: {dac.SourceType}"); + Debug.Assert(cdac.Source == dac.Source, $"Address {dac.Address:x}: Source cDAC: {cdac.Source:x}, DAC: {dac.Source:x}"); + Debug.Assert(cdac.Flags == dac.Flags, $"Address {dac.Address:x}: Flags cDAC: {cdac.Flags:x}, DAC: {dac.Flags:x}"); + found = true; + break; + } + } + Debug.Assert(found, $"DAC ref at Address {dac.Address:x} (Object {dac.Object:x}) not found in cDAC results"); + } + } + } + } +#endif + + return hr; + } int ISOSDacInterface.GetStressLogAddress(ClrDataAddress* stressLog) { diff --git a/src/native/managed/cdac/README.md b/src/native/managed/cdac/README.md index 7dc210c56116ee..39b37b3deb8b1d 100644 --- a/src/native/managed/cdac/README.md +++ b/src/native/managed/cdac/README.md @@ -51,6 +51,44 @@ ISOSDacInterface* / IXCLRDataProcess (COM-style API surface) | `mscordaccore_universal` | Entry point that wires everything together | | `tests` | Unit tests with mock memory infrastructure | +## GC Stress Verification (GCSTRESS_CDAC) + +The cDAC includes a GC stress verification mode that compares the cDAC's stack reference +enumeration against the runtime's own GC root scanning at every GC stress instruction-level +trigger point. + +### How it works + +When `DOTNET_GCStress=0x24` (0x20 cDAC + 0x4 instruction JIT), at each stress point: +1. The cDAC is loaded in-process and enumerates stack GC references via `GetStackReferences` +2. The runtime enumerates the same references via `StackWalkFrames` + `GcStackCrawlCallBack` +3. The tool compares the two sets and reports mismatches + +### Usage + +```bash +DOTNET_GCStress=0x24 DOTNET_GCStressCdacLogFile=results.txt corerun test.dll +``` + +Configuration variables: +- `DOTNET_GCStress=0x24` — Enable instruction-level GC stress + cDAC verification +- `DOTNET_GCStressCdacFailFast=1` — Assert on mismatch (default: log and continue) +- `DOTNET_GCStressCdacLogFile=` — Write detailed results to a log file + +### Files + +| File | Location | Purpose | +|------|----------|---------| +| `cdacgcstress.h/cpp` | `src/coreclr/vm/` | In-process cDAC loading and comparison | +| `test-cdac-gcstress.ps1` | `src/native/managed/cdac/tests/gcstress/` | Build and test script | +| `known-issues.md` | `src/native/managed/cdac/tests/gcstress/` | Documented gaps | + +### Known limitations + +See [tests/gcstress/known-issues.md](tests/gcstress/known-issues.md) for the full list. +Key gaps include dynamic method (IL Stub) GC refs, frame duplication on deep stacks, +and unimplemented `PromoteCallerStack` for stub frames. Current pass rate: ~99.7%. + ## Contract specifications Each contract has a specification document in diff --git a/src/native/managed/cdac/cdac.slnx b/src/native/managed/cdac/cdac.slnx index 7449d30624ec2d..4abe615fe50f3b 100644 --- a/src/native/managed/cdac/cdac.slnx +++ b/src/native/managed/cdac/cdac.slnx @@ -14,5 +14,6 @@ + diff --git a/src/native/managed/cdac/tests/ClrDataExceptionStateTests.cs b/src/native/managed/cdac/tests/ClrDataExceptionStateTests.cs index 65b90e88637557..0319362d07f0c2 100644 --- a/src/native/managed/cdac/tests/ClrDataExceptionStateTests.cs +++ b/src/native/managed/cdac/tests/ClrDataExceptionStateTests.cs @@ -77,6 +77,7 @@ private static (TestPlaceholderTarget Target, IXCLRDataTask Task) CreateTargetWi var mockThread = new Mock(); mockThread.Setup(t => t.GetCurrentExceptionHandle(threadAddr)).Returns(thrownObjectHandle); mockThread.Setup(t => t.GetThreadData(threadAddr)).Returns(new ThreadData( + ThreadAddress: threadAddr, Id: 1, OSId: new TargetNUInt(1234), State: default, @@ -450,6 +451,7 @@ private static (IXCLRDataTask Task, string ExpectedMessage) CreateTargetWithLast var target = new TestPlaceholderTarget(arch, builder.GetMemoryContext().ReadFromTarget); var mockThread = new Mock(); mockThread.Setup(t => t.GetThreadData(threadAddr)).Returns(new ThreadData( + ThreadAddress: threadAddr, Id: 1, OSId: new TargetNUInt(1234), State: default, 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/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 new file mode 100644 index 00000000000000..5bd28cd0a67ebb --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs @@ -0,0 +1,212 @@ +// 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 Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// 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, "StackWalk", "full"); + 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, "StackWalk", "full"); + 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"); + } + } + + // --- GCRoots debuggee: objects kept alive on stack --- + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void GCRoots_WalkStackReferences_FindsRefs(TestConfiguration config) + { + InitializeDumpTest(config, "GCRoots", "full"); + 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 GCRoots_RefsPointToValidObjects(TestConfiguration config) + { + InitializeDumpTest(config, "GCRoots", "full"); + 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; + + 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})"); + } + + // --- 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; + 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"); + + bool foundMarker = false; + string expectedMarker = "cDAC-StackRefs-Marker-12345"; + + foreach (StackReferenceData r in refs) + { + if (r.Object == TargetPointer.Null) + continue; + + try + { + string value = objectContract.GetStringValue(r.Object); + 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; + 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 using the Object contract. + bool foundArray = false; + + foreach (StackReferenceData r in refs) + { + if (r.Object == TargetPointer.Null) + continue; + + try + { + TargetPointer dataStart = objectContract.GetArrayData(r.Object, out uint count, out _, out _); + if (count != 5) + continue; + + 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) + { + 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 PInvoke_WalkStackReferences_ReturnsWithoutThrowing(TestConfiguration config) + { + InitializeDumpTest(config, "PInvokeStub", "full"); + IStackWalk stackWalk = Target.Contracts.StackWalk; + + ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "Main"); + + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + Assert.NotNull(refs); + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/BasicGCStressTests.cs b/src/native/managed/cdac/tests/GCStressTests/BasicGCStressTests.cs new file mode 100644 index 00000000000000..45a83b2694e87a --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/BasicGCStressTests.cs @@ -0,0 +1,61 @@ +// 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.Runtime.InteropServices; +using Microsoft.DotNet.XUnitExtensions; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; + +/// +/// Runs each debuggee app under corerun with DOTNET_GCStress=0x24 and asserts +/// that the cDAC stack reference verification achieves 100% pass rate. +/// +/// +/// Prerequisites: +/// - Build CoreCLR native + cDAC: build.cmd -subset clr.native+tools.cdac -c Debug -rc Checked -lc Release +/// - Generate core_root: src\tests\build.cmd Checked generatelayoutonly /p:LibrariesConfiguration=Release +/// - Build debuggees: dotnet build this test project +/// +/// The tests use CORE_ROOT env var if set, otherwise default to the standard artifacts path. +/// +public class BasicGCStressTests : GCStressTestBase +{ + public BasicGCStressTests(ITestOutputHelper output) : base(output) { } + + public static IEnumerable Debuggees => + [ + ["BasicAlloc"], + ["DeepStack"], + ["Generics"], + ["MultiThread"], + ["Comprehensive"], + ["ExceptionHandling"], + ]; + + public static IEnumerable WindowsOnlyDebuggees => + [ + ["PInvoke"], + ]; + + [Theory] + [MemberData(nameof(Debuggees))] + public void GCStress_AllVerificationsPass(string debuggeeName) + { + GCStressResults results = RunGCStress(debuggeeName); + AssertAllPassed(results, debuggeeName); + } + + [Theory] + [MemberData(nameof(WindowsOnlyDebuggees))] + public void GCStress_WindowsOnly_AllVerificationsPass(string debuggeeName) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + throw new SkipTestException("P/Invoke debuggee uses kernel32.dll (Windows only)"); + + GCStressResults results = RunGCStress(debuggeeName); + AssertAllPassed(results, debuggeeName); + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/BasicAlloc/BasicAlloc.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/BasicAlloc/BasicAlloc.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/BasicAlloc/BasicAlloc.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/BasicAlloc/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/BasicAlloc/Program.cs new file mode 100644 index 00000000000000..f886c0ef72cefe --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/BasicAlloc/Program.cs @@ -0,0 +1,56 @@ +// 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; + +/// +/// Exercises basic object allocation patterns: objects, strings, arrays. +/// +internal static class Program +{ + [MethodImpl(MethodImplOptions.NoInlining)] + static object AllocAndHold() + { + object o = new object(); + string s = "hello world"; + int[] arr = new int[] { 1, 2, 3 }; + byte[] buf = new byte[256]; + GC.KeepAlive(o); + GC.KeepAlive(s); + GC.KeepAlive(arr); + GC.KeepAlive(buf); + return o; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ManyLiveRefs() + { + object r0 = new object(); + object r1 = new object(); + object r2 = new object(); + object r3 = new object(); + object r4 = new object(); + object r5 = new object(); + object r6 = new object(); + object r7 = new object(); + string r8 = "live-string"; + int[] r9 = new int[10]; + + GC.KeepAlive(r0); GC.KeepAlive(r1); + GC.KeepAlive(r2); GC.KeepAlive(r3); + GC.KeepAlive(r4); GC.KeepAlive(r5); + GC.KeepAlive(r6); GC.KeepAlive(r7); + GC.KeepAlive(r8); GC.KeepAlive(r9); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + AllocAndHold(); + ManyLiveRefs(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/Comprehensive/Comprehensive.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Comprehensive/Comprehensive.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Comprehensive/Comprehensive.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/Comprehensive/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Comprehensive/Program.cs new file mode 100644 index 00000000000000..6a2f26f146ef0f --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Comprehensive/Program.cs @@ -0,0 +1,253 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +/// +/// All-in-one comprehensive debuggee that exercises every scenario +/// in a single run: allocations, exceptions, generics, P/Invoke, threading. +/// +internal static class Program +{ + interface IKeepAlive { object GetRef(); } + class BoxHolder : IKeepAlive + { + object _value; + public BoxHolder() { _value = new object(); } + public BoxHolder(object v) { _value = v; } + [MethodImpl(MethodImplOptions.NoInlining)] + public object GetRef() => _value; + } + + struct LargeStruct { public object A, B, C, D; } + + [MethodImpl(MethodImplOptions.NoInlining)] + static object AllocAndHold() + { + object o = new object(); + string s = "hello world"; + int[] arr = new int[] { 1, 2, 3 }; + GC.KeepAlive(o); + GC.KeepAlive(s); + GC.KeepAlive(arr); + return o; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedCall(int depth) + { + object o = new object(); + if (depth > 0) + NestedCall(depth - 1); + GC.KeepAlive(o); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryCatchScenario() + { + object before = new object(); + try + { + throw new InvalidOperationException("test"); + } + catch (InvalidOperationException ex) + { + object inCatch = new object(); + GC.KeepAlive(ex); + GC.KeepAlive(inCatch); + } + GC.KeepAlive(before); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryFinallyScenario() + { + object outerRef = new object(); + try + { + object innerRef = new object(); + GC.KeepAlive(innerRef); + } + finally + { + object finallyRef = new object(); + GC.KeepAlive(finallyRef); + } + GC.KeepAlive(outerRef); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedExceptionScenario() + { + object a = new object(); + try + { + try + { + throw new ArgumentException("inner"); + } + catch (ArgumentException ex1) + { + GC.KeepAlive(ex1); + throw new InvalidOperationException("outer", ex1); + } + } + catch (InvalidOperationException ex2) + { + GC.KeepAlive(ex2); + } + GC.KeepAlive(a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void FilterExceptionScenario() + { + object holder = new object(); + try + { + throw new ArgumentException("filter-test"); + } + catch (ArgumentException ex) when (FilterCheck(ex)) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(holder); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static bool FilterCheck(Exception ex) + { + object filterLocal = new object(); + GC.KeepAlive(filterLocal); + return ex.Message.Contains("filter"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static T GenericAlloc() where T : new() + { + T val = new T(); + object marker = new object(); + GC.KeepAlive(marker); + return val; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void InterfaceDispatchScenario() + { + IKeepAlive holder = new BoxHolder(new int[] { 42, 43 }); + object r = holder.GetRef(); + GC.KeepAlive(holder); + GC.KeepAlive(r); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void DelegateScenario() + { + object captured = new object(); + Func fn = () => { GC.KeepAlive(captured); return new object(); }; + object result = fn(); + GC.KeepAlive(result); + GC.KeepAlive(fn); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void StructWithRefsScenario() + { + LargeStruct ls; + ls.A = new object(); + ls.B = "struct-string"; + ls.C = new int[] { 10, 20 }; + ls.D = new BoxHolder(ls.A); + GC.KeepAlive(ls.A); + GC.KeepAlive(ls.B); + GC.KeepAlive(ls.C); + GC.KeepAlive(ls.D); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void PinnedScenario() + { + byte[] buffer = new byte[64]; + GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned); + try + { + object other = new object(); + GC.KeepAlive(other); + GC.KeepAlive(buffer); + } + finally + { + pin.Free(); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void MultiThreadScenario() + { + ManualResetEventSlim ready = new ManualResetEventSlim(false); + ManualResetEventSlim go = new ManualResetEventSlim(false); + Thread t = new Thread(() => + { + object threadLocal = new object(); + ready.Set(); + go.Wait(); + NestedCall(5); + GC.KeepAlive(threadLocal); + }); + t.Start(); + ready.Wait(); + go.Set(); + NestedCall(3); + t.Join(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void RethrowScenario() + { + object outerRef = new object(); + try + { + try + { + throw new ApplicationException("rethrow-test"); + } + catch (ApplicationException) + { + object catchRef = new object(); + GC.KeepAlive(catchRef); + throw; + } + } + catch (ApplicationException ex) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(outerRef); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + AllocAndHold(); + NestedCall(5); + TryCatchScenario(); + TryFinallyScenario(); + NestedExceptionScenario(); + FilterExceptionScenario(); + GenericAlloc(); + GenericAlloc>(); + InterfaceDispatchScenario(); + DelegateScenario(); + StructWithRefsScenario(); + PinnedScenario(); + MultiThreadScenario(); + RethrowScenario(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/DeepStack/DeepStack.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/DeepStack/DeepStack.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/DeepStack/DeepStack.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/DeepStack/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/DeepStack/Program.cs new file mode 100644 index 00000000000000..c98679aea54ac2 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/DeepStack/Program.cs @@ -0,0 +1,43 @@ +// 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; + +/// +/// Exercises deep recursion with live GC references at each frame level. +/// +internal static class Program +{ + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedCall(int depth) + { + object o = new object(); + if (depth > 0) + NestedCall(depth - 1); + GC.KeepAlive(o); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedWithMultipleRefs(int depth) + { + object a = new object(); + string b = $"depth-{depth}"; + int[] c = new int[depth + 1]; + if (depth > 0) + NestedWithMultipleRefs(depth - 1); + GC.KeepAlive(a); + GC.KeepAlive(b); + GC.KeepAlive(c); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + NestedCall(10); + NestedWithMultipleRefs(8); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/Directory.Build.props b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Directory.Build.props new file mode 100644 index 00000000000000..eca2240b31f08c --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Directory.Build.props @@ -0,0 +1,15 @@ + + + + + Exe + $(NetCoreAppToolCurrent) + true + enable + $(ArtifactsBinDir)GCStressTests\$(MSBuildProjectName)\$(Configuration)\ + true + + false + $(NoWarn);SA1400;IDE0059;SYSLIB1054;CA1852;CA1861 + + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/ExceptionHandling/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/ExceptionHandling/Program.cs new file mode 100644 index 00000000000000..4bd0a12fe6d145 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/ExceptionHandling/Program.cs @@ -0,0 +1,143 @@ +// 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; + +/// +/// Exercises exception handling: try/catch/finally funclets, nested exceptions, +/// filter funclets, and rethrow. +/// +internal static class Program +{ + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryCatchScenario() + { + object before = new object(); + try + { + object inside = new object(); + ThrowHelper(); + GC.KeepAlive(inside); + } + catch (InvalidOperationException ex) + { + object inCatch = new object(); + GC.KeepAlive(ex); + GC.KeepAlive(inCatch); + } + GC.KeepAlive(before); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowHelper() + { + throw new InvalidOperationException("test exception"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryFinallyScenario() + { + object outerRef = new object(); + try + { + object innerRef = new object(); + GC.KeepAlive(innerRef); + } + finally + { + object finallyRef = new object(); + GC.KeepAlive(finallyRef); + } + GC.KeepAlive(outerRef); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedExceptionScenario() + { + object a = new object(); + try + { + try + { + object c = new object(); + throw new ArgumentException("inner"); + } + catch (ArgumentException ex1) + { + GC.KeepAlive(ex1); + throw new InvalidOperationException("outer", ex1); + } + finally + { + object d = new object(); + GC.KeepAlive(d); + } + } + catch (InvalidOperationException ex2) + { + GC.KeepAlive(ex2); + } + GC.KeepAlive(a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void FilterExceptionScenario() + { + object holder = new object(); + try + { + throw new ArgumentException("filter-test"); + } + catch (ArgumentException ex) when (FilterCheck(ex)) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(holder); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static bool FilterCheck(Exception ex) + { + object filterLocal = new object(); + GC.KeepAlive(filterLocal); + return ex.Message.Contains("filter"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void RethrowScenario() + { + object outerRef = new object(); + try + { + try + { + throw new ApplicationException("rethrow-test"); + } + catch (ApplicationException) + { + object catchRef = new object(); + GC.KeepAlive(catchRef); + throw; + } + } + catch (ApplicationException ex) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(outerRef); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + TryCatchScenario(); + TryFinallyScenario(); + NestedExceptionScenario(); + FilterExceptionScenario(); + RethrowScenario(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/Generics/Generics.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Generics/Generics.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Generics/Generics.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/Generics/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Generics/Program.cs new file mode 100644 index 00000000000000..54b7060c040f5a --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Generics/Program.cs @@ -0,0 +1,81 @@ +// 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.Runtime.CompilerServices; + +/// +/// Exercises generic method instantiations and interface dispatch. +/// +internal static class Program +{ + interface IKeepAlive + { + object GetRef(); + } + + class BoxHolder : IKeepAlive + { + object _value; + public BoxHolder() { _value = new object(); } + public BoxHolder(object v) { _value = v; } + + [MethodImpl(MethodImplOptions.NoInlining)] + public object GetRef() => _value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static T GenericAlloc() where T : new() + { + T val = new T(); + object marker = new object(); + GC.KeepAlive(marker); + return val; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void GenericScenario() + { + var o = GenericAlloc(); + var l = GenericAlloc>(); + var s = GenericAlloc(); + GC.KeepAlive(o); + GC.KeepAlive(l); + GC.KeepAlive(s); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void InterfaceDispatchScenario() + { + IKeepAlive holder = new BoxHolder(new int[] { 42, 43 }); + object r = holder.GetRef(); + GC.KeepAlive(holder); + GC.KeepAlive(r); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void DelegateScenario() + { + object captured = new object(); + Func fn = () => + { + GC.KeepAlive(captured); + return new object(); + }; + object result = fn(); + GC.KeepAlive(result); + GC.KeepAlive(fn); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + GenericScenario(); + InterfaceDispatchScenario(); + DelegateScenario(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/MultiThread/MultiThread.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/MultiThread/MultiThread.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/MultiThread/MultiThread.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/MultiThread/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/MultiThread/Program.cs new file mode 100644 index 00000000000000..0eea731a6bd313 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/MultiThread/Program.cs @@ -0,0 +1,53 @@ +// 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; +using System.Threading; + +/// +/// Exercises concurrent threads with GC references, exercising multi-threaded +/// stack walks and GC ref enumeration. +/// +internal static class Program +{ + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedCall(int depth) + { + object o = new object(); + if (depth > 0) + NestedCall(depth - 1); + GC.KeepAlive(o); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThreadWork(int id) + { + object threadLocal = new object(); + string threadName = $"thread-{id}"; + NestedCall(5); + GC.KeepAlive(threadLocal); + GC.KeepAlive(threadName); + } + + static int Main() + { + for (int iteration = 0; iteration < 2; iteration++) + { + ManualResetEventSlim ready = new ManualResetEventSlim(false); + ManualResetEventSlim go = new ManualResetEventSlim(false); + Thread t = new Thread(() => + { + ready.Set(); + go.Wait(); + ThreadWork(1); + }); + t.Start(); + ready.Wait(); + go.Set(); + ThreadWork(0); + t.Join(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/PInvoke/PInvoke.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/PInvoke/PInvoke.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/PInvoke/PInvoke.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/PInvoke/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/PInvoke/Program.cs new file mode 100644 index 00000000000000..83aece921baaea --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/PInvoke/Program.cs @@ -0,0 +1,74 @@ +// 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; +using System.Runtime.InteropServices; + +/// +/// Exercises P/Invoke transitions with GC references before and after native calls, +/// and pinned GC handles. +/// +internal static class Program +{ + [DllImport("kernel32.dll")] + static extern uint GetCurrentThreadId(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static void PInvokeScenario() + { + object before = new object(); + uint tid = GetCurrentThreadId(); + object after = new object(); + GC.KeepAlive(before); + GC.KeepAlive(after); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void PinnedScenario() + { + byte[] buffer = new byte[64]; + GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned); + try + { + object other = new object(); + GC.KeepAlive(other); + GC.KeepAlive(buffer); + } + finally + { + pin.Free(); + } + } + + struct LargeStruct + { + public object A, B, C, D; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void StructWithRefsScenario() + { + LargeStruct ls; + ls.A = new object(); + ls.B = "struct-string"; + ls.C = new int[] { 10, 20 }; + ls.D = new object(); + GC.KeepAlive(ls.A); + GC.KeepAlive(ls.B); + GC.KeepAlive(ls.C); + GC.KeepAlive(ls.D); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + PInvokeScenario(); + PinnedScenario(); + StructWithRefsScenario(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/GCStressResults.cs b/src/native/managed/cdac/tests/GCStressTests/GCStressResults.cs new file mode 100644 index 00000000000000..4004740bbcdcdb --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/GCStressResults.cs @@ -0,0 +1,77 @@ +// 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.IO; +using System.Text.RegularExpressions; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; + +/// +/// Parses the cdac-gcstress results log file written by the native cdacgcstress.cpp hook. +/// +internal sealed partial class GCStressResults +{ + public int TotalVerifications { get; private set; } + public int Passed { get; private set; } + public int Failed { get; private set; } + public int Skipped { get; private set; } + public string LogFilePath { get; private set; } = ""; + public List FailureDetails { get; } = []; + public List SkipDetails { get; } = []; + + [GeneratedRegex(@"^\[PASS\]")] + private static partial Regex PassPattern(); + + [GeneratedRegex(@"^\[FAIL\]")] + private static partial Regex FailPattern(); + + [GeneratedRegex(@"^\[SKIP\]")] + private static partial Regex SkipPattern(); + + [GeneratedRegex(@"^Total verifications:\s*(\d+)")] + private static partial Regex TotalPattern(); + + public static GCStressResults Parse(string logFilePath) + { + if (!File.Exists(logFilePath)) + throw new FileNotFoundException($"GC stress results log not found: {logFilePath}"); + + var results = new GCStressResults { LogFilePath = logFilePath }; + + foreach (string line in File.ReadLines(logFilePath)) + { + if (PassPattern().IsMatch(line)) + { + results.Passed++; + } + else if (FailPattern().IsMatch(line)) + { + results.Failed++; + results.FailureDetails.Add(line); + } + else if (SkipPattern().IsMatch(line)) + { + results.Skipped++; + results.SkipDetails.Add(line); + } + + Match totalMatch = TotalPattern().Match(line); + if (totalMatch.Success) + { + results.TotalVerifications = int.Parse(totalMatch.Groups[1].Value); + } + } + + if (results.TotalVerifications == 0) + { + results.TotalVerifications = results.Passed + results.Failed + results.Skipped; + } + + return results; + } + + public override string ToString() => + $"Total={TotalVerifications}, Passed={Passed}, Failed={Failed}, Skipped={Skipped}"; +} diff --git a/src/native/managed/cdac/tests/GCStressTests/GCStressTestBase.cs b/src/native/managed/cdac/tests/GCStressTests/GCStressTestBase.cs new file mode 100644 index 00000000000000..75c253ce1eafa0 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/GCStressTestBase.cs @@ -0,0 +1,209 @@ +// 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.IO; +using System.Runtime.InteropServices; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; + +/// +/// Base class for cDAC GC stress tests. Runs a debuggee app under corerun +/// with DOTNET_GCStress=0x24 and parses the verification results. +/// +public abstract class GCStressTestBase +{ + private readonly ITestOutputHelper _output; + + protected GCStressTestBase(ITestOutputHelper output) + { + _output = output; + } + + /// + /// Runs the named debuggee under GC stress and returns the parsed results. + /// + internal GCStressResults RunGCStress(string debuggeeName, int timeoutSeconds = 300) + { + string coreRoot = GetCoreRoot(); + string corerun = GetCoreRunPath(coreRoot); + string debuggeeDll = GetDebuggeePath(debuggeeName); + string logFile = Path.Combine(Path.GetTempPath(), $"cdac-gcstress-{debuggeeName}-{Guid.NewGuid():N}.txt"); + + _output.WriteLine($"Running GC stress: {debuggeeName}"); + _output.WriteLine($" corerun: {corerun}"); + _output.WriteLine($" debuggee: {debuggeeDll}"); + _output.WriteLine($" log: {logFile}"); + + var psi = new ProcessStartInfo + { + FileName = corerun, + Arguments = debuggeeDll, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + psi.Environment["CORE_ROOT"] = coreRoot; + psi.Environment["DOTNET_CdacStress"] = "0x11"; + psi.Environment["DOTNET_CdacStressFailFast"] = "0"; + psi.Environment["DOTNET_CdacStressLogFile"] = logFile; + psi.Environment["DOTNET_CdacStressStep"] = "1"; + psi.Environment["DOTNET_ContinueOnAssert"] = "1"; + + using var process = Process.Start(psi)!; + + // Read stderr asynchronously to avoid deadlock when both pipe buffers fill. + string stderr = ""; + process.ErrorDataReceived += (_, e) => + { + if (e.Data is not null) + stderr += e.Data + Environment.NewLine; + }; + process.BeginErrorReadLine(); + + string stdout = process.StandardOutput.ReadToEnd(); + + bool exited = process.WaitForExit(timeoutSeconds * 1000); + if (!exited) + { + process.Kill(entireProcessTree: true); + Assert.Fail($"GC stress test '{debuggeeName}' timed out after {timeoutSeconds}s"); + } + + _output.WriteLine($" exit code: {process.ExitCode}"); + if (!string.IsNullOrWhiteSpace(stdout)) + _output.WriteLine($" stdout: {stdout.TrimEnd()}"); + if (!string.IsNullOrWhiteSpace(stderr)) + _output.WriteLine($" stderr: {stderr.TrimEnd()}"); + + Assert.True(process.ExitCode == 100, + $"GC stress test '{debuggeeName}' exited with {process.ExitCode} (expected 100).\nstdout: {stdout}\nstderr: {stderr}"); + + Assert.True(File.Exists(logFile), + $"GC stress results log not created: {logFile}"); + + GCStressResults results = GCStressResults.Parse(logFile); + + _output.WriteLine($" results: {results}"); + + return results; + } + + /// + /// Asserts that GC stress verification produced 100% pass rate with no failures or skips. + /// + internal static void AssertAllPassed(GCStressResults results, string debuggeeName) + { + Assert.True(results.TotalVerifications > 0, + $"GC stress test '{debuggeeName}' produced zero verifications — " + + "GCStress may not have triggered or cDAC may not be loaded."); + + if (results.Failed > 0) + { + string details = string.Join("\n", results.FailureDetails); + Assert.Fail( + $"GC stress test '{debuggeeName}' had {results.Failed} failure(s) " + + $"out of {results.TotalVerifications} verifications.\n" + + $"Log: {results.LogFilePath}\n{details}"); + } + + if (results.Skipped > 0) + { + string details = string.Join("\n", results.SkipDetails); + Assert.Fail( + $"GC stress test '{debuggeeName}' had {results.Skipped} skip(s) " + + $"out of {results.TotalVerifications} verifications.\n" + + $"Log: {results.LogFilePath}\n{details}"); + } + } + + /// + /// Asserts that GC stress verification produced a pass rate at or above the given threshold. + /// A small number of failures is expected due to unimplemented frame scanning for + /// dynamic method stubs (InvokeStub / PromoteCallerStack). + /// + internal static void AssertHighPassRate(GCStressResults results, string debuggeeName, double minPassRate) + { + Assert.True(results.TotalVerifications > 0, + $"GC stress test '{debuggeeName}' produced zero verifications — " + + "GCStress may not have triggered or cDAC may not be loaded."); + + double passRate = (double)results.Passed / results.TotalVerifications; + if (passRate < minPassRate) + { + string details = string.Join("\n", results.FailureDetails); + Assert.Fail( + $"GC stress test '{debuggeeName}' pass rate {passRate:P2} is below " + + $"{minPassRate:P1} threshold. {results.Failed} failure(s) out of " + + $"{results.TotalVerifications} verifications.\n{details}"); + } + } + + private static string GetCoreRoot() + { + // Check environment variable first + string? coreRoot = Environment.GetEnvironmentVariable("CORE_ROOT"); + if (!string.IsNullOrEmpty(coreRoot) && Directory.Exists(coreRoot)) + return coreRoot; + + // Default path based on repo layout + string repoRoot = FindRepoRoot(); + string rid = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "windows" : "linux"; + string arch = RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(); + coreRoot = Path.Combine(repoRoot, "artifacts", "tests", "coreclr", $"{rid}.{arch}.Checked", "Tests", "Core_Root"); + + if (!Directory.Exists(coreRoot)) + throw new DirectoryNotFoundException( + $"Core_Root not found at '{coreRoot}'. " + + "Set the CORE_ROOT environment variable or run 'src/tests/build.cmd Checked generatelayoutonly'."); + + return coreRoot; + } + + private static string GetCoreRunPath(string coreRoot) + { + string exe = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "corerun.exe" : "corerun"; + string path = Path.Combine(coreRoot, exe); + Assert.True(File.Exists(path), $"corerun not found at '{path}'"); + + return path; + } + + private static string GetDebuggeePath(string debuggeeName) + { + string repoRoot = FindRepoRoot(); + + // Debuggees are built to artifacts/bin/GCStressTests//Release// + string binDir = Path.Combine(repoRoot, "artifacts", "bin", "GCStressTests", debuggeeName); + + if (!Directory.Exists(binDir)) + throw new DirectoryNotFoundException( + $"Debuggee '{debuggeeName}' not found at '{binDir}'. Build the GCStressTests project first."); + + // Find the dll in any Release/ subdirectory + foreach (string dir in Directory.GetDirectories(binDir, "*", SearchOption.AllDirectories)) + { + string dll = Path.Combine(dir, $"{debuggeeName}.dll"); + if (File.Exists(dll)) + return dll; + } + + throw new FileNotFoundException($"Could not find {debuggeeName}.dll under '{binDir}'"); + } + + private static string FindRepoRoot() + { + string? dir = AppContext.BaseDirectory; + while (dir is not null) + { + if (File.Exists(Path.Combine(dir, "global.json"))) + return dir; + dir = Path.GetDirectoryName(dir); + } + + throw new InvalidOperationException("Could not find repo root (global.json)"); + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/GCStressTests.targets b/src/native/managed/cdac/tests/GCStressTests/GCStressTests.targets new file mode 100644 index 00000000000000..a06b8ea4263caf --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/GCStressTests.targets @@ -0,0 +1,25 @@ + + + + $(MSBuildThisFileDirectory)Debuggees\ + Release + + + + + + + + + + + + diff --git a/src/native/managed/cdac/tests/GCStressTests/Microsoft.Diagnostics.DataContractReader.GCStressTests.csproj b/src/native/managed/cdac/tests/GCStressTests/Microsoft.Diagnostics.DataContractReader.GCStressTests.csproj new file mode 100644 index 00000000000000..ce6b6c14efadab --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Microsoft.Diagnostics.DataContractReader.GCStressTests.csproj @@ -0,0 +1,20 @@ + + + true + $(NetCoreAppToolCurrent) + enable + true + + + + + + + + + + + + + + diff --git a/src/native/managed/cdac/tests/GCStressTests/README.md b/src/native/managed/cdac/tests/GCStressTests/README.md new file mode 100644 index 00000000000000..ad1ee681b3944d --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/README.md @@ -0,0 +1,83 @@ +# cDAC GC Stress Tests + +Integration tests that verify the cDAC's stack reference enumeration matches the runtime's +GC root scanning under GC stress conditions. + +## How It Works + +Each test runs a debuggee console app under `corerun` with `DOTNET_GCStress=0x24`, which enables: +- **0x4**: Instruction-level JIT stress (triggers GC at every safe point) +- **0x20**: cDAC verification (compares cDAC stack refs against runtime refs) + +`DOTNET_GCStressCdacStep` throttles verification to every Nth stress point. The default +is 1 (verify every point). Higher values reduce cDAC overhead while maintaining instruction-level +breakpoint coverage for code path diversity. + +The native `cdacgcstress.cpp` hook writes `[PASS]`/`[FAIL]`/`[SKIP]` lines to a log file. +The test framework parses this log and asserts a high pass rate (≥99.9% for most debuggees, +≥99% for ExceptionHandling which has known funclet gaps). + +## Prerequisites + +Build the runtime with the cDAC GC stress hook enabled: + +```powershell +# From repo root +.\build.cmd -subset clr.native+tools.cdac -c Debug -rc Checked -lc Release +.\.dotnet\dotnet.exe msbuild src\libraries\externals.csproj /t:Build /p:Configuration=Release /p:RuntimeConfiguration=Checked /p:TargetOS=windows /p:TargetArchitecture=x64 -v:minimal +.\src\tests\build.cmd Checked generatelayoutonly -SkipRestorePackages /p:LibrariesConfiguration=Release +``` + +## Running Tests + +```powershell +# Build and run all GC stress tests +.\.dotnet\dotnet.exe test src\native\managed\cdac\tests\GCStressTests + +# Run a specific debuggee +.\.dotnet\dotnet.exe test src\native\managed\cdac\tests\GCStressTests --filter "debuggeeName=BasicAlloc" + +# Set CORE_ROOT manually if needed +$env:CORE_ROOT = "path\to\Core_Root" +.\.dotnet\dotnet.exe test src\native\managed\cdac\tests\GCStressTests +``` + +## Adding a New Debuggee + +1. Create a folder under `Debuggees/` with a `.csproj` and `Program.cs` +2. The `.csproj` just needs: `` + (inherits OutputType=Exe and TFM from `Directory.Build.props`) +3. `Main()` must return `100` on success +4. Use `[MethodImpl(MethodImplOptions.NoInlining)]` on methods to prevent inlining +5. Use `GC.KeepAlive()` to ensure objects are live at GC stress points +6. Add the debuggee name to `BasicGCStressTests.Debuggees` + +## Debuggee Catalog + +| Debuggee | Scenarios | +|----------|-----------| +| **BasicAlloc** | Objects, strings, arrays, many live refs | +| **ExceptionHandling** | try/catch/finally funclets, nested exceptions, filter funclets, rethrow | +| **DeepStack** | Deep recursion with live refs at each frame | +| **Generics** | Generic method instantiations, interface dispatch, delegates | +| **PInvoke** | P/Invoke transitions, pinned GC handles, struct with object refs | +| **MultiThread** | Concurrent threads with synchronized GC stress | +| **Comprehensive** | All-in-one: every scenario in a single run | + +## Architecture + +``` +GCStressTestBase.RunGCStress(debuggeeName) + │ + ├── Locate core_root/corerun (CORE_ROOT env or default path) + ├── Locate debuggee DLL (artifacts/bin/GCStressTests//...) + ├── Start Process: corerun + │ Environment: + │ DOTNET_GCStress=0x24 + │ DOTNET_GCStressCdacStep=1 + │ DOTNET_GCStressCdacLogFile= + │ DOTNET_ContinueOnAssert=1 + ├── Wait for exit (timeout: 300s) + ├── Parse results log → GCStressResults + └── Assert: exit=100, pass rate ≥ 99.9% +``` diff --git a/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj b/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj index c9de2a1bac2da7..669f76a1631839 100644 --- a/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj +++ b/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj @@ -6,8 +6,9 @@ - + + diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs index 524ff8e21405dc..0d2e1518483a0c 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs @@ -17,7 +17,7 @@ internal class ExecutionManager { public const ulong ExecutionManagerCodeRangeMapAddress = 0x000a_fff0; - const int RealCodeHeaderSize = 0x30; // must be big enough for the offsets of RealCodeHeader size in ExecutionManagerTestTarget, below + const int RealCodeHeaderSize = 0x38; // must be big enough for the offsets of RealCodeHeader size in ExecutionManagerTestTarget, below public struct AllocationRange { @@ -233,10 +233,11 @@ public static RangeSectionMapTestBuilder CreateRangeSection(MockTarget.Architect [ new(nameof(Data.RealCodeHeader.MethodDesc), DataType.pointer), new(nameof(Data.RealCodeHeader.DebugInfo), DataType.pointer), + new(nameof(Data.RealCodeHeader.EHInfo), DataType.pointer), new(nameof(Data.RealCodeHeader.GCInfo), DataType.pointer), new(nameof(Data.RealCodeHeader.NumUnwindInfos), DataType.uint32), + new(nameof(Data.RealCodeHeader.EHInfo), DataType.pointer), new(nameof(Data.RealCodeHeader.UnwindInfos), DataType.pointer), - new(nameof(Data.RealCodeHeader.JitEHInfo), DataType.pointer), ] }; @@ -253,6 +254,7 @@ public static RangeSectionMapTestBuilder CreateRangeSection(MockTarget.Architect new(nameof(Data.ReadyToRunInfo.HotColdMap), DataType.pointer), new(nameof(Data.ReadyToRunInfo.DelayLoadMethodCallThunks), DataType.pointer), new(nameof(Data.ReadyToRunInfo.DebugInfoSection), DataType.pointer), + new(nameof(Data.ReadyToRunInfo.ExceptionInfoSection), DataType.pointer), new(nameof(Data.ReadyToRunInfo.EntryPointToMethodDescMap), DataType.Unknown, helpers.LayoutFields(MockDescriptors.HashMap.HashMapFields.Fields).Stride), new(nameof(Data.ReadyToRunInfo.LoadedImageBase), DataType.pointer), new(nameof(Data.ReadyToRunInfo.Composite), DataType.pointer), @@ -514,6 +516,7 @@ public TargetCodePointer AddJittedMethod(JittedCodeRange jittedCodeRange, uint c Builder.TargetTestHelpers.Write(chf.Slice(tyInfo.Fields[nameof(Data.RealCodeHeader.NumUnwindInfos)].Offset, sizeof(uint)), 0u); Builder.TargetTestHelpers.WritePointer(chf.Slice(tyInfo.Fields[nameof(Data.RealCodeHeader.UnwindInfos)].Offset, Builder.TargetTestHelpers.PointerSize), TargetPointer.Null); Builder.TargetTestHelpers.WritePointer(chf.Slice(tyInfo.Fields[nameof(Data.RealCodeHeader.JitEHInfo)].Offset, Builder.TargetTestHelpers.PointerSize), TargetPointer.Null); + Builder.TargetTestHelpers.WritePointer(chf.Slice(tyInfo.Fields[nameof(Data.RealCodeHeader.EHInfo)].Offset, Builder.TargetTestHelpers.PointerSize), TargetPointer.Null); return codeStart; } diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.cs index 7a0b4fd7a95c18..663a914444377f 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.cs @@ -183,7 +183,14 @@ internal record TypeFields [ new(nameof(Data.ExceptionInfo.PreviousNestedInfo), DataType.pointer), new(nameof(Data.ExceptionInfo.ThrownObjectHandle), DataType.pointer), + new(nameof(Data.ExceptionInfo.ExceptionFlags), DataType.uint32), + new(nameof(Data.ExceptionInfo.StackLowBound), DataType.pointer), + new(nameof(Data.ExceptionInfo.StackHighBound), DataType.pointer), new(nameof(Data.ExceptionInfo.ExceptionWatsonBucketTrackerBuckets), DataType.pointer), + new(nameof(Data.ExceptionInfo.PassNumber), DataType.uint8), + new(nameof(Data.ExceptionInfo.CSFEHClause), DataType.pointer), + new(nameof(Data.ExceptionInfo.CSFEnclosingClause), DataType.pointer), + new(nameof(Data.ExceptionInfo.CallerOfActualHandlerFrame), DataType.pointer), ] }; diff --git a/src/native/managed/cdac/tests/gcstress/known-issues.md b/src/native/managed/cdac/tests/gcstress/known-issues.md new file mode 100644 index 00000000000000..323ece6d0e0794 --- /dev/null +++ b/src/native/managed/cdac/tests/gcstress/known-issues.md @@ -0,0 +1,57 @@ +# cDAC Stack Reference Walking — Known Issues + +This document tracks known gaps between the cDAC's stack reference enumeration +and the legacy DAC's `GetStackReferences`. + +## Current Test Results + +Using `DOTNET_CdacStress` with cDAC-vs-DAC comparison: + +| Mode | Non-EH debuggees (6) | ExceptionHandling | +|------|-----------------------|-------------------| +| INSTR (0x8 + GCStress=0x4, step=10) | 0 failures | 0-2 failures | +| ALLOC+UNIQUE (0x5) | 0 failures | 4 failures | +| Walk comparison (0x20, IP+SP) | 0 mismatches | N/A | + +## Known Issue: cDAC Cannot Unwind Through Native Frames + +**Severity**: Low — only affects live-process stress testing during active +exception first-pass dispatch. Does not affect dump analysis where the thread +is suspended with a consistent Frame chain. + +**Pattern**: `cDAC < DAC` (cDAC reports 4 refs, DAC reports 10-13). +ExceptionHandling debuggee only, 4 deterministic occurrences per run. + +**Root cause**: The cDAC's `AMD64Unwinder.Unwind` (and equivalents for other +architectures) can only unwind **managed** frames — it checks +`ExecutionManager.GetCodeBlockHandle(IP)` first and returns false if the IP +is not in a managed code range. This means it cannot unwind through native +runtime frames (allocation helpers, EH dispatch code, etc.). + +When the allocation stress point fires during exception first-pass dispatch: + +1. The thread's `m_pFrame` is `FRAME_TOP` (no explicit Frames in the chain + because the InlinedCallFrame/SoftwareExceptionFrame have been popped or + not yet pushed at that point in the EH dispatch sequence) +2. The initial IP is in native code (allocation helper) +3. The cDAC attempts to unwind through native frames but + `GetCodeBlockHandle` returns null for native IPs → unwind fails +4. With no Frames and no ability to unwind, the walk stops early + +The legacy DAC's `DacStackReferenceWalker::WalkStack` succeeds because +`StackWalkFrames` calls `VirtualUnwindToFirstManagedCallFrame` which uses +OS-level unwind (`RtlVirtualUnwind` on Windows, `PAL_VirtualUnwind` on Unix) +that can unwind ANY native frame using PE `.pdata`/`.xdata` sections. + +**Possible fixes**: +1. **Ensure Frames are always available** — change the runtime to keep + an explicit Frame pushed during allocation points within EH dispatch. + The cDAC cannot do OS-level native unwind (it operates on dumps where + `RtlVirtualUnwind` is not available). The Frame chain is the only + mechanism the cDAC has for transitioning through native code to reach + managed frames. If `m_pFrame = FRAME_TOP` when the IP is native, the + cDAC cannot proceed. +2. **Accept as known limitation** — these failures only occur during + live-process stress testing at a narrow window during EH first-pass + dispatch. In dumps, the exception state is frozen and the Frame chain + is consistent. diff --git a/src/native/managed/cdac/tests/gcstress/test-cdac-gcstress.ps1 b/src/native/managed/cdac/tests/gcstress/test-cdac-gcstress.ps1 new file mode 100644 index 00000000000000..ea16f2d9cfca42 --- /dev/null +++ b/src/native/managed/cdac/tests/gcstress/test-cdac-gcstress.ps1 @@ -0,0 +1,574 @@ +<# +.SYNOPSIS + Build and test the cDAC GC stress verification mode (GCSTRESS_CDAC = 0x20). + +.DESCRIPTION + This script: + 1. Builds CoreCLR native + cDAC tools (incremental) + 2. Generates core_root layout + 3. Compiles a small managed test app + 4. Runs the test with DOTNET_GCStress=0x24 (instruction-level JIT stress + cDAC verification) + + Supports Windows, Linux, and macOS. + +.PARAMETER Configuration + Runtime configuration: Checked (default) or Debug. + +.PARAMETER FailFast + If set, assert on cDAC/runtime mismatch. Otherwise log and continue. + +.PARAMETER SkipBuild + Skip the build step (use existing artifacts). + +.EXAMPLE + ./test-cdac-gcstress.ps1 + ./test-cdac-gcstress.ps1 -Configuration Debug -FailFast + ./test-cdac-gcstress.ps1 -SkipBuild +#> +param( + [ValidateSet("Checked", "Debug")] + [string]$Configuration = "Checked", + + [switch]$FailFast, + + [switch]$SkipBuild +) + +$ErrorActionPreference = "Stop" +$repoRoot = $PSScriptRoot + +# Resolve repo root — walk up from script location to find build script +$buildScript = if ($IsWindows -or $env:OS -eq "Windows_NT") { "build.cmd" } else { "build.sh" } +while ($repoRoot -and !(Test-Path (Join-Path $repoRoot $buildScript))) { + $parent = Split-Path $repoRoot -Parent + if ($parent -eq $repoRoot) { $repoRoot = $null; break } + $repoRoot = $parent +} +if (-not $repoRoot) { + Write-Error "Could not find repo root ($buildScript). Place this script inside the runtime repo." + exit 1 +} + +# Detect platform +$isWin = ($IsWindows -or $env:OS -eq "Windows_NT") +$osName = if ($isWin) { "windows" } elseif ($IsMacOS) { "osx" } else { "linux" } +$arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant() +# Map .NET arch names to runtime conventions +$arch = switch ($arch) { + "x64" { "x64" } + "arm64" { "arm64" } + "arm" { "arm" } + "x86" { "x86" } + default { "x64" } +} + +$platformId = "$osName.$arch" +$coreRoot = Join-Path $repoRoot "artifacts" "tests" "coreclr" "$platformId.$Configuration" "Tests" "Core_Root" +$testDir = Join-Path $repoRoot "artifacts" "tests" "coreclr" "$platformId.$Configuration" "Tests" "cdacgcstresstest" +$buildCmd = Join-Path $repoRoot $buildScript +$dotnetName = if ($isWin) { "dotnet.exe" } else { "dotnet" } +$corerunName = if ($isWin) { "corerun.exe" } else { "corerun" } +$dotnetExe = Join-Path $repoRoot ".dotnet" $dotnetName +$corerunExe = Join-Path $coreRoot $corerunName +$cdacDll = if ($isWin) { "mscordaccore_universal.dll" } elseif ($IsMacOS) { "libmscordaccore_universal.dylib" } else { "libmscordaccore_universal.so" } + +Write-Host "=== cDAC GC Stress Test ===" -ForegroundColor Cyan +Write-Host " Repo root: $repoRoot" +Write-Host " Platform: $platformId" +Write-Host " Configuration: $Configuration" +Write-Host " FailFast: $FailFast" +Write-Host "" + +# --------------------------------------------------------------------------- +# Step 1: Build +# --------------------------------------------------------------------------- +if (-not $SkipBuild) { + Write-Host ">>> Step 1: Building CoreCLR native + cDAC tools ($Configuration)..." -ForegroundColor Yellow + Push-Location $repoRoot + try { + $buildArgs = @("-subset", "clr.native+tools.cdac", "-c", $Configuration, "-rc", $Configuration, "-lc", "Release", "-bl") + & $buildCmd @buildArgs + if ($LASTEXITCODE -ne 0) { Write-Error "Build failed with exit code $LASTEXITCODE"; exit 1 } + } finally { + Pop-Location + } + + Write-Host ">>> Step 1b: Generating core_root layout..." -ForegroundColor Yellow + $testBuildScript = if ($isWin) { + Join-Path $repoRoot "src" "tests" "build.cmd" + } else { + Join-Path $repoRoot "src" "tests" "build.sh" + } + & $testBuildScript $Configuration generatelayoutonly -SkipRestorePackages /p:LibrariesConfiguration=Release + if ($LASTEXITCODE -ne 0) { Write-Error "Core_root generation failed"; exit 1 } +} else { + Write-Host ">>> Step 1: Skipping build (--SkipBuild)" -ForegroundColor DarkGray + if (!(Test-Path $corerunExe)) { + Write-Error "Core_root not found at $coreRoot. Run without -SkipBuild first." + exit 1 + } +} + +# Verify cDAC library exists +if (!(Test-Path (Join-Path $coreRoot $cdacDll))) { + Write-Error "$cdacDll not found in core_root. Ensure cDAC was built." + exit 1 +} + +# --------------------------------------------------------------------------- +# Step 2: Compile test app +# --------------------------------------------------------------------------- +Write-Host ">>> Step 2: Compiling test app..." -ForegroundColor Yellow +New-Item -ItemType Directory -Force $testDir | Out-Null + +$testSource = @" +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +// ------------------------------------------------------------------- +// Comprehensive cDAC GC stress test exercising many frame types +// ------------------------------------------------------------------- + +interface IKeepAlive +{ + object GetRef(); +} + +class BoxHolder : IKeepAlive +{ + object _value; + public BoxHolder() { _value = new object(); } + public BoxHolder(object v) { _value = v; } + + [MethodImpl(MethodImplOptions.NoInlining)] + public object GetRef() => _value; +} + +struct LargeStruct +{ + public object A, B, C, D; +} + +class CdacGcStressTest +{ + // 1. Basic allocation — the original test + [MethodImpl(MethodImplOptions.NoInlining)] + static object AllocAndHold() + { + object o = new object(); + string s = "hello world"; + int[] arr = new int[] { 1, 2, 3 }; + GC.KeepAlive(o); + GC.KeepAlive(s); + GC.KeepAlive(arr); + return o; + } + + // 2. Deep recursion — many managed frames + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedCall(int depth) + { + object o = new object(); + if (depth > 0) + NestedCall(depth - 1); + GC.KeepAlive(o); + } + + // 3. Try/catch — funclet frames (catch handler is a funclet on AMD64) + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryCatchScenario() + { + object before = new object(); + try + { + object inside = new object(); + ThrowHelper(); + GC.KeepAlive(inside); + } + catch (InvalidOperationException ex) + { + object inCatch = new object(); + GC.KeepAlive(ex); + GC.KeepAlive(inCatch); + } + GC.KeepAlive(before); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowHelper() + { + throw new InvalidOperationException("test exception"); + } + + // 4. Try/finally — finally funclet + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryFinallyScenario() + { + object outerRef = new object(); + try + { + object innerRef = new object(); + GC.KeepAlive(innerRef); + } + finally + { + object finallyRef = new object(); + GC.KeepAlive(finallyRef); + } + GC.KeepAlive(outerRef); + } + + // 5. Nested exception handling — funclet within funclet parent + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedExceptionScenario() + { + object a = new object(); + try + { + object b = new object(); + try + { + object c = new object(); + throw new ArgumentException("inner"); + } + catch (ArgumentException ex1) + { + GC.KeepAlive(ex1); + throw new InvalidOperationException("outer", ex1); + } + finally + { + object d = new object(); + GC.KeepAlive(d); + } + } + catch (InvalidOperationException ex2) + { + GC.KeepAlive(ex2); + } + GC.KeepAlive(a); + } + + // 6. Filter funclet (when clause via helper) + [MethodImpl(MethodImplOptions.NoInlining)] + static void FilterExceptionScenario() + { + object holder = new object(); + try + { + throw new ArgumentException("filter-test"); + } + catch (ArgumentException ex) when (FilterCheck(ex)) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(holder); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static bool FilterCheck(Exception ex) + { + object filterLocal = new object(); + GC.KeepAlive(filterLocal); + return ex.Message.Contains("filter"); + } + + // 7. Generic methods — different instantiations + [MethodImpl(MethodImplOptions.NoInlining)] + static T GenericAlloc() where T : new() + { + T val = new T(); + object marker = new object(); + GC.KeepAlive(marker); + return val; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void GenericScenario() + { + var o = GenericAlloc(); + var l = GenericAlloc>(); + var s = GenericAlloc(); + GC.KeepAlive(o); + GC.KeepAlive(l); + GC.KeepAlive(s); + } + + // 8. Interface dispatch — virtual calls through interface + [MethodImpl(MethodImplOptions.NoInlining)] + static void InterfaceDispatchScenario() + { + IKeepAlive holder = new BoxHolder(new int[] { 42, 43 }); + object r = holder.GetRef(); + GC.KeepAlive(holder); + GC.KeepAlive(r); + } + + // 9. Delegate invocation + [MethodImpl(MethodImplOptions.NoInlining)] + static void DelegateScenario() + { + object captured = new object(); + Func fn = () => + { + GC.KeepAlive(captured); + return new object(); + }; + object result = fn(); + GC.KeepAlive(result); + GC.KeepAlive(fn); + } + + // 10. Struct with object references on stack + [MethodImpl(MethodImplOptions.NoInlining)] + static void StructWithRefsScenario() + { + LargeStruct ls; + ls.A = new object(); + ls.B = "struct-string"; + ls.C = new int[] { 10, 20 }; + ls.D = new BoxHolder(ls.A); + GC.KeepAlive(ls.A); + GC.KeepAlive(ls.B); + GC.KeepAlive(ls.C); + GC.KeepAlive(ls.D); + } + + // 11. Pinned references via GCHandle + [MethodImpl(MethodImplOptions.NoInlining)] + static void PinnedScenario() + { + byte[] buffer = new byte[64]; + GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned); + try + { + object other = new object(); + GC.KeepAlive(other); + GC.KeepAlive(buffer); + } + finally + { + pin.Free(); + } + } + + // 12. Multiple threads — concurrent stack walks + [MethodImpl(MethodImplOptions.NoInlining)] + static void MultiThreadScenario() + { + ManualResetEventSlim ready = new ManualResetEventSlim(false); + ManualResetEventSlim go = new ManualResetEventSlim(false); + Thread t = new Thread(() => + { + object threadLocal = new object(); + ready.Set(); + go.Wait(); + NestedCall(5); + GC.KeepAlive(threadLocal); + }); + t.Start(); + ready.Wait(); + go.Set(); + + // Main thread also does work concurrently + NestedCall(3); + t.Join(); + } + + // 13. Many live references — stress GC slot reporting + [MethodImpl(MethodImplOptions.NoInlining)] + static void ManyLiveRefsScenario() + { + object r0 = new object(); + object r1 = new object(); + object r2 = new object(); + object r3 = new object(); + object r4 = new object(); + object r5 = new object(); + object r6 = new object(); + object r7 = new object(); + string r8 = "live-string"; + int[] r9 = new int[10]; + List r10 = new List { r0, r1, r2 }; + object[] r11 = new object[] { r3, r4, r5, r6, r7 }; + + GC.KeepAlive(r0); GC.KeepAlive(r1); + GC.KeepAlive(r2); GC.KeepAlive(r3); + GC.KeepAlive(r4); GC.KeepAlive(r5); + GC.KeepAlive(r6); GC.KeepAlive(r7); + GC.KeepAlive(r8); GC.KeepAlive(r9); + GC.KeepAlive(r10); GC.KeepAlive(r11); + } + + // 14. P/Invoke transition — native frame on stack + [DllImport("kernel32.dll")] + static extern uint GetCurrentThreadId(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static void PInvokeScenario() + { + object before = new object(); + uint tid = GetCurrentThreadId(); + object after = new object(); + GC.KeepAlive(before); + GC.KeepAlive(after); + } + + // 15. Exception rethrow — stack trace preservation + [MethodImpl(MethodImplOptions.NoInlining)] + static void RethrowScenario() + { + object outerRef = new object(); + try + { + try + { + throw new ApplicationException("rethrow-test"); + } + catch (ApplicationException) + { + object catchRef = new object(); + GC.KeepAlive(catchRef); + throw; // rethrow preserves original stack + } + } + catch (ApplicationException ex) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(outerRef); + } + + static int Main() + { + Console.WriteLine("Starting comprehensive cDAC GC Stress test..."); + + for (int i = 0; i < 3; i++) + { + Console.WriteLine($" Iteration {i + 1}/3"); + + AllocAndHold(); + NestedCall(5); + TryCatchScenario(); + TryFinallyScenario(); + NestedExceptionScenario(); + FilterExceptionScenario(); + GenericScenario(); + InterfaceDispatchScenario(); + DelegateScenario(); + StructWithRefsScenario(); + PinnedScenario(); + MultiThreadScenario(); + ManyLiveRefsScenario(); + PInvokeScenario(); + RethrowScenario(); + } + + Console.WriteLine("cDAC GC Stress test completed successfully."); + return 100; + } +} +"@ +$testCs = Join-Path $testDir "test.cs" +$testDll = Join-Path $testDir "test.dll" + +Set-Content $testCs $testSource + +$cscPath = Get-ChildItem (Join-Path $repoRoot ".dotnet" "sdk") -Recurse -Filter "csc.dll" | Select-Object -First 1 +if (-not $cscPath) { Write-Error "Could not find csc.dll in .dotnet SDK"; exit 1 } + +$sysRuntime = Join-Path $coreRoot "System.Runtime.dll" +$sysConsole = Join-Path $coreRoot "System.Console.dll" +$sysCoreLib = Join-Path $coreRoot "System.Private.CoreLib.dll" +$sysThread = Join-Path $coreRoot "System.Threading.dll" +$sysInterop = Join-Path $coreRoot "System.Runtime.InteropServices.dll" + +& $dotnetExe exec $cscPath.FullName ` + "/out:$testDll" /target:exe /nologo /unsafe ` + "/r:$sysRuntime" ` + "/r:$sysConsole" ` + "/r:$sysCoreLib" ` + "/r:$sysThread" ` + "/r:$sysInterop" ` + $testCs +if ($LASTEXITCODE -ne 0) { Write-Error "Test compilation failed"; exit 1 } + +# --------------------------------------------------------------------------- +# Step 3: Run baseline (no GCStress) to verify test works +# --------------------------------------------------------------------------- +Write-Host ">>> Step 3: Running baseline (no GCStress)..." -ForegroundColor Yellow +$env:CORE_ROOT = $coreRoot + +# Clear any leftover GCStress env vars +Remove-Item Env:\DOTNET_GCStress -ErrorAction SilentlyContinue +Remove-Item Env:\DOTNET_GCStressCdacFailFast -ErrorAction SilentlyContinue +Remove-Item Env:\DOTNET_ContinueOnAssert -ErrorAction SilentlyContinue + +& $corerunExe (Join-Path $testDir "test.dll") +if ($LASTEXITCODE -ne 100) { + Write-Error "Baseline test failed with exit code $LASTEXITCODE (expected 100)" + exit 1 +} +Write-Host " Baseline passed." -ForegroundColor Green + +# --------------------------------------------------------------------------- +# Step 4: Run with GCStress=0x4 only (no cDAC) to verify GCStress works +# --------------------------------------------------------------------------- +Write-Host ">>> Step 4: Running with GCStress=0x4 (baseline, no cDAC)..." -ForegroundColor Yellow +$env:DOTNET_GCStress = "0x4" +$env:DOTNET_ContinueOnAssert = "1" + +& $corerunExe (Join-Path $testDir "test.dll") +if ($LASTEXITCODE -ne 100) { + Write-Error "GCStress=0x4 baseline failed with exit code $LASTEXITCODE (expected 100)" + exit 1 +} +Write-Host " GCStress=0x4 baseline passed." -ForegroundColor Green + +# --------------------------------------------------------------------------- +# Step 5: Run with GCStress=0x24 (instruction JIT + cDAC verification) +# --------------------------------------------------------------------------- +Write-Host ">>> Step 5: Running with GCStress=0x24 (cDAC verification)..." -ForegroundColor Yellow +$logFile = Join-Path $testDir "cdac-gcstress-results.txt" +$env:DOTNET_GCStress = "0x24" +$env:DOTNET_GCStressCdacFailFast = if ($FailFast) { "1" } else { "0" } +$env:DOTNET_GCStressCdacLogFile = $logFile +if (-not $FailFast) { + $env:DOTNET_ContinueOnAssert = "1" +} + +& $corerunExe (Join-Path $testDir "test.dll") +$testExitCode = $LASTEXITCODE + +# --------------------------------------------------------------------------- +# Cleanup +# --------------------------------------------------------------------------- +Remove-Item Env:\DOTNET_GCStress -ErrorAction SilentlyContinue +Remove-Item Env:\DOTNET_GCStressCdacFailFast -ErrorAction SilentlyContinue +Remove-Item Env:\DOTNET_GCStressCdacLogFile -ErrorAction SilentlyContinue +Remove-Item Env:\DOTNET_ContinueOnAssert -ErrorAction SilentlyContinue + +# --------------------------------------------------------------------------- +# Report results +# --------------------------------------------------------------------------- +Write-Host "" +if ($testExitCode -eq 100) { + Write-Host "=== ALL TESTS PASSED ===" -ForegroundColor Green + Write-Host " cDAC GC stress verification completed successfully." + Write-Host " GCStress=0x24 ran without fatal errors." +} else { + Write-Host "=== TEST FAILED ===" -ForegroundColor Red + Write-Host " GCStress=0x24 test exited with code $testExitCode (expected 100)." +} + +if (Test-Path $logFile) { + Write-Host "" + Write-Host "Results written to: $logFile" -ForegroundColor Cyan + Write-Host "" + Get-Content $logFile | Select-Object -Last 20 +} + +exit $(if ($testExitCode -eq 100) { 0 } else { 1 })