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
9 changes: 7 additions & 2 deletions docs/design/datacontracts/StackWalk.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ Unwinding call frames on the stack usually requires an OS specific implementatio
| `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. |
| `RuntimeFunction` | `UnwindData` | `uint32` | Pointer to the unwind info for the function |
| `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 @@ -212,6 +213,7 @@ Unwinding call frames on the stack usually requires an OS specific implementatio
| `TransitionBlock` | `CalleeSavedRegisters` | `pointer` | Platform specific CalleeSavedRegisters struct associated with the TransitionBlock |
| `TransitionBlock` | `FirstGCRefMapSlot` | `pointer` | Byte offset where GCRefMap slot enumeration begins. ARM64: RetBuffArgReg offset; others: ArgumentRegisters offset |
| `TransitionBlock` | `ReturnAddress` | `CodePointer` | Return address associated with the TransitionBlock |
| `TransitionBlock` | `StackPointer` | `pointer` | WASM only: R2R linear-stack pointer of the caller, saved by the transition helper |
| `VASigCookie` | `SizeOfArgs` | `uint32` | Total size in bytes of the varargs argument area; used on x86 to locate the argument base |

### Global variables used
Expand Down Expand Up @@ -451,7 +453,7 @@ 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.
* 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.

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.

Expand All @@ -472,8 +474,11 @@ When updating the context from a TransitionFrame, the IP, SP, and all ABI specif
* On ARM, the additional register values stored in `ArgumentRegisters` are copied over. The `TransitionBlock` holds a pointer to the `ArgumentRegister` struct containing these values.
* On x86, the caller SP also skips callee-popped stack arguments. For `ExternalMethodFrame` and `StubDispatchFrame`, the argument size comes from the frame's GCRefMap, including when its MethodDesc is null. `ReadStackPop()` gives the number of pointer-sized slots to add to the SP. If no GCRefMap is available, MethodDesc-backed frames use the argument map computed from their signature. `PInvokeCalliFrame` instead uses `VASigCookie.SizeOfArgs`.
* 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.

**Return Address**: Read from `TransitionBlock.ReturnAddress`. For an x86 `StubDispatchFrame` with neither a method nor a GCRefMap, subtract five bytes to return the adjusted call address, matching the context update.
**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).

**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.

The following Frame types also use this mechanism:
* FramedMethodFrame
Expand Down
1 change: 1 addition & 0 deletions docs/design/datacontracts/data-descriptor-meanings.json
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,7 @@
"TransitionBlock.FirstGCRefMapSlot": "Byte offset where GCRefMap slot enumeration begins. ARM64: RetBuffArgReg offset; others: ArgumentRegisters offset",
"TransitionBlock.ReturnAddress": "Return address associated with the TransitionBlock",
"TransitionBlock.Size": "Size in bytes of the transition block, used to restore the caller's stack pointer",
"TransitionBlock.StackPointer": "WASM only: R2R linear-stack pointer of the caller, saved by the transition helper",
"TypedByRef.Data": "Managed pointer (the byref) stored in a System.TypedReference value",
"TypedByRef.Type": "Raw TypeHandle pointer of the referent type",
"type.Size": "Size in bytes of each SHash table entry",
Expand Down
3 changes: 3 additions & 0 deletions src/coreclr/vm/datadescriptor/datadescriptor.inc
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,9 @@ CDAC_TYPE_END(InterpreterFrame)
CDAC_TYPE_BEGIN(TransitionBlock)
CDAC_TYPE_SIZE(sizeof(TransitionBlock))
CDAC_TYPE_FIELD(TransitionBlock, TYPE(CodePointer), ReturnAddress, offsetof(TransitionBlock, m_ReturnAddress))
#ifdef TARGET_WASM
CDAC_TYPE_FIELD(TransitionBlock, T_POINTER, StackPointer, offsetof(TransitionBlock, m_StackPointer))
#endif // TARGET_WASM
CDAC_TYPE_FIELD(TransitionBlock, TYPE(CalleeSavedRegisters), CalleeSavedRegisters, offsetof(TransitionBlock, m_calleeSavedRegisters))
// Offset to argument registers and first GCRefMap slot (platform-specific)
#if (defined(TARGET_AMD64) && !defined(UNIX_AMD64_ABI)) || defined(TARGET_WASM)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ public bool TryGetVirtualIPBase(uint functionTableIndex, out ulong baseVirtualIP
}
}

// Mirrors ExecutionManager::IsFuncletFunctionIndex.
public bool TryIsFunclet(uint functionTableIndex, out bool isFunclet)
{
isFunclet = false;
Data.FunctionTableIndexRangeSection? section = FindSection(functionTableIndex);
if (section is null)
return false;

Data.ReadyToRunInfo r2rInfo = GetReadyToRunInfo(section);
isFunclet = _runtimeFunctions.IsFunclet(GetRuntimeFunction(r2rInfo, functionTableIndex - section.MinFunctionTableIndex));
return true;
}

