diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index c77d5f296736f3..51a55df2b63165 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -57,6 +57,9 @@ This contract depends on the following descriptors: | `StubDispatchFrame` | `MethodDescPtr` | Pointer to Frame's method desc | | `StubDispatchFrame` | `RepresentativeMTPtr` | Pointer to Frame's method table pointer | | `StubDispatchFrame` | `RepresentativeSlot` | Frame's method table slot | +| `StubDispatchFrame` | `GCRefMap` | Cached pointer to GC reference map blob for caller stack promotion | +| `ExternalMethodFrame` | `GCRefMap` | Cached pointer to GC reference map blob for caller stack promotion | +| `DynamicHelperFrame` | `DynamicHelperFrameFlags` | Flags indicating which argument registers contain GC references | | `TransitionBlock` | `ReturnAddress` | Return address associated with the TransitionBlock | | `TransitionBlock` | `CalleeSavedRegisters` | Platform specific CalleeSavedRegisters struct associated with the TransitionBlock | | `TransitionBlock` (arm) | `ArgumentRegisters` | ARM specific `ArgumentRegisters` struct | @@ -87,6 +90,9 @@ Global variables used: | Global Name | Type | Purpose | | --- | --- | --- | | For each FrameType ``, `##Identifier` | `FrameIdentifier` enum value | Identifier used to determine concrete type of Frames | +| `TransitionBlockOffsetOfFirstGCRefMapSlot` | `uint32` | Byte offset within TransitionBlock where GCRefMap slot enumeration begins. ARM64: RetBuffArgReg offset; others: ArgumentRegisters offset. | +| `TransitionBlockOffsetOfArgumentRegisters` | `uint32` | Byte offset of the ArgumentRegisters within the TransitionBlock | +| `TransitionBlockOffsetOfArgs` | `uint32` | Byte offset of stack arguments (first arg after registers) = `sizeof(TransitionBlock)` | Constants used: | Source | Name | Value | Purpose | diff --git a/eng/Subsets.props b/eng/Subsets.props index 21db6bbdcb8609..f4ea63b49be08f 100644 --- a/eng/Subsets.props +++ b/eng/Subsets.props @@ -254,6 +254,7 @@ + @@ -528,6 +529,10 @@ + + + + diff --git a/src/coreclr/vm/cdacstress.cpp b/src/coreclr/vm/cdacstress.cpp index 08a33df3d818c4..3c35e9006f37f6 100644 --- a/src/coreclr/vm/cdacstress.cpp +++ b/src/coreclr/vm/cdacstress.cpp @@ -16,7 +16,7 @@ #ifdef HAVE_GCCOVER -#include "cdacstress.h" +#include "CdacStress.h" #include "../../native/managed/cdac/inc/cdac_reader.h" #include "../../debug/datadescriptor-shared/inc/contract-descriptor.h" #include @@ -72,7 +72,7 @@ static CrstStatic s_cdacLock; // Serializes cDAC access from concurr // Unique-stack filtering: hash set of previously seen stack traces. // Protected by s_cdacLock (already held during VerifyAtStressPoint). - +static const int UNIQUE_STACK_DEPTH = 8; // Number of return addresses to hash static SHash>>* s_seenStacks = nullptr; // Thread-local reentrancy guard — prevents infinite recursion when @@ -146,12 +146,11 @@ static int ReadThreadContextCallback(uint32_t threadId, uint32_t contextFlags, u // Minimal ICLRDataTarget implementation for loading the legacy DAC in-process. // Routes ReadVirtual/GetThreadContext to the same callbacks as the cDAC. //----------------------------------------------------------------------------- -class InProcessDataTarget : public ICLRDataTarget, public ICLRRuntimeLocator +class InProcessDataTarget : public ICLRDataTarget { volatile LONG m_refCount; public: InProcessDataTarget() : m_refCount(1) {} - virtual ~InProcessDataTarget() = default; HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppObj) override { @@ -161,12 +160,6 @@ class InProcessDataTarget : public ICLRDataTarget, public ICLRRuntimeLocator AddRef(); return S_OK; } - if (riid == __uuidof(ICLRRuntimeLocator)) - { - *ppObj = static_cast(this); - AddRef(); - return S_OK; - } *ppObj = nullptr; return E_NOINTERFACE; } @@ -178,14 +171,6 @@ class InProcessDataTarget : public ICLRDataTarget, public ICLRRuntimeLocator return c; } - // ICLRRuntimeLocator — provides the CLR base address directly so the DAC - // does not fall back to GetImageBase (which needs GetModuleHandleW, unavailable on Linux). - HRESULT STDMETHODCALLTYPE GetRuntimeBase(CLRDATA_ADDRESS* baseAddress) override - { - *baseAddress = (CLRDATA_ADDRESS)GetCurrentModuleBase(); - return S_OK; - } - HRESULT STDMETHODCALLTYPE GetMachineType(ULONG32* machineType) override { #ifdef TARGET_AMD64 @@ -208,8 +193,10 @@ class InProcessDataTarget : public ICLRDataTarget, public ICLRRuntimeLocator HRESULT STDMETHODCALLTYPE GetImageBase(LPCWSTR imagePath, CLRDATA_ADDRESS* baseAddress) override { - // Not needed — the DAC uses ICLRRuntimeLocator::GetRuntimeBase() instead. - return E_NOTIMPL; + HMODULE hMod = ::GetModuleHandleW(imagePath); + if (hMod == NULL) return E_FAIL; + *baseAddress = (CLRDATA_ADDRESS)hMod; + return S_OK; } HRESULT STDMETHODCALLTYPE ReadVirtual(CLRDATA_ADDRESS address, BYTE* buffer, ULONG32 bytesRequested, ULONG32* bytesRead) override @@ -282,8 +269,8 @@ bool CdacStress::Initialize() } else { - // Legacy: GCSTRESS_CDAC maps to allocation-point + reference verification - s_cdacStressLevel = CDACSTRESS_ALLOC | CDACSTRESS_REFS; + // Legacy: GCSTRESS_CDAC maps to allocation-point verification + s_cdacStressLevel = CDACSTRESS_ALLOC; } // Load mscordaccore_universal from next to coreclr @@ -876,11 +863,10 @@ static void CompareStackWalks(Thread* pThread, PCONTEXT regs) if (cdacIP != dacIP || cdacSP != dacSP) { - if (s_logFile) - fprintf(s_logFile, " [WALK_MISMATCH] Frame %d: Context differs cDAC_IP=0x%llx cDAC_SP=0x%llx DAC_IP=0x%llx DAC_SP=0x%llx\n", - frameIdx, - (unsigned long long)cdacIP, (unsigned long long)cdacSP, - (unsigned long long)dacIP, (unsigned long long)dacSP); + fprintf(s_logFile, " [WALK_MISMATCH] Frame %d: Context differs cDAC_IP=0x%llx cDAC_SP=0x%llx DAC_IP=0x%llx DAC_SP=0x%llx\n", + frameIdx, + (unsigned long long)cdacIP, (unsigned long long)cdacSP, + (unsigned long long)dacIP, (unsigned long long)dacSP); mismatch = true; } } @@ -945,8 +931,6 @@ static bool CompareRefSets(StackRef* refsA, int countA, StackRef* refsB, int cou return false; if (countA == 0) return true; - if (countA > MAX_COLLECTED_REFS) - return false; bool matched[MAX_COLLECTED_REFS] = {}; @@ -1027,6 +1011,9 @@ void CdacStress::VerifyAtAllocPoint() if (t_inVerification) return; + if (ShouldSkipStressPoint()) + return; + Thread* pThread = GetThreadNULLOk(); if (pThread == nullptr || !pThread->PreemptiveGCDisabled()) return; @@ -1112,20 +1099,14 @@ void CdacStress::VerifyAtStressPoint(Thread* pThread, PCONTEXT regs) StackRef runtimeRefsBuf[MAX_COLLECTED_REFS]; int runtimeCount = 0; - bool haveRuntime = CollectRuntimeStackRefs(pThread, regs, runtimeRefsBuf, &runtimeCount); + CollectRuntimeStackRefs(pThread, regs, runtimeRefsBuf, &runtimeCount); - if (!haveCdac || !haveRuntime) + if (!haveCdac) { InterlockedIncrement(&s_verifySkip); if (s_logFile != nullptr) - { - if (!haveCdac) - fprintf(s_logFile, "[SKIP] Thread=0x%x IP=0x%p - cDAC GetStackReferences failed\n", - osThreadId, (void*)GetIP(regs)); - else - fprintf(s_logFile, "[SKIP] Thread=0x%x IP=0x%p - runtime CollectRuntimeStackRefs overflowed\n", - osThreadId, (void*)GetIP(regs)); - } + fprintf(s_logFile, "[SKIP] Thread=0x%x IP=0x%p - cDAC GetStackReferences failed\n", + osThreadId, (void*)GetIP(regs)); return; } diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index f1f1a22a003996..2ce60c9e78ab2a 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -146,6 +146,8 @@ CDAC_TYPE_FIELD(ExceptionInfo, /*uint8*/, PassNumber, offsetof(ExInfo, m_passNum CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, CSFEHClause, offsetof(ExInfo, m_csfEHClause)) CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, CSFEnclosingClause, offsetof(ExInfo, m_csfEnclosingClause)) CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, CallerOfActualHandlerFrame, offsetof(ExInfo, m_sfCallerOfActualHandlerFrame)) +CDAC_TYPE_FIELD(ExceptionInfo, /*uint32*/, ClauseForCatchHandlerStartPC, offsetof(ExInfo, m_ClauseForCatch) + offsetof(EE_ILEXCEPTION_CLAUSE, HandlerStartPC)) +CDAC_TYPE_FIELD(ExceptionInfo, /*uint32*/, ClauseForCatchHandlerEndPC, offsetof(ExInfo, m_ClauseForCatch) + offsetof(EE_ILEXCEPTION_CLAUSE, HandlerEndPC)) CDAC_TYPE_END(ExceptionInfo) CDAC_TYPE_BEGIN(GCHandle) @@ -915,8 +917,19 @@ CDAC_TYPE_SIZE(sizeof(StubDispatchFrame)) CDAC_TYPE_FIELD(StubDispatchFrame, /*pointer*/, RepresentativeMTPtr, cdac_data::RepresentativeMTPtr) CDAC_TYPE_FIELD(StubDispatchFrame, /*pointer*/, MethodDescPtr, cdac_data::MethodDescPtr) CDAC_TYPE_FIELD(StubDispatchFrame, /*uint32*/, RepresentativeSlot, cdac_data::RepresentativeSlot) +CDAC_TYPE_FIELD(StubDispatchFrame, /*pointer*/, GCRefMap, cdac_data::GCRefMap) CDAC_TYPE_END(StubDispatchFrame) +CDAC_TYPE_BEGIN(ExternalMethodFrame) +CDAC_TYPE_SIZE(sizeof(ExternalMethodFrame)) +CDAC_TYPE_FIELD(ExternalMethodFrame, /*pointer*/, GCRefMap, cdac_data::GCRefMap) +CDAC_TYPE_END(ExternalMethodFrame) + +CDAC_TYPE_BEGIN(DynamicHelperFrame) +CDAC_TYPE_SIZE(sizeof(DynamicHelperFrame)) +CDAC_TYPE_FIELD(DynamicHelperFrame, /*int32*/, DynamicHelperFrameFlags, cdac_data::DynamicHelperFrameFlags) +CDAC_TYPE_END(DynamicHelperFrame) + #ifdef FEATURE_HIJACK CDAC_TYPE_BEGIN(ResumableFrame) CDAC_TYPE_SIZE(sizeof(ResumableFrame)) @@ -1289,6 +1302,18 @@ CDAC_GLOBAL_POINTER(GCThread, &::g_pSuspensionThread) #undef FRAME_TYPE_NAME CDAC_GLOBAL(MethodDescTokenRemainderBitCount, uint8, METHOD_TOKEN_REMAINDER_BIT_COUNT) + +CDAC_GLOBAL(TransitionBlockOffsetOfArgs, uint32, sizeof(TransitionBlock)) +#if (defined(TARGET_AMD64) && !defined(UNIX_AMD64_ABI)) || defined(TARGET_WASM) +CDAC_GLOBAL(TransitionBlockOffsetOfArgumentRegisters, uint32, sizeof(TransitionBlock)) +CDAC_GLOBAL(TransitionBlockOffsetOfFirstGCRefMapSlot, uint32, sizeof(TransitionBlock)) +#elif defined(TARGET_ARM64) +CDAC_GLOBAL(TransitionBlockOffsetOfArgumentRegisters, uint32, offsetof(TransitionBlock, m_argumentRegisters)) +CDAC_GLOBAL(TransitionBlockOffsetOfFirstGCRefMapSlot, uint32, offsetof(TransitionBlock, m_x8RetBuffReg)) +#else +CDAC_GLOBAL(TransitionBlockOffsetOfArgumentRegisters, uint32, offsetof(TransitionBlock, m_argumentRegisters)) +CDAC_GLOBAL(TransitionBlockOffsetOfFirstGCRefMapSlot, uint32, offsetof(TransitionBlock, m_argumentRegisters)) +#endif #if FEATURE_COMINTEROP CDAC_GLOBAL(FeatureCOMInterop, uint8, 1) #else diff --git a/src/coreclr/vm/frames.h b/src/coreclr/vm/frames.h index 55072025229b0f..eb3fa240ee9148 100644 --- a/src/coreclr/vm/frames.h +++ b/src/coreclr/vm/frames.h @@ -1728,6 +1728,7 @@ struct cdac_data { static constexpr size_t RepresentativeMTPtr = offsetof(StubDispatchFrame, m_pRepresentativeMT); static constexpr uint32_t RepresentativeSlot = offsetof(StubDispatchFrame, m_representativeSlot); + static constexpr size_t GCRefMap = offsetof(StubDispatchFrame, m_pGCRefMap); }; typedef DPTR(class StubDispatchFrame) PTR_StubDispatchFrame; @@ -1763,6 +1764,8 @@ class CallCountingHelperFrame : public FramedMethodFrame class ExternalMethodFrame : public FramedMethodFrame { + friend struct ::cdac_data; + // Indirection and containing module. Used to compute pGCRefMap lazily. PTR_Module m_pZapModule; TADDR m_pIndirection; @@ -1803,8 +1806,16 @@ class ExternalMethodFrame : public FramedMethodFrame typedef DPTR(class ExternalMethodFrame) PTR_ExternalMethodFrame; +template <> +struct cdac_data +{ + static constexpr size_t GCRefMap = offsetof(ExternalMethodFrame, m_pGCRefMap); +}; + class DynamicHelperFrame : public FramedMethodFrame { + friend struct ::cdac_data; + int m_dynamicHelperFrameFlags; public: @@ -1825,6 +1836,12 @@ class DynamicHelperFrame : public FramedMethodFrame typedef DPTR(class DynamicHelperFrame) PTR_DynamicHelperFrame; +template <> +struct cdac_data +{ + static constexpr size_t DynamicHelperFrameFlags = offsetof(DynamicHelperFrame, m_dynamicHelperFrameFlags); +}; + #ifdef FEATURE_COMINTEROP //------------------------------------------------------------------------ diff --git a/src/coreclr/vm/gccover.cpp b/src/coreclr/vm/gccover.cpp index 249c3b9c21910f..8b8081d17e59d4 100644 --- a/src/coreclr/vm/gccover.cpp +++ b/src/coreclr/vm/gccover.cpp @@ -853,6 +853,24 @@ void DoGcStress (PCONTEXT regs, NativeCodeVersion nativeCodeVersion) enableWhenDone = true; } + // When DOTNET_GCStressCdacStep > 1, skip most stress points (both cDAC verification + // and StressHeap) to reduce overhead. + if (CdacStress::IsInitialized() && CdacStress::ShouldSkipStressPoint()) + { + if(pThread->HasPendingGCStressInstructionUpdate()) + UpdateGCStressInstructionWithoutGC(); + + FlushInstructionCache(GetCurrentProcess(), (LPCVOID)instrPtr, 4); + + if (enableWhenDone) + { + BOOL b = GC_ON_TRANSITIONS(FALSE); + pThread->EnablePreemptiveGC(); + GC_ON_TRANSITIONS(b); + } + return; + } + // // If we redirect for gc stress, we don't need this frame on the stack, // the redirection will push a resumable frame. @@ -1177,6 +1195,18 @@ void DoGcStress (PCONTEXT regs, NativeCodeVersion nativeCodeVersion) // code and it will just raise a STATUS_ACCESS_VIOLATION. pThread->PostGCStressInstructionUpdate((BYTE*)instrPtr, &gcCover->savedCode[offset]); + // When DOTNET_GCStressCdacStep > 1, skip most stress points (both cDAC verification + // and StressHeap) to reduce overhead. We still restore the instruction since the + // breakpoint must be removed regardless. + if (CdacStress::IsInitialized() && CdacStress::ShouldSkipStressPoint()) + { + if(pThread->HasPendingGCStressInstructionUpdate()) + UpdateGCStressInstructionWithoutGC(); + + FlushInstructionCache(GetCurrentProcess(), (LPCVOID)instrPtr, 4); + return; + } + // we should be in coop mode. _ASSERTE(pThread->PreemptiveGCDisabled()); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs index 2a49c5a0d11569..579472df9193ce 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -151,6 +151,8 @@ public enum DataType HijackFrame, TailCallFrame, StubDispatchFrame, + ExternalMethodFrame, + DynamicHelperFrame, ComCallWrapper, SimpleComCallWrapper, ComMethodTable, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs index 2585e72902f7ac..185e3ef8486841 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs @@ -75,6 +75,10 @@ public static class Globals public const string MethodDescTokenRemainderBitCount = nameof(MethodDescTokenRemainderBitCount); public const string DirectorySeparator = nameof(DirectorySeparator); + public const string TransitionBlockOffsetOfFirstGCRefMapSlot = nameof(TransitionBlockOffsetOfFirstGCRefMapSlot); + public const string TransitionBlockOffsetOfArgumentRegisters = nameof(TransitionBlockOffsetOfArgumentRegisters); + public const string TransitionBlockOffsetOfArgs = nameof(TransitionBlockOffsetOfArgs); + public const string ExecutionManagerCodeRangeMapAddress = nameof(ExecutionManagerCodeRangeMapAddress); public const string EEJitManagerAddress = nameof(EEJitManagerAddress); public const string StubCodeBlockLast = nameof(StubCodeBlockLast); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs index ab75e861b790c2..ada2d0a4a45459 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs @@ -65,13 +65,17 @@ public override void GetMethodRegionInfo( public override TargetPointer GetUnwindInfo(RangeSection rangeSection, TargetCodePointer jittedCodeAddress) { if (rangeSection.IsRangeList) + { return TargetPointer.Null; + } if (rangeSection.Data == null) throw new ArgumentException(nameof(rangeSection)); TargetPointer codeStart = FindMethodCode(rangeSection, jittedCodeAddress); if (codeStart == TargetPointer.Null) + { return TargetPointer.Null; + } Debug.Assert(codeStart.Value <= jittedCodeAddress.Value); if (!GetRealCodeHeader(rangeSection, codeStart, out Data.RealCodeHeader? realCodeHeader)) @@ -188,7 +192,10 @@ public override void GetExceptionClauses(RangeSection rangeSection, CodeBlockHan throw new ArgumentException(nameof(rangeSection)); Data.RealCodeHeader? realCodeHeader; - if (!GetRealCodeHeader(rangeSection, codeInfoHandle.Address, out realCodeHeader) || realCodeHeader == null) + // codeInfoHandle.Address is the IP, not the code start. We need to find the actual + // method start via the nibble map so GetRealCodeHeader reads at the correct offset. + TargetPointer codeStart = FindMethodCode(rangeSection, new TargetCodePointer(codeInfoHandle.Address.Value)); + if (!GetRealCodeHeader(rangeSection, codeStart, out realCodeHeader) || realCodeHeader == null) return; if (realCodeHeader.EHInfo == TargetPointer.Null) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs index d6a6a0da8b39f4..219dbaf1fa68c0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs @@ -520,6 +520,26 @@ public IReadOnlyList GetInterruptibleRanges() return _interruptibleRanges; } + /// + public uint? FindFirstInterruptiblePoint(uint startOffset, uint endOffset) + { + EnsureDecodedTo(DecodePoints.InterruptibleRanges); + + foreach (InterruptibleRange range in _interruptibleRanges) + { + if (range.EndOffset <= startOffset) + continue; + + if (startOffset >= range.StartOffset && startOffset < range.EndOffset) + return startOffset; + + if (range.StartOffset < endOffset) + return range.StartOffset; + } + + return null; + } + public uint StackBaseRegister { get diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs index 86f4210a7cb91d..7c25381f31fb38 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs @@ -24,6 +24,12 @@ internal interface IGCInfoDecoder : IGCInfoHandle uint GetCodeLength(); uint StackBaseRegister { get; } + /// + /// Finds the first interruptible point within the given handler range [startOffset, endOffset). + /// Returns null if no interruptible point exists in the range. + /// + uint? FindFirstInterruptiblePoint(uint startOffset, uint endOffset) => null; + /// /// Enumerates all live GC slots at the given instruction offset. /// diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/CorSigParser.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/CorSigParser.cs new file mode 100644 index 00000000000000..b8c6a0a173552b --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/CorSigParser.cs @@ -0,0 +1,212 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +/// +/// Minimal signature parser for GC reference classification of method parameters. +/// Parses the ECMA-335 II.23.2.1 MethodDefSig format, classifying each parameter +/// type as a GC reference, interior pointer, value type, or non-GC primitive. +/// +internal ref struct CorSigParser +{ + private ReadOnlySpan _sig; + private int _index; + private readonly int _pointerSize; + + public CorSigParser(ReadOnlySpan signature, int pointerSize) + { + _sig = signature; + _index = 0; + _pointerSize = pointerSize; + } + + public bool AtEnd => _index >= _sig.Length; + + public byte ReadByte() + { + if (_index >= _sig.Length) + throw new InvalidOperationException("Unexpected end of signature."); + return _sig[_index++]; + } + + /// + /// Reads a compressed unsigned integer (ECMA-335 II.23.2). + /// + public uint ReadCompressedUInt() + { + byte b = ReadByte(); + if ((b & 0x80) == 0) + return b; + if ((b & 0xC0) == 0x80) + { + byte b2 = ReadByte(); + return (uint)(((b & 0x3F) << 8) | b2); + } + if ((b & 0xE0) == 0xC0) + { + byte b2 = ReadByte(); + byte b3 = ReadByte(); + byte b4 = ReadByte(); + return (uint)(((b & 0x1F) << 24) | (b2 << 16) | (b3 << 8) | b4); + } + throw new InvalidOperationException("Invalid compressed integer encoding."); + } + + /// + /// Reads the next type from the signature and classifies it for GC scanning. + /// Advances past the full type encoding. + /// + public GcTypeKind ReadTypeAndClassify() + { + CorElementType elemType = (CorElementType)ReadCompressedUInt(); + + switch (elemType) + { + case CorElementType.Void: + case CorElementType.Boolean: + case CorElementType.Char: + case CorElementType.I1: + case CorElementType.U1: + case CorElementType.I2: + case CorElementType.U2: + case CorElementType.I4: + case CorElementType.U4: + case CorElementType.I8: + case CorElementType.U8: + case CorElementType.R4: + case CorElementType.R8: + case CorElementType.I: + case CorElementType.U: + return GcTypeKind.None; + + case CorElementType.String: + case CorElementType.Object: + return GcTypeKind.Ref; + + case CorElementType.Class: + ReadCompressedUInt(); // TypeDefOrRefOrSpecEncoded + return GcTypeKind.Ref; + + case CorElementType.ValueType: + ReadCompressedUInt(); // TypeDefOrRefOrSpecEncoded + return GcTypeKind.Other; + + case CorElementType.SzArray: + SkipType(); // element type + return GcTypeKind.Ref; + + case CorElementType.Array: + SkipType(); // element type + SkipArrayShape(); + return GcTypeKind.Ref; + + case CorElementType.GenericInst: + { + byte baseType = ReadByte(); // CLASS, VALUETYPE, or INTERNAL + if (baseType == (byte)CorElementType.Internal) + { + // ELEMENT_TYPE_INTERNAL embeds a raw pointer to a TypeHandle + _index += _pointerSize; + } + else + { + ReadCompressedUInt(); // TypeDefOrRefOrSpecEncoded + } + uint argCount = ReadCompressedUInt(); + for (uint i = 0; i < argCount; i++) + SkipType(); + // Conservative: treat INTERNAL base as Ref (could be either class or valuetype). + // CLASS-based generics are Ref; VALUETYPE-based and unknown are Other. + return baseType == (byte)CorElementType.Class ? GcTypeKind.Ref : GcTypeKind.Other; + } + + case CorElementType.Byref: + SkipType(); // inner type + return GcTypeKind.Interior; + + case CorElementType.Ptr: + SkipType(); // pointee type + return GcTypeKind.None; + + case CorElementType.FnPtr: + SkipMethodSignature(); + return GcTypeKind.None; + + case CorElementType.TypedByRef: + return GcTypeKind.Other; + + case CorElementType.Var: + case CorElementType.MVar: + ReadCompressedUInt(); // type parameter index + // Conservative: generic type params could be GC refs. + // The runtime resolves these via the generic context. + // For now, treat as potential GC ref to avoid missing references. + return GcTypeKind.Ref; + + case CorElementType.CModReqd: + case CorElementType.CModOpt: + ReadCompressedUInt(); // TypeDefOrRefOrSpecEncoded + return ReadTypeAndClassify(); // recurse past the modifier + + case CorElementType.Sentinel: + return ReadTypeAndClassify(); // skip sentinel, read next type + + case CorElementType.Internal: + // Runtime-internal type: raw pointer to TypeHandle follows. + // Skip the pointer bytes. Conservative: treat as potential GC ref. + _index += _pointerSize; + return GcTypeKind.Ref; + + default: + return GcTypeKind.None; + } + } + + /// + /// Skips over a complete type encoding in the signature. + /// + public void SkipType() + { + ReadTypeAndClassify(); // Same traversal, just discard the result + } + + private void SkipArrayShape() + { + _ = ReadCompressedUInt(); // rank + uint numSizes = ReadCompressedUInt(); + for (uint i = 0; i < numSizes; i++) + ReadCompressedUInt(); + uint numLoBounds = ReadCompressedUInt(); + for (uint i = 0; i < numLoBounds; i++) + ReadCompressedUInt(); // lo bounds are signed but encoded as unsigned + } + + private void SkipMethodSignature() + { + byte callingConv = ReadByte(); + if ((callingConv & 0x10) != 0) // GENERIC + ReadCompressedUInt(); // generic param count + uint paramCount = ReadCompressedUInt(); + SkipType(); // return type + for (uint i = 0; i < paramCount; i++) + SkipType(); + } +} + +/// +/// Classification of a signature type for GC scanning purposes. +/// +internal enum GcTypeKind +{ + /// Not a GC reference (primitives, pointers). + None, + /// Object reference (class, string, array). + Ref, + /// Interior pointer (byref). + Interior, + /// Value type that may contain embedded GC references. + Other, +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs new file mode 100644 index 00000000000000..c384a1394431db --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +/// +/// Token values from CORCOMPILE_GCREFMAP_TOKENS (corcompile.h). +/// These indicate the type of GC reference at each transition block slot. +/// +internal enum GCRefMapToken +{ + Skip = 0, + Ref = 1, + Interior = 2, + MethodParam = 3, + TypeParam = 4, + VASigCookie = 5, +} + +/// +/// Managed port of the native GCRefMapDecoder (gcrefmap.h:158-246). +/// Decodes a compact bitstream describing which transition block slots +/// contain GC references for a given call site. +/// +internal ref struct GCRefMapDecoder +{ + private readonly Target _target; + private TargetPointer _currentByte; + private int _pendingByte; + private int _pos; + + public GCRefMapDecoder(Target target, TargetPointer blob) + { + _target = target; + _currentByte = blob; + _pendingByte = 0x80; // Forces first byte read + _pos = 0; + } + + public bool AtEnd => _pendingByte == 0; + + public int CurrentPos => _pos; + + private int GetBit() + { + int x = _pendingByte; + if ((x & 0x80) != 0) + { + x = _target.Read(_currentByte); + _currentByte = new TargetPointer(_currentByte.Value + 1); + x |= ((x & 0x80) << 7); + } + _pendingByte = x >> 1; + return x & 1; + } + + private int GetTwoBit() + { + int result = GetBit(); + result |= GetBit() << 1; + return result; + } + + private int GetInt() + { + int result = 0; + int bit = 0; + do + { + result |= GetBit() << (bit++); + result |= GetBit() << (bit++); + result |= GetBit() << (bit++); + } + while (GetBit() != 0); + return result; + } + + /// + /// x86 only: Read the stack pop count from the stream. + /// + public uint ReadStackPop() + { + int x = GetTwoBit(); + if (x == 3) + x = GetInt() + 3; + return (uint)x; + } + + /// + /// Read the next GC reference token from the stream. + /// Advances CurrentPos as appropriate. + /// + public GCRefMapToken ReadToken() + { + int val = GetTwoBit(); + if (val == 3) + { + int ext = GetInt(); + if ((ext & 1) == 0) + { + _pos += (ext >> 1) + 4; + return GCRefMapToken.Skip; + } + else + { + _pos++; + return (GCRefMapToken)((ext >> 1) + 3); + } + } + _pos++; + return (GCRefMapToken)val; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs index fa72eb606fad75..72063a93fa6c01 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -23,7 +23,8 @@ public bool EnumGcRefs( IPlatformAgnosticContext context, CodeBlockHandle cbh, CodeManagerFlags flags, - GcScanContext scanContext) + GcScanContext scanContext, + uint? relOffsetOverride = null) { TargetNUInt relativeOffset = _eman.GetRelativeOffset(cbh); _eman.GetGCInfo(cbh, out TargetPointer gcInfoAddr, out uint gcVersion); @@ -41,8 +42,10 @@ public bool EnumGcRefs( // The native code uses GET_CALLER_SP(pRD) which comes from EnsureCallerContextIsValid. TargetPointer? callerSP = null; + uint offsetToUse = relOffsetOverride ?? (uint)relativeOffset.Value; + return decoder.EnumerateLiveSlots( - (uint)relativeOffset.Value, + offsetToUse, flags, (bool isRegister, uint registerNumber, int spOffset, uint spBase, uint gcFlags) => { 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 aa3005621dfb0f..5f0615d037c8f9 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 @@ -147,7 +147,6 @@ private IEnumerable CreateStackWalkCore(ThreadData thread } } - // if the next Frame is not valid and we are not in managed code, there is nothing to return if (state == StackWalkState.SW_FRAME && !frameIterator.IsValid()) { yield break; @@ -155,15 +154,6 @@ private IEnumerable CreateStackWalkCore(ThreadData thread StackWalkData stackWalkData = new(context, state, frameIterator, threadData); - // Mirror native Init() -> ProcessCurrentFrame() -> CheckForSkippedFrames(): - // When the initial frame is managed (SW_FRAMELESS), check if there are explicit - // Frames below the caller SP that should be reported first. The native walker - // yields skipped frames BEFORE the containing managed frame on non-x86. - if (state == StackWalkState.SW_FRAMELESS && CheckForSkippedFrames(stackWalkData)) - { - stackWalkData.State = StackWalkState.SW_SKIPPED_FRAME; - } - yield return stackWalkData.ToDataFrame(); stackWalkData.AdvanceIsFirst(); @@ -176,6 +166,8 @@ private IEnumerable CreateStackWalkCore(ThreadData thread IReadOnlyList IStackWalk.WalkStackReferences(ThreadData threadData) { + // TODO(stackref): This isn't quite right. We need to check if the FilterContext or ProfilerFilterContext + // is set and prefer that if either is not null. IEnumerable stackFrames = CreateStackWalkCore(threadData, skipInitialFrames: true); IEnumerable frames = stackFrames.Select(AssertCorrectHandle); IEnumerable gcFrames = Filter(frames); @@ -190,6 +182,7 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre bool reportGcReferences = gcFrame.ShouldCrawlFrameReportGCReferences; + TargetPointer pFrame = ((IStackWalk)this).GetFrameAddress(gcFrame.Frame); scanContext.UpdateScanContext( gcFrame.Frame.Context.StackPointer, @@ -210,27 +203,30 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre if (gcFrame.ShouldParentToFuncletSkipReportingGCReferences) codeManagerFlags |= CodeManagerFlags.ParentOfFuncletStackFrame; - // TODO: When ShouldParentFrameUseUnwindTargetPCforGCReporting is set, - // use FindFirstInterruptiblePoint on the catch handler clause range - // to override the relOffset for GC liveness lookup. This mirrors - // native gcenv.ee.common.cpp behavior for catch-handler resumption. + uint? relOffsetOverride = null; + if (gcFrame.ShouldParentFrameUseUnwindTargetPCforGCReporting) + { + _eman.GetGCInfo(cbh.Value, out TargetPointer gcInfoAddr, out uint gcVersion); + IGCInfoHandle gcHandle = _target.Contracts.GCInfo.DecodePlatformSpecificGCInfo(gcInfoAddr, gcVersion); + if (gcHandle is IGCInfoDecoder decoder) + { + relOffsetOverride = decoder.FindFirstInterruptiblePoint( + gcFrame.ClauseForCatchHandlerStartPC, + gcFrame.ClauseForCatchHandlerEndPC); + } + } GcScanner gcScanner = new(_target); - gcScanner.EnumGcRefs(gcFrame.Frame.Context, cbh.Value, codeManagerFlags, scanContext); + gcScanner.EnumGcRefs(gcFrame.Frame.Context, cbh.Value, codeManagerFlags, scanContext, relOffsetOverride); } else { - // TODO: Frame-based GC root scanning (ScanFrameRoots) not yet implemented. - // Frames that call PromoteCallerStack (StubDispatchFrame, ExternalMethodFrame, - // DynamicHelperFrame, etc.) will be handled in a follow-up PR. + ScanFrameRoots(gcFrame.Frame, scanContext); } } } catch (System.Exception ex) { - // Per-frame exceptions are intentionally swallowed to provide partial results - // rather than failing the entire stack walk. This matches the resilience model - // of the legacy DAC. Callers can detect incomplete results by comparing counts. Debug.WriteLine($"Exception during WalkStackReferences at IP=0x{gcFrame.Frame.Context.InstructionPointer:X}: {ex.GetType().Name}: {ex.Message}"); } } @@ -260,6 +256,8 @@ public GCFrameData(StackDataFrameHandle frame) public bool ShouldParentToFuncletSkipReportingGCReferences { get; set; } public bool ShouldCrawlFrameReportGCReferences { get; set; } // required public bool ShouldParentFrameUseUnwindTargetPCforGCReporting { get; set; } + public uint ClauseForCatchHandlerStartPC { get; set; } + public uint ClauseForCatchHandlerEndPC { get; set; } } private IEnumerable Filter(IEnumerable handles) @@ -267,6 +265,7 @@ private IEnumerable Filter(IEnumerable handle // StackFrameIterator::Filter assuming GC_FUNCLET_REFERENCE_REPORTING is defined // global tracking variables + bool movedPastFirstExInfo = false; bool processNonFilterFunclet = false; bool processIntermediaryNonFilterFunclet = false; bool didFuncletReportGCReferences = true; @@ -286,6 +285,15 @@ private IEnumerable Filter(IEnumerable handle TargetPointer pExInfo = GetCurrentExceptionTracker(handle); + TargetPointer frameSp = handle.State == StackWalkState.SW_FRAME ? handle.FrameAddress : handle.Context.StackPointer; + if (pExInfo != TargetPointer.Null && frameSp > pExInfo) + { + if (!movedPastFirstExInfo) + { + movedPastFirstExInfo = true; + } + } + // by default, there is no funclet for the current frame // that reported GC references gcFrame.ShouldParentToFuncletSkipReportingGCReferences = false; @@ -494,6 +502,9 @@ private IEnumerable Filter(IEnumerable handle didFuncletReportGCReferences = true; gcFrame.ShouldParentFrameUseUnwindTargetPCforGCReporting = true; + + gcFrame.ClauseForCatchHandlerStartPC = exInfo.ClauseForCatchHandlerStartPC; + gcFrame.ClauseForCatchHandlerEndPC = exInfo.ClauseForCatchHandlerEndPC; } else if (!IsFunclet(handle)) { @@ -597,7 +608,18 @@ private bool Next(StackWalkData handle) } break; case StackWalkState.SW_SKIPPED_FRAME: + // Native SFITER_SKIPPED_FRAME_FUNCTION: advance past the frame, then + // check for MORE skipped frames before transitioning to FRAMELESS. + // This prevents yielding the managed method multiple times between + // consecutive skipped frames. handle.FrameIter.Next(); + if (CheckForSkippedFrames(handle)) + { + // More skipped frames — stay in SW_SKIPPED_FRAME, don't go through UpdateState + handle.State = StackWalkState.SW_SKIPPED_FRAME; + return true; + } + // No more skipped frames — fall through to UpdateState which will set SW_FRAMELESS break; case StackWalkState.SW_FRAME: handle.FrameIter.UpdateContextFromFrame(handle.Context); @@ -796,4 +818,329 @@ private static StackDataFrameHandle AssertCorrectHandle(IStackDataFrameHandle st return handle; } + + /// + /// Scans GC roots for a non-frameless (capital "F" Frame) stack frame. + /// Dispatches based on frame type identifier. Most frame types have a no-op + /// GcScanRoots (the base Frame implementation does nothing). + /// + /// Frame types with meaningful GcScanRoots that call PromoteCallerStack: + /// StubDispatchFrame, ExternalMethodFrame, CallCountingHelperFrame, + /// DynamicHelperFrame, CLRToCOMMethodFrame, HijackFrame, ProtectValueClassFrame. + /// + private void ScanFrameRoots(StackDataFrameHandle frame, GcScanContext scanContext) + { + TargetPointer frameAddress = frame.FrameAddress; + if (frameAddress == TargetPointer.Null) + return; + + // Read the frame's VTable pointer (Identifier) to determine its type. + // GetFrameName expects a VTable identifier, not a frame address. + Data.Frame frameData = _target.ProcessedData.GetOrAdd(frameAddress); + string frameName = ((IStackWalk)this).GetFrameName(frameData.Identifier); + + switch (frameName) + { + case "StubDispatchFrame": + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + Data.StubDispatchFrame sdf = _target.ProcessedData.GetOrAdd(frameAddress); + if (sdf.GCRefMap != TargetPointer.Null) + { + PromoteCallerStackUsingGCRefMap(fmf.TransitionBlockPtr, sdf.GCRefMap, scanContext); + } + else + { + PromoteCallerStackUsingMetaSig(frameAddress, fmf.TransitionBlockPtr, scanContext); + } + break; + } + + case "ExternalMethodFrame": + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + Data.ExternalMethodFrame emf = _target.ProcessedData.GetOrAdd(frameAddress); + if (emf.GCRefMap != TargetPointer.Null) + { + PromoteCallerStackUsingGCRefMap(fmf.TransitionBlockPtr, emf.GCRefMap, scanContext); + } + break; + } + + case "DynamicHelperFrame": + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + Data.DynamicHelperFrame dhf = _target.ProcessedData.GetOrAdd(frameAddress); + ScanDynamicHelperFrame(fmf.TransitionBlockPtr, dhf.DynamicHelperFrameFlags, scanContext); + break; + } + + case "CallCountingHelperFrame": + case "PrestubMethodFrame": + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + PromoteCallerStackUsingMetaSig(frameAddress, fmf.TransitionBlockPtr, scanContext); + break; + } + + case "CLRToCOMMethodFrame": + case "ComPrestubMethodFrame": + // These frames call PromoteCallerStack to report method arguments. + // TODO(stackref): Implement PromoteCallerStack for COM interop frames + break; + + case "HijackFrame": + // Reports return value registers (X86 only with FEATURE_HIJACK) + // TODO(stackref): Implement HijackFrame scanning + break; + + case "ProtectValueClassFrame": + // Scans value types in linked list + // TODO(stackref): Implement ProtectValueClassFrame scanning + break; + + default: + // Base Frame::GcScanRoots_Impl is a no-op — nothing to report. + break; + } + } + + /// + /// Decodes a GCRefMap bitstream and reports GC references in the transition block. + /// Port of native TransitionFrame::PromoteCallerStackUsingGCRefMap (frames.cpp). + /// + private void PromoteCallerStackUsingGCRefMap( + TargetPointer transitionBlock, + TargetPointer gcRefMapBlob, + GcScanContext scanContext) + { + GCRefMapDecoder decoder = new(_target, gcRefMapBlob); + + // x86: skip stack pop count + if (_target.PointerSize == 4) + decoder.ReadStackPop(); + + while (!decoder.AtEnd) + { + int pos = decoder.CurrentPos; + GCRefMapToken token = decoder.ReadToken(); + uint offset = OffsetFromGCRefMapPos(pos); + TargetPointer slotAddress = new(transitionBlock.Value + offset); + + switch (token) + { + case GCRefMapToken.Skip: + break; + + case GCRefMapToken.Ref: + scanContext.GCReportCallback(slotAddress, GcScanFlags.None); + break; + + case GCRefMapToken.Interior: + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + break; + + case GCRefMapToken.MethodParam: + case GCRefMapToken.TypeParam: + // The DAC skips these (guarded by #ifndef DACCESS_COMPILE in native). + // They represent loader allocator references, not managed GC refs. + break; + + case GCRefMapToken.VASigCookie: + // VASigCookie requires MetaSig parsing — not yet implemented. + // TODO(stackref): Implement VASIG_COOKIE handling + break; + } + } + } + + /// + /// Converts a GCRefMap position to a byte offset within the transition block. + /// Port of native OffsetFromGCRefMapPos (frames.cpp:1624-1633). + /// + private uint OffsetFromGCRefMapPos(int pos) + { + uint firstSlotOffset = _target.ReadGlobal(Constants.Globals.TransitionBlockOffsetOfFirstGCRefMapSlot); + + return firstSlotOffset + (uint)(pos * _target.PointerSize); + } + + /// + /// Scans GC roots for a DynamicHelperFrame based on its flags. + /// Port of native DynamicHelperFrame::GcScanRoots_Impl (frames.cpp:1071-1105). + /// + private void ScanDynamicHelperFrame( + TargetPointer transitionBlock, + int dynamicHelperFrameFlags, + GcScanContext scanContext) + { + const int DynamicHelperFrameFlags_ObjectArg = 1; + const int DynamicHelperFrameFlags_ObjectArg2 = 2; + + uint argRegOffset = _target.ReadGlobal(Constants.Globals.TransitionBlockOffsetOfArgumentRegisters); + + if ((dynamicHelperFrameFlags & DynamicHelperFrameFlags_ObjectArg) != 0) + { + TargetPointer argAddr = new(transitionBlock.Value + argRegOffset); + // On x86, this would need offsetof(ArgumentRegisters, ECX) adjustment. + // For AMD64/ARM64, the first argument register is at the base offset. + scanContext.GCReportCallback(argAddr, GcScanFlags.None); + } + + if ((dynamicHelperFrameFlags & DynamicHelperFrameFlags_ObjectArg2) != 0) + { + TargetPointer argAddr = new(transitionBlock.Value + argRegOffset + (uint)_target.PointerSize); + // On x86, this would need offsetof(ArgumentRegisters, EDX) adjustment. + // For AMD64/ARM64, the second argument is pointer-size after the first. + scanContext.GCReportCallback(argAddr, GcScanFlags.None); + } + } + + /// + /// Promotes caller stack GC references by parsing the method signature via MetaSig. + /// Used when a frame has no precomputed GCRefMap (e.g., dynamic/LCG methods). + /// Port of native TransitionFrame::PromoteCallerStack + PromoteCallerStackHelper (frames.cpp). + /// + private void PromoteCallerStackUsingMetaSig( + TargetPointer frameAddress, + TargetPointer transitionBlock, + GcScanContext scanContext) + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + TargetPointer methodDescPtr = fmf.MethodDescPtr; + if (methodDescPtr == TargetPointer.Null) + return; + + ReadOnlySpan signature; + try + { + signature = GetMethodSignatureBytes(methodDescPtr); + } + catch (System.Exception) + { + return; + } + + if (signature.IsEmpty) + return; + + CorSigParser parser = new(signature, _target.PointerSize); + + // Parse calling convention + byte callingConvByte = parser.ReadByte(); + bool hasThis = (callingConvByte & 0x20) != 0; // IMAGE_CEE_CS_CALLCONV_HASTHIS + bool isGeneric = (callingConvByte & 0x10) != 0; + + if (isGeneric) + parser.ReadCompressedUInt(); // skip generic param count + + uint paramCount = parser.ReadCompressedUInt(); + + // Skip return type + parser.SkipType(); + + // Walk through GCRefMap positions. + // The position numbering matches how GCRefMap encodes slots: + // ARM64: pos 0 = RetBuf (x8), pos 1+ = argument registers (x0-x7), then stack + // Others: pos 0 = first argument register/slot, etc. + int pos = 0; + + // On ARM64, position 0 is the return buffer register (x8). + // Methods without a return buffer skip this slot. + // TODO: detect HasRetBuf from the signature's return type when needed. + // For now, we skip the retbuf slot on ARM64 since the common case + // (dynamic invoke stubs) doesn't use return buffers. + bool isArm64 = IsTargetArm64(); + if (isArm64) + pos++; + + // Promote 'this' if present + if (hasThis) + { + uint offset = OffsetFromGCRefMapPos(pos); + TargetPointer slotAddress = new(transitionBlock.Value + offset); + scanContext.GCReportCallback(slotAddress, GcScanFlags.None); + pos++; + } + + // Walk each parameter + for (uint i = 0; i < paramCount; i++) + { + uint offset = OffsetFromGCRefMapPos(pos); + TargetPointer slotAddress = new(transitionBlock.Value + offset); + + GcTypeKind kind = parser.ReadTypeAndClassify(); + + switch (kind) + { + case GcTypeKind.Ref: + scanContext.GCReportCallback(slotAddress, GcScanFlags.None); + break; + + case GcTypeKind.Interior: + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + break; + + case GcTypeKind.Other: + // Value types may contain embedded GC references. + // Full scanning requires reading the MethodTable's GCDesc. + // TODO(stackref): Implement value type GCDesc scanning for MetaSig path. + break; + + case GcTypeKind.None: + break; + } + + pos++; + } + } + + /// + /// Gets the raw signature bytes for a MethodDesc. + /// For StoredSigMethodDesc (dynamic, array, EEImpl methods), reads the embedded signature. + /// For normal IL methods, reads from module metadata. + /// + private ReadOnlySpan GetMethodSignatureBytes(TargetPointer methodDescPtr) + { + IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + MethodDescHandle mdh = rts.GetMethodDescHandle(methodDescPtr); + + // Try StoredSigMethodDesc first (dynamic/LCG/array methods) + if (rts.IsStoredSigMethodDesc(mdh, out ReadOnlySpan storedSig)) + return storedSig; + + // Normal IL methods: get signature from metadata + uint methodToken = rts.GetMethodToken(mdh); + if (methodToken == 0x06000000) // mdtMethodDef with RID 0 = no token + return default; + + TargetPointer methodTablePtr = rts.GetMethodTable(mdh); + TypeHandle typeHandle = rts.GetTypeHandle(methodTablePtr); + TargetPointer modulePtr = rts.GetModule(typeHandle); + + ILoader loader = _target.Contracts.Loader; + ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(modulePtr); + + IEcmaMetadata ecmaMetadata = _target.Contracts.EcmaMetadata; + MetadataReader? mdReader = ecmaMetadata.GetMetadata(moduleHandle); + if (mdReader is null) + return default; + + MethodDefinitionHandle methodDefHandle = MetadataTokens.MethodDefinitionHandle((int)(methodToken & 0x00FFFFFF)); + MethodDefinition methodDef = mdReader.GetMethodDefinition(methodDefHandle); + BlobReader blobReader = mdReader.GetBlobReader(methodDef.Signature); + return blobReader.ReadBytes(blobReader.Length); + } + + /// + /// Detects if the target architecture is ARM64 based on TransitionBlock layout. + /// On ARM64, GetOffsetOfFirstGCRefMapSlot != GetOffsetOfArgumentRegisters + /// (because the first GCRefMap slot is the x8 RetBuf register, not x0). + /// + private bool IsTargetArm64() + { + uint firstGCRefMapSlot = _target.ReadGlobal(Constants.Globals.TransitionBlockOffsetOfFirstGCRefMapSlot); + uint argRegsOffset = _target.ReadGlobal(Constants.Globals.TransitionBlockOffsetOfArgumentRegisters); + return firstGCRefMapSlot != argRegsOffset; + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs index 8f2470d6e71996..c5d5eaffaf43fd 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs @@ -23,6 +23,8 @@ public ExceptionInfo(Target target, TargetPointer address) CSFEHClause = target.ReadPointer(address + (ulong)type.Fields[nameof(CSFEHClause)].Offset); CSFEnclosingClause = target.ReadPointer(address + (ulong)type.Fields[nameof(CSFEnclosingClause)].Offset); CallerOfActualHandlerFrame = target.ReadPointer(address + (ulong)type.Fields[nameof(CallerOfActualHandlerFrame)].Offset); + ClauseForCatchHandlerStartPC = target.Read(address + (ulong)type.Fields[nameof(ClauseForCatchHandlerStartPC)].Offset); + ClauseForCatchHandlerEndPC = target.Read(address + (ulong)type.Fields[nameof(ClauseForCatchHandlerEndPC)].Offset); } public TargetPointer PreviousNestedInfo { get; } @@ -35,4 +37,6 @@ public ExceptionInfo(Target target, TargetPointer address) public TargetPointer CSFEHClause { get; } public TargetPointer CSFEnclosingClause { get; } public TargetPointer CallerOfActualHandlerFrame { get; } + public uint ClauseForCatchHandlerStartPC { get; } + public uint ClauseForCatchHandlerEndPC { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs new file mode 100644 index 00000000000000..652b60fb7bb49d --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal class DynamicHelperFrame : IData +{ + static DynamicHelperFrame IData.Create(Target target, TargetPointer address) + => new DynamicHelperFrame(target, address); + + public DynamicHelperFrame(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.DynamicHelperFrame); + DynamicHelperFrameFlags = target.Read(address + (ulong)type.Fields[nameof(DynamicHelperFrameFlags)].Offset); + } + + public int DynamicHelperFrameFlags { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs new file mode 100644 index 00000000000000..1a07c91757f705 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal class ExternalMethodFrame : IData +{ + static ExternalMethodFrame IData.Create(Target target, TargetPointer address) + => new ExternalMethodFrame(target, address); + + public ExternalMethodFrame(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.ExternalMethodFrame); + GCRefMap = target.ReadPointer(address + (ulong)type.Fields[nameof(GCRefMap)].Offset); + } + + public TargetPointer GCRefMap { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs index f4e677dafddaa9..07d9f199523eb5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs @@ -14,6 +14,7 @@ public StubDispatchFrame(Target target, TargetPointer address) MethodDescPtr = target.ReadPointer(address + (ulong)type.Fields[nameof(MethodDescPtr)].Offset); RepresentativeMTPtr = target.ReadPointer(address + (ulong)type.Fields[nameof(RepresentativeMTPtr)].Offset); RepresentativeSlot = target.Read(address + (ulong)type.Fields[nameof(RepresentativeSlot)].Offset); + GCRefMap = target.ReadPointer(address + (ulong)type.Fields[nameof(GCRefMap)].Offset); Address = address; } @@ -21,4 +22,5 @@ public StubDispatchFrame(Target target, TargetPointer address) public TargetPointer MethodDescPtr { get; } public TargetPointer RepresentativeMTPtr { get; } public uint RepresentativeSlot { get; } + public TargetPointer GCRefMap { get; } } diff --git a/src/native/managed/cdac/cdac.slnx b/src/native/managed/cdac/cdac.slnx index 7449d30624ec2d..4abe615fe50f3b 100644 --- a/src/native/managed/cdac/cdac.slnx +++ b/src/native/managed/cdac/cdac.slnx @@ -14,5 +14,6 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/BasicGCStressTests.cs b/src/native/managed/cdac/tests/GCStressTests/BasicGCStressTests.cs new file mode 100644 index 00000000000000..45a83b2694e87a --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/BasicGCStressTests.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Microsoft.DotNet.XUnitExtensions; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; + +/// +/// Runs each debuggee app under corerun with DOTNET_GCStress=0x24 and asserts +/// that the cDAC stack reference verification achieves 100% pass rate. +/// +/// +/// Prerequisites: +/// - Build CoreCLR native + cDAC: build.cmd -subset clr.native+tools.cdac -c Debug -rc Checked -lc Release +/// - Generate core_root: src\tests\build.cmd Checked generatelayoutonly /p:LibrariesConfiguration=Release +/// - Build debuggees: dotnet build this test project +/// +/// The tests use CORE_ROOT env var if set, otherwise default to the standard artifacts path. +/// +public class BasicGCStressTests : GCStressTestBase +{ + public BasicGCStressTests(ITestOutputHelper output) : base(output) { } + + public static IEnumerable Debuggees => + [ + ["BasicAlloc"], + ["DeepStack"], + ["Generics"], + ["MultiThread"], + ["Comprehensive"], + ["ExceptionHandling"], + ]; + + public static IEnumerable WindowsOnlyDebuggees => + [ + ["PInvoke"], + ]; + + [Theory] + [MemberData(nameof(Debuggees))] + public void GCStress_AllVerificationsPass(string debuggeeName) + { + GCStressResults results = RunGCStress(debuggeeName); + AssertAllPassed(results, debuggeeName); + } + + [Theory] + [MemberData(nameof(WindowsOnlyDebuggees))] + public void GCStress_WindowsOnly_AllVerificationsPass(string debuggeeName) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + throw new SkipTestException("P/Invoke debuggee uses kernel32.dll (Windows only)"); + + GCStressResults results = RunGCStress(debuggeeName); + AssertAllPassed(results, debuggeeName); + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/BasicAlloc/BasicAlloc.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/BasicAlloc/BasicAlloc.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/BasicAlloc/BasicAlloc.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/BasicAlloc/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/BasicAlloc/Program.cs new file mode 100644 index 00000000000000..f886c0ef72cefe --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/BasicAlloc/Program.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; + +/// +/// Exercises basic object allocation patterns: objects, strings, arrays. +/// +internal static class Program +{ + [MethodImpl(MethodImplOptions.NoInlining)] + static object AllocAndHold() + { + object o = new object(); + string s = "hello world"; + int[] arr = new int[] { 1, 2, 3 }; + byte[] buf = new byte[256]; + GC.KeepAlive(o); + GC.KeepAlive(s); + GC.KeepAlive(arr); + GC.KeepAlive(buf); + return o; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ManyLiveRefs() + { + object r0 = new object(); + object r1 = new object(); + object r2 = new object(); + object r3 = new object(); + object r4 = new object(); + object r5 = new object(); + object r6 = new object(); + object r7 = new object(); + string r8 = "live-string"; + int[] r9 = new int[10]; + + GC.KeepAlive(r0); GC.KeepAlive(r1); + GC.KeepAlive(r2); GC.KeepAlive(r3); + GC.KeepAlive(r4); GC.KeepAlive(r5); + GC.KeepAlive(r6); GC.KeepAlive(r7); + GC.KeepAlive(r8); GC.KeepAlive(r9); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + AllocAndHold(); + ManyLiveRefs(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/Comprehensive/Comprehensive.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Comprehensive/Comprehensive.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Comprehensive/Comprehensive.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/Comprehensive/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Comprehensive/Program.cs new file mode 100644 index 00000000000000..6a2f26f146ef0f --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Comprehensive/Program.cs @@ -0,0 +1,253 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +/// +/// All-in-one comprehensive debuggee that exercises every scenario +/// in a single run: allocations, exceptions, generics, P/Invoke, threading. +/// +internal static class Program +{ + interface IKeepAlive { object GetRef(); } + class BoxHolder : IKeepAlive + { + object _value; + public BoxHolder() { _value = new object(); } + public BoxHolder(object v) { _value = v; } + [MethodImpl(MethodImplOptions.NoInlining)] + public object GetRef() => _value; + } + + struct LargeStruct { public object A, B, C, D; } + + [MethodImpl(MethodImplOptions.NoInlining)] + static object AllocAndHold() + { + object o = new object(); + string s = "hello world"; + int[] arr = new int[] { 1, 2, 3 }; + GC.KeepAlive(o); + GC.KeepAlive(s); + GC.KeepAlive(arr); + return o; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedCall(int depth) + { + object o = new object(); + if (depth > 0) + NestedCall(depth - 1); + GC.KeepAlive(o); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryCatchScenario() + { + object before = new object(); + try + { + throw new InvalidOperationException("test"); + } + catch (InvalidOperationException ex) + { + object inCatch = new object(); + GC.KeepAlive(ex); + GC.KeepAlive(inCatch); + } + GC.KeepAlive(before); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryFinallyScenario() + { + object outerRef = new object(); + try + { + object innerRef = new object(); + GC.KeepAlive(innerRef); + } + finally + { + object finallyRef = new object(); + GC.KeepAlive(finallyRef); + } + GC.KeepAlive(outerRef); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedExceptionScenario() + { + object a = new object(); + try + { + try + { + throw new ArgumentException("inner"); + } + catch (ArgumentException ex1) + { + GC.KeepAlive(ex1); + throw new InvalidOperationException("outer", ex1); + } + } + catch (InvalidOperationException ex2) + { + GC.KeepAlive(ex2); + } + GC.KeepAlive(a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void FilterExceptionScenario() + { + object holder = new object(); + try + { + throw new ArgumentException("filter-test"); + } + catch (ArgumentException ex) when (FilterCheck(ex)) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(holder); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static bool FilterCheck(Exception ex) + { + object filterLocal = new object(); + GC.KeepAlive(filterLocal); + return ex.Message.Contains("filter"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static T GenericAlloc() where T : new() + { + T val = new T(); + object marker = new object(); + GC.KeepAlive(marker); + return val; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void InterfaceDispatchScenario() + { + IKeepAlive holder = new BoxHolder(new int[] { 42, 43 }); + object r = holder.GetRef(); + GC.KeepAlive(holder); + GC.KeepAlive(r); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void DelegateScenario() + { + object captured = new object(); + Func fn = () => { GC.KeepAlive(captured); return new object(); }; + object result = fn(); + GC.KeepAlive(result); + GC.KeepAlive(fn); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void StructWithRefsScenario() + { + LargeStruct ls; + ls.A = new object(); + ls.B = "struct-string"; + ls.C = new int[] { 10, 20 }; + ls.D = new BoxHolder(ls.A); + GC.KeepAlive(ls.A); + GC.KeepAlive(ls.B); + GC.KeepAlive(ls.C); + GC.KeepAlive(ls.D); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void PinnedScenario() + { + byte[] buffer = new byte[64]; + GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned); + try + { + object other = new object(); + GC.KeepAlive(other); + GC.KeepAlive(buffer); + } + finally + { + pin.Free(); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void MultiThreadScenario() + { + ManualResetEventSlim ready = new ManualResetEventSlim(false); + ManualResetEventSlim go = new ManualResetEventSlim(false); + Thread t = new Thread(() => + { + object threadLocal = new object(); + ready.Set(); + go.Wait(); + NestedCall(5); + GC.KeepAlive(threadLocal); + }); + t.Start(); + ready.Wait(); + go.Set(); + NestedCall(3); + t.Join(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void RethrowScenario() + { + object outerRef = new object(); + try + { + try + { + throw new ApplicationException("rethrow-test"); + } + catch (ApplicationException) + { + object catchRef = new object(); + GC.KeepAlive(catchRef); + throw; + } + } + catch (ApplicationException ex) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(outerRef); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + AllocAndHold(); + NestedCall(5); + TryCatchScenario(); + TryFinallyScenario(); + NestedExceptionScenario(); + FilterExceptionScenario(); + GenericAlloc(); + GenericAlloc>(); + InterfaceDispatchScenario(); + DelegateScenario(); + StructWithRefsScenario(); + PinnedScenario(); + MultiThreadScenario(); + RethrowScenario(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/DeepStack/DeepStack.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/DeepStack/DeepStack.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/DeepStack/DeepStack.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/DeepStack/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/DeepStack/Program.cs new file mode 100644 index 00000000000000..c98679aea54ac2 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/DeepStack/Program.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; + +/// +/// Exercises deep recursion with live GC references at each frame level. +/// +internal static class Program +{ + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedCall(int depth) + { + object o = new object(); + if (depth > 0) + NestedCall(depth - 1); + GC.KeepAlive(o); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedWithMultipleRefs(int depth) + { + object a = new object(); + string b = $"depth-{depth}"; + int[] c = new int[depth + 1]; + if (depth > 0) + NestedWithMultipleRefs(depth - 1); + GC.KeepAlive(a); + GC.KeepAlive(b); + GC.KeepAlive(c); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + NestedCall(10); + NestedWithMultipleRefs(8); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/Directory.Build.props b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Directory.Build.props new file mode 100644 index 00000000000000..eca2240b31f08c --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Directory.Build.props @@ -0,0 +1,15 @@ + + + + + Exe + $(NetCoreAppToolCurrent) + true + enable + $(ArtifactsBinDir)GCStressTests\$(MSBuildProjectName)\$(Configuration)\ + true + + false + $(NoWarn);SA1400;IDE0059;SYSLIB1054;CA1852;CA1861 + + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/ExceptionHandling/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/ExceptionHandling/Program.cs new file mode 100644 index 00000000000000..4bd0a12fe6d145 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/ExceptionHandling/Program.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; + +/// +/// Exercises exception handling: try/catch/finally funclets, nested exceptions, +/// filter funclets, and rethrow. +/// +internal static class Program +{ + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryCatchScenario() + { + object before = new object(); + try + { + object inside = new object(); + ThrowHelper(); + GC.KeepAlive(inside); + } + catch (InvalidOperationException ex) + { + object inCatch = new object(); + GC.KeepAlive(ex); + GC.KeepAlive(inCatch); + } + GC.KeepAlive(before); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowHelper() + { + throw new InvalidOperationException("test exception"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryFinallyScenario() + { + object outerRef = new object(); + try + { + object innerRef = new object(); + GC.KeepAlive(innerRef); + } + finally + { + object finallyRef = new object(); + GC.KeepAlive(finallyRef); + } + GC.KeepAlive(outerRef); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedExceptionScenario() + { + object a = new object(); + try + { + try + { + object c = new object(); + throw new ArgumentException("inner"); + } + catch (ArgumentException ex1) + { + GC.KeepAlive(ex1); + throw new InvalidOperationException("outer", ex1); + } + finally + { + object d = new object(); + GC.KeepAlive(d); + } + } + catch (InvalidOperationException ex2) + { + GC.KeepAlive(ex2); + } + GC.KeepAlive(a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void FilterExceptionScenario() + { + object holder = new object(); + try + { + throw new ArgumentException("filter-test"); + } + catch (ArgumentException ex) when (FilterCheck(ex)) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(holder); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static bool FilterCheck(Exception ex) + { + object filterLocal = new object(); + GC.KeepAlive(filterLocal); + return ex.Message.Contains("filter"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void RethrowScenario() + { + object outerRef = new object(); + try + { + try + { + throw new ApplicationException("rethrow-test"); + } + catch (ApplicationException) + { + object catchRef = new object(); + GC.KeepAlive(catchRef); + throw; + } + } + catch (ApplicationException ex) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(outerRef); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + TryCatchScenario(); + TryFinallyScenario(); + NestedExceptionScenario(); + FilterExceptionScenario(); + RethrowScenario(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/Generics/Generics.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Generics/Generics.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Generics/Generics.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/Generics/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Generics/Program.cs new file mode 100644 index 00000000000000..54b7060c040f5a --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Generics/Program.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +/// +/// Exercises generic method instantiations and interface dispatch. +/// +internal static class Program +{ + interface IKeepAlive + { + object GetRef(); + } + + class BoxHolder : IKeepAlive + { + object _value; + public BoxHolder() { _value = new object(); } + public BoxHolder(object v) { _value = v; } + + [MethodImpl(MethodImplOptions.NoInlining)] + public object GetRef() => _value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static T GenericAlloc() where T : new() + { + T val = new T(); + object marker = new object(); + GC.KeepAlive(marker); + return val; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void GenericScenario() + { + var o = GenericAlloc(); + var l = GenericAlloc>(); + var s = GenericAlloc(); + GC.KeepAlive(o); + GC.KeepAlive(l); + GC.KeepAlive(s); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void InterfaceDispatchScenario() + { + IKeepAlive holder = new BoxHolder(new int[] { 42, 43 }); + object r = holder.GetRef(); + GC.KeepAlive(holder); + GC.KeepAlive(r); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void DelegateScenario() + { + object captured = new object(); + Func fn = () => + { + GC.KeepAlive(captured); + return new object(); + }; + object result = fn(); + GC.KeepAlive(result); + GC.KeepAlive(fn); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + GenericScenario(); + InterfaceDispatchScenario(); + DelegateScenario(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/MultiThread/MultiThread.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/MultiThread/MultiThread.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/MultiThread/MultiThread.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/MultiThread/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/MultiThread/Program.cs new file mode 100644 index 00000000000000..0eea731a6bd313 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/MultiThread/Program.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +/// +/// Exercises concurrent threads with GC references, exercising multi-threaded +/// stack walks and GC ref enumeration. +/// +internal static class Program +{ + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedCall(int depth) + { + object o = new object(); + if (depth > 0) + NestedCall(depth - 1); + GC.KeepAlive(o); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThreadWork(int id) + { + object threadLocal = new object(); + string threadName = $"thread-{id}"; + NestedCall(5); + GC.KeepAlive(threadLocal); + GC.KeepAlive(threadName); + } + + static int Main() + { + for (int iteration = 0; iteration < 2; iteration++) + { + ManualResetEventSlim ready = new ManualResetEventSlim(false); + ManualResetEventSlim go = new ManualResetEventSlim(false); + Thread t = new Thread(() => + { + ready.Set(); + go.Wait(); + ThreadWork(1); + }); + t.Start(); + ready.Wait(); + go.Set(); + ThreadWork(0); + t.Join(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/PInvoke/PInvoke.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/PInvoke/PInvoke.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/PInvoke/PInvoke.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/PInvoke/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/PInvoke/Program.cs new file mode 100644 index 00000000000000..83aece921baaea --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/PInvoke/Program.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +/// +/// Exercises P/Invoke transitions with GC references before and after native calls, +/// and pinned GC handles. +/// +internal static class Program +{ + [DllImport("kernel32.dll")] + static extern uint GetCurrentThreadId(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static void PInvokeScenario() + { + object before = new object(); + uint tid = GetCurrentThreadId(); + object after = new object(); + GC.KeepAlive(before); + GC.KeepAlive(after); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void PinnedScenario() + { + byte[] buffer = new byte[64]; + GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned); + try + { + object other = new object(); + GC.KeepAlive(other); + GC.KeepAlive(buffer); + } + finally + { + pin.Free(); + } + } + + struct LargeStruct + { + public object A, B, C, D; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void StructWithRefsScenario() + { + LargeStruct ls; + ls.A = new object(); + ls.B = "struct-string"; + ls.C = new int[] { 10, 20 }; + ls.D = new object(); + GC.KeepAlive(ls.A); + GC.KeepAlive(ls.B); + GC.KeepAlive(ls.C); + GC.KeepAlive(ls.D); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + PInvokeScenario(); + PinnedScenario(); + StructWithRefsScenario(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/GCStressResults.cs b/src/native/managed/cdac/tests/GCStressTests/GCStressResults.cs new file mode 100644 index 00000000000000..429bbd5b0b3bc6 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/GCStressResults.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; + +/// +/// Parses the cdac-gcstress results log file written by the native cdacgcstress.cpp hook. +/// +internal sealed partial class GCStressResults +{ + public int TotalVerifications { get; private set; } + public int Passed { get; private set; } + public int Failed { get; private set; } + public int Skipped { get; private set; } + public List FailureDetails { get; } = []; + public List SkipDetails { get; } = []; + + [GeneratedRegex(@"^\[PASS\]")] + private static partial Regex PassPattern(); + + [GeneratedRegex(@"^\[FAIL\]")] + private static partial Regex FailPattern(); + + [GeneratedRegex(@"^\[SKIP\]")] + private static partial Regex SkipPattern(); + + [GeneratedRegex(@"^Total verifications:\s*(\d+)")] + private static partial Regex TotalPattern(); + + public static GCStressResults Parse(string logFilePath) + { + if (!File.Exists(logFilePath)) + throw new FileNotFoundException($"GC stress results log not found: {logFilePath}"); + + var results = new GCStressResults(); + + foreach (string line in File.ReadLines(logFilePath)) + { + if (PassPattern().IsMatch(line)) + { + results.Passed++; + } + else if (FailPattern().IsMatch(line)) + { + results.Failed++; + results.FailureDetails.Add(line); + } + else if (SkipPattern().IsMatch(line)) + { + results.Skipped++; + results.SkipDetails.Add(line); + } + + Match totalMatch = TotalPattern().Match(line); + if (totalMatch.Success) + { + results.TotalVerifications = int.Parse(totalMatch.Groups[1].Value); + } + } + + if (results.TotalVerifications == 0) + { + results.TotalVerifications = results.Passed + results.Failed + results.Skipped; + } + + return results; + } + + public override string ToString() => + $"Total={TotalVerifications}, Passed={Passed}, Failed={Failed}, Skipped={Skipped}"; +} diff --git a/src/native/managed/cdac/tests/GCStressTests/GCStressTestBase.cs b/src/native/managed/cdac/tests/GCStressTests/GCStressTestBase.cs new file mode 100644 index 00000000000000..75c253ce1eafa0 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/GCStressTestBase.cs @@ -0,0 +1,209 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; + +/// +/// Base class for cDAC GC stress tests. Runs a debuggee app under corerun +/// with DOTNET_GCStress=0x24 and parses the verification results. +/// +public abstract class GCStressTestBase +{ + private readonly ITestOutputHelper _output; + + protected GCStressTestBase(ITestOutputHelper output) + { + _output = output; + } + + /// + /// Runs the named debuggee under GC stress and returns the parsed results. + /// + internal GCStressResults RunGCStress(string debuggeeName, int timeoutSeconds = 300) + { + string coreRoot = GetCoreRoot(); + string corerun = GetCoreRunPath(coreRoot); + string debuggeeDll = GetDebuggeePath(debuggeeName); + string logFile = Path.Combine(Path.GetTempPath(), $"cdac-gcstress-{debuggeeName}-{Guid.NewGuid():N}.txt"); + + _output.WriteLine($"Running GC stress: {debuggeeName}"); + _output.WriteLine($" corerun: {corerun}"); + _output.WriteLine($" debuggee: {debuggeeDll}"); + _output.WriteLine($" log: {logFile}"); + + var psi = new ProcessStartInfo + { + FileName = corerun, + Arguments = debuggeeDll, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + psi.Environment["CORE_ROOT"] = coreRoot; + psi.Environment["DOTNET_CdacStress"] = "0x11"; + psi.Environment["DOTNET_CdacStressFailFast"] = "0"; + psi.Environment["DOTNET_CdacStressLogFile"] = logFile; + psi.Environment["DOTNET_CdacStressStep"] = "1"; + psi.Environment["DOTNET_ContinueOnAssert"] = "1"; + + using var process = Process.Start(psi)!; + + // Read stderr asynchronously to avoid deadlock when both pipe buffers fill. + string stderr = ""; + process.ErrorDataReceived += (_, e) => + { + if (e.Data is not null) + stderr += e.Data + Environment.NewLine; + }; + process.BeginErrorReadLine(); + + string stdout = process.StandardOutput.ReadToEnd(); + + bool exited = process.WaitForExit(timeoutSeconds * 1000); + if (!exited) + { + process.Kill(entireProcessTree: true); + Assert.Fail($"GC stress test '{debuggeeName}' timed out after {timeoutSeconds}s"); + } + + _output.WriteLine($" exit code: {process.ExitCode}"); + if (!string.IsNullOrWhiteSpace(stdout)) + _output.WriteLine($" stdout: {stdout.TrimEnd()}"); + if (!string.IsNullOrWhiteSpace(stderr)) + _output.WriteLine($" stderr: {stderr.TrimEnd()}"); + + Assert.True(process.ExitCode == 100, + $"GC stress test '{debuggeeName}' exited with {process.ExitCode} (expected 100).\nstdout: {stdout}\nstderr: {stderr}"); + + Assert.True(File.Exists(logFile), + $"GC stress results log not created: {logFile}"); + + GCStressResults results = GCStressResults.Parse(logFile); + + _output.WriteLine($" results: {results}"); + + return results; + } + + /// + /// Asserts that GC stress verification produced 100% pass rate with no failures or skips. + /// + internal static void AssertAllPassed(GCStressResults results, string debuggeeName) + { + Assert.True(results.TotalVerifications > 0, + $"GC stress test '{debuggeeName}' produced zero verifications — " + + "GCStress may not have triggered or cDAC may not be loaded."); + + if (results.Failed > 0) + { + string details = string.Join("\n", results.FailureDetails); + Assert.Fail( + $"GC stress test '{debuggeeName}' had {results.Failed} failure(s) " + + $"out of {results.TotalVerifications} verifications.\n" + + $"Log: {results.LogFilePath}\n{details}"); + } + + if (results.Skipped > 0) + { + string details = string.Join("\n", results.SkipDetails); + Assert.Fail( + $"GC stress test '{debuggeeName}' had {results.Skipped} skip(s) " + + $"out of {results.TotalVerifications} verifications.\n" + + $"Log: {results.LogFilePath}\n{details}"); + } + } + + /// + /// Asserts that GC stress verification produced a pass rate at or above the given threshold. + /// A small number of failures is expected due to unimplemented frame scanning for + /// dynamic method stubs (InvokeStub / PromoteCallerStack). + /// + internal static void AssertHighPassRate(GCStressResults results, string debuggeeName, double minPassRate) + { + Assert.True(results.TotalVerifications > 0, + $"GC stress test '{debuggeeName}' produced zero verifications — " + + "GCStress may not have triggered or cDAC may not be loaded."); + + double passRate = (double)results.Passed / results.TotalVerifications; + if (passRate < minPassRate) + { + string details = string.Join("\n", results.FailureDetails); + Assert.Fail( + $"GC stress test '{debuggeeName}' pass rate {passRate:P2} is below " + + $"{minPassRate:P1} threshold. {results.Failed} failure(s) out of " + + $"{results.TotalVerifications} verifications.\n{details}"); + } + } + + private static string GetCoreRoot() + { + // Check environment variable first + string? coreRoot = Environment.GetEnvironmentVariable("CORE_ROOT"); + if (!string.IsNullOrEmpty(coreRoot) && Directory.Exists(coreRoot)) + return coreRoot; + + // Default path based on repo layout + string repoRoot = FindRepoRoot(); + string rid = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "windows" : "linux"; + string arch = RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(); + coreRoot = Path.Combine(repoRoot, "artifacts", "tests", "coreclr", $"{rid}.{arch}.Checked", "Tests", "Core_Root"); + + if (!Directory.Exists(coreRoot)) + throw new DirectoryNotFoundException( + $"Core_Root not found at '{coreRoot}'. " + + "Set the CORE_ROOT environment variable or run 'src/tests/build.cmd Checked generatelayoutonly'."); + + return coreRoot; + } + + private static string GetCoreRunPath(string coreRoot) + { + string exe = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "corerun.exe" : "corerun"; + string path = Path.Combine(coreRoot, exe); + Assert.True(File.Exists(path), $"corerun not found at '{path}'"); + + return path; + } + + private static string GetDebuggeePath(string debuggeeName) + { + string repoRoot = FindRepoRoot(); + + // Debuggees are built to artifacts/bin/GCStressTests//Release// + string binDir = Path.Combine(repoRoot, "artifacts", "bin", "GCStressTests", debuggeeName); + + if (!Directory.Exists(binDir)) + throw new DirectoryNotFoundException( + $"Debuggee '{debuggeeName}' not found at '{binDir}'. Build the GCStressTests project first."); + + // Find the dll in any Release/ subdirectory + foreach (string dir in Directory.GetDirectories(binDir, "*", SearchOption.AllDirectories)) + { + string dll = Path.Combine(dir, $"{debuggeeName}.dll"); + if (File.Exists(dll)) + return dll; + } + + throw new FileNotFoundException($"Could not find {debuggeeName}.dll under '{binDir}'"); + } + + private static string FindRepoRoot() + { + string? dir = AppContext.BaseDirectory; + while (dir is not null) + { + if (File.Exists(Path.Combine(dir, "global.json"))) + return dir; + dir = Path.GetDirectoryName(dir); + } + + throw new InvalidOperationException("Could not find repo root (global.json)"); + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/GCStressTests.targets b/src/native/managed/cdac/tests/GCStressTests/GCStressTests.targets new file mode 100644 index 00000000000000..a06b8ea4263caf --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/GCStressTests.targets @@ -0,0 +1,25 @@ + + + + $(MSBuildThisFileDirectory)Debuggees\ + Release + + + + + + + + + + + + diff --git a/src/native/managed/cdac/tests/GCStressTests/Microsoft.Diagnostics.DataContractReader.GCStressTests.csproj b/src/native/managed/cdac/tests/GCStressTests/Microsoft.Diagnostics.DataContractReader.GCStressTests.csproj new file mode 100644 index 00000000000000..ce6b6c14efadab --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Microsoft.Diagnostics.DataContractReader.GCStressTests.csproj @@ -0,0 +1,20 @@ + + + true + $(NetCoreAppToolCurrent) + enable + true + + + + + + + + + + + + + + diff --git a/src/native/managed/cdac/tests/GCStressTests/README.md b/src/native/managed/cdac/tests/GCStressTests/README.md new file mode 100644 index 00000000000000..ad1ee681b3944d --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/README.md @@ -0,0 +1,83 @@ +# cDAC GC Stress Tests + +Integration tests that verify the cDAC's stack reference enumeration matches the runtime's +GC root scanning under GC stress conditions. + +## How It Works + +Each test runs a debuggee console app under `corerun` with `DOTNET_GCStress=0x24`, which enables: +- **0x4**: Instruction-level JIT stress (triggers GC at every safe point) +- **0x20**: cDAC verification (compares cDAC stack refs against runtime refs) + +`DOTNET_GCStressCdacStep` throttles verification to every Nth stress point. The default +is 1 (verify every point). Higher values reduce cDAC overhead while maintaining instruction-level +breakpoint coverage for code path diversity. + +The native `cdacgcstress.cpp` hook writes `[PASS]`/`[FAIL]`/`[SKIP]` lines to a log file. +The test framework parses this log and asserts a high pass rate (≥99.9% for most debuggees, +≥99% for ExceptionHandling which has known funclet gaps). + +## Prerequisites + +Build the runtime with the cDAC GC stress hook enabled: + +```powershell +# From repo root +.\build.cmd -subset clr.native+tools.cdac -c Debug -rc Checked -lc Release +.\.dotnet\dotnet.exe msbuild src\libraries\externals.csproj /t:Build /p:Configuration=Release /p:RuntimeConfiguration=Checked /p:TargetOS=windows /p:TargetArchitecture=x64 -v:minimal +.\src\tests\build.cmd Checked generatelayoutonly -SkipRestorePackages /p:LibrariesConfiguration=Release +``` + +## Running Tests + +```powershell +# Build and run all GC stress tests +.\.dotnet\dotnet.exe test src\native\managed\cdac\tests\GCStressTests + +# Run a specific debuggee +.\.dotnet\dotnet.exe test src\native\managed\cdac\tests\GCStressTests --filter "debuggeeName=BasicAlloc" + +# Set CORE_ROOT manually if needed +$env:CORE_ROOT = "path\to\Core_Root" +.\.dotnet\dotnet.exe test src\native\managed\cdac\tests\GCStressTests +``` + +## Adding a New Debuggee + +1. Create a folder under `Debuggees/` with a `.csproj` and `Program.cs` +2. The `.csproj` just needs: `` + (inherits OutputType=Exe and TFM from `Directory.Build.props`) +3. `Main()` must return `100` on success +4. Use `[MethodImpl(MethodImplOptions.NoInlining)]` on methods to prevent inlining +5. Use `GC.KeepAlive()` to ensure objects are live at GC stress points +6. Add the debuggee name to `BasicGCStressTests.Debuggees` + +## Debuggee Catalog + +| Debuggee | Scenarios | +|----------|-----------| +| **BasicAlloc** | Objects, strings, arrays, many live refs | +| **ExceptionHandling** | try/catch/finally funclets, nested exceptions, filter funclets, rethrow | +| **DeepStack** | Deep recursion with live refs at each frame | +| **Generics** | Generic method instantiations, interface dispatch, delegates | +| **PInvoke** | P/Invoke transitions, pinned GC handles, struct with object refs | +| **MultiThread** | Concurrent threads with synchronized GC stress | +| **Comprehensive** | All-in-one: every scenario in a single run | + +## Architecture + +``` +GCStressTestBase.RunGCStress(debuggeeName) + │ + ├── Locate core_root/corerun (CORE_ROOT env or default path) + ├── Locate debuggee DLL (artifacts/bin/GCStressTests//...) + ├── Start Process: corerun + │ Environment: + │ DOTNET_GCStress=0x24 + │ DOTNET_GCStressCdacStep=1 + │ DOTNET_GCStressCdacLogFile= + │ DOTNET_ContinueOnAssert=1 + ├── Wait for exit (timeout: 300s) + ├── Parse results log → GCStressResults + └── Assert: exit=100, pass rate ≥ 99.9% +``` diff --git a/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj b/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj index 5b33a365154275..e90fbd6832356a 100644 --- a/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj +++ b/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj @@ -10,6 +10,8 @@ + + diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs index 94693eddddc185..9bea0e290b2080 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs @@ -236,6 +236,7 @@ public static RangeSectionMapTestBuilder CreateRangeSection(MockTarget.Architect new(nameof(Data.RealCodeHeader.EHInfo), DataType.pointer), new(nameof(Data.RealCodeHeader.GCInfo), DataType.pointer), new(nameof(Data.RealCodeHeader.NumUnwindInfos), DataType.uint32), + new(nameof(Data.RealCodeHeader.EHInfo), DataType.pointer), new(nameof(Data.RealCodeHeader.UnwindInfos), DataType.pointer), ] }; diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.cs index 1b961bb9c0cd55..9976cca7750ac2 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.cs @@ -191,6 +191,8 @@ internal record TypeFields new(nameof(Data.ExceptionInfo.CSFEHClause), DataType.pointer), new(nameof(Data.ExceptionInfo.CSFEnclosingClause), DataType.pointer), new(nameof(Data.ExceptionInfo.CallerOfActualHandlerFrame), DataType.pointer), + new(nameof(Data.ExceptionInfo.ClauseForCatchHandlerStartPC), DataType.uint32), + new(nameof(Data.ExceptionInfo.ClauseForCatchHandlerEndPC), DataType.uint32), ] }; @@ -211,6 +213,8 @@ internal record TypeFields new(nameof(Data.Thread.LastThrownObject), DataType.pointer), new(nameof(Data.Thread.LinkNext), DataType.pointer), new(nameof(Data.Thread.ExceptionTracker), DataType.pointer), + new(nameof(Data.Thread.DebuggerFilterContext), DataType.pointer), + new(nameof(Data.Thread.ProfilerFilterContext), DataType.pointer), new(nameof(Data.Thread.ThreadLocalDataPtr), DataType.pointer), new(nameof(Data.Thread.UEWatsonBucketTrackerBuckets), DataType.pointer), new(nameof(Data.Thread.DebuggerFilterContext), 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..8f83a99176cba9 --- /dev/null +++ b/src/native/managed/cdac/tests/gcstress/known-issues.md @@ -0,0 +1,73 @@ +# cDAC Stack Reference Walking — Known Issues + +This document tracks known gaps between the cDAC's stack reference enumeration +and the runtime's GC stack scanning. + +## Current Test Results + +Using `DOTNET_CdacStress=0x11` (ALLOC+REFS): + +| Debuggee | Result | +|----------|--------| +| BasicAlloc | 0-2 failures | +| Comprehensive | 0 failures | +| DeepStack | 0 failures | +| ExceptionHandling | 4-12 failures | +| Generics | 0 failures | +| MultiThread | 0 failures | +| PInvoke | 0 failures | + +## Issue 1: Intermittent content mismatches (snapshot timing) + +**Affected debuggees**: BasicAlloc (intermittent, 0-2 per run) + +**Pattern**: `cDAC=33 DAC=33 RT=33` — same count but different content. +The cDAC reports register-held refs (`Address=0x0`) while the runtime +reports all refs with stack addresses (registers are spilled to the stack +before the stress hook fires). The two-phase `CompareRefSets` matching +tries exact `(Address, Object, Flags)` for stack refs then fuzzy +`(Object, Flags)` for register refs, but timing differences in object +values between the cDAC snapshot and the runtime scan cause the fuzzy +phase to fail. + +**Root cause**: The cDAC reads the thread's saved context and walks the +stack from that snapshot. The runtime's GC scan happens at a slightly +different execution point where all registers have been spilled to the +stack. This is inherent to comparing a diagnostic snapshot against a +live internal scan — the cDAC itself is correct (cDAC matches DAC). + +## Issue 2: cDAC cannot unwind through native frames (EH first-pass) + +**Affected debuggees**: ExceptionHandling (4-12 failures per run) + +**Severity**: Low — only affects live-process stress testing during active +exception first-pass dispatch. Does not affect dump analysis where the +thread is suspended with a consistent Frame chain. + +**Pattern**: `cDAC < RT` — the cDAC reports fewer refs than the runtime +(e.g., cDAC=7 RT=16). Occurs when `m_pFrame` is `FRAME_TOP` during EH +first-pass dispatch. + +**Root cause**: The cDAC's `AMD64Unwinder.Unwind` (and equivalents for +other architectures) can only unwind **managed** frames — it checks +`ExecutionManager.GetCodeBlockHandle(IP)` first and returns false if the +IP is not in a managed code range. This means it cannot unwind through +native runtime frames (allocation helpers, EH dispatch code, etc.). + +When the allocation stress point fires during exception first-pass dispatch: + +1. The thread's `m_pFrame` is `FRAME_TOP` (no explicit Frames in the + chain because they have been popped during EH dispatch) +2. The initial IP is in native code (allocation helper) +3. The cDAC cannot unwind past native frames → walk stops early +4. The runtime uses OS-level unwind (`RtlVirtualUnwind`) which handles + native frames, so it walks more of the stack + +**Possible fixes**: +1. **Ensure Frames are always available** — change the runtime to keep + an explicit Frame pushed during allocation points within EH dispatch. + The Frame chain is the only mechanism the cDAC has for transitioning + through native code to reach managed frames. +2. **Accept as known limitation** — these failures only occur during + live-process stress testing at a narrow window. In dumps, the + exception state is frozen and the Frame chain is consistent.