Skip to content
Merged
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
54 changes: 54 additions & 0 deletions docs/design/datacontracts/Debugger.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ This contract is for reading debugger state from the target process, including i
record struct DebuggerData(bool IsLeftSideInitialized, uint DefinesBitField, uint MDStructuresVersion);
```

```csharp
enum HijackKind
{
None,
UnhandledException,
Other
}
```

```csharp
bool TryGetDebuggerData(out DebuggerData data);
int GetAttachStateFlags();
Expand All @@ -18,6 +27,7 @@ void RequestSyncAtEvent();
void SetSendExceptionsOutsideOfJMC(bool sendExceptionsOutsideOfJMC);
TargetPointer GetDebuggerControlBlockAddress();
void EnableGCNotificationEvents(bool fEnable);
HijackKind GetHijackKind(TargetCodePointer controlPC);
```

## Version 1
Expand All @@ -30,6 +40,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. |

The contract additionally depends on these data descriptors

Expand All @@ -42,7 +53,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 +153,39 @@ void EnableGCNotificationEvents(bool fEnable)
debuggerAddress + /* Debugger::GCNotificationEventsEnabled offset */,
fEnable ? 1 : 0);
}

HijackKind GetHijackKind(TargetCodePointer controlPC)
{
if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress))
return HijackKind.None;

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

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

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)
{
return (i == UnhandledExceptionHijackIndex)
? HijackKind.UnhandledException
: HijackKind.Other;
}
}
return HijackKind.None;
}
```
58 changes: 56 additions & 2 deletions docs/design/datacontracts/StackWalk.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,27 @@ 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.
Comment thread
rcj1 marked this conversation as resolved.
IEnumerable<IStackDataFrameHandle> CreateStackWalk(
ThreadData threadData,
byte[] contextBuffer,
bool isFirst = true);

// Gets the thread context at the given stack dataframe.
byte[] GetRawContext(IStackDataFrameHandle stackDataFrameHandle);
// `flags` lets the caller request platform-specific shaping of the returned context.
byte[] GetRawContext(
IStackDataFrameHandle stackDataFrameHandle,
StackwalkFlag flags = StackwalkFlag.Default);

[Flags]
enum StackwalkFlag
{
Default = 0,
X86ESPIgnoresCalleePoppedArgs = 0x1,
}

// 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 @@ -164,6 +183,9 @@ 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. |
| N/A | `REDIRECTSTUB_ESTABLISHER_OFFSET_RBP` | 0 | AMD64 offset for redirect stubs. |
| N/A | `REDIRECTSTUB_SP_OFFSET_CONTEXT` | 0 | ARM, ARM64, Loongarch & RISCV64 offset for redirect stubs. |
| N/A | `REDIRECTSTUB_EBP_OFFSET_CONTEXT` | -4 | X86 offset for redirect stubs. |

Contracts used:
| Contract Name |
Expand Down Expand Up @@ -463,9 +485,14 @@ The rest of the APIs convey state about the stack walk at a given point which fa

This context is not guaranteed to be complete. Not all capital "F" Frames store the entire context, some only store the IP/SP/FP. Therefore, at points where the context is based on these Frames it will be incomplete.
```csharp
byte[] GetRawContext(IStackDataFrameHandle stackDataFrameHandle);
byte[] GetRawContext(
IStackDataFrameHandle stackDataFrameHandle,
StackwalkFlag flags = StackwalkFlag.Default);
```

