Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion docs/design/datacontracts/StackWalk.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ This contract depends on the following descriptors:
| `ExceptionInfo` | `PassNumber` | Exception handling pass (1 or 2) |
| `ExceptionInfo` | `ClauseForCatchHandlerStartPC` | Start PC offset of the catch handler clause, used for interruptible offset override |
| `ExceptionInfo` | `ClauseForCatchHandlerEndPC` | End PC offset of the catch handler clause, used for interruptible offset override |
| `Thread` | `GCFrame` | Pointer to the head of the thread's GCFrame (GCPROTECT) chain, scanned by `WalkStackReferences` (optional) |
| `GCFrame` | `Next` | Pointer to the next `GCFrame` toward the top of the chain (terminated by `GCFRAME_TOP`) |
| `GCFrame` | `ObjRefs` | Pointer to the array of protected object reference slots |
| `GCFrame` | `NumObjRefs` | Count of protected object reference slots starting at `ObjRefs` |
| `GCFrame` | `GCFlags` | `GC_CALL_*` promotion flags applied when reporting the protected slots |

Global variables used:
| Global Name | Type | Purpose |
Expand All @@ -137,6 +142,7 @@ Contracts used:
| `Thread` |
| `RuntimeTypeSystem` |
| `GCInfo` |
| `Exception` |


### Stackwalk Algorithm
Expand Down Expand Up @@ -555,7 +561,7 @@ The implementation uses the same stack walk algorithm as `CreateStackWalk`, but

### GC Stack Reference Scanning

`WalkStackReferences` scans the stack for GC references by walking through each frame and reporting live object references and interior pointers. The native equivalent is `DacStackReferenceWalker` which calls `GcStackCrawlCallBack` at each frame.
`WalkStackReferences` scans the stack for GC references by walking through each frame and reporting live object references and interior pointers, then reporting the thread's GCFrame (GCPROTECT) chain and in-flight exception (ExInfo) chain. This mirrors the GC's own root enumeration, `ScanStackRoots` (`src/coreclr/vm/gcenv.ee.cpp`); the legacy `DacStackReferenceWalker` reports only the per-frame subset and omits the GCFrame and ExInfo roots.

#### Stack Walk Integration

Expand Down Expand Up @@ -589,6 +595,15 @@ At each frame yielded by `Filter`, the walk determines whether to scan for GC re