public bool TryGetUnwindData(uint functionTableIndex, out TargetPointer unwindDataAddress)
{
unwindDataAddress = TargetPointer.Null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,7 @@ public bool TryGetVirtualIPBase(uint functionTableIndex, out ulong baseVirtualIP

public bool TryGetUnwindData(uint functionTableIndex, out TargetPointer unwindDataAddress)
=> _lookup.TryGetUnwindData(functionTableIndex, out unwindDataAddress);

public bool TryIsFunclet(uint functionTableIndex, out bool isFunclet)
=> _lookup.TryIsFunclet(functionTableIndex, out isFunclet);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ internal interface IWasmR2RInfo
/// frame size. Returns false when the index does not map to a known R2R function.
/// </summary>
bool TryGetUnwindData(uint functionTableIndex, out TargetPointer unwindDataAddress);

/// <summary>
/// Reports whether an R2R function table entry is a funclet rather than a method's root
/// function (<c>ExecutionManager::IsFuncletFunctionIndex</c>). Returns false when the index
/// does not map to a known R2R function.
/// </summary>
bool TryIsFunclet(uint functionTableIndex, out bool isFunclet);
}

/// <summary>
Expand Down Expand Up @@ -174,6 +181,87 @@ public bool TryUnwindOneFrame(ref TargetPointer sp, out TargetCodePointer ip)
return true;
}

/// <summary>
/// Advances <paramref name="sp"/> by one R2R frame and returns the caller's stack pointer,
/// without requiring the caller to be R2R-generated code. This is the frame-size half of
/// <c>WasmUnwindStackFrameCore</c>, which <see cref="TryGetLogicalFramePointer"/> needs in
/// order to inspect a synthetic <see cref="TerminateR2RStackWalk"/> frame.
/// </summary>
private bool TryUnwindToCallerStackPointer(TargetPointer sp, out TargetPointer callerSp)
{
callerSp = TargetPointer.Null;
if (!TryGetFramePointer(sp, out TargetPointer frameBase))
return false;

uint functionIndex = _target.Read<uint>(frameBase.Value + FunctionIndexOffset);
if (!_r2rInfo.TryGetUnwindData(functionIndex, out TargetPointer unwindData))
return false;

uint frameSize = DecodeULEB128(unwindData.Value);
if (frameSize == 0)
return false;

callerSp = new TargetPointer(frameBase.Value + frameSize);
return true;
}

/// <summary>
/// Returns the logical (establishing) frame pointer for the frame at <paramref name="sp"/>,
/// mirroring <c>GetWasmFramePointerFromStackPointer</c> in
/// <c>src/coreclr/vm/wasm/helpers.cpp</c>.
/// </summary>
/// <remarks>
/// For a method's root function this is its own frame base. For a funclet it is the frame
/// base of the establishing method: the funclet's FP local is the FP its caller passed in
/// (<c>WasmRegAlloc::AllocateFramePointer</c>, <c>CodeGen::genCallFinally</c>), so reaching it
/// means unwinding out of the funclet, either to its containing method or funclet, or to the
/// synthetic <see cref="TerminateR2RStackWalk"/> frame that
/// <c>CallFuncletWith[out]Throwable</c> pushes, which carries the establishing frame pointer
/// beside the marker.
/// </remarks>
public bool TryGetLogicalFramePointer(TargetPointer sp, out TargetPointer framePointer)
{
framePointer = TargetPointer.Null;

// Native recurses until it reaches a non-funclet frame or a CallFunclet terminator. The
// cDAC reads untrusted memory, so require each step to move toward the caller; the step
// count is only a backstop.
const int MaxUnwindSteps = 4096;
TargetPointer current = sp;

for (int i = 0; i < MaxUnwindSteps; i++)
{
if (!TryGetFramePointer(current, out TargetPointer frameBase))
return false;

uint functionIndex = _target.Read<uint>(frameBase.Value + FunctionIndexOffset);
// Native treats an unknown index as a root function; report no frame pointer instead.
if (!_r2rInfo.TryIsFunclet(functionIndex, out bool isFunclet))
return false;

if (!isFunclet)
{
framePointer = frameBase;
return true;
}

if (!TryUnwindToCallerStackPointer(current, out TargetPointer callerSp) || callerSp.Value <= current.Value)
return false;

if (_target.Read<uint>(callerSp.Value + FunctionIndexOffset) == TerminateR2RStackWalk)
{
// Invoked by the VM through CallFuncletWith[out]Throwable.
framePointer = GetEstablishingFramePointerFromTerminator(callerSp);
return true;
}

// Called by its containing method or funclet; keep walking out.
current = callerSp;
}

return false;
}

