Skip to content
2 changes: 2 additions & 0 deletions docs/design/datacontracts/Debugger.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

This contract is for reading debugger state from the target process, including initialization status, metadata update state, and JIT attach state.

On WebAssembly the in-process debugger is not built, so `g_pDebugger` is always null and `CLRJitAttachState` is always 0. The contract is still advertised there, and Version 1 reports what it reports before a debugger initializes: no debugger data, no hijacks, and no JIT attach state.

## APIs of contract

```csharp
Expand Down
31 changes: 31 additions & 0 deletions docs/design/datacontracts/PrecodeStubs.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,34 @@ computes the entry point of the precode.
return new TargetPointer(entryPointAddress);
}
```

## Version 2

<!-- BEGIN GENERATED: usage contract=PrecodeStubs version=c2 -->
### Data descriptors used

| Data Descriptor | Field | Type | Meaning |
| --- | --- | --- | --- |
| `PortableEntryPoint` | `MethodDesc` | `pointer` | Method desc of portable entrypoint (only defined if `FeaturePortableEntrypoints` is enabled) |

### Global variables used

_None._

### Contracts used

_None._
<!-- END GENERATED: usage contract=PrecodeStubs version=c2 -->

Version 2 is advertised by runtimes built with `FEATURE_PORTABLE_ENTRYPOINTS` (for example WebAssembly). Those runtimes have no executable precode stubs and do not describe `PrecodeMachineDescriptor`: every entry point is a `PortableEntryPoint` that records its owning `MethodDesc`.

```csharp
// Mirrors the FEATURE_PORTABLE_ENTRYPOINTS path of MethodDesc::GetMethodDescFromPrecode.
TargetPointer IPrecodeStubs.GetMethodDescFromStubAddress(TargetCodePointer entryPoint)
{
Data.PortableEntryPoint portableEntryPoint = // read PortableEntryPoint at entryPoint
return portableEntryPoint.MethodDesc;
}
```

