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
46 changes: 46 additions & 0 deletions docs/design/datacontracts/Debugger.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ void RequestSyncAtEvent();
void SetSendExceptionsOutsideOfJMC(bool sendExceptionsOutsideOfJMC);
TargetPointer GetDebuggerControlBlockAddress();
void EnableGCNotificationEvents(bool fEnable);
bool IsRuntimeUnwindableStub(TargetPointer controlPC, out bool isUnhandledException);
```

## Version 1
Expand All @@ -30,6 +31,7 @@ The contract depends on the following globals
| `CLRJitAttachState` | TargetPointer | Pointer to the CLR JIT attach state flags |
| `CORDebuggerControlFlags` | TargetPointer | Pointer to `g_CORDebuggerControlFlags` |
| `MetadataUpdatesApplied` | TargetPointer | Pointer to the g_metadataUpdatesApplied flag |
| `MaxHijackFunctions` | uint32 | Number of entries in the hijack function array. Zero on platforms/configurations where the hijack-function table is not present. |
Comment thread
rcj1 marked this conversation as resolved.

The contract additionally depends on these data descriptors

Expand All @@ -42,7 +44,15 @@ The contract additionally depends on these data descriptors
| `Debugger` | `RSRequestedSync` | Sync-at-event request flag |
| `Debugger` | `SendExceptionsOutsideOfJMC` | Exception delivery policy flag |
| `Debugger` | `GCNotificationEventsEnabled` | Whether GC notification events are enabled |
| `Debugger` | `RgHijackFunction` | Pointer to the runtime's array of hijack-stub address ranges. |
| `DebuggerRCThread` | `DCB` | Pointer to `DebuggerIPCControlBlock` |
| `MemoryRange` | `StartAddress` | Inclusive start address of the range |
| `MemoryRange` | `Size` | Size of the range in bytes; the range covers `[StartAddress, StartAddress + Size)` |

### Contract Constants:
| Name | Type | Purpose | Value |
| --- | --- | --- | --- |
| `UnhandledExceptionHijackIndex` | uint | Index of unhandled exception hijack memory range. | `0` |

```csharp

Expand Down Expand Up @@ -134,4 +144,40 @@ void EnableGCNotificationEvents(bool fEnable)
debuggerAddress + /* Debugger::GCNotificationEventsEnabled offset */,
fEnable ? 1 : 0);
}

bool IsRuntimeUnwindableStub(TargetPointer controlPC, out bool isUnhandledException)
{
isUnhandledException = false;

if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress))
return false;

TargetPointer rgHijack = target.ReadPointer(
debuggerAddress + /* Debugger::RgHijackFunction offset */);
if (rgHijack == TargetPointer.Null)
return false;

uint maxHijackFunctions = target.ReadGlobal<uint>("MaxHijackFunctions");
if (maxHijackFunctions == 0)
return false;

uint stride = // Size of one MemoryRange entry

