diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index 12869acd36f828..fdc7f032f21b9c 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -50,7 +50,10 @@ struct CodeBlockHandle List GetExceptionClauses(CodeBlockHandle codeInfoHandle); // Extension Methods (implemented in terms of other APIs) + // Returns true if the code block is a funclet (exception handler, filter, or finally) bool IsFunclet(CodeBlockHandle codeInfoHandle); + // Returns true if the code block is specifically a filter funclet + bool IsFilterFunclet(CodeBlockHandle codeInfoHandle); ``` ```csharp @@ -450,6 +453,8 @@ There are two distinct clause data types. JIT-compiled code uses `EEExceptionCla After obtaining the clause array bounds, the common iteration logic classifies each clause by its flags. The native `COR_ILEXCEPTION_CLAUSE` flags are bit flags: `Filter` (0x1), `Finally` (0x2), `Fault` (0x4). If none are set, the clause is `Typed`. For typed clauses, if the `CachedClass` flag (0x10000000) is set (JIT-only, used for dynamic methods), the union field contains a resolved `TypeHandle` pointer; the clause is a catch-all if this pointer equals the `ObjectMethodTable` global. Otherwise, the union field is a metadata `ClassToken`. To determine whether a typed clause is a catch-all handler, the `ClassToken` (which may be a `TypeDef` or `TypeRef`) is resolved to a `MethodTable` via the `Loader` contract's module lookup maps (`TypeDefToMethodTable` or `TypeRefToMethodTable`) and compared against the `ObjectMethodTable` global. For typed clauses without a cached type handle, the module address is resolved by walking `CodeBlockHandle` → `MethodDesc` → `MethodTable` → `TypeHandle` → `Module` via the `RuntimeTypeSystem` contract. +`IsFilterFunclet` first checks `IsFunclet`. If the code block is a funclet, it retrieves the EH clauses for the method and checks whether any filter clause's handler offset matches the funclet's relative offset. If a match is found, the funclet is a filter funclet. + ### RangeSectionMap The range section map logically partitions the entire 32-bit or 64-bit addressable space into chunks. diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index bc83127e4df2c8..c77d5f296736f3 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -74,12 +74,25 @@ This contract depends on the following descriptors: | `CalleeSavedRegisters` | For each callee saved register `r`, `r` | Register names associated with stored register values | | `TailCallFrame` (x86 Windows) | `CalleeSavedRegisters` | CalleeSavedRegisters data structure | | `TailCallFrame` (x86 Windows) | `ReturnAddress` | Frame's stored instruction pointer | +| `ExceptionInfo` | `ExceptionFlags` | Bit flags from `ExceptionFlags` class (`exstatecommon.h`). Used for GC reference reporting during stack walks with funclet handling. | +| `ExceptionInfo` | `StackLowBound` | Low bound of the stack range unwound by this exception | +| `ExceptionInfo` | `StackHighBound` | High bound of the stack range unwound by this exception | +| `ExceptionInfo` | `CSFEHClause` | Caller stack frame of the current EH clause | +| `ExceptionInfo` | `CSFEnclosingClause` | Caller stack frame of the enclosing clause | +| `ExceptionInfo` | `CallerOfActualHandlerFrame` | Stack frame of the caller of the catch handler | +| `ExceptionInfo` | `PreviousNestedInfo` | Pointer to previous nested ExInfo | +| `ExceptionInfo` | `PassNumber` | Exception handling pass (1 or 2) | Global variables used: | Global Name | Type | Purpose | | --- | --- | --- | | For each FrameType ``, `##Identifier` | `FrameIdentifier` enum value | Identifier used to determine concrete type of Frames | +Constants used: +| Source | Name | Value | Purpose | +| --- | --- | --- | --- | +| `ExceptionFlags` (`exstatecommon.h`) | `Ex_UnwindHasStarted` | `0x00000004` | Bit flag in `ExceptionInfo.ExceptionFlags` indicating exception unwinding (2nd pass) has started. Used by `IsInStackRegionUnwoundBySpecifiedException` to skip ExInfo trackers still in the 1st pass. | + Contracts used: | Contract Name | | --- | @@ -369,11 +382,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/src/coreclr/inc/clrconfigvalues.h b/src/coreclr/inc/clrconfigvalues.h index fd64be3df1b59f..e5e025d82a18a8 100644 --- a/src/coreclr/inc/clrconfigvalues.h +++ b/src/coreclr/inc/clrconfigvalues.h @@ -747,6 +747,8 @@ 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_GCStressCdacFailFast, W("GCStressCdacFailFast"), 0, "If nonzero, assert on cDAC/runtime GC ref mismatch during GC stress (GCSTRESS_CDAC mode).") +RETAIL_CONFIG_STRING_INFO(INTERNAL_GCStressCdacLogFile, W("GCStressCdacLogFile"), "Log file path for cDAC GC stress verification results.") 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..c0bb372e19a9ae 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, diff --git a/src/coreclr/vm/CMakeLists.txt b/src/coreclr/vm/CMakeLists.txt index 3a4c0babdab259..b765e7018f0453 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 + cdacgcstress.cpp frozenobjectheap.cpp gccover.cpp gcenv.ee.cpp diff --git a/src/coreclr/vm/cdacgcstress.cpp b/src/coreclr/vm/cdacgcstress.cpp new file mode 100644 index 00000000000000..ead6223d73eff2 --- /dev/null +++ b/src/coreclr/vm/cdacgcstress.cpp @@ -0,0 +1,814 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// +// cdacgcstress.cpp +// +// Implements in-process cDAC loading and stack reference verification +// for GC stress testing. When GCSTRESS_CDAC (0x20) is enabled, at each +// instruction-level GC 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 "cdacgcstress.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" + +#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 — common +static bool s_initialized = false; +static bool s_failFast = true; +static FILE* s_logFile = nullptr; +static CrstStatic s_cdacLock; // Serializes cDAC access from concurrent GC stress threads + +// 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; +} + +//----------------------------------------------------------------------------- +// Initialization / Shutdown +//----------------------------------------------------------------------------- + +bool CdacGcStress::IsEnabled() +{ + return (g_pConfig->GetGCStressLevel() & EEConfig::GCSTRESS_CDAC) != 0; +} + +bool CdacGcStress::IsInitialized() +{ + return s_initialized; +} + +bool CdacGcStress::Initialize() +{ + if (!IsEnabled()) + return false; + + // 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_GCStressCdacFailFast) != 0; + + // 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_GCStressCdacLogFile)); + 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\n", s_failFast ? "true" : "false"); + } + } + + s_cdacLock.Init(CrstGCCover, CRST_DEFAULT); + 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 CdacGcStress::Shutdown() +{ + if (!s_initialized) + return; + + // Print summary to stderr so results are always visible + fprintf(stderr, "CDAC GC Stress: %ld verifications (%ld pass / %ld fail, %ld skipped)\n", + (long)s_verifyCount, (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)s_verifyCount, (int)s_verifyPass, (int)s_verifyFail); + + if (s_logFile != nullptr) + { + fprintf(s_logFile, "\n=== Summary ===\n"); + fprintf(s_logFile, "Total verifications: %ld\n", (long)s_verifyCount); + 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; + } + + if (s_cdacModule != NULL) + { + ::FreeLibrary(s_cdacModule); + s_cdacModule = NULL; + } + + s_initialized = false; + LOG((LF_GCROOTS, LL_INFO10, "CDAC GC Stress: Shutdown complete\n")); +} + +//----------------------------------------------------------------------------- +// Collect stack refs from the cDAC +//----------------------------------------------------------------------------- + +static bool CollectCdacStackRefs(Thread* pThread, PCONTEXT regs, SArray* pRefs) +{ + _ASSERTE(s_cdacSosDac != nullptr); + + ISOSStackRefEnum* pEnum = nullptr; + HRESULT hr = s_cdacSosDac->GetStackReferences(pThread->GetOSThreadId(), &pEnum); + + if (FAILED(hr) || pEnum == nullptr) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: GetStackReferences failed (hr=0x%08x)\n", hr)); + return false; + } + + // Enumerate all refs + 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); + } + + 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++]; + + // Detect whether ppObj is a register save slot (in REGDISPLAY/CONTEXT on the native + // C stack) or a real managed stack slot. The cDAC reports register refs as (Address=0, + // Object=value), so we normalize the runtime's output to match. + // REGDISPLAY slots live below stack_limit; managed stack slots are at or above it. + bool isRegisterRef = reinterpret_cast(ppObj) < sc->stack_limit; + + if (isRegisterRef) + { + ref.Address = 0; + ref.Object = reinterpret_cast(*ppObj); + } + else + { + 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; + ref.Register = 0; + ref.Offset = 0; + ref.StackPointer = 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; + + pThread->StackWalkFrames(GcStackCrawlCallBack, &gcctx, 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); + } +} + +//----------------------------------------------------------------------------- +// Main entry point: verify at a GC stress point +//----------------------------------------------------------------------------- + +void CdacGcStress::VerifyAtStressPoint(Thread* pThread, PCONTEXT regs) +{ + _ASSERTE(s_initialized); + _ASSERTE(pThread != nullptr); + _ASSERTE(regs != nullptr); + + InterlockedIncrement(&s_verifyCount); + + // 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); + + // 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(); + } + + // Collect from cDAC + SArray cdacRefs; + bool haveCdac = CollectCdacStackRefs(pThread, regs, &cdacRefs); + + // Clear the stored context + s_currentContext = nullptr; + s_currentThreadId = 0; + + // Collect runtime refs (doesn't use cDAC, no timing issue) + StackRef runtimeRefsBuf[MAX_COLLECTED_REFS]; + int runtimeCount = 0; + bool runtimeComplete = 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", + pThread->GetOSThreadId(), (void*)GetIP(regs)); + return; + } + + if (!runtimeComplete) + { + InterlockedIncrement(&s_verifySkip); + if (s_logFile != nullptr) + fprintf(s_logFile, "[SKIP] Thread=0x%x IP=0x%p - runtime ref buffer overflow (>%d refs)\n", + pThread->GetOSThreadId(), (void*)GetIP(regs), MAX_COLLECTED_REFS); + return; + } + + // Filter cDAC refs to match runtime PromoteCarefully behavior: + // remove interior pointers whose Object value is a stack address. + // These are register slots (RSP/RBP) that GcInfo marks as live interior + // but don't point to managed heap objects. + 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 cdacCount = (int)cdacRefs.GetCount(); + if (cdacCount > 0) + { + StackRef* cdacBuf = cdacRefs.OpenRawBuffer(); + cdacCount = FilterInteriorStackRefs(cdacBuf, cdacCount, pThread, stackLimit); + cdacCount = DeduplicateRefs(cdacBuf, cdacCount); + cdacRefs.CloseRawBuffer(); + // Trim the SArray to the filtered count + while ((int)cdacRefs.GetCount() > cdacCount) + cdacRefs.Delete(cdacRefs.End() - 1); + } + + // Sort and deduplicate runtime refs to match cDAC ordering for element-wise comparison. + runtimeCount = DeduplicateRefs(runtimeRefsBuf, runtimeCount); + + // Compare cDAC vs runtime. + // If the stress IP is in a RangeList section (dynamic method / IL Stub), + // the cDAC can't decode GcInfo for it (known gap matching DAC behavior). + // Skip comparison for these — the runtime reports refs from the Frame chain + // that neither DAC nor cDAC can reproduce via GetStackReferences. + PCODE stressIP = GetIP(regs); + bool isDynamicMethod = false; + { + RangeSection* pRS = ExecutionManager::FindCodeRange(stressIP, ExecutionManager::ScanReaderLock); + if (pRS != nullptr) + { + isDynamicMethod = (pRS->_flags & RangeSection::RANGE_SECTION_RANGELIST) != 0; + // Also check if this is a dynamic method by checking the MethodDesc + if (!isDynamicMethod) + { + EECodeInfo ci(stressIP); + if (ci.IsValid() && ci.GetMethodDesc() != nullptr && + (ci.GetMethodDesc()->IsLCGMethod() || ci.GetMethodDesc()->IsILStub())) + isDynamicMethod = true; + } + } + } + + bool pass = (cdacCount == runtimeCount); + if (pass && cdacCount > 0) + { + // Counts match — verify that the same (Address, Object, Flags) tuples are reported. + // Both sides normalize register refs to Address=0 and stack refs to the actual + // stack slot address, so all three fields should match. + StackRef* cdacBuf = cdacRefs.OpenRawBuffer(); + + // Build sorted (Address, Object, Flags) arrays for both sets + struct RefTuple { CLRDATA_ADDRESS Address; CLRDATA_ADDRESS Object; unsigned int Flags; }; + auto compareRefTuple = [](const void* a, const void* b) -> int { + const RefTuple* ra = static_cast(a); + const RefTuple* rb = static_cast(b); + if (ra->Address != rb->Address) + return (ra->Address < rb->Address) ? -1 : 1; + if (ra->Object != rb->Object) + return (ra->Object < rb->Object) ? -1 : 1; + if (ra->Flags != rb->Flags) + return (ra->Flags < rb->Flags) ? -1 : 1; + return 0; + }; + + // Use stack buffers — counts are bounded by MAX_COLLECTED_REFS + RefTuple cdacRT[MAX_COLLECTED_REFS]; + RefTuple rtRT[MAX_COLLECTED_REFS]; + for (int i = 0; i < cdacCount; i++) + { + cdacRT[i] = { cdacBuf[i].Address, cdacBuf[i].Object, cdacBuf[i].Flags }; + rtRT[i] = { runtimeRefsBuf[i].Address, runtimeRefsBuf[i].Object, runtimeRefsBuf[i].Flags }; + } + qsort(cdacRT, cdacCount, sizeof(RefTuple), compareRefTuple); + qsort(rtRT, cdacCount, sizeof(RefTuple), compareRefTuple); + + for (int i = 0; i < cdacCount; i++) + { + if (cdacRT[i].Address != rtRT[i].Address || + cdacRT[i].Object != rtRT[i].Object || + cdacRT[i].Flags != rtRT[i].Flags) + { + pass = false; + break; + } + } + cdacRefs.CloseRawBuffer(); + } + if (!pass && isDynamicMethod) + { + // Known gap: dynamic method refs not in cDAC. Treat as pass but log. + pass = true; + } + + if (pass) + InterlockedIncrement(&s_verifyPass); + else + InterlockedIncrement(&s_verifyFail); + + if (s_logFile != nullptr) + { + fprintf(s_logFile, "[%s] Thread=0x%x IP=0x%p cDAC=%d RT=%d\n", + pass ? "PASS" : "FAIL", pThread->GetOSThreadId(), (void*)GetIP(regs), cdacCount, runtimeCount); + + if (!pass) + { + // Log the stress point IP and the first cDAC Source for debugging + fprintf(s_logFile, " stressIP=0x%p firstCdacSource=0x%llx\n", + (void*)stressIP, + cdacCount > 0 ? (unsigned long long)cdacRefs[0].Source : 0ULL); + + // Check if any cDAC ref has the stress IP as its Source + bool leafFound = false; + for (int i = 0; i < cdacCount; i++) + { + if ((PCODE)cdacRefs[i].Source == stressIP) + { + leafFound = true; + break; + } + } + if (!leafFound && cdacCount < runtimeCount) + { + fprintf(s_logFile, " DIAG: Leaf frame at stressIP NOT in cDAC sources (cDAC < RT)\n"); + + // Check if the stress IP is in a managed method + bool isManaged = ExecutionManager::IsManagedCode(stressIP); + fprintf(s_logFile, " DIAG: IsManaged(stressIP)=%d\n", isManaged); + + if (isManaged) + { + // Get the method's code range to see if cDAC walks ANY offset in this method + EECodeInfo codeInfo(stressIP); + if (codeInfo.IsValid()) + { + PCODE methodStart = codeInfo.GetStartAddress(); + MethodDesc* pMD = codeInfo.GetMethodDesc(); + fprintf(s_logFile, " DIAG: Method start=0x%p relOffset=0x%x %s::%s\n", + (void*)methodStart, codeInfo.GetRelOffset(), + pMD ? pMD->m_pszDebugClassName : "?", + pMD ? pMD->m_pszDebugMethodName : "?"); + + // Check if the cDAC can resolve this IP to a MethodDesc + if (s_cdacSosDac != nullptr) + { + CLRDATA_ADDRESS cdacMD = 0; + HRESULT hrMD = s_cdacSosDac->GetMethodDescPtrFromIP((CLRDATA_ADDRESS)stressIP, &cdacMD); + fprintf(s_logFile, " DIAG: cDAC GetMethodDescPtrFromIP hr=0x%x MD=0x%llx\n", + hrMD, (unsigned long long)cdacMD); + } + + // Check if cDAC has ANY ref from this method (Source near methodStart) + bool methodFound = false; + for (int i = 0; i < cdacCount; i++) + { + PCODE src = (PCODE)cdacRefs[i].Source; + if (src >= methodStart && src < methodStart + 0x10000) // rough range + { + methodFound = true; + fprintf(s_logFile, " DIAG: cDAC has ref from same method at Source=0x%llx (offset=0x%llx)\n", + (unsigned long long)src, (unsigned long long)(src - methodStart)); + break; + } + } + if (!methodFound) + fprintf(s_logFile, " DIAG: cDAC has NO refs from this method at all\n"); + } + } + + // Check what the first RT ref looks like + if (runtimeCount > 0) + fprintf(s_logFile, " DIAG: RT[0]: Address=0x%llx Object=0x%llx Flags=0x%x\n", + (unsigned long long)runtimeRefsBuf[0].Address, + (unsigned long long)runtimeRefsBuf[0].Object, + runtimeRefsBuf[0].Flags); + } + + for (int i = 0; i < cdacCount; i++) + fprintf(s_logFile, " cDAC [%d]: Address=0x%llx Object=0x%llx Flags=0x%x Source=0x%llx SourceType=%d Reg=%d Offset=%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, + cdacRefs[i].Register, cdacRefs[i].Offset, (unsigned long long)cdacRefs[i].StackPointer); + 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); + } + } + + if (!pass) + { + ReportMismatch("cDAC stack reference verification failed - mismatch between cDAC and runtime GC refs", pThread, regs); + } +} + +#endif // HAVE_GCCOVER diff --git a/src/coreclr/vm/cdacgcstress.h b/src/coreclr/vm/cdacgcstress.h new file mode 100644 index 00000000000000..5b421becbec050 --- /dev/null +++ b/src/coreclr/vm/cdacgcstress.h @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// +// cdacgcstress.h +// +// Infrastructure for verifying cDAC stack reference reporting against the +// runtime's own GC root enumeration at GC stress instruction-level trigger points. +// +// Enabled via GCSTRESS_CDAC (0x20) flag in DOTNET_GCStress. +// + +#ifndef _CDAC_GC_STRESS_H_ +#define _CDAC_GC_STRESS_H_ + +#ifdef HAVE_GCCOVER + +// Forward declarations +class Thread; + +class CdacGcStress +{ +public: + // Initialize the cDAC in-process for GC stress verification. + // Must be called after the contract descriptor is built and GC is initialized. + // Returns true if initialization succeeded. + static bool Initialize(); + + // Shutdown and release cDAC resources. + static void Shutdown(); + + // Returns true if cDAC GC stress verification is initialized and ready. + static bool IsInitialized(); + + // Returns true if GCSTRESS_CDAC flag is set in the GCStress level. + static bool IsEnabled(); + + // Main entry point: verify cDAC stack refs match runtime stack refs at a GC stress point. + // Called from DoGcStress before StressHeap(). + // pThread - the thread being stress-tested + // regs - the register context at the stress point + static void VerifyAtStressPoint(Thread* pThread, PCONTEXT regs); +}; + +#endif // HAVE_GCCOVER +#endif // _CDAC_GC_STRESS_H_ diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 6ce92ecfff8e6a..ce5e4d016c9ed0 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -209,6 +209,10 @@ #include "genanalysis.h" +#ifdef HAVE_GCCOVER +#include "cdacgcstress.h" +#endif + HRESULT EEStartup(); @@ -963,6 +967,10 @@ void EEStartupHelper() #ifdef HAVE_GCCOVER MethodDesc::Init(); + if (GCStress::IsEnabled() && (g_pConfig->GetGCStressLevel() & EEConfig::GCSTRESS_CDAC)) + { + CdacGcStress::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 + CdacGcStress::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.h b/src/coreclr/vm/datadescriptor/datadescriptor.h index 36c62393091e66..0f3f5161b4020f 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.h +++ b/src/coreclr/vm/datadescriptor/datadescriptor.h @@ -28,6 +28,12 @@ #include "../debug/ee/debugger.h" #include "patchpointinfo.h" +template<> +struct cdac_data +{ + static constexpr size_t LocalCount = offsetof(PatchpointInfo, m_numberOfLocals); +}; + #ifdef HAVE_GCCOVER #include "gccover.h" #endif // HAVE_GCCOVER diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index b6cd256d09dc32..a66b587bf02add 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -132,14 +132,20 @@ 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_END(ExceptionInfo) - CDAC_TYPE_BEGIN(GCHandle) CDAC_TYPE_SIZE(sizeof(OBJECTHANDLE)) CDAC_TYPE_END(GCHandle) @@ -663,6 +669,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,6 +771,7 @@ CDAC_TYPE_BEGIN(RealCodeHeader) CDAC_TYPE_INDETERMINATE(RealCodeHeader) CDAC_TYPE_FIELD(RealCodeHeader, /*pointer*/, MethodDesc, offsetof(RealCodeHeader, phdrMDesc)) CDAC_TYPE_FIELD(RealCodeHeader, /*pointer*/, DebugInfo, offsetof(RealCodeHeader, phdrDebugInfo)) +CDAC_TYPE_FIELD(RealCodeHeader, /*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)) @@ -795,6 +803,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)) 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/gccover.cpp b/src/coreclr/vm/gccover.cpp index 26d0f0b78efa8b..725e935957cad2 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 "cdacgcstress.h" #if defined(TARGET_AMD64) || defined(TARGET_ARM) #include "gcinfodecoder.h" @@ -887,6 +888,12 @@ void DoGcStress (PCONTEXT regs, NativeCodeVersion nativeCodeVersion) // Do the actual stress work // + // Verify cDAC stack references before triggering the GC (while refs haven't moved). + if (CdacGcStress::IsInitialized()) + { + CdacGcStress::VerifyAtStressPoint(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,6 +1202,12 @@ void DoGcStress (PCONTEXT regs, NativeCodeVersion nativeCodeVersion) // Do the actual stress work // + // Verify cDAC stack references before triggering the GC (while refs haven't moved). + if (CdacGcStress::IsInitialized()) + { + CdacGcStress::VerifyAtStressPoint(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()); 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/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/TargetPointer.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/TargetPointer.cs index d145347f3220a9..2c1f199bb8e1e7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/TargetPointer.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/TargetPointer.cs @@ -1,5 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. + using System; namespace Microsoft.Diagnostics.DataContractReader; @@ -19,6 +20,8 @@ namespace Microsoft.Diagnostics.DataContractReader; public static bool operator ==(TargetPointer left, TargetPointer right) => left.Value == right.Value; public static bool operator !=(TargetPointer left, TargetPointer right) => left.Value != right.Value; + public static TargetPointer PlatformMaxValue(Target target) => target.PointerSize == 4 ? Max32Bit : Max64Bit; + public override bool Equals(object? obj) => obj is TargetPointer pointer && Equals(pointer); public bool Equals(TargetPointer other) => Value == other.Value; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.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..d6a6a0da8b39f4 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,368 @@ public uint GetCodeLength() return _codeLength; } + public IReadOnlyList GetInterruptibleRanges() + { + EnsureDecodedTo(DecodePoints.InterruptibleRanges); + return _interruptibleRanges; + } + + 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..86f4210a7cb91d 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,39 @@ // 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; } + + /// + /// 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/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..8f9c79fa6f1cdf --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/ExceptionHandling.cs @@ -0,0 +1,182 @@ +// 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); + } + +} 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..fa72eb606fad75 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +internal class GcScanner +{ + private readonly Target _target; + private readonly IExecutionManager _eman; + private readonly IGCInfo _gcInfo; + + internal GcScanner(Target target) + { + _target = target; + _eman = target.Contracts.ExecutionManager; + _gcInfo = target.Contracts.GCInfo; + } + + public bool EnumGcRefs( + IPlatformAgnosticContext context, + CodeBlockHandle cbh, + CodeManagerFlags flags, + GcScanContext scanContext) + { + TargetNUInt relativeOffset = _eman.GetRelativeOffset(cbh); + _eman.GetGCInfo(cbh, out TargetPointer gcInfoAddr, out uint gcVersion); + + if (_eman.IsFilterFunclet(cbh)) + flags |= CodeManagerFlags.NoReportUntracked; + + IGCInfoHandle handle = _gcInfo.DecodePlatformSpecificGCInfo(gcInfoAddr, gcVersion); + if (handle is not IGCInfoDecoder decoder) + return false; + + uint stackBaseRegister = decoder.StackBaseRegister; + + // Lazily compute the caller SP for GC_CALLER_SP_REL slots. + // The native code uses GET_CALLER_SP(pRD) which comes from EnsureCallerContextIsValid. + TargetPointer? callerSP = null; + + return decoder.EnumerateLiveSlots( + (uint)relativeOffset.Value, + flags, + (bool isRegister, uint registerNumber, int spOffset, uint spBase, uint gcFlags) => + { + GcScanFlags scanFlags = GcScanFlags.None; + if ((gcFlags & 0x1) != 0) // GC_SLOT_INTERIOR + scanFlags |= GcScanFlags.GC_CALL_INTERIOR; + if ((gcFlags & 0x2) != 0) // GC_SLOT_PINNED + scanFlags |= GcScanFlags.GC_CALL_PINNED; + + if (isRegister) + { + TargetPointer regValue = ReadRegisterValue(context, (int)registerNumber); + GcScanSlotLocation loc = new((int)registerNumber, 0, false); + scanContext.GCEnumCallback(regValue, scanFlags, loc); + } + else + { + int spReg = context.StackPointerRegister; + int reg = spBase switch + { + 1 => spReg, // GC_SP_REL → SP register number + 2 => (int)stackBaseRegister, // GC_FRAMEREG_REL → frame base register + 0 => -(spReg + 1), // GC_CALLER_SP_REL → -(SP + 1) + _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), + }; + TargetPointer baseAddr = spBase switch + { + 1 => context.StackPointer, // GC_SP_REL + 2 => ReadRegisterValue(context, (int)stackBaseRegister), // GC_FRAMEREG_REL + 0 => GetCallerSP(context, ref callerSP), // GC_CALLER_SP_REL + _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), + }; + + TargetPointer addr = new(baseAddr.Value + (ulong)(long)spOffset); + GcScanSlotLocation loc = new(reg, spOffset, true); + scanContext.GCEnumCallback(addr, scanFlags, loc); + } + }); + } + + /// + /// Compute the caller's SP by unwinding the current context one frame. + /// Cached in to avoid repeated unwinds for the same frame. + /// + private TargetPointer GetCallerSP(IPlatformAgnosticContext context, ref TargetPointer? cached) + { + if (cached is null) + { + IPlatformAgnosticContext callerContext = context.Clone(); + callerContext.Unwind(_target); + cached = callerContext.StackPointer; + } + return cached.Value; + } + + private static TargetPointer ReadRegisterValue(IPlatformAgnosticContext context, int registerNumber) + { + if (!context.TryReadRegister(registerNumber, out TargetNUInt value)) + throw new ArgumentOutOfRangeException(nameof(registerNumber), $"Register number {registerNumber} not found"); + + return new TargetPointer(value.Value); + } + +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/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..8b354f9d72c78f 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,22 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics; using System.Collections.Generic; -using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; +using Microsoft.Diagnostics.DataContractReader.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,16 +41,67 @@ 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) { 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); + // 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; + + // When an active InlinedCallFrame is processed as SW_FRAME without advancing + // the FrameIterator, the same Frame would be re-encountered by + // CheckForSkippedFrames. This flag tells CheckForSkippedFrames to advance + // past it, preventing a duplicate SW_SKIPPED_FRAME -> SW_FRAMELESS yield. + public bool SkipCurrentFrameInCheck { get; set; } + + public bool IsCurrentFrameResumable() + { + if (State is not (StackWalkState.SW_FRAME or StackWalkState.SW_SKIPPED_FRAME)) + return false; + + var ft = FrameIter.GetCurrentFrameType(); + // Only frame types with FRAME_ATTR_RESUMABLE set isFirst=true. + // FaultingExceptionFrame has FRAME_ATTR_FAULTED (sets hasFaulted) + // but NOT FRAME_ATTR_RESUMABLE, so it must not be included here. + // TODO: HijackFrame only has FRAME_ATTR_RESUMABLE on non-x86 platforms. + // When x86 stack walking is supported, this should be conditioned on + // the target architecture. + return ft is FrameIterator.FrameType.ResumableFrame + or FrameIterator.FrameType.RedirectedThreadFrame + or FrameIterator.FrameType.HijackFrame; + } + + /// + /// Update the IsFirst state for the NEXT frame, matching native stackwalk.cpp: + /// - After a frameless frame: isFirst = false (line 2202) + /// - After a ResumableFrame: isFirst = true (line 2235) + /// - After other Frames: isFirst = false + /// + public void AdvanceIsFirst() + { + if (State == StackWalkState.SW_FRAMELESS) + IsFirst = false; + else + IsFirst = IsCurrentFrameResumable(); + } + + public StackDataFrameHandle ToDataFrame() + { + bool isResumable = IsCurrentFrameResumable(); + bool isActiveFrame = IsFirst && State == StackWalkState.SW_FRAMELESS; + return new(Context.Clone(), State, FrameIter.CurrentFrameAddress, ThreadData, isResumable, isActiveFrame); + } } IEnumerable IStackWalk.CreateStackWalk(ThreadData threadData) @@ -63,16 +117,589 @@ IEnumerable IStackWalk.CreateStackWalk(ThreadData threadD yield break; } - StackWalkData stackWalkData = new(context, state, frameIterator); + StackWalkData stackWalkData = new(context, state, frameIterator, threadData); + + yield return stackWalkData.ToDataFrame(); + stackWalkData.AdvanceIsFirst(); + + while (Next(stackWalkData)) + { + yield return stackWalkData.ToDataFrame(); + stackWalkData.AdvanceIsFirst(); + } + } + + /// + /// Wraps CreateStackWalk and pre-advances the FrameIterator past explicit Frames + /// below the initial managed frame's caller SP. + /// + /// This is separated from CreateStackWalk because ClrDataStackWalk must yield the + /// same frame sequence as the legacy DAC (including these initial skipped frames), + /// whereas WalkStackReferences should skip them to match the native + /// DacStackReferenceWalker behavior. + /// + private IEnumerable CreateStackWalkForGCReferences(ThreadData threadData) + { + 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); + + // Skip Frames below the initial managed frame's caller SP, matching the native + // DacStackReferenceWalker behavior. All Frames below this SP belong to the + // current managed frame or frames pushed more recently (e.g., + // RedirectedThreadFrame from GC stress, active InlinedCallFrames). + 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, threadData); 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 = CreateStackWalkForGCReferences(threadData); + 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"); + + // IsActiveFrame was computed during CreateStackWalk, matching native + // CrawlFrame::IsActiveFunc() semantics. Active frames report scratch + // registers; non-active frames skip them. + CodeManagerFlags codeManagerFlags = gcFrame.Frame.IsActiveFrame + ? CodeManagerFlags.ActiveStackFrame + : 0; + + // TODO(stackref): Wire up funclet parent frame flags from Filter: + // - ShouldParentToFuncletSkipReportingGCReferences → ParentOfFuncletStackFrame + // (tells GCInfoDecoder to skip reporting since funclet already reported) + // - ShouldParentFrameUseUnwindTargetPCforGCReporting → use exception's + // unwind target IP instead of current IP for GC liveness lookup + // - ShouldParentToFuncletReportSavedFuncletSlots → report funclet's + // callee-saved register slots from the parent frame + // These require careful validation to ensure Filter sets them correctly + // for all stack configurations before wiring them into EnumGcRefs. + + GcScanner gcScanner = new(_target); + gcScanner.EnumGcRefs(gcFrame.Frame.Context, cbh.Value, codeManagerFlags, scanContext); + } + else + { + // Non-frameless: capital "F" Frame GcScanRoots dispatch. + // The base Frame::GcScanRoots_Impl is a no-op for most frame types. + // Frame types that override it (StubDispatchFrame, ExternalMethodFrame, + // CallCountingHelperFrame, DynamicHelperFrame, CLRToCOMMethodFrame, + // HijackFrame, ProtectValueClassFrame) call PromoteCallerStack to + // report method arguments from the transition block. + // + // GCFrame is NOT part of the Frame chain — it has its own linked list + // that the GC scans separately. The DAC's DacStackReferenceWalker + // does not scan GCFrame roots. + // + // For now, this is a no-op matching the base Frame behavior. + // TODO(stackref): Implement PromoteCallerStack for stub frames that + // report caller arguments (StubDispatchFrame, ExternalMethodFrame, etc.) + ScanFrameRoots(gcFrame.Frame, scanContext); + } + } + } + catch (System.Exception ex) + { + Debug.WriteLine($"Exception during WalkStackReferences: {ex}"); + // Matching native DAC behavior: capture errors, don't propagate + } + } + + 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 IsFilterFunclet { get; set; } + public bool IsFilterFuncletCached { get; set; } + public bool ShouldParentToFuncletSkipReportingGCReferences { get; set; } + public bool ShouldCrawlFrameReportGCReferences { get; set; } // required + public bool ShouldParentFrameUseUnwindTargetPCforGCReporting { get; set; } + public bool ShouldSaveFuncletInfo { get; set; } + public bool ShouldParentToFuncletReportSavedFuncletSlots { get; set; } + } + + private enum ForceGcReportingStage + { + Off, + LookForManagedFrame, + LookForMarkerFrame, + } + + private IEnumerable Filter(IEnumerable handles) + { + // StackFrameIterator::Filter assuming GC_FUNCLET_REFERENCE_REPORTING is defined + + // global tracking variables + bool movedPastFirstExInfo = false; + bool processNonFilterFunclet = false; + bool processIntermediaryNonFilterFunclet = false; + bool didFuncletReportGCReferences = true; + bool funcletNotSeen = false; + TargetPointer parentStackFrame = TargetPointer.Null; + TargetPointer funcletParentStackFrame = TargetPointer.Null; + TargetPointer intermediaryFuncletParentStackFrame; + + ForceGcReportingStage forceReportingWhileSkipping = ForceGcReportingStage.Off; + bool foundFirstFunclet = false; + + foreach (StackDataFrameHandle handle in handles) + { + GCFrameData gcFrame = new(handle); + + // per-frame tracking variables + bool stop = false; + bool skippingFunclet = false; + bool recheckCurrentFrame = false; + bool skipFuncletCallback = true; + + TargetPointer pExInfo = GetCurrentExceptionTracker(handle); + TargetPointer frameSp = handle.State == StackWalkState.SW_FRAME ? handle.FrameAddress : handle.Context.StackPointer; + if (pExInfo != TargetPointer.Null && frameSp > pExInfo) + { + if (!movedPastFirstExInfo) + { + Data.ExceptionInfo exInfo = _target.ProcessedData.GetOrAdd(pExInfo); + // TODO: The native StackFrameIterator::Filter checks pExInfo->m_lastReportedFunclet.IP + // to handle the case where a finally funclet was reported in a previous GC run. + // This requires runtime support to persist LastReportedFuncletInfo on ExInfo, + // which is not yet implemented. Until then this block is unreachable. + if (exInfo.PassNumber == 2 && + exInfo.CSFEnclosingClause != TargetPointer.Null && + funcletParentStackFrame == TargetPointer.Null && + false) // TODO: check lastReportedFunclet.IP != 0 when runtime support is added + { + funcletParentStackFrame = exInfo.CSFEnclosingClause; + parentStackFrame = exInfo.CSFEnclosingClause; + processNonFilterFunclet = true; + didFuncletReportGCReferences = false; + funcletNotSeen = true; + } + movedPastFirstExInfo = true; + } + } + + gcFrame.ShouldParentToFuncletReportSavedFuncletSlots = false; + + // by default, there is no funclet for the current frame + // that reported GC references + gcFrame.ShouldParentToFuncletSkipReportingGCReferences = false; + + // by default, assume that we are going to report GC references + gcFrame.ShouldCrawlFrameReportGCReferences = true; + + gcFrame.ShouldSaveFuncletInfo = false; + + // by default, assume that parent frame is going to report GC references from + // the actual location reported by the stack walk + gcFrame.ShouldParentFrameUseUnwindTargetPCforGCReporting = false; + + if (parentStackFrame != TargetPointer.Null) + { + // we are now skipping frames to get to the funclet's parent + skippingFunclet = true; + } + + switch (handle.State) + { + case StackWalkState.SW_FRAMELESS: + do + { + recheckCurrentFrame = false; + if (funcletParentStackFrame != TargetPointer.Null) + { + // Have we been processing a filter funclet without encountering any non-filter funclets? + if (!processNonFilterFunclet && !processIntermediaryNonFilterFunclet) + { + if (IsUnwoundToTargetParentFrame(handle, funcletParentStackFrame)) + { + gcFrame.ShouldParentToFuncletSkipReportingGCReferences = false; + + /* ResetGCRefReportingState */ + funcletParentStackFrame = TargetPointer.Null; + processNonFilterFunclet = false; + intermediaryFuncletParentStackFrame = TargetPointer.Null; + processIntermediaryNonFilterFunclet = false; + + // We have reached the parent of the filter funclet. + // It is possible this is another funclet (e.g. a catch/fault/finally), + // so reexamine this frame and see if it needs any skipping. + recheckCurrentFrame = true; + } + else + { + Debug.Assert(!IsFilterFunclet(handle)); + if (IsFunclet(handle)) + { + intermediaryFuncletParentStackFrame = FindParentStackFrameForStackWalk(handle, forGCReporting: true); + Debug.Assert(intermediaryFuncletParentStackFrame != TargetPointer.Null); + processIntermediaryNonFilterFunclet = true; + + // Set the parent frame so that the funclet skipping logic (below) can use it. + parentStackFrame = intermediaryFuncletParentStackFrame; + skippingFunclet = false; + + IPlatformAgnosticContext callerContext = handle.Context.Clone(); + callerContext.Unwind(_target); + if (!IsManaged(callerContext.InstructionPointer, out _)) + { + // Initiate force reporting of references in the new managed exception handling code frames. + // These frames are still alive when we are in a finally funclet. + forceReportingWhileSkipping = ForceGcReportingStage.LookForManagedFrame; + } + } + } + } + } + else + { + Debug.Assert(funcletParentStackFrame == TargetPointer.Null); + + // We don't have any funclet parent reference. Check if the current frame represents a funclet. + if (IsFunclet(handle)) + { + // Get a reference to the funclet's parent frame. + funcletParentStackFrame = FindParentStackFrameForStackWalk(handle, forGCReporting: true); + + bool frameWasUnwound = HasFrameBeenUnwoundByAnyActiveException(handle); + + if (funcletParentStackFrame == TargetPointer.Null) + { + Debug.Assert(frameWasUnwound, "This can only happen if the funclet (and its parent) have been unwound"); + } + else + { + Debug.Assert(funcletParentStackFrame != TargetPointer.Null); + + bool isFilterFunclet = IsFilterFunclet(handle); + + if (!isFilterFunclet) + { + processNonFilterFunclet = true; + + // Set the parent frame so that the funclet skipping logic (below) can use it. + parentStackFrame = funcletParentStackFrame; + + if (!foundFirstFunclet && + pExInfo > handle.Context.StackPointer && + parentStackFrame > pExInfo) + { + Debug.Assert(pExInfo != TargetPointer.Null); + gcFrame.ShouldSaveFuncletInfo = true; + foundFirstFunclet = true; + } + + IPlatformAgnosticContext callerContext = handle.Context.Clone(); + callerContext.Unwind(_target); + if (!frameWasUnwound && IsManaged(callerContext.InstructionPointer, out _)) + { + // Initiate force reporting of references in the new managed exception handling code frames. + // These frames are still alive when we are in a finally funclet. + forceReportingWhileSkipping = ForceGcReportingStage.LookForManagedFrame; + } + + // For non-filter funclets, we will make the callback for the funclet + // but skip all the frames until we reach the parent method. When we do, + // we will make a callback for it as well and then continue to make callbacks + // for all upstack frames, until we reach another funclet or the top of the stack + // is reached. + skipFuncletCallback = false; + } + else + { + Debug.Assert(isFilterFunclet); + processNonFilterFunclet = false; + + // Nothing more to do as we have come across a filter funclet. In this case, we will: + // + // 1) Get a reference to the parent frame + // 2) Report the funclet + // 3) Continue to report the parent frame, along with a flag that funclet has been reported (see above) + // 4) Continue to report all upstack frames + } + } + } + } + } while (recheckCurrentFrame); + + if (processNonFilterFunclet || processIntermediaryNonFilterFunclet) + { + bool skipFrameDueToUnwind = false; + + if (HasFrameBeenUnwoundByAnyActiveException(handle)) + { + // This frame has been unwound by an active exception. It is not part of the live stack. + gcFrame.ShouldCrawlFrameReportGCReferences = false; + skipFrameDueToUnwind = true; + + if (IsFunclet(handle) && !skippingFunclet) + { + // we have come across a funclet that has been unwound and we haven't yet started to + // look for its parent. in such a case, the funclet will not have anything to report + // so set the corresponding flag to indicate so. + + Debug.Assert(didFuncletReportGCReferences); + didFuncletReportGCReferences = false; + } + } + + if (skipFrameDueToUnwind) + { + if (parentStackFrame != TargetPointer.Null) + { + // Check if our have reached our target method frame. + // parentStackFrame == MaxValue is a special value to indicate that we should skip one frame. + if (parentStackFrame == TargetPointer.PlatformMaxValue(_target) || + IsUnwoundToTargetParentFrame(handle, parentStackFrame)) + { + // Reset flag as we have reached target method frame so no more skipping required + skippingFunclet = false; + + // We've finished skipping as told. Now check again. + + if (processIntermediaryNonFilterFunclet || processNonFilterFunclet) + { + gcFrame.ShouldParentToFuncletSkipReportingGCReferences = true; + + didFuncletReportGCReferences = true; + + /* ResetGCRefReportingState */ + if (!processIntermediaryNonFilterFunclet) + { + funcletParentStackFrame = TargetPointer.Null; + processNonFilterFunclet = false; + } + intermediaryFuncletParentStackFrame = TargetPointer.Null; + processIntermediaryNonFilterFunclet = false; + } + + parentStackFrame = TargetPointer.Null; + + if (IsFunclet(handle)) + { + // We have reached another funclet. Reexamine this frame. + recheckCurrentFrame = true; + goto case StackWalkState.SW_FRAMELESS; + } + } + } + + if (gcFrame.ShouldCrawlFrameReportGCReferences) + { + // Skip the callback for this frame - we don't do this for unwound frames encountered + // in GC stackwalk since they may represent dynamic methods whose resolver objects + // the GC may need to keep alive. + break; + } + } + else + { + Debug.Assert(!skipFrameDueToUnwind); + + if (parentStackFrame != TargetPointer.Null) + { + // Check if our have reached our target method frame. + // parentStackFrame == MaxValue is a special value to indicate that we should skip one frame. + if (parentStackFrame == TargetPointer.PlatformMaxValue(_target) || + IsUnwoundToTargetParentFrame(handle, parentStackFrame)) + { + if (processIntermediaryNonFilterFunclet || processNonFilterFunclet) + { + bool shouldSkipReporting = true; + + if (!didFuncletReportGCReferences) + { + Debug.Assert(pExInfo != TargetPointer.Null); + Data.ExceptionInfo exInfo = _target.ProcessedData.GetOrAdd(pExInfo); + if (exInfo.CallerOfActualHandlerFrame == funcletParentStackFrame) + { + shouldSkipReporting = false; + + didFuncletReportGCReferences = true; + + gcFrame.ShouldParentFrameUseUnwindTargetPCforGCReporting = true; + + // TODO(stackref): Is this required? + // gcFrame.ehClauseForCatch = exInfo.ClauseForCatch; + } + else if (!IsFunclet(handle)) + { + if (funcletNotSeen) + { + gcFrame.ShouldParentToFuncletReportSavedFuncletSlots = true; + funcletNotSeen = false; + } + + didFuncletReportGCReferences = true; + } + } + gcFrame.ShouldParentToFuncletSkipReportingGCReferences = shouldSkipReporting; + + /* ResetGCRefReportingState */ + if (!processIntermediaryNonFilterFunclet) + { + funcletParentStackFrame = TargetPointer.Null; + processNonFilterFunclet = false; + } + intermediaryFuncletParentStackFrame = TargetPointer.Null; + processIntermediaryNonFilterFunclet = false; + } + + parentStackFrame = TargetPointer.Null; + } + } + + if (parentStackFrame == TargetPointer.Null && IsFunclet(handle)) + { + recheckCurrentFrame = true; + goto case StackWalkState.SW_FRAMELESS; + } + + if (skipFuncletCallback) + { + if (parentStackFrame != TargetPointer.Null && + forceReportingWhileSkipping == ForceGcReportingStage.Off) + { + break; + } + + if (forceReportingWhileSkipping == ForceGcReportingStage.LookForManagedFrame) + { + // State indicating that the next marker frame should turn off the reporting again. That would be the caller of the managed RhThrowEx + forceReportingWhileSkipping = ForceGcReportingStage.LookForMarkerFrame; + // TODO(stackref): Implement marker frame detection. The native code checks + // if the caller IP is within DispatchManagedException / RhThrowEx to + // transition back to Off. Without this, force-reporting stays active + // indefinitely during funclet skipping. + } + + if (forceReportingWhileSkipping != ForceGcReportingStage.Off) + { + // TODO(stackref): add debug assert that we are in the EH code + } + } + } + } + else + { + // If we are enumerating frames for GC reporting and we determined that + // the current frame needs to be reported, ensure that it has not already + // been unwound by the active exception. If it has been, then we will + // simply skip it and not deliver a callback for it. + if (HasFrameBeenUnwoundByAnyActiveException(handle)) + { + // Invoke the GC callback for this crawlframe (to keep any dynamic methods alive) but do not report its references. + gcFrame.ShouldCrawlFrameReportGCReferences = false; + } + } + + stop = true; + break; + + case StackWalkState.SW_FRAME: + case StackWalkState.SW_SKIPPED_FRAME: + if (!skippingFunclet) + { + if (HasFrameBeenUnwoundByAnyActiveException(handle)) + { + // This frame has been unwound by an active exception. It is not part of the live stack. + gcFrame.ShouldCrawlFrameReportGCReferences = false; + } + stop = true; + } + break; + default: + stop = true; + break; + } + + if (stop) + yield return gcFrame; + } + } + + private bool IsUnwoundToTargetParentFrame(StackDataFrameHandle handle, TargetPointer targetParentFrame) + { + Debug.Assert(handle.State is StackWalkState.SW_FRAMELESS); + + IPlatformAgnosticContext callerContext = handle.Context.Clone(); + callerContext.Unwind(_target); + + return callerContext.StackPointer == targetParentFrame; + } + private bool Next(StackWalkData handle) { switch (handle.State) @@ -97,6 +724,13 @@ 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. + handle.SkipCurrentFrameInCheck = true; + } break; case StackWalkState.SW_ERROR: case StackWalkState.SW_COMPLETE: @@ -149,6 +783,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 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 +828,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 +848,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 +876,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; @@ -272,4 +917,58 @@ private static StackDataFrameHandle AssertCorrectHandle(IStackDataFrameHandle st return handle; } -}; + + /// + /// Scans GC roots for a non-frameless (capital "F" Frame) stack frame. + /// Dispatches based on frame type identifier. Most frame types have a no-op + /// GcScanRoots (the base Frame implementation does nothing). + /// + /// Frame types with meaningful GcScanRoots that call PromoteCallerStack: + /// StubDispatchFrame, ExternalMethodFrame, CallCountingHelperFrame, + /// DynamicHelperFrame, CLRToCOMMethodFrame, HijackFrame, ProtectValueClassFrame. + /// + private void ScanFrameRoots(StackDataFrameHandle frame, GcScanContext scanContext) + { + _ = scanContext; // Will be used when stub frame scanning is implemented + // Read the frame type identifier + TargetPointer frameAddress = frame.FrameAddress; + if (frameAddress == TargetPointer.Null) + return; + + // Get the frame name to identify the type + string frameName = ((IStackWalk)this).GetFrameName(frameAddress); + + // Most frame types use the base no-op GcScanRoots_Impl. + // The ones that do work (stub frames) need PromoteCallerStack which + // requires reading the transition block and decoding method signatures. + // This is not yet implemented. + switch (frameName) + { + case "StubDispatchFrame": + case "ExternalMethodFrame": + case "CallCountingHelperFrame": + case "DynamicHelperFrame": + case "CLRToCOMMethodFrame": + case "ComPrestubMethodFrame": + // These frames call PromoteCallerStack to report method arguments. + // TODO(stackref): Implement PromoteCallerStack / PromoteCallerStackUsingGCRefMap + break; + + case "HijackFrame": + // Reports return value registers (X86 only with FEATURE_HIJACK) + // TODO(stackref): Implement HijackFrame scanning + break; + + case "ProtectValueClassFrame": + // Scans value types in linked list + // TODO(stackref): Implement ProtectValueClassFrame scanning + break; + + default: + // Base Frame::GcScanRoots_Impl is a no-op — nothing to report. + // This covers: InlinedCallFrame, SoftwareExceptionFrame, FaultingExceptionFrame, + // ResumableFrame, FuncEvalFrame, PrestubMethodFrame, PInvokeCalliFrame, etc. + break; + } + } +} 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..8f2470d6e71996 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs @@ -14,11 +14,25 @@ public ExceptionInfo(Target target, TargetPointer address) PreviousNestedInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(PreviousNestedInfo)].Offset); ThrownObjectHandle = target.ReadPointer(address + (ulong)type.Fields[nameof(ThrownObjectHandle)].Offset); + ExceptionFlags = target.Read(address + (ulong)type.Fields[nameof(ExceptionFlags)].Offset); + StackLowBound = target.ReadPointer(address + (ulong)type.Fields[nameof(StackLowBound)].Offset); + StackHighBound = target.ReadPointer(address + (ulong)type.Fields[nameof(StackHighBound)].Offset); if (type.Fields.ContainsKey(nameof(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); } - 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; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs index ec460c242cb58e..843bc82f7f1328 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs @@ -31,6 +31,7 @@ public ReadyToRunInfo(Target target, TargetPointer address) DelayLoadMethodCallThunks = target.ReadPointer(address + (ulong)type.Fields[nameof(DelayLoadMethodCallThunks)].Offset); DebugInfoSection = target.ReadPointer(address + (ulong)type.Fields[nameof(DebugInfoSection)].Offset); + ExceptionInfoSection = target.ReadPointer(address + (ulong)type.Fields[nameof(ExceptionInfoSection)].Offset); // Map is from the composite info pointer (set to itself for non-multi-assembly composite images) EntryPointToMethodDescMap = CompositeInfo + (ulong)type.Fields[nameof(EntryPointToMethodDescMap)].Offset; @@ -50,6 +51,7 @@ public ReadyToRunInfo(Target target, TargetPointer address) public TargetPointer DelayLoadMethodCallThunks { get; } public TargetPointer DebugInfoSection { get; } + public TargetPointer ExceptionInfoSection { get; } public TargetPointer EntryPointToMethodDescMap { get; } public TargetPointer LoadedImageBase { get; } public TargetPointer Composite { get; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RealCodeHeader.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RealCodeHeader.cs index da639b342d4274..9718f0ab4fec6f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RealCodeHeader.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RealCodeHeader.cs @@ -13,6 +13,7 @@ public RealCodeHeader(Target target, TargetPointer address) Target.TypeInfo type = target.GetTypeInfo(DataType.RealCodeHeader); MethodDesc = target.ReadPointer(address + (ulong)type.Fields[nameof(MethodDesc)].Offset); DebugInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(DebugInfo)].Offset); + EHInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(EHInfo)].Offset); GCInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(GCInfo)].Offset); NumUnwindInfos = target.Read(address + (ulong)type.Fields[nameof(NumUnwindInfos)].Offset); UnwindInfos = address + (ulong)type.Fields[nameof(UnwindInfos)].Offset; @@ -20,10 +21,11 @@ public RealCodeHeader(Target target, TargetPointer address) JitEHInfo = jitEHInfoAddr != TargetPointer.Null ? target.ProcessedData.GetOrAdd(jitEHInfoAddr) : null; } - public TargetPointer MethodDesc { get; init; } - public TargetPointer DebugInfo { get; init; } - public TargetPointer GCInfo { get; init; } - public uint NumUnwindInfos { get; init; } - public TargetPointer UnwindInfos { get; init; } - public EEILException? JitEHInfo { get; init; } + public TargetPointer MethodDesc { get; } + public TargetPointer DebugInfo { get; } + public TargetPointer EHInfo { get; } + public TargetPointer GCInfo { get; } + public uint NumUnwindInfos { get; } + public TargetPointer UnwindInfos { get; } + public EEILException? JitEHInfo { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/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/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/MockDescriptors/MockDescriptors.ExecutionManager.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs index 524ff8e21405dc..2cc7a9334daf36 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,6 +233,7 @@ 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.UnwindInfos), 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), 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..1a9afea91f8852 --- /dev/null +++ b/src/native/managed/cdac/tests/gcstress/known-issues.md @@ -0,0 +1,113 @@ +# cDAC Stack Reference Walking — Known Issues + +This document tracks known gaps and differences between the cDAC's stack reference +enumeration (`ISOSDacInterface::GetStackReferences`) and the runtime's GC root scanning. + +## GC Stress Test Results + +With `DOTNET_GCStress=0x24` (instruction-level JIT stress + cDAC verification): +- ~25,000 PASS / ~125 FAIL out of ~25,100 stress points (99.5% pass rate) + +## Known Issues + +### 1. Dynamic Method / IL Stub GC Refs Not Enumerated + +**Severity**: Low — matches legacy DAC behavior +**Affected methods**: `dynamicclass::InvokeStub_*` (reflection invoke stubs), LCG methods +**Pattern**: `cDAC < RT` (diff=-1), always missing `RT[0]` register ref + +The cDAC (and legacy DAC) cannot resolve code blocks for methods in RangeList-based +code heaps (HostCodeHeap). Both `EEJitManager::JitCodeToMethodInfo` and the cDAC's +`FindMethodCode` return failure for `RANGE_SECTION_RANGELIST` sections. This means +GcInfo cannot be decoded for these methods, and their GC refs are not reported. + +The runtime's `GcStackCrawlCallBack` reports additional refs from these methods +because it processes them through the Frame chain (`ResumableFrame`, `InlinedCallFrame`) +which has access to the register state. + +This is a pre-existing gap in the DAC's diagnostic API, not a cDAC regression. + +**Follow-up**: Implement RangeList-based code lookup in the cDAC's ExecutionManager. +This requires reading the `HostCodeHeap` linked list and matching IPs to code headers +within dynamic code heaps. + +### 2. Frame Context Restoration Causes Duplicate Walks + +**Severity**: Low — mitigated by dedup in stress tool +**Pattern**: `cDAC > RT` (diff=+1 to +3), same Address/Object from two Source IPs + +When a non-leaf Frame's `UpdateContextFromFrame` restores a managed IP that was +already walked from the initial context (or will be walked via normal unwinding), +the same managed frame gets walked twice at different offsets. This produces +duplicate GC slot reports. + +The stress tool's `DeduplicateRefs` filter removes stack-based duplicates +(same Address/Object/Flags), but register-based duplicates (Address=0) with +different Source IPs are not caught. + +**Mitigations in place**: +- `callerSP` Frame skip in `CreateStackWalk` (prevents most leaf-level duplicates) +- `SkipCurrentFrameInCheck` for active `InlinedCallFrame` (prevents ICF re-encounter) +- `DeduplicateRefs` in stress tool (removes stack-based duplicates) + +**Follow-up**: Track walked method address ranges in the cDAC's stack walker and +suppress duplicate `SW_FRAMELESS` yields for methods already visited. + +### 3. PromoteCallerStack Not Implemented for Stub Frames + +**Severity**: Low — not currently manifesting in GC stress tests +**Affected frames**: `StubDispatchFrame`, `ExternalMethodFrame`, `CallCountingHelperFrame`, +`DynamicHelperFrame`, `CLRToCOMMethodFrame` + +These Frame types call `PromoteCallerStack` / `PromoteCallerStackUsingGCRefMap` +to report method arguments from the transition block. The cDAC's `ScanFrameRoots` +is a no-op for these frame types. + +This gap doesn't manifest in GC stress testing because stub frame arguments are +not the source of the current count differences. However, it IS a DAC parity gap — +the legacy DAC reports these refs via `Frame::GcScanRoots`. + +**Follow-up**: Port `GCRefMapDecoder` to managed code and implement +`PromoteCallerStackUsingGCRefMap` in `ScanFrameRoots`. Prototype implementation +exists (stashed as "PromoteCallerStack implementation + GCRefMapDecoder"). + +### 4. Funclet Parent Frame Flags Not Consumed + +**Severity**: Low — only affects exception handling scenarios +**Flags**: `ShouldParentToFuncletSkipReportingGCReferences`, +`ShouldParentFrameUseUnwindTargetPCforGCReporting`, +`ShouldParentToFuncletReportSavedFuncletSlots` + +The `Filter` method computes these flags for funclet parent frames, but +`WalkStackReferences` does not act on them. This could cause: +- Double-reporting of slots already reported by a funclet +- Using the wrong IP for GC liveness lookup on catch/finally parent frames +- Missing callee-saved register slots from unwound funclets + +**Follow-up**: Wire up `ParentOfFuncletStackFrame` flag to `EnumGcRefs`. +Requires careful validation — an initial attempt caused 253 regressions +because `Filter` sets the flag too aggressively. + +### 5. Interior Stack Pointers + +**Severity**: Informational — handled in stress tool +**Pattern**: cDAC reports interior pointers whose Object is a stack address + +The runtime's `PromoteCarefully` (siginfo.cpp) filters out interior pointers +whose object value is a stack address. These are callee-saved register values +(RSP/RBP) that GcInfo marks as live interior slots but don't point to managed +heap objects. The cDAC reports all GcInfo slots faithfully. + +**Mitigation**: The stress tool's `FilterInteriorStackRefs` removes these +before comparison, matching the runtime's behavior. + +### 6. forceReportingWhileSkipping State Machine Incomplete + +**Severity**: Low — theoretical gap +**Location**: `StackWalk_1.cs` Filter method + +The `ForceGcReportingStage` state machine transitions `Off → LookForManagedFrame +→ LookForMarkerFrame` but never transitions back to `Off`. The native code checks +if the caller IP is within `DispatchManagedException` / `RhThrowEx` to deactivate. + +**Follow-up**: Implement marker frame detection. 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..cfd78c303e61d4 --- /dev/null +++ b/src/native/managed/cdac/tests/gcstress/test-cdac-gcstress.ps1 @@ -0,0 +1,259 @@ +<# +.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.Runtime.CompilerServices; + +class CdacGcStressTest +{ + [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); + } + + static int Main() + { + Console.WriteLine("Starting cDAC GC Stress test..."); + for (int i = 0; i < 5; i++) + { + AllocAndHold(); + NestedCall(3); + } + 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" + +& $dotnetExe exec $cscPath.FullName ` + "/out:$testDll" /target:exe /nologo ` + "/r:$sysRuntime" ` + "/r:$sysConsole" ` + "/r:$sysCoreLib" ` + $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 })