There are no interpreter precodes, so `GetInterpreterCodeFromInterpreterPrecodeIfPresent` returns the entry point unchanged. `GetPrecodeEntryPointFromInteriorAddress` is not supported.
19 changes: 17 additions & 2 deletions docs/design/datacontracts/StackWalk.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ Unwinding call frames on the stack usually requires an OS specific implementatio
| `FramedMethodFrame` | `TransitionBlockPtr` | `pointer` | Pointer to Frame's TransitionBlock |
| `FuncEvalFrame` | `DebuggerEvalPtr` | `pointer` | Pointer to the Frame's DebuggerEval object |
| `FuncEvalFrame` | `ReturnAddress` | `CodePointer` | Return address of the frame |
| `FunctionTableIndexRangeSection` | `MinFunctionTableIndex` | `uint32` | First runtime-global shared function-table index owned by the R2R module |
| `FunctionTableIndexRangeSection` | `Next` | `pointer` | Pointer to the next registered WASM R2R function-table range |
| `FunctionTableIndexRangeSection` | `NumRuntimeFunctions` | `uint32` | Number of consecutive RUNTIME_FUNCTION entries owned by the R2R module |
| `FunctionTableIndexRangeSection` | `R2RModule` | `pointer` | Pointer to the Module that owns this function-table range |
| `GCFrame` | `GCFlags` | `uint32` | GC_CALL_* promotion flags applied when reporting the protected slots |
| `GCFrame` | `Next` | `pointer` | Pointer to the next GCFrame toward the top of the chain |
| `GCFrame` | `NumObjRefs` | `uint32` | Count of protected object reference slots starting at ObjRefs |
Expand All @@ -185,8 +189,13 @@ Unwinding call frames on the stack usually requires an OS specific implementatio
| `PInvokeCalliFrame` | `VASigCookiePtr` | `pointer` | Pointer to the varargs signature cookie for the unmanaged call |
| `ReadyToRunInfo` | `ImportSections` | `pointer` | Pointer to the array of ReadyToRun import sections |
| `ReadyToRunInfo` | `LoadedImageBase` | `pointer` | Base address of the loaded R2R image |
| `ReadyToRunInfo` | `MinVirtualIP` | `pointer` | Base virtual IP assigned to the ReadyToRun module on WebAssembly |
| `ReadyToRunInfo` | `NumImportSections` | `uint32` | Number of ReadyToRun import sections |
| `ReadyToRunInfo` | `NumRuntimeFunctions` | `uint32` | Number of `RuntimeFunctions` |
| `ReadyToRunInfo` | `RuntimeFunctions` | `pointer` | Pointer to an array of `RuntimeFunctions` - [see R2R format](../coreclr/botr/readytorun-format.md#readytorunsectiontyperuntimefunctions) |
| `ResumableFrame` | `TargetContextPtr` | `pointer` | Pointer to the Frame's Target Context |
| `RuntimeFunction` | *(type size)* | `uint32` | Size of a runtime function entry in bytes |
| `RuntimeFunction` | `BeginAddress` | `uint32` | Begin address of the function. On ARM32, bit 0 is the Thumb bit; on WebAssembly, bit 31 marks a funclet and is excluded from address arithmetic. |
| `SoftwareExceptionFrame` | `ReturnAddress` | `CodePointer` | Return address saved in Frame |
| `SoftwareExceptionFrame` | `TargetContext` | `pointer` | Context object saved in Frame |
| `String` | `m_StringLength` | `uint32` | Length of the string in UTF-16 characters |
Expand All @@ -211,6 +220,7 @@ Unwinding call frames on the stack usually requires an OS specific implementatio
| --- | --- | --- |
| `<FrameType>Identifier` *(name pattern)* | `pointer` | Per-frame-type sentinel address used to identify and classify runtime frames |
| `Architecture` | `string` | Target architecture |
| `FunctionTableIndexRangeList` | `pointer` | Pointer to the head pointer of the registered WASM R2R function-table range list |
| `ObjectToMethodTableUnmask` | `uint8` | Bits to clear when converting an object header value to a method table address |

### Contracts used
Expand Down Expand Up @@ -280,7 +290,9 @@ InterpreterFrame

This produces three frames in order: C, B, A (innermost to outermost).

When the stack walk starts with an explicit context in interpreted code (e.g., from a debugger breakpoint), the interpreted frames are already yielded from the initial context as frameless frames. When the walker subsequently encounters the corresponding `InterpreterFrame`, it skips expanding it to prevent the same frames from being walked twice.
When the stack walk starts with a context in interpreted code (e.g., from a debugger breakpoint, or a context seeded from an interpreted P/Invoke's `InlinedCallFrame`), the interpreted frames are already yielded from the initial context as frameless frames. Like native `StackFrameIterator::Init`, the walker reads the owning `InterpreterFrame` from the context's first-argument register and sets the Frame iterator to that Frame's `Next`, so the same frames are not walked twice. If the first-argument register is null or does not name an `InterpreterFrame`, the walk fails (native asserts both).

An interpreted P/Invoke pushes an active `InlinedCallFrame` whose `CallSiteSP` is the top `InterpMethodContextFrame` of the `InterpreterFrame` that immediately follows it (native `InlinedCallFrame::IsInInterpreter`). When the walker reaches such a Frame, it moves to that `InterpreterFrame` without updating the context; the `InterpreterFrame` then switches into the interpreted chain.


#### Simple Example
Expand Down Expand Up @@ -439,6 +451,9 @@ Most of the handlers are implemented in `BaseFrameHandler`. Platform specific co
InlinedCallFrames store and update only the IP, SP, and FP of a given context. If the stored IP (CallerReturnAddress) is 0 then the InlinedCallFrame does not have an active call and should not update the context.

* On ARM, the InlinedCallFrame stores the value of the SP after the prolog (`SPAfterProlog`) to allow unwinding for functions with stackalloc. When a function uses stackalloc, the CallSiteSP can already have been adjusted. This value should be placed in R9.
* On WASM, a `CallerReturnAddress` of `INLINED_PINVOKE_FROM_R2R` (`1`) marks an active inlined P/Invoke from ReadyToRun code rather than an address. SP is taken from `CallSiteSP`, IP is the R2R virtual IP of the shadow frame at `CallSiteSP`, and FP is that shadow frame's base. If no virtual IP can be recovered, IP is set to null.

An active InlinedCallFrame stays the current Frame after its context update so the skipped-Frame check can step past it once the walk reaches the managed caller. If the updated IP is not managed code (for example, no WASM R2R virtual IP could be recovered), the walk fails, matching native `StackFrameIterator::NextRaw`; otherwise it would never advance past the Frame.

**Return Address**: `CallerReturnAddress`, but only when the frame has an active call (i.e., `CallerReturnAddress != 0`). Returns null otherwise.

Expand Down Expand Up @@ -721,7 +736,7 @@ The runtime installs a small set of redirect/hijack stubs whose code blocks are

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.None` — the IP is not inside any tracked stub; `Next()` does nothing special. WASM has no hijack stubs; its `Debugger` contract reports `HijackKind.None` for every IP.
* `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.

Expand Down
16 changes: 10 additions & 6 deletions src/coreclr/vm/datadescriptor/datadescriptor.inc
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ CDAC_TYPE_FIELD(SystemDomain, TYPE(LoaderAllocator), GlobalLoaderAllocator, cdac
CDAC_TYPE_FIELD(SystemDomain, T_POINTER, SystemAssembly, cdac_data<SystemDomain>::SystemAssembly)
CDAC_TYPE_END(SystemDomain)

#if defined(DEBUGGING_SUPPORTED) && !defined(TARGET_WASM)
#ifdef DEBUGGING_SUPPORTED
CDAC_TYPE_BEGIN(Debugger)
CDAC_TYPE_INDETERMINATE(Debugger)
CDAC_TYPE_FIELD(Debugger, T_INT32, LeftSideInitialized, offsetof(Debugger, m_fLeftSideInitialized))
Expand Down Expand Up @@ -532,7 +532,7 @@ 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
#endif // DEBUGGING_SUPPORTED

CDAC_TYPE_BEGIN(ArrayListBase)
CDAC_TYPE_INDETERMINATE(ArrayListBase)
Expand Down Expand Up @@ -1733,15 +1733,15 @@ CDAC_GLOBAL_POINTER(EEConfig, &::g_pConfig)
#ifndef FEATURE_PORTABLE_ENTRYPOINTS
CDAC_GLOBAL_POINTER(ThePreStub, &g_cdacThePreStub)
#endif // !FEATURE_PORTABLE_ENTRYPOINTS
#if defined(DEBUGGING_SUPPORTED) && !defined(TARGET_WASM)
#ifdef DEBUGGING_SUPPORTED
CDAC_GLOBAL_POINTER(Debugger, &::g_pDebugger)
#if defined(TARGET_AMD64)
CDAC_GLOBAL_POINTER(DebuggerPatchTable, cdac_data<DebuggerController>::PatchTable)
#endif // TARGET_AMD64
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
#endif // DEBUGGING_SUPPORTED
#ifdef FEATURE_METADATA_UPDATER
CDAC_GLOBAL_POINTER(MetadataUpdatesApplied, &::g_metadataUpdatesApplied)
#endif
Expand Down Expand Up @@ -1901,9 +1901,9 @@ CDAC_GLOBAL_CONTRACT(ComWrappers, c1)
#endif // FEATURE_COMWRAPPERS
CDAC_GLOBAL_CONTRACT(ConditionalWeakTable, c1)
CDAC_GLOBAL_CONTRACT(DacStreams, c1)
#if defined(DEBUGGING_SUPPORTED) && !defined(TARGET_WASM)
#ifdef DEBUGGING_SUPPORTED
CDAC_GLOBAL_CONTRACT(Debugger, c1)
#endif // DEBUGGING_SUPPORTED && !TARGET_WASM
#endif // DEBUGGING_SUPPORTED
CDAC_GLOBAL_CONTRACT(DebugInfo, c1)
CDAC_GLOBAL_CONTRACT(EcmaMetadata, c1)
#ifdef FEATURE_METADATA_UPDATER
Expand All @@ -1922,7 +1922,11 @@ CDAC_GLOBAL_CONTRACT(ObjectiveCMarshal, c1)
CDAC_GLOBAL_CONTRACT(Object, c1)
CDAC_GLOBAL_CONTRACT(FeatureFlags, c1)
CDAC_GLOBAL_CONTRACT(PlatformMetadata, c1)
#ifdef FEATURE_PORTABLE_ENTRYPOINTS
CDAC_GLOBAL_CONTRACT(PrecodeStubs, c2)
#else
CDAC_GLOBAL_CONTRACT(PrecodeStubs, c1)
#endif // FEATURE_PORTABLE_ENTRYPOINTS
#ifdef PROFILING_SUPPORTED
CDAC_GLOBAL_CONTRACT(ReJIT, c1)
#endif // PROFILING_SUPPORTED
Expand Down
8 changes: 6 additions & 2 deletions src/coreclr/vm/wasm/helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -679,8 +679,12 @@ void _DacGlobals::Initialize()
/* no-op on wasm */
}

// Incorrectly typed temporary symbol to satisfy the linker.
int g_pDebugger;
// The in-process debugger (src/coreclr/debug/ee) is not built for wasm. These definitions back the
// declarations in debug/ee/debugger.h so the cDAC Debugger contract can be advertised: g_pDebugger
// stays null (no debugger, so no hijacks) and CLRJitAttachState stays 0 (no JIT attach).
class Debugger;
Debugger* g_pDebugger = nullptr;
ULONG CLRJitAttachState = 0;

void InvokeCalliStub(PCODE ftn, InterpreterCalliCookie cookie, int8_t *pArgs, int8_t *pRet, Object** pContinuationRet)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// 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;

// Runtimes built with FEATURE_PORTABLE_ENTRYPOINTS (e.g. WebAssembly) have no executable precode
// stubs: every entry point is a PortableEntryPoint that records its owning MethodDesc. There are no
// interpreter precodes, so GetInterpreterCodeFromInterpreterPrecodeIfPresent keeps the interface
// default (the entry point unchanged), and GetPrecodeEntryPointFromInteriorAddress is not supported.
internal sealed class PrecodeStubs_2 : IPrecodeStubs
{
private readonly Target _target;

public PrecodeStubs_2(Target target)
{
_target = target;
}

// Mirrors the FEATURE_PORTABLE_ENTRYPOINTS path of MethodDesc::GetMethodDescFromPrecode.
TargetPointer IPrecodeStubs.GetMethodDescFromStubAddress(TargetCodePointer entryPoint)
=> _target.ProcessedData.GetOrAdd<Data.PortableEntryPoint>(entryPoint.AsTargetPointer).MethodDesc;
}
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,29 @@ private IPlatformFrameHandler GetFrameHandler(IPlatformAgnosticContext context)
};
}