See [GCRefMap Format and Resolution](#gcrefmap-format-and-resolution) for the GCRefMap scanning path and [Signature-Based Scanning](#signature-based-scanning) for the signature decoding path.

#### GCFrame and Exception Tracker Roots

After walking the thread's frames, `WalkStackReferences` reports two additional sets of roots that the GC keeps alive but that are not surfaced by per-frame GC info (matching native `gcenv.ee.cpp` `ScanStackRoots`):

- **GCFrame (GCPROTECT) chain**: starting from `Thread.GCFrame`, each `GCFrame` is walked via its `Next` pointer until the `GCFRAME_TOP` terminator (`~0`, sized to the pointer width). For each node, the `NumObjRefs` slots starting at `ObjRefs` are reported, applying the node's `GCFlags` (`GC_CALL_INTERIOR` / `GC_CALL_PINNED`) as the promotion flags. This mirrors native `GCFrame::GcScanRoots`.
- **Exception tracker (ExInfo) chain**: starting from `Thread.ExceptionTracker`, each in-flight exception object (the current one and any superseded/nested ones reached via `PreviousNestedInfo`) is reported through its thrown-object slot.

Both sets are reported as frame-sourced roots: `Source` is the GCFrame / ExInfo node address, and `StackPointer` is that same address (the node lives on the stack), so the roots carry a non-zero, stack-resident location consistent with the per-frame roots. Each helper is independently wrapped in a try/catch so a single unreadable node yields partial results rather than failing the whole walk.

### Signature-Based Scanning

When a transition frame's calling convention is not described by a precomputed GCRefMap (`PrestubMethodFrame`, `CallCountingHelperFrame`, and the fallback path for `StubDispatchFrame`/`ExternalMethodFrame`), the GC reference walk classifies caller-stack arguments by decoding the callee's method signature. This corresponds to native `TransitionFrame::PromoteCallerStack` (`src/coreclr/vm/frames.cpp`).
Expand Down
1 change: 1 addition & 0 deletions docs/design/datacontracts/Thread.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ The contract additionally depends on these data descriptors
| `Thread` | `DebuggerControlledThreadState` | Thread state flags controlled by the debugger |
| `Thread` | `PreemptiveGCDisabled` | Flag indicating if preemptive GC is disabled |
| `Thread` | `Frame` | Pointer to current frame |
| `Thread` | `GCFrame` | Pointer to the head of the thread's GCFrame (GCPROTECT) chain (optional; readers should expect `TargetPointer.Null` when the field is absent) |
| `Thread` | `CachedStackBase` | Pointer to the base of the stack |
Comment thread
leculver marked this conversation as resolved.
| `Thread` | `CachedStackLimit` | Pointer to the limit of the stack |
| `Thread` | `ExposedObject` | Handle to the managed `Thread` object exposed to the debugger |
Expand Down
22 changes: 18 additions & 4 deletions src/coreclr/vm/cdacstress.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -692,10 +692,24 @@ static bool CollectRuntimeStackRefs(Thread* pThread, PCONTEXT regs, StackRef* ou

pThread->StackWalkFrames(dacLikeCallback, &diagCtx, flagsStackWalk);

// NOTE: ScanStackRoots also scans the separate GCFrame linked list
// (Thread::GetGCFrame), but the DAC's GetStackReferences / DacStackReferenceWalker
// does NOT include those. We intentionally omit GCFrame scanning here so our
// runtime-side collection matches what the cDAC is expected to produce.
// ScanStackRoots also scans two root sets that are not part of the frame walk: the
// GCFrame (GCPROTECT) chain and the in-flight ExInfo chain. GetStackReferences reports
// both, so mirror them here to keep the runtime-side collection in parity. See
// ScanStackRoots in gcenv.ee.cpp.
GCFrame* pGCFrame = pThread->GetGCFrame();
while (pGCFrame != GCFRAME_TOP)
{
pGCFrame->GcScanRoots(gcctx.f, gcctx.sc);
pGCFrame = pGCFrame->PtrNextFrame();
}

PTR_ExInfo pExInfo = pThread->GetExceptionState()->GetCurrentExceptionTracker();
while (pExInfo != NULL)
{
PTR_PTR_Object pRef = dac_cast<PTR_PTR_Object>(&pExInfo->m_exception);
gcctx.f(pRef, gcctx.sc, 0);
pExInfo = pExInfo->GetPreviousExceptionTracker();
}

// Copy results out
*outCount = collectCtx.count;
Expand Down
9 changes: 9 additions & 0 deletions src/coreclr/vm/datadescriptor/datadescriptor.inc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ CDAC_TYPE_FIELD(Thread, T_UINT32, State, cdac_data<Thread>::State)
CDAC_TYPE_FIELD(Thread, T_UINT32, PreemptiveGCDisabled, cdac_data<Thread>::PreemptiveGCDisabled)
CDAC_TYPE_FIELD(Thread, T_POINTER, RuntimeThreadLocals, cdac_data<Thread>::RuntimeThreadLocals)
CDAC_TYPE_FIELD(Thread, T_POINTER, Frame, cdac_data<Thread>::Frame)
CDAC_TYPE_FIELD(Thread, T_POINTER, GCFrame, cdac_data<Thread>::GCFrame)
CDAC_TYPE_FIELD(Thread, T_POINTER, CachedStackBase, cdac_data<Thread>::CachedStackBase)
CDAC_TYPE_FIELD(Thread, T_POINTER, CachedStackLimit, cdac_data<Thread>::CachedStackLimit)
CDAC_TYPE_FIELD(Thread, T_POINTER, ExceptionTracker, cdac_data<Thread>::ExceptionTracker)
Expand Down Expand Up @@ -70,6 +71,14 @@ CDAC_TYPE_FIELD(ThreadStore, T_INT32, PendingCount, cdac_data<ThreadStore>::Pend
CDAC_TYPE_FIELD(ThreadStore, T_INT32, DeadCount, cdac_data<ThreadStore>::DeadCount)
CDAC_TYPE_END(ThreadStore)

CDAC_TYPE_BEGIN(GCFrame)
CDAC_TYPE_INDETERMINATE(GCFrame)
CDAC_TYPE_FIELD(GCFrame, T_POINTER, Next, cdac_data<GCFrame>::Next)
CDAC_TYPE_FIELD(GCFrame, T_POINTER, ObjRefs, cdac_data<GCFrame>::ObjRefs)
CDAC_TYPE_FIELD(GCFrame, T_UINT32, NumObjRefs, cdac_data<GCFrame>::NumObjRefs)
CDAC_TYPE_FIELD(GCFrame, T_UINT32, GCFlags, cdac_data<GCFrame>::GCFlags)
CDAC_TYPE_END(GCFrame)

CDAC_TYPE_BEGIN(RuntimeThreadLocals)
CDAC_TYPE_INDETERMINATE(RuntimeThreadLocals)
CDAC_TYPE_FIELD(RuntimeThreadLocals, TYPE(EEAllocContext), AllocContext, offsetof(RuntimeThreadLocals, alloc_context))
Expand Down
11 changes: 11 additions & 0 deletions src/coreclr/vm/frames.h
Original file line number Diff line number Diff line change
Expand Up @@ -1764,6 +1764,17 @@ class GCFrame
#ifdef FEATURE_INTERPRETER
PTR_VOID m_osStackLocation;
#endif

friend struct ::cdac_data<GCFrame>;
};

template<>
struct cdac_data<GCFrame>
{
static constexpr size_t Next = offsetof(GCFrame, m_Next);
static constexpr size_t ObjRefs = offsetof(GCFrame, m_pObjRefs);
static constexpr size_t NumObjRefs = offsetof(GCFrame, m_numObjRefs);
static constexpr size_t GCFlags = offsetof(GCFrame, m_gcFlags);
};

//-----------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/vm/threads.h
Original file line number Diff line number Diff line change
Expand Up @@ -3766,6 +3766,7 @@ struct cdac_data<Thread>
static constexpr size_t PreemptiveGCDisabled = offsetof(Thread, m_fPreemptiveGCDisabled);
static constexpr size_t RuntimeThreadLocals = offsetof(Thread, m_pRuntimeThreadLocals);
static constexpr size_t Frame = offsetof(Thread, m_pFrame);
static constexpr size_t GCFrame = offsetof(Thread, m_pGCFrame);
static constexpr size_t CachedStackBase = offsetof(Thread, m_CacheStackBase);
static constexpr size_t CachedStackLimit = offsetof(Thread, m_CacheStackLimit);
static constexpr size_t ExposedObject = offsetof(Thread, m_ExposedObject);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,14 @@ IReadOnlyList<StackReferenceData> IStackWalk.WalkStackReferences(ThreadData thre
}
}

// Report the thread's GCFrame (GCPROTECT) chain: each GCFrame keeps a set of object
// references alive across a runtime operation, so report them as roots.
ReportGCFrameRoots(threadData, scanContext);

// Report the thread's exception-tracking (ExInfo) chain: the current in-flight exception
// and any superseded/nested ones are kept alive by the runtime, so report them as roots.
ReportExceptionTrackerRoots(threadData, scanContext);

return scanContext.StackRefs.Select(r => new StackReferenceData
{
HasRegisterInformation = r.HasRegisterInformation,
Expand All @@ -306,6 +314,80 @@ IReadOnlyList<StackReferenceData> IStackWalk.WalkStackReferences(ThreadData thre
}).ToList();
}

// Reports each in-flight exception object held on the thread's exception-tracking (ExInfo)
// chain: the current exception and any superseded/nested ones. The GC reports the same set in
// gcenv.ee.cpp ScanStackRoots.
private void ReportExceptionTrackerRoots(ThreadData threadData, GcScanContext scanContext)
{
try
{
Data.Thread thread = _target.ProcessedData.GetOrAdd<Data.Thread>(threadData.ThreadAddress);
TargetPointer pExInfo = _target.ReadPointer(thread.ExceptionTracker);
if (pExInfo == TargetPointer.Null)
return;

IException exceptionContract = _target.Contracts.Exception;
HashSet<TargetPointer> seen = new();
while (pExInfo != TargetPointer.Null && seen.Add(pExInfo))
{
// GetNestedExceptionInfo yields the address of the thrown-object slot (ExInfo::m_exception)
// and the previous (nested) ExInfo; GCReportCallback reads the object through that slot.
// The ExInfo lives on the stack, so report its address as the StackPointer: native
// (DacStackReferenceWalker) reports frame-sourced roots with a non-zero SP, and consumers
// (SOSDacImpl.GetStackReferences) forward it.
exceptionContract.GetNestedExceptionInfo(pExInfo, out TargetPointer previous, out TargetPointer thrownObjectSlot);
scanContext.UpdateScanContext(pExInfo, TargetPointer.Null, pExInfo);
scanContext.GCReportCallback(thrownObjectSlot, GcScanFlags.None);
pExInfo = previous;
}
}
catch (System.Exception ex)
{
Debug.WriteLine($"Exception during {nameof(ReportExceptionTrackerRoots)}: {ex.GetType().Name}: {ex.Message}");
}
}

// Reports each object reference protected by the thread's GCFrame (GCPROTECT) chain.
// GCFrame::GcScanRoots reports m_pObjRefs[0..m_numObjRefs), using an interior promotion when
// m_gcFlags != 0; the GC reports the same set in gcenv.ee.cpp ScanStackRoots.
private void ReportGCFrameRoots(ThreadData threadData, GcScanContext scanContext)
{
try
{
Data.Thread thread = _target.ProcessedData.GetOrAdd<Data.Thread>(threadData.ThreadAddress);

// The GCFrame field is optional in the data contract; nothing to scan when the target
// does not describe it.
if (thread.GCFrame is not TargetPointer head)
return;

// The chain is terminated by GCFRAME_TOP (FRAME_TOP_VALUE == ~0), sized to the pointer width.
TargetPointer terminator = TargetPointer.PlatformMaxValue(_target);
ulong pointerSize = (ulong)_target.PointerSize;
HashSet<TargetPointer> seen = new();
TargetPointer pGCFrame = head;
while (pGCFrame != TargetPointer.Null && pGCFrame != terminator && seen.Add(pGCFrame))
{
Data.GCFrame gcFrame = _target.ProcessedData.GetOrAdd<Data.GCFrame>(pGCFrame);
// The GCFrame lives on the stack, so report its address as the StackPointer: native
// (DacStackReferenceWalker) reports frame-sourced roots with a non-zero SP, and consumers
// (SOSDacImpl.GetStackReferences) forward it.
scanContext.UpdateScanContext(pGCFrame, TargetPointer.Null, pGCFrame);
GcScanFlags flags = (GcScanFlags)gcFrame.GCFlags;
for (uint i = 0; i < gcFrame.NumObjRefs; i++)
{
TargetPointer slot = new(gcFrame.ObjRefs.Value + (ulong)i * pointerSize);
scanContext.GCReportCallback(slot, flags);
}
pGCFrame = gcFrame.Next;
}
}
catch (System.Exception ex)
{
Debug.WriteLine($"Exception during {nameof(ReportGCFrameRoots)}: {ex.GetType().Name}: {ex.Message}");
}
}

private record GCFrameData
{
public GCFrameData(StackDataFrameHandle frame)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.Diagnostics.DataContractReader.Data;

[CdacType(nameof(DataType.GCFrame))]
internal sealed partial class GCFrame : IData<GCFrame>
{
[Field] public TargetPointer Next { get; }
[Field] public TargetPointer ObjRefs { get; }
[Field] public uint NumObjRefs { get; }
[Field] public uint GCFlags { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ internal sealed partial class Thread : IData<Thread>
[Field(Writable = true)] public uint DebuggerControlledThreadState { get; private set; }
[Field] public uint PreemptiveGCDisabled { get; }
[Field] public TargetPointer Frame { get; }
// Optional: only targets that describe the GCFrame chain carry this field.
[Field] public TargetPointer? GCFrame { get; }
[Field] public TargetPointer CachedStackBase { get; }
[Field] public TargetPointer CachedStackLimit { get; }
[Field] public ObjectHandle ExposedObject { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ public enum DataType
HijackArgs,

Frame,
GCFrame,
InlinedCallFrame,
SoftwareExceptionFrame,
FramedMethodFrame,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<DumpTypes>Full</DumpTypes>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Reflection;
using System.Runtime.CompilerServices;

/// <summary>
/// Debuggee for cDAC dump tests — exercises GCFrame (GCPROTECT) root reporting.
/// Triggers AppDomain.AssemblyResolve by loading a missing assembly. The native
/// AppDomain::RaiseAssemblyResolveEvent invokes the managed handler while holding a
/// GCPROTECT frame over the requesting Assembly reference. The handler FailFasts so the
/// dump captures the thread with that GCFrame still live. The GC reports the GCFrame's protected
/// objects via GCFrame::GcScanRoots; the test verifies WalkStackReferences reports them too.
/// </summary>
internal static class Program
{
private static void Main()
{
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
TriggerResolve();
Environment.FailFast("cDAC dump test: GCProtect debuggee did not hit the resolve handler");
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static void TriggerResolve()
{
try
{
Assembly.Load("cDAC_Missing_Assembly_GCFrameMarker, Version=9.9.9.9, Culture=neutral, PublicKeyToken=null");
}
catch
{
// Resolution ultimately fails; the crash happens inside the handler below first.
}
}

private static Assembly? OnAssemblyResolve(object? sender, ResolveEventArgs args)
{
// Runs inside AppDomain::RaiseAssemblyResolveEvent's GCPROTECT(gc) scope, so the
// requesting Assembly reference is held only by that native GCFrame at this point.
Environment.FailFast("cDAC dump test: GCProtect debuggee intentional crash");
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<DumpTypes>Full</DumpTypes>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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.IO;
using System.Runtime.CompilerServices;

/// <summary>
/// Debuggee for cDAC dump tests — exercises in-flight exception root reporting.
/// Builds a nested exception chain: a superseded <see cref="FileNotFoundException"/>
/// (kept alive on the thread's ExInfo chain as the previous nested tracker) plus the
/// current <see cref="InvalidOperationException"/>, then crashes via FailFast while both
/// are still in flight. The GC reports these via gcenv.ee.cpp ScanStackRoots; the test verifies
/// WalkStackReferences reports them as stack references.
/// </summary>
internal static class Program
{
private static void Main()
{
try
{
ThrowNested();
}
catch (Exception current)
{
// 'current' is the InvalidOperationException; the superseded
// FileNotFoundException is still held on the thread's ExInfo chain.
GC.KeepAlive(current);
Environment.FailFast("cDAC dump test: NestedException debuggee intentional crash");
}
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowNested()
{
try
{
throw new FileNotFoundException("cDAC-NestedException-inner");
}
catch (FileNotFoundException inner)
{
// Throwing while handling 'inner' supersedes its ExInfo (kept as the
// previous nested tracker) and starts a new tracker for the outer exception.
throw new InvalidOperationException("cDAC-NestedException-outer", inner);
}
}
}
Loading