Skip to content
Closed
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
3 changes: 2 additions & 1 deletion docs/design/datacontracts/GCInfo.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ public readonly record struct GCInfoHeader(
uint GSCookieValidRangeEnd, // End (exclusive) of the GS cookie valid range
SpecialSlot? PSPSym, // PSP sym stack slot, or null if none
SpecialSlot? GenericsInstContext, // Generics instantiation context stack slot, or null if none
GenericsContextKind GenericsInstContextKind); // Kind of the generics instantiation context
GenericsContextKind GenericsInstContextKind, // Kind of the generics instantiation context
bool HasReversePInvokeFrame); // True if the method has a reverse P/Invoke frame (its caller is native code)

// Unified lifetime (live code range) of a GC slot, register or stack.
public readonly record struct GCSlotLifetime(
Expand Down
30 changes: 27 additions & 3 deletions docs/design/datacontracts/StackWalk.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ Most of the handlers are implemented in `BaseFrameHandler`. Platform specific co

#### InlinedCallFrame

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.
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 WASM an active InlinedCallFrame directly above an `InterpreterFrame` also records that `InterpreterFrame` in the context's first-argument register; an inactive one does not.

* 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 the WASM logical frame pointer at `CallSiteSP` (see below). If no virtual IP can be recovered, IP is set to null.
Expand All @@ -461,7 +461,7 @@ An active InlinedCallFrame stays the current Frame after its context update so t

#### SoftwareExceptionFrame

SoftwareExceptionFrames store a copy of the context struct. The IP, SP, and all ABI specified (platform specific) callee-saved registers are copied from the stored context to the working context.
SoftwareExceptionFrames store a copy of the context struct. The IP, SP, and all ABI specified (platform specific) callee-saved registers are copied from the stored context to the working context. On WASM the callee-saved register set is the frame pointer (`InterpreterFP`), so IP, SP, and FP are copied.

**Return Address**: Read from the `ReturnAddress` field on the frame.

Expand All @@ -476,7 +476,31 @@ When updating the context from a TransitionFrame, the IP, SP, and all ABI specif
* For an x86 `StubDispatchFrame` without a GCRefMap, resolve its method from the stored MethodDesc or its representative MethodTable and slot. If no method is available, the SP stays at the end of the transition block and the IP is adjusted to the call instruction by subtracting five bytes from the saved return address.
* On WASM, a transition helper called from R2R code records the caller's R2R linear-stack pointer in `TransitionBlock.StackPointer`, and may leave `ReturnAddress` 0. When `ReturnAddress` is 0 and `StackPointer` is set, the return address is the R2R virtual IP of the frame at `StackPointer` (native `FramedMethodFrame::GetTransitionBlock_Impl`). When `StackPointer` is set and a return address is known, the caller's SP is `StackPointer` and its FP is the WASM logical frame pointer at `StackPointer` (native `TransitionFrame::GetSP`); otherwise the SP is the end of the TransitionBlock and the FP is null. This also applies when the interpreted chain under an `InterpreterFrame` is exhausted and the walker applies the `InterpreterFrame`'s transition.

**WASM logical frame pointer.** R2R frames on WASM keep a record on the linear stack: the function-table index, then the function-local virtual IP / 2. Unwinding one frame (`WasmContext.Unwind`) adds the function's frame size from its unwind data to the frame base, and sets the caller's FP as native `GetWasmFramePointerFromStackPointer` does. For a method, the FP is its own frame base. For a funclet (its `RUNTIME_FUNCTION.BeginAddress` has bit 31 set), the FP is the establishing method's frame. The walker unwinds out of the funclet: if the caller slot holds the `TERMINATE_R2R_STACK_WALK` marker, the funclet was invoked by the VM through `CallFuncletWith[out]Throwable`, and the establishing frame pointer is stored one pointer after the marker. Otherwise the funclet was called by its parent method or funclet, and the walker repeats from there. Each step must move toward the caller, or the frame pointer is unknown (null).
**WASM logical frame pointer.** R2R frames on WASM keep a record on the linear stack: the function-table index, then the function-local virtual IP / 2. Unwinding one frame (`WasmUnwinder.Unwind`) adds the function's frame size from its unwind data to the frame base, and sets the caller's FP as native `GetWasmFramePointerFromStackPointer` does. For a method, the FP is its own frame base. For a funclet (its `RUNTIME_FUNCTION.BeginAddress` has bit 31 set), the FP is the establishing method's frame. The walker unwinds out of the funclet: if the caller slot holds the `TERMINATE_R2R_STACK_WALK` marker, the funclet was invoked by the VM through `CallFuncletWith[out]Throwable`, and the establishing frame pointer is stored one pointer after the marker. Otherwise the funclet was called by its parent method or funclet, and the walker repeats from there. Each step must move toward the caller, or the frame pointer is unknown (null).

Like native `RtlVirtualUnwind`, unwinding a WASM R2R frame always advances SP to the caller's stack pointer. The caller's IP and FP are null when the caller is not R2R code: when no R2R virtual IP can be read at the caller's SP, or when the frame being unwound has a reverse P/Invoke frame in its GC info (`GCInfoHeader.HasReversePInvokeFrame`) and is not a funclet. A reverse P/Invoke frame's caller is native code, so its stack bytes are never read as an R2R frame. The walker then continues through the explicit Frame chain.

```csharp
// WasmUnwinder.Unwind: one R2R frame, mirroring native RtlVirtualUnwind / WasmUnwindStackFrameCore.
void Unwind()
{
TargetPointer frameBase = /* frame base of the R2R frame at SP (frame record: function index, VIP / 2) */;
uint functionIndex = target.Read<uint>(frameBase + FunctionIndexOffset);
uint frameSize = /* ULEB128 at the start of the function's unwind data */;
if (/* no frame at SP, no unwind data, or frameSize == 0 */)
{
SP = IP = FP = null; // the walk cannot advance
return;
}

SP = frameBase + frameSize;
bool callerIsNative = IP != null
&& !IsFunclet(functionIndex)
&& GCInfo.GetHeader(/* GC info of the code block containing IP */).HasReversePInvokeFrame;
IP = callerIsNative ? null : /* R2R virtual IP at SP, or null if SP has no R2R frame record */;
FP = IP != null ? /* WASM logical frame pointer at SP */ : null;
}
```

**Return Address**: Read from `TransitionBlock.ReturnAddress`. On WASM, it is derived from `TransitionBlock.StackPointer` when it is 0, as above. For an x86 `StubDispatchFrame` with neither a method nor a GCRefMap, subtract five bytes to return the adjusted call address, matching the context update.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ public readonly record struct GCInfoHeader(
uint GSCookieValidRangeEnd,
SpecialSlot? PSPSym,
SpecialSlot? GenericsInstContext,
GenericsContextKind GenericsInstContextKind);
GenericsContextKind GenericsInstContextKind,
bool HasReversePInvokeFrame);