/// <summary>
/// Mirrors native <c>InlinedCallFrame::IsInInterpreter</c> (frames.cpp): an active
/// InlinedCallFrame pushed by the interpreter for a P/Invoke is directly followed by the
/// owning InterpreterFrame, whose top InterpMethodContextFrame is the ICF's CallSiteSP.
/// </summary>
public bool IsInlinedCallFrameInInterpreter(Data.Frame frame)
{
if (GetFrameType(frame.Identifier) != FrameType.InlinedCallFrame)
return false;

ulong terminator = _target.PointerSize == 8 ? ulong.MaxValue : uint.MaxValue;
if (frame.Next == TargetPointer.Null || frame.Next.Value == terminator)
return false;

Data.Frame next = _target.ProcessedData.GetOrAdd<Data.Frame>(frame.Next);
if (GetFrameType(next.Identifier) != FrameType.InterpreterFrame)
return false;

Data.InlinedCallFrame icf = _target.ProcessedData.GetOrAdd<Data.InlinedCallFrame>(frame.Address);
Data.InterpreterFrame interpreterFrame = _target.ProcessedData.GetOrAdd<Data.InterpreterFrame>(next.Address);
return ResolveTopInterpMethodContextFrame(interpreterFrame) == icf.CallSiteSP;
}