##### `StackwalkFlag.X86ESPIgnoresCalleePoppedArgs`
See [comment](https://github.com/dotnet/runtime/blob/7f8276da27a20943339702df0abdfc02e21110a4/src/coreclr/debug/daccess/dacdbiimplstackwalk.cpp#L1016-L1063)


`GetFrameAddress` gets the address of the current capital "F" Frame. This is only valid if the `IStackDataFrameHandle` is at a point where the context is based on a capital "F" Frame. For example, it is not valid when when the current context was created by using the stack frame unwinder.
If the Frame is not valid, returns `TargetPointer.Null`.
Expand Down Expand Up @@ -597,6 +624,33 @@ If no Frame in the chain produces a usable context (thread is not running manage

`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)` seeds the walker from `contextBuffer` rather than from the thread's saved CONTEXT. `isFirst` (default `true`) is used to determine whether the walker starts with internal state `isFirst` set to true.

1. Compute the caller SP by cloning the seed context and unwinding the clone.
2. Iterate the explicit Frame chain; update context from the first Frame `>= callerSP` (on non-x86) or after the additional ReturnAddress/FP cross-check See [text](https://github.com/dotnet/runtime/blob/ad50b412069ee7f274c585d191df797ac5548525/src/coreclr/vm/stackwalk.cpp#L1238). Do not update if no Frame meets these criteria.
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 walker state `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`, if such a frame exists.

#### Hijack-stub recovery in `Next()`

The runtime installs a small set of redirect/hijack stubs whose code blocks are tracked in `Debugger::s_hijackFunction`. When the walker is stopped at one of these stubs (state is `InitialNativeContext` or `NativeMarker` and the IP falls inside one of the tracked ranges), the on-thread CONTEXT does not represent the real pre-hijack execution state — the real CONTEXT was stashed on the stack by the stub at entry. `Next()` recovers it before continuing.

The recovery step is driven by `IDebugger.GetHijackKind(controlPC)`, which returns a `HijackKind`:

* `HijackKind.None` — the IP is not inside any tracked stub; `Next()` does nothing special.
* `HijackKind.UnhandledException` — the IP is inside the `ExceptionHijack` stub. The saved `PT_CONTEXT*` is at `*SP` (the stub pushed it directly), so the implementation reads `*context.StackPointer`.
* `HijackKind.Other` — the IP is inside another redirect stub. The saved `PT_CONTEXT*` is at a fixed offset from SP or FP, matching the `REDIRECTSTUB_*` constants.

When a non-`None` `HijackKind` is returned, the walker:

1. Reads the saved CONTEXT pointer from the appropriate stack slot, then materializes a fresh `IPlatformAgnosticContext` from target memory at that address.
2. Reclassifies the walker state from the recovered IP.
3. Re-runs the CONTEXT/Frame-chain reconciliation step (see [CreateStackWalk with a caller-provided CONTEXT](#createstackwalk-with-a-caller-provided-context)) against the freshly created `FrameIterator`, then writes back the updated walker state.

### GC Stack Reference Scanning

`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`.
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 @@ -2856,7 +2856,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 @@ -3848,6 +3848,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 @@ -485,12 +485,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 @@ -1563,6 +1570,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 @@ -7,6 +7,13 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts;

public record struct DebuggerData(bool IsLeftSideInitialized, uint DefinesBitField, uint MDStructuresVersion);

public enum HijackKind
{
None,
UnhandledException,
Other,
}

public interface IDebugger : IContract
{
static string IContract.Name { get; } = nameof(Debugger);
Expand All @@ -20,6 +27,7 @@ public interface IDebugger : IContract
void SetSendExceptionsOutsideOfJMC(bool sendExceptionsOutsideOfJMC) => throw new NotImplementedException();
TargetPointer GetDebuggerControlBlockAddress() => throw new NotImplementedException();
void EnableGCNotificationEvents(bool fEnable) => throw new NotImplementedException();
HijackKind GetHijackKind(TargetCodePointer controlPC) => throw new NotImplementedException();
}

public readonly struct Debugger : IDebugger
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,20 @@ public record struct DebuggerEvalData(
uint MethodToken,
TargetPointer AssemblyPtr);

[Flags]
public enum StackwalkFlag
Comment thread
rcj1 marked this conversation as resolved.
{
Default = 0,
X86ESPIgnoresCalleePoppedArgs = 0x1,
}
Comment thread
rcj1 marked this conversation as resolved.

public interface IStackWalk : IContract
{
static string IContract.Name => nameof(StackWalk);

public virtual IEnumerable<IStackDataFrameHandle> CreateStackWalk(ThreadData threadData) => throw new NotImplementedException();
IEnumerable<IStackDataFrameHandle> CreateStackWalk(ThreadData threadData) => throw new NotImplementedException();
IEnumerable<IStackDataFrameHandle> CreateStackWalk(ThreadData threadData, byte[] contextBuffer, bool isFirst = true) => throw new NotImplementedException();
IReadOnlyList<StackReferenceData> WalkStackReferences(ThreadData threadData) => throw new NotImplementedException();
byte[] GetRawContext(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException();
byte[] GetRawContext(IStackDataFrameHandle stackDataFrameHandle, StackwalkFlag flags = StackwalkFlag.Default) => throw new NotImplementedException();
TargetPointer GetFrameAddress(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException();
Comment thread
rcj1 marked this conversation as resolved.
Comment thread
rcj1 marked this conversation as resolved.
string GetFrameName(TargetPointer frameIdentifier) => throw new NotImplementedException();
TargetPointer GetMethodDescPtr(TargetPointer framePtr) => throw new NotImplementedException();
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ private enum DebuggerControlFlag_1 : uint
PendingAttach = 0x0100,
Attached = 0x0200,
}
private const uint UnhandledExceptionHijackIndex = 0;

private readonly Target _target;

Expand Down Expand Up @@ -117,4 +118,35 @@ void IDebugger.EnableGCNotificationEvents(bool fEnable)
Data.Debugger debugger = _target.ProcessedData.GetOrAdd<Data.Debugger>(debuggerAddress);
debugger.WriteGCNotificationEventsEnabled(fEnable ? 1 : 0);
}

HijackKind IDebugger.GetHijackKind(TargetCodePointer controlPC)
{
if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress))
return HijackKind.None;

Data.Debugger debugger = _target.ProcessedData.GetOrAdd<Data.Debugger>(debuggerAddress);
if (debugger.RgHijackFunction == TargetPointer.Null)
return HijackKind.None;

uint maxHijackFunctions = _target.ReadGlobal<uint>(Constants.Globals.MaxHijackFunctions);
if (maxHijackFunctions == 0)
return HijackKind.None;

Target.TypeInfo memoryRangeTypeInfo = _target.GetTypeInfo(DataType.MemoryRange);
uint stride = memoryRangeTypeInfo.Size!.Value;

for (uint i = 0; i < maxHijackFunctions; i++)
{
TargetPointer entryAddress = debugger.RgHijackFunction + (ulong)(i * stride);
Data.MemoryRange entry = _target.ProcessedData.GetOrAdd<Data.MemoryRange>(entryAddress);

ulong start = entry.StartAddress.Value;
ulong end = start + entry.Size.Value;
if (controlPC.Value >= start && controlPC.Value < end)
Comment thread
rcj1 marked this conversation as resolved.
Comment thread
rcj1 marked this conversation as resolved.
{
return i == UnhandledExceptionHijackIndex ? HijackKind.UnhandledException : HijackKind.Other;
}
Comment thread
rcj1 marked this conversation as resolved.
}
Comment thread
rcj1 marked this conversation as resolved.
return HijackKind.None;
}
}
Loading
Loading