/// <summary>Unified lifetime of a GC slot (register or stack).</summary>
/// <param name="IsRegister">True if the slot is a CPU register; false if it is a stack location.</param>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,8 @@ public GCInfoHeader GetHeader()
GSCookieValidRangeEnd: gsCookie.HasValue ? _validRangeEnd : 0,
PSPSym: pspSym,
GenericsInstContext: genericsInstContext,
GenericsInstContextKind: genericsContextKind);
GenericsInstContextKind: genericsContextKind,
HasReversePInvokeFrame: _reversePInvokeFrameStackSlot != TTraits.NO_REVERSE_PINVOKE_FRAME);
}

public uint GetCodeLength()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1024,7 +1024,8 @@ GCInfoHeader IGCInfoDecoder.GetHeader()
GSCookieValidRangeEnd: gsCookie.HasValue ? MethodSize : 0,
PSPSym: null,
GenericsInstContext: null,
GenericsInstContextKind: genericsKind);
GenericsInstContextKind: genericsKind,
HasReversePInvokeFrame: Header.RevPInvokeOffset != InfoHdr.INVALID_REV_PINVOKE_OFFSET);
}

IReadOnlyList<uint> IGCInfoDecoder.GetSafePoints()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,17 @@ public TargetCodePointer GetVirtualIP(TargetPointer sp)