for (uint i = 0; i < maxHijackFunctions; i++)
{
TargetPointer entryAddress = rgHijack + (ulong)(i * stride);
TargetPointer start = target.ReadPointer(
entryAddress + /* MemoryRange::StartAddress offset */);
TargetNUInt size = target.Read<TargetNUInt>(
entryAddress + /* MemoryRange::Size offset */);

ulong end = start.Value + size.Value;
if (controlPC.Value >= start.Value && controlPC.Value < end)
{
isUnhandledException = (i == UnhandledExceptionHijackIndex);
return true;
}
}
return false;
}
```
57 changes: 52 additions & 5 deletions docs/design/datacontracts/StackWalk.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,24 @@ public enum StackWalkState
// Creates a stack walk and returns a handle
IEnumerable<IStackDataFrameHandle> CreateStackWalk(ThreadData threadData);

// Creates a stack walk and returns a handle, using a caller-provided seed CONTEXT.
// `contextBuffer` must be at least `IPlatformAgnosticContext.Size` bytes.
// `isFirst` indicates whether the seed frame should be treated as the active leaf.
// `skipFrames` controls whether explicit Frames below the seed SP are yielded as
// `SkippedFrame` entries before the containing managed frame.
IEnumerable<IStackDataFrameHandle> CreateStackWalk(
ThreadData threadData,
byte[] contextBuffer,
bool isFirst = true,
bool skipFrames = false);

// Gets the thread context at the given stack dataframe.
byte[] GetRawContext(IStackDataFrameHandle stackDataFrameHandle);

// Recovers the pre-hijack thread CONTEXT saved by a runtime redirection stub when
// the walker is currently stopped inside one of those stubs.
byte[] RetrieveHijackedContext(IStackDataFrameHandle stackDataFrameHandle, bool isUnhandledException);

// Gets the Frame address at the given stack dataframe. Returns TargetPointer.Null if the current dataframe does not have a valid Frame.
TargetPointer GetFrameAddress(IStackDataFrameHandle stackDataFrameHandle);

Expand Down Expand Up @@ -69,8 +85,10 @@ DebuggerEvalData GetDebuggerEvalData(TargetPointer funcEvalFrameAddress);
IReadOnlyList<StackReferenceData> WalkStackReferences(ThreadData threadData);

// Returns a context for the thread, trying (in order): the debugger filter context,
// the OS thread context, or a context derived from the explicit Frame chain.
byte[] GetContext(ThreadData threadData, ThreadContextSource contextSource, uint contextFlags);
// the OS thread context, or a context derived from the explicit Frame chain. The caller
// owns the buffer and must ensure it is at least sizeof(CONTEXT) bytes and aligned to
// IPlatformAgnosticContext.ContextAlignment (16).
int GetContext(ThreadData threadData, ThreadContextSource contextSource, uint contextFlags, Span<byte> contextBuffer);

// Returns the saved TargetContext pointer carried by the head Frame, if applicable.
TargetPointer GetRedirectedContextPointer(ThreadData threadData);
Expand Down Expand Up @@ -160,6 +178,7 @@ Constants used:
| --- | --- | --- | --- |
| `ExceptionFlags` (`exstatecommon.h`) | `Ex_UnwindHasStarted` | `0x00000004` | Bit flag in `ExceptionInfo.ExceptionFlags` indicating exception unwinding (2nd pass) has started. Used by `IsInStackRegionUnwoundBySpecifiedException` to skip ExInfo trackers still in the 1st pass. |
| `InlinedCallFrameMarker` (`exceptionhandling.h`) | `ExceptionHandlingHelper` | `2 (64-bit), 1(32-bit)` | Used to determine whether an active call on an InlinedCallFrame is an EH helper. |
| `Debugger::s_hijackFunction` (`debugger.h`) | `UnhandledExceptionHijackIndex` | `0` | Index of the unhandled-exception hijack entry in the runtime's hijack-function table. Used by `RetrieveHijackedContext` to choose between the SP-based and FP/SP-offset-based CONTEXT recovery paths, and by `IDebugger.IsRuntimeUnwindableStub` to set its `isUnhandledException` out parameter. |

Contracts used:
| Contract Name |
Expand Down Expand Up @@ -584,14 +603,42 @@ IReadOnlyList<StackReferenceData> WalkStackReferences(ThreadData threadData)

The implementation uses the same stack walk algorithm as `CreateStackWalk`, but integrates the GC-aware `Filter` directly (rather than consuming pre-generated frames) and performs GC reference enumeration at each frame. See [GC Stack Reference Scanning](#gc-stack-reference-scanning) for details.

`GetContext` returns a thread context by trying three sources in order: (1) the debugger filter context from `ThreadData.DebuggerFilterContext` (when `ThreadContextSource.Debugger` is requested), (2) the OS thread context via `TryGetThreadContext`, or (3) a context derived from walking the explicit Frame chain (`Thread::Frame` linked list), returning the first frame that yields a usable context:
* If the current Frame is an `InterpreterFrame`, clear the context and update it from the Frame. Return the resulting bytes.
`GetContext` fills the caller-provided buffer with a thread context by trying three sources in order: (1) the debugger filter context from `ThreadData.DebuggerFilterContext` (when `ThreadContextSource.Debugger` is requested), (2) the OS thread context via `TryGetThreadContext`, or (3) a context derived from walking the explicit Frame chain (`Thread::Frame` linked list), returning the first frame that yields a usable context:
* If the current Frame is an `InterpreterFrame`, clear the context and update it from the Frame. Write the resulting bytes to the destination.
* Otherwise, clear the context and update it from the current Frame; accept the context when both the stack pointer and instruction pointer are non-zero (e.g. `RedirectedThreadFrame`, `InlinedCallFrame`, `DynamicHelperFrame`). Mark `RawContextFlags = FullContextFlags` so callers know SP/PC/FP are valid.

If no Frame in the chain produces a usable context (thread is not running managed code), a zeroed context of the target architecture's size is returned.
If no Frame in the chain produces a usable context (thread is not running managed code), the destination is left zero-initialized (it is cleared by `GetContext` before any source writes into it).

The caller owns the buffer and must ensure it is at least `IPlatformAgnosticContext.Size` bytes and aligned to `IPlatformAgnosticContext.ContextAlignment`.

`GetRedirectedContextPointer` returns the saved `TargetContext` pointer carried by the head Frame when that Frame is a `RedirectedThreadFrame` (a `ResumableFrame`). Otherwise it returns `TargetPointer.Null`.

#### CreateStackWalk with a caller-provided CONTEXT

`CreateStackWalk(ThreadData, byte[], bool isFirst, bool skipFrames)` seeds the walker from `contextBuffer` rather than from the thread's saved CONTEXT, and exposes two extra knobs:

* `isFirst` (default `true`) is propagated to the first yielded `StackWalkData` as its `IsFirst` value.
* `skipFrames` (default `false`) controls whether the initial `CheckForSkippedFrames` step runs.
When the seed sits at a managed-to-unmanaged (M2U) boundary, the overload runs a pre-loop that mirrors native `StackFrameIterator::ResetRegDisp` (`stackwalk.cpp:1261-1308`):

1. Compute the caller SP by cloning the seed context and unwinding the clone.
2. Iterate the explicit Frame chain; stop at the first Frame `>= callerSP` (on non-x86) or after the additional ReturnAddress/FP cross-check (on x86, where an external OS unwind can produce a slightly wrong SP — see `stackwalk.cpp:1240-1241`).
3. For every Frame whose `GetCurrentReturnAddress() == seedIP`, rewrite the seed context via `UpdateContextFromCurrentFrame` and record the matched Frame type.
4. After the loop, if a match was found, override the first yielded frame's `IsFirst` (true for `ResumableFrame`/`RedirectedThreadFrame`, and for `HijackFrame` on non-x86) and `IsInterrupted` (true for `FaultingExceptionFrame`/`SoftwareExceptionFrame`).

The frame iterator is left positioned at the first Frame `>= callerSP`, so the rest of the walk picks up from the same logical position as the native iterator.

#### RetrieveHijackedContext

`RetrieveHijackedContext(handle, isUnhandledException)` returned byte buffer is the pre-hijack thread CONTEXT that the stub stashed on the stack at entry, and is used to re-seed the walker.

The location of the saved `PT_CONTEXT*` differs by stub kind and by architecture:

* `isUnhandledException == true` (the `ExceptionHijack` stub): the pointer is at `*SP` — the stub pushes it directly. The implementation reads `*context.StackPointer`.
* `isUnhandledException == false` (the `RedirectedHandledJITCaseFor*` stubs): the pointer is at a fixed offset from SP or FP, matching the `REDIRECTSTUB_{SP,EBP,RBP}_OFFSET_CONTEXT` constants in `src/coreclr/vm/{arch}/asmconstants.h`:

The implementation reads the pointer from that slot via `Target.ReadPointer`, then materializes a fresh `IPlatformAgnosticContext` from the target memory at the resulting address and returns its raw bytes.

### 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.
Expand Down
4 changes: 2 additions & 2 deletions src/coreclr/debug/di/rsstackwalk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -427,8 +427,8 @@ BOOL CordbStackWalk::UnwindStackFrame()
IfFailThrow(pDAC->UnwindStackWalkFrame(m_pSFIHandle, &retVal));

// Now that we have unwound, make sure we update the CONTEXT buffer to reflect the current stack frame.
// This call is safe regardless of whether the unwind is successful or not.
IfFailThrow(pDAC->GetStackWalkCurrentContext(m_pSFIHandle, &m_context));
if (retVal)
IfFailThrow(pDAC->GetStackWalkCurrentContext(m_pSFIHandle, &m_context));

return retVal;
} // CordbStackWalk::UnwindStackWalkFrame
Expand Down
4 changes: 3 additions & 1 deletion src/coreclr/debug/ee/debugger.h
Original file line number Diff line number Diff line change
Expand Up @@ -2874,7 +2874,7 @@ class Debugger : public DebugInterface
// represents different thead redirection functions recognized by the debugger
enum HijackFunction
{
kUnhandledException = 0,
kUnhandledException = 0, // [cDAC] [Debugger]: Contract depends on this value.
kRedirectedForGCThreadControl,
kRedirectedForDbgThreadControl,
kRedirectedForUserSuspend,
Expand Down Expand Up @@ -3866,6 +3866,8 @@ struct cdac_data<Debugger>
static constexpr size_t RSRequestedSync = offsetof(Debugger, m_RSRequestedSync);
static constexpr size_t SendExceptionsOutsideOfJMC = offsetof(Debugger, m_sendExceptionsOutsideOfJMC);
static constexpr size_t GCNotificationEventsEnabled = offsetof(Debugger, m_isGarbageCollectionEventsEnabled);
static constexpr size_t RgHijackFunction = offsetof(Debugger, m_rgHijackFunction);
static constexpr size_t MaxHijackFunctions = Debugger::kMaxHijackFunctions;
};

template<>
Expand Down
9 changes: 9 additions & 0 deletions src/coreclr/inc/memoryrange.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

#include "daccess.h"
#include "contract.h"
#include "cdacdata.h"

// MemoryRange is a descriptor of a memory range. This groups (pointer + size).
//
Expand Down Expand Up @@ -88,6 +89,14 @@ class MemoryRange
// This is s SIZE_T so that it can describe any memory range in the process (for example, larger than 4gb on 64-bit machines)
const SIZE_T m_cbBytes;

friend struct ::cdac_data<MemoryRange>;
};

template<>
struct cdac_data<MemoryRange>
{
static constexpr size_t StartAddress = offsetof(MemoryRange, m_pStartAddress);
static constexpr size_t Size = offsetof(MemoryRange, m_cbBytes);
};

typedef ArrayDPTR(MemoryRange) ARRAY_PTR_MemoryRange;
Expand Down
8 changes: 8 additions & 0 deletions src/coreclr/vm/datadescriptor/datadescriptor.inc
Original file line number Diff line number Diff line change
Expand Up @@ -446,12 +446,19 @@ CDAC_TYPE_FIELD(Debugger, T_POINTER, RCThread, cdac_data<Debugger>::RCThread)
CDAC_TYPE_FIELD(Debugger, T_INT32, RSRequestedSync, cdac_data<Debugger>::RSRequestedSync)
CDAC_TYPE_FIELD(Debugger, T_INT32, SendExceptionsOutsideOfJMC, cdac_data<Debugger>::SendExceptionsOutsideOfJMC)
CDAC_TYPE_FIELD(Debugger, T_INT32, GCNotificationEventsEnabled, cdac_data<Debugger>::GCNotificationEventsEnabled)
CDAC_TYPE_FIELD(Debugger, T_POINTER, RgHijackFunction, cdac_data<Debugger>::RgHijackFunction)
CDAC_TYPE_END(Debugger)

CDAC_TYPE_BEGIN(DebuggerRCThread)
CDAC_TYPE_INDETERMINATE(DebuggerRCThread)
CDAC_TYPE_FIELD(DebuggerRCThread, T_POINTER, DCB, cdac_data<DebuggerRCThread>::DCB)
CDAC_TYPE_END(DebuggerRCThread)

CDAC_TYPE_BEGIN(MemoryRange)
CDAC_TYPE_SIZE(sizeof(MemoryRange))
CDAC_TYPE_FIELD(MemoryRange, T_POINTER, StartAddress, cdac_data<MemoryRange>::StartAddress)
CDAC_TYPE_FIELD(MemoryRange, T_NUINT, Size, cdac_data<MemoryRange>::Size)
CDAC_TYPE_END(MemoryRange)
#endif // DEBUGGING_SUPPORTED && !TARGET_WASM

CDAC_TYPE_BEGIN(ArrayListBase)
Expand Down Expand Up @@ -1517,6 +1524,7 @@ CDAC_GLOBAL_POINTER(EEConfig, &::g_pConfig)
CDAC_GLOBAL_POINTER(Debugger, &::g_pDebugger)
CDAC_GLOBAL_POINTER(CLRJitAttachState, &::CLRJitAttachState)
CDAC_GLOBAL_POINTER(CORDebuggerControlFlags, &::g_CORDebuggerControlFlags)
CDAC_GLOBAL(MaxHijackFunctions, T_UINT32, cdac_data<Debugger>::MaxHijackFunctions)
#endif // DEBUGGING_SUPPORTED && !TARGET_WASM
#ifdef FEATURE_METADATA_UPDATER
CDAC_GLOBAL_POINTER(MetadataUpdatesApplied, &::g_metadataUpdatesApplied)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public interface IDebugger : IContract
void SetSendExceptionsOutsideOfJMC(bool sendExceptionsOutsideOfJMC) => throw new NotImplementedException();
TargetPointer GetDebuggerControlBlockAddress() => throw new NotImplementedException();
void EnableGCNotificationEvents(bool fEnable) => throw new NotImplementedException();
bool IsRuntimeUnwindableStub(TargetPointer controlPC, out bool isUnhandledException) => throw new NotImplementedException();
}

public readonly struct Debugger : IDebugger
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ public record struct DebuggerEvalData(
public interface IStackWalk : IContract
{
static string IContract.Name => nameof(StackWalk);

public virtual IEnumerable<IStackDataFrameHandle> CreateStackWalk(ThreadData threadData) => throw new NotImplementedException();
public virtual IEnumerable<IStackDataFrameHandle> CreateStackWalk(ThreadData threadData, byte[] contextBuffer, bool isFirst = true, bool skipFrames = false) => throw new NotImplementedException();
Comment thread
rcj1 marked this conversation as resolved.
IReadOnlyList<StackReferenceData> WalkStackReferences(ThreadData threadData) => throw new NotImplementedException();
byte[] GetRawContext(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException();
TargetPointer GetFrameAddress(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException();
Comment thread
rcj1 marked this conversation as resolved.
Expand All @@ -84,7 +84,8 @@ public interface IStackWalk : IContract
bool IsExceptionHandlingHelperInlinedCallFrame(TargetPointer frameAddress) => throw new NotImplementedException();
DebuggerEvalData GetDebuggerEvalData(TargetPointer funcEvalFrameAddress) => throw new NotImplementedException();
TargetPointer GetRedirectedContextPointer(ThreadData threadData) => throw new NotImplementedException();
byte[] GetContext(ThreadData threadData, ThreadContextSource contextSource, uint contextFlags) => throw new NotImplementedException();
int GetContext(ThreadData threadData, ThreadContextSource contextSource, uint contextFlags, Span<byte> contextBuffer) => throw new NotImplementedException();
byte[] RetrieveHijackedContext(IStackDataFrameHandle stackDataFrameHandle, bool isUnhandledException) => throw new NotImplementedException();
Comment thread
rcj1 marked this conversation as resolved.
}
Comment thread
rcj1 marked this conversation as resolved.

public struct StackWalk : IStackWalk
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ public abstract class Target
/// <param name="threadId">The identifier of the thread whose context is to be retrieved. The identifier is defined by the operating system.</param>
/// <param name="contextFlags">A bitwise combination of platform-dependent flags that indicate which portions of the context should be read.</param>
/// <param name="buffer">Buffer to be filled with thread context.</param>
/// <returns>true if successful, false otherwise</returns>
public abstract bool TryGetThreadContext(ulong threadId, uint contextFlags, Span<byte> buffer);
/// <returns>HRESULT indicating success or failure</returns>
public abstract int TryGetThreadContext(ulong threadId, uint contextFlags, Span<byte> buffer);

/// <summary>
/// Reads a well-known global pointer value from the target process
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public static class Globals
public const string FinalizerThread = nameof(FinalizerThread);
public const string GCThread = nameof(GCThread);
public const string Debugger = nameof(Debugger);
public const string MaxHijackFunctions = nameof(MaxHijackFunctions);
public const string CLRJitAttachState = nameof(CLRJitAttachState);
public const string CORDebuggerControlFlags = nameof(CORDebuggerControlFlags);
public const string MetadataUpdatesApplied = nameof(MetadataUpdatesApplied);
Expand Down
Loading
Loading