private static bool InlinedCallFrameHasActiveCall(Data.InlinedCallFrame frame)
{
return frame.CallerReturnAddress != TargetCodePointer.Null;
Expand Down Expand Up @@ -543,7 +566,11 @@ private void ApplyInterpreterFrameTransition(IPlatformAgnosticContext context, T
GetFrameHandler(context).HandleTransitionFrame(framedMethodFrame);
}

private TargetPointer GetFirstArgRegister(IPlatformAgnosticContext context)
/// <summary>
/// Returns the first-argument register, which holds the owning InterpreterFrame for a context
/// in interpreted code (native <c>GetFirstArgReg</c>).
/// </summary>
public TargetPointer GetFirstArgRegister(IPlatformAgnosticContext context)
{
string registerName = GetFirstArgRegisterName();
if (!context.TryReadRegister(registerName, out TargetNUInt value))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ public bool Next()
return currentFramePointer != terminator;
}

/// <summary>
/// Moves the cursor to <paramref name="frameAddress"/> (native <c>m_crawl.pFrame = ...</c>).
/// </summary>
public void MoveTo(TargetPointer frameAddress)
=> currentFramePointer = frameAddress;

/// <summary>
/// Returns the <see cref="FrameType"/> of the current frame.
/// </summary>
Expand All @@ -57,6 +63,13 @@ public FrameType GetCurrentFrameType()
public TargetCodePointer GetCurrentReturnAddress()
=> frameHelpers.GetReturnAddress(CurrentFrame);

/// <summary>
/// Returns whether the current frame is an InlinedCallFrame for a P/Invoke made by the
/// interpreter (native <c>InlinedCallFrame::IsInInterpreter</c>).
/// </summary>
public bool IsCurrentInlinedCallFrameInInterpreter()
=> frameHelpers.IsInlinedCallFrameInInterpreter(CurrentFrame);

/// <summary>
/// Updates <paramref name="context"/> based on the current frame's type.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,40 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers;
/// pointer. The base <see cref="BaseFrameHandler.HandleInlinedCallFrame"/> already reads that
/// <c>InlinedCallFrame.CallSiteSP</c> (plus the caller return address and callee-saved frame
/// pointer) into the three synthetic <see cref="WasmContext"/> slots, which is the common
/// P/Invoke-boundary seeding path. The software/faulting exception frame handlers likewise read a
/// P/Invoke-boundary seeding path; an inlined P/Invoke from R2R code instead stores a marker and is
/// resolved from its R2R shadow frame. The software/faulting exception frame handlers likewise read a
/// serialized <see cref="WasmContext"/> blob from the frame's <c>TargetContext</c>.
///
/// Hijack frames are a debugger / GC-suspension concept that is not yet supported on WASM.
/// </remarks>
internal sealed class WasmFrameHandler(Target target, ContextHolder<WasmContext> contextHolder)
: BaseFrameHandler(target, contextHolder), IPlatformFrameHandler
{
// INLINED_PINVOKE_FROM_R2R from src/coreclr/vm/frames.h. An R2R inlined P/Invoke has no native
// return address on WASM, so the runtime stores this marker and derives IP/SP from CallSiteSP.
private const ulong InlinedPInvokeFromR2R = 1;

private readonly ContextHolder<WasmContext> _holder = contextHolder;

public override void HandleInlinedCallFrame(InlinedCallFrame inlinedCallFrame)
{
base.HandleInlinedCallFrame(inlinedCallFrame);
if (inlinedCallFrame.CallerReturnAddress.Value == InlinedPInvokeFromR2R)
{
// Mirrors InlinedCallFrame::UpdateRegDisplay_Impl in src/coreclr/vm/wasm/helpers.cpp.
// If no R2R virtual IP can be recovered the IP is left null (not managed code), and the
// stack walker fails the walk as native does, rather than treating the marker as an address.
Wasm.WasmUnwinder unwinder = new(_target, new Wasm.WasmR2RInfo(_target));
_holder.Context.StackPointer = inlinedCallFrame.CallSiteSP;
_holder.Context.InstructionPointer = unwinder.GetVirtualIP(inlinedCallFrame.CallSiteSP);
// Root-function frame base; the funclet-aware logical frame pointer is not modeled yet.
_holder.Context.FramePointer = unwinder.TryGetFramePointer(inlinedCallFrame.CallSiteSP, out TargetPointer framePointer)
? framePointer
: TargetPointer.Null;
}
else
{
base.HandleInlinedCallFrame(inlinedCallFrame);
}

// When the frame directly above this P/Invoke transition is an InterpreterFrame, stash its
// address in the synthetic first-argument register so the subsequent interpreter virtual
Expand Down
Loading
Loading