/// <summary>
/// Advances <paramref name="sp"/> by one R2R frame and produces the caller's virtual IP,
/// mirroring <c>WasmUnwindStackFrameCore</c>. Returns false when the R2R walk terminates
/// (no R2R frame at <paramref name="sp"/>), in which case <paramref name="sp"/> is set to
/// <see cref="TargetPointer.Null"/>.
/// mirroring <c>WasmUnwindStackFrameCore</c>. Returns false when the R2R walk cannot advance
/// (no R2R frame at <paramref name="sp"/>, missing unwind data, or a zero frame size), in
/// which case <paramref name="sp"/> is set to <see cref="TargetPointer.Null"/>. Otherwise
/// <paramref name="sp"/> is the caller's stack pointer, and <paramref name="ip"/> is null when
/// the caller is not ReadyToRun code.
/// </summary>
public bool TryUnwindOneFrame(ref TargetPointer sp, out TargetCodePointer ip)
/// <param name="controlPC">
/// The virtual IP of the frame at <paramref name="sp"/>, used to detect a reverse P/Invoke
/// frame as native <c>RtlVirtualUnwind</c> does. Pass null when it is unknown.
/// </param>
public bool TryUnwindOneFrame(ref TargetPointer sp, TargetCodePointer controlPC, out TargetCodePointer ip)
{
ip = TargetCodePointer.Null;
if (!TryGetFramePointer(sp, out TargetPointer frameBase))
Expand All @@ -169,16 +175,54 @@ public bool TryUnwindOneFrame(ref TargetPointer sp, out TargetCodePointer ip)
}

sp = new TargetPointer(frameBase.Value + frameSize);
ip = GetVirtualIP(sp);
if (ip == TargetCodePointer.Null)
// A reverse P/Invoke frame's caller is native code, so its stack bytes must not be read as
// an R2R frame. Funclets share their parent's GC info and are never entered from native code.
if (!IsReversePInvokeFrame(functionIndex, controlPC))
ip = GetVirtualIP(sp);

return true;
}

/// <summary>
/// Unwinds <paramref name="context"/> by one R2R frame, mirroring native <c>RtlVirtualUnwind</c>.
/// When the caller is not R2R code (an interpreter transition, native code above a reverse
/// P/Invoke, or the stack top), SP is the caller's SP, IP and FP are null, and the walker
/// continues through the explicit Frame chain. If no R2R frame can be unwound, SP, IP and FP
/// are all null.
/// </summary>
public void Unwind(ref WasmContext context)
{
TargetPointer sp = context.StackPointer;
if (!TryUnwindOneFrame(ref sp, context.InstructionPointer, out TargetCodePointer ip))
{
context.StackPointer = TargetPointer.Null;
context.InstructionPointer = TargetCodePointer.Null;
context.FramePointer = TargetPointer.Null;
return;
}

context.StackPointer = sp;
context.InstructionPointer = ip;
// Native recomputes the caller's FP only for an R2R caller; funclets report the
// establishing method's frame (GetWasmFramePointerFromStackPointer).
context.FramePointer = ip != TargetCodePointer.Null && TryGetLogicalFramePointer(sp, out TargetPointer fp)
? fp
: TargetPointer.Null;
}