// Standard little-endian base-128 varint, matching the native DecodeULEB128AsU32. A ULEB128
// uint32 is at most 5 bytes (5 * 7 = 35 >= 32 bits); a longer encoding is malformed.
private uint DecodeULEB128(ulong address)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,15 @@ public void Unwind(Target target)
{
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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,25 @@ public void UpdateContextFromFrame(Data.Frame frame, IPlatformAgnosticContext co
}
}

/// <summary>
/// Returns the return address recorded in <paramref name="transitionBlock"/>. On WASM, a
/// transition helper called from R2R code may store only the caller's linear-stack pointer;
/// the return address is then that frame's R2R virtual IP, matching the lazy computation in
/// native <c>FramedMethodFrame::GetTransitionBlock_Impl</c>.
/// </summary>
public TargetCodePointer GetTransitionBlockReturnAddress(Data.TransitionBlock transitionBlock)
{
if (transitionBlock.ReturnAddress == TargetCodePointer.Null
&& transitionBlock.StackPointer is TargetPointer stackPointer
&& stackPointer != TargetPointer.Null)
{
Wasm.WasmUnwinder unwinder = new(_target, new Wasm.WasmR2RInfo(_target));
return unwinder.GetVirtualIP(stackPointer);
}

return transitionBlock.ReturnAddress;
}

/// <summary>
/// Returns the return address for <paramref name="frame"/>, matching native Frame::GetReturnAddress().
/// Returns TargetCodePointer.Null if the Frame has no return address (e.g., non-active ICF,
Expand Down Expand Up @@ -319,7 +338,7 @@ public TargetCodePointer GetReturnAddress(Data.Frame frame)
if (FindGCRefMap(dispatchFrame.Indirection) == TargetPointer.Null)
return new TargetCodePointer((uint)tb.ReturnAddress - X86FrameHandler.CallInstructionSize);
}
return tb.ReturnAddress;
return GetTransitionBlockReturnAddress(tb);

// SoftwareExceptionFrame: stored m_ReturnAddress
case FrameType.SoftwareExceptionFrame:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ public override void HandleInlinedCallFrame(InlinedCallFrame inlinedCallFrame)
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)
// Native GetWasmFramePointerFromStackPointer: a funclet reports its establishing method's frame.
_holder.Context.FramePointer = unwinder.TryGetLogicalFramePointer(inlinedCallFrame.CallSiteSP, out TargetPointer framePointer)
? framePointer
: TargetPointer.Null;
}
Expand All @@ -64,6 +64,32 @@ public override void HandleInlinedCallFrame(InlinedCallFrame inlinedCallFrame)
}
}

// 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.
public override void HandleTransitionFrame(FramedMethodFrame framedMethodFrame)
{
Data.TransitionBlock transitionBlock = _target.ProcessedData.GetOrAdd<Data.TransitionBlock>(framedMethodFrame.TransitionBlockPtr);
TargetCodePointer instructionPointer = _frameHelpers.GetTransitionBlockReturnAddress(transitionBlock);
TargetPointer stackPointer = transitionBlock.StackPointer ?? TargetPointer.Null;

_holder.Context.InstructionPointer = instructionPointer;
if (stackPointer != TargetPointer.Null && instructionPointer != TargetCodePointer.Null)
{
_holder.Context.StackPointer = stackPointer;
// Native GetWasmFramePointerFromStackPointer: a funclet reports its establishing method's frame.
Wasm.WasmUnwinder unwinder = new(_target, new Wasm.WasmR2RInfo(_target));
_holder.Context.FramePointer = unwinder.TryGetLogicalFramePointer(stackPointer, out TargetPointer framePointer)
? framePointer
: TargetPointer.Null;
}
else
{
_holder.Context.StackPointer = framedMethodFrame.TransitionBlockPtr + Data.TransitionBlock.GetSize(_target);
_holder.Context.FramePointer = TargetPointer.Null;
}
}

public void HandleHijackFrame(HijackFrame frame)
=> throw new PlatformNotSupportedException("HijackFrame handling is not supported on WASM.");
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ internal partial class TransitionBlock : IData<TransitionBlock>
{
[Field] public partial TargetCodePointer ReturnAddress { get; }

// WASM only: the R2R linear-stack pointer of the caller, saved by transition helpers.
Comment thread
lewing marked this conversation as resolved.
[Field] public partial TargetPointer? StackPointer { get; }

[FieldAddress]
public partial TargetPointer CalleeSavedRegisters { get; }

Expand Down
Loading
Loading