private bool IsReversePInvokeFrame(uint functionIndex, TargetCodePointer controlPC)
{
if (controlPC == TargetCodePointer.Null ||
!_r2rInfo.TryIsFunclet(functionIndex, out bool isFunclet) ||
isFunclet ||
_target.Contracts.ExecutionManager.GetCodeBlockHandle(controlPC) is not CodeBlockHandle codeBlock)
{
// The caller is not R2R-generated code (an interpreter transition or the stack top);
// the R2R walk is exhausted.
sp = TargetPointer.Null;
return false;
}

return true;
_target.Contracts.ExecutionManager.GetGCInfo(codeBlock, out TargetPointer gcInfoAddress, out uint gcVersion);
IGCInfoHandle gcInfo = _target.Contracts.GCInfo.DecodePlatformSpecificGCInfo(gcInfoAddress, gcVersion);
return _target.Contracts.GCInfo.GetHeader(gcInfo).HasReversePInvokeFrame;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,25 +79,8 @@ public TargetPointer FramePointer

public void Unwind(Target target)
{
// Advance one ReadyToRun frame over the managed linear stack. When the R2R walk
// terminates (an interpreter transition or the stack top), StackPointer becomes null and
// the caller falls back to the explicit Frame chain / interpreter frame chain.
Wasm.WasmUnwinder unwinder = new(target, new Wasm.WasmR2RInfo(target));
TargetPointer sp = StackPointer;
if (unwinder.TryUnwindOneFrame(ref sp, out TargetCodePointer ip))
{
StackPointer = sp;
InstructionPointer = ip;
// Native WasmUnwindStackFrame recomputes the caller's frame pointer from its stack pointer;
// funclets report the establishing method's frame base (GetWasmFramePointerFromStackPointer).
FramePointer = unwinder.TryGetLogicalFramePointer(sp, out TargetPointer fp) ? fp : TargetPointer.Null;
}
else
{
StackPointer = TargetPointer.Null;
InstructionPointer = TargetCodePointer.Null;
FramePointer = TargetPointer.Null;
}
unwinder.Unwind(ref this);
}

// WASM has no hardware single-step flag; like other architectures without one (ARM, LoongArch64,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers;
/// pointer) into the three synthetic <see cref="WasmContext"/> slots, which is the common
/// 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>.
/// serialized <see cref="WasmContext"/> blob from the frame's <c>TargetContext</c>, restoring
/// IP, SP and the frame pointer.
///
/// Hijack frames are a debugger / GC-suspension concept that is not yet supported on WASM.
/// </remarks>
Expand All @@ -33,6 +34,11 @@ internal sealed class WasmFrameHandler(Target target, ContextHolder<WasmContext>

public override void HandleInlinedCallFrame(InlinedCallFrame inlinedCallFrame)
{
// Native InlinedCallFrame::UpdateRegDisplay_Impl returns before any update, including the
// interpreter-frame stash below, when the frame has no active call.
if (inlinedCallFrame.CallerReturnAddress == TargetCodePointer.Null)
return;

if (inlinedCallFrame.CallerReturnAddress.Value == InlinedPInvokeFromR2R)
{
// Mirrors InlinedCallFrame::UpdateRegDisplay_Impl in src/coreclr/vm/wasm/helpers.cpp.
Expand Down Expand Up @@ -64,6 +70,20 @@ public override void HandleInlinedCallFrame(InlinedCallFrame inlinedCallFrame)
}
}

// Mirrors SoftwareExceptionFrame::UpdateRegDisplay_Impl: on WASM the callee-saved register set
// is InterpreterFP (ENUM_CALLEE_SAVED_REGISTERS in src/coreclr/vm/wasm/cgencpu.h), so FP is
// restored from the saved context along with IP and SP. The CalleeSavedRegisters data
// descriptor is empty on WASM, so the base handler would leave FP stale.
public override void HandleSoftwareExceptionFrame(SoftwareExceptionFrame softwareExceptionFrame)
{
ContextHolder<WasmContext> saved = new();
saved.ReadFromAddress(_target, softwareExceptionFrame.TargetContext);

_holder.Context.InstructionPointer = saved.Context.InstructionPointer;
_holder.Context.StackPointer = saved.Context.StackPointer;
_holder.Context.FramePointer = saved.Context.FramePointer;
}

// Mirrors TransitionFrame::UpdateRegDisplay_Impl in src/coreclr/vm/wasm/helpers.cpp. With a recorded
// R2R stack pointer and a known return address, the caller is the R2R frame at that stack pointer
// (TransitionFrame::GetSP); otherwise the caller's stack pointer is the end of the TransitionBlock.
Expand Down
Loading
Loading