diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md
index ca205179f1f8f6..659cb19799244d 100644
--- a/docs/design/datacontracts/StackWalk.md
+++ b/docs/design/datacontracts/StackWalk.md
@@ -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 |
@@ -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
@@ -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.
@@ -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
diff --git a/docs/design/datacontracts/data-descriptor-meanings.json b/docs/design/datacontracts/data-descriptor-meanings.json
index d322db96ba051a..f937a53882440e 100644
--- a/docs/design/datacontracts/data-descriptor-meanings.json
+++ b/docs/design/datacontracts/data-descriptor-meanings.json
@@ -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",
diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc
index fd00097964a744..a96bd5358a2cf0 100644
--- a/src/coreclr/vm/datadescriptor/datadescriptor.inc
+++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc
@@ -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)
diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/WasmFunctionTableIndexLookup.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/WasmFunctionTableIndexLookup.cs
index 3137eaaaef66d5..5589f8ee8c365e 100644
--- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/WasmFunctionTableIndexLookup.cs
+++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/WasmFunctionTableIndexLookup.cs
@@ -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;
diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs
index d0c3d88455a11b..b2823fa028d096 100644
--- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs
+++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs
@@ -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);
}
diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmUnwinder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmUnwinder.cs
index f5d7c9049348c5..8ac71e0d66542f 100644
--- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmUnwinder.cs
+++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmUnwinder.cs
@@ -26,6 +26,13 @@ internal interface IWasmR2RInfo
/// frame size. Returns false when the index does not map to a known R2R function.
///
bool TryGetUnwindData(uint functionTableIndex, out TargetPointer unwindDataAddress);
+
+ ///
+ /// Reports whether an R2R function table entry is a funclet rather than a method's root
+ /// function (ExecutionManager::IsFuncletFunctionIndex). Returns false when the index
+ /// does not map to a known R2R function.
+ ///
+ bool TryIsFunclet(uint functionTableIndex, out bool isFunclet);
}
///
@@ -174,6 +181,87 @@ public bool TryUnwindOneFrame(ref TargetPointer sp, out TargetCodePointer ip)
return true;
}
+ ///
+ /// Advances 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
+ /// WasmUnwindStackFrameCore, which needs in
+ /// order to inspect a synthetic frame.
+ ///
+ private bool TryUnwindToCallerStackPointer(TargetPointer sp, out TargetPointer callerSp)
+ {
+ callerSp = TargetPointer.Null;
+ if (!TryGetFramePointer(sp, out TargetPointer frameBase))
+ return false;
+
+ uint functionIndex = _target.Read(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;
+ }
+
+ ///
+ /// Returns the logical (establishing) frame pointer for the frame at ,
+ /// mirroring GetWasmFramePointerFromStackPointer in
+ /// src/coreclr/vm/wasm/helpers.cpp.
+ ///
+ ///
+ /// 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
+ /// (WasmRegAlloc::AllocateFramePointer, CodeGen::genCallFinally), so reaching it
+ /// means unwinding out of the funclet, either to its containing method or funclet, or to the
+ /// synthetic frame that
+ /// CallFuncletWith[out]Throwable pushes, which carries the establishing frame pointer
+ /// beside the marker.
+ ///
+ 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(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(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)
diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/WasmContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/WasmContext.cs
index 24ac092843f18b..e4d54de44b9733 100644
--- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/WasmContext.cs
+++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/WasmContext.cs
@@ -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;
}
}
diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs
index 8f787e96dcbf4f..46cd63058e4aca 100644
--- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs
+++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs
@@ -286,6 +286,25 @@ public void UpdateContextFromFrame(Data.Frame frame, IPlatformAgnosticContext co
}
}
+ ///
+ /// Returns the return address recorded in . 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 FramedMethodFrame::GetTransitionBlock_Impl.
+ ///
+ 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;
+ }
+
///
/// Returns the return address for , matching native Frame::GetReturnAddress().
/// Returns TargetCodePointer.Null if the Frame has no return address (e.g., non-active ICF,
@@ -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:
diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/WasmFrameHandler.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/WasmFrameHandler.cs
index ca71ea5a716c37..a09ef0fb12afe8 100644
--- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/WasmFrameHandler.cs
+++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/WasmFrameHandler.cs
@@ -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;
}
@@ -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(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.");
}
diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs
index 0bc4da6f58800e..184920a8dfd2b4 100644
--- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs
+++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs
@@ -8,6 +8,9 @@ internal partial class TransitionBlock : IData
{
[Field] public partial TargetCodePointer ReturnAddress { get; }
+ // WASM only: the R2R linear-stack pointer of the caller, saved by transition helpers.
+ [Field] public partial TargetPointer? StackPointer { get; }
+
[FieldAddress]
public partial TargetPointer CalleeSavedRegisters { get; }
diff --git a/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs b/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs
index 4fc1c0701262ad..7c692ad791b0bf 100644
--- a/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs
+++ b/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs
@@ -1236,9 +1236,7 @@ public void CreateStackWalk_WasmInterpretedPInvoke_WalksInterpretedChainOnce(boo
AddWasmR2RFunction(targetBuilder, allocator, functionTableIndex: 5, minVirtualIP: 0x0005_0000, functionBeginAddress: 0x100);
AddWasmNullDebugger(targetBuilder, allocator);
- // TransitionBlock: ReturnAddress followed by the (empty) callee-saved register area.
- MockMemorySpace.HeapFragment transitionBlock = allocator.Allocate((ulong)pointerSize, "TransitionBlock");
- helpers.WritePointer(transitionBlock.Data.AsSpan(0, pointerSize), NativeCallerIp);
+ ulong transitionBlock = AddWasmTransitionBlock(helpers, allocator, NativeCallerIp, stackPointer: 0);
// InterpreterFrame derives from FramedMethodFrame.
Layout fmfLayout = frames!.FramedMethodFrameLayout;
@@ -1253,7 +1251,7 @@ public void CreateStackWalk_WasmInterpretedPInvoke_WalksInterpretedChainOnce(boo
MockFramedMethodFrame fmf = fmfLayout.Create(interpreterFrame);
fmf.Identifier = MockFrameBuilder.InterpreterFrameIdentifierValue;
fmf.Next = terminator;
- helpers.WritePointer(interpreterFrame.Data.AsSpan(fmfLayout.Fields.Single(f => f.Name == nameof(Data.FramedMethodFrame.TransitionBlockPtr)).Offset, pointerSize), transitionBlock.Address);
+ helpers.WritePointer(interpreterFrame.Data.AsSpan(fmfLayout.Fields.Single(f => f.Name == nameof(Data.FramedMethodFrame.TransitionBlockPtr)).Offset, pointerSize), transitionBlock);
helpers.WritePointer(interpreterFrame.Data.AsSpan(topOffset, pointerSize), imcfLeaf);
interpreterFrameAddr = interpreterFrame.Address;
@@ -1268,24 +1266,14 @@ public void CreateStackWalk_WasmInterpretedPInvoke_WalksInterpretedChainOnce(boo
icfAddr = icf.Address;
thread!.Frame = icfAddr;
- targetBuilder.AddTypes(new Dictionary
- {
- [DataType.InterpreterFrame] = new() { Fields = interpreterFrameFields, Size = (uint)(isFaultingOffset + pointerSize) },
- [DataType.TransitionBlock] = new()
+ targetBuilder
+ .AddTypes(CreateWasmTransitionBlockTypes(pointerSize))
+ .AddTypes(new Dictionary
{
- Fields = new Dictionary
- {
- [nameof(Data.TransitionBlock.ReturnAddress)] = new() { Offset = 0 },
- [nameof(Data.TransitionBlock.CalleeSavedRegisters)] = new() { Offset = pointerSize },
- [nameof(Data.TransitionBlock.ArgumentRegisters)] = new() { Offset = pointerSize },
- [nameof(Data.TransitionBlock.FirstGCRefMapSlot)] = new() { Offset = pointerSize },
- },
- Size = (uint)pointerSize,
- },
- [DataType.CalleeSavedRegisters] = new() { Fields = new Dictionary(), Size = 0 },
- });
+ [DataType.InterpreterFrame] = new() { Fields = interpreterFrameFields, Size = (uint)(isFaultingOffset + pointerSize) },
+ });
},
- executionManager: CreateInterpreterExecutionManager(InterpIp1, InterpIp2));
+ executionManager: CreateInterpreterExecutionManager([InterpIp1, InterpIp2]));
IStackWalk stackWalk = target.Contracts.StackWalk;
ThreadData threadData = target.Contracts.Thread.GetThreadData(new TargetPointer(thread!.Address));
@@ -1319,18 +1307,277 @@ public void CreateStackWalk_WasmInterpretedPInvoke_WalksInterpretedChainOnce(boo
Assert.True(walked.Length <= 8, $"Walk did not terminate: {walked.Length} frames");
}
- private static IExecutionManager CreateInterpreterExecutionManager(params ulong[] interpreterIps)
+ // A WASM TransitionFrame mirrors native TransitionFrame::GetSP / UpdateRegDisplay_Impl: when the
+ // helper recorded the caller's R2R linear-stack pointer and a return address is known (stored, or
+ // derived lazily from that stack pointer as in FramedMethodFrame::GetTransitionBlock_Impl), the
+ // caller is the R2R frame at that stack pointer; otherwise it is the end of the TransitionBlock.
+ [Theory]
+ [InlineData(0ul, true, true)]
+ [InlineData(0x0009_0000ul, true, false)]
+ [InlineData(0x0009_0000ul, false, false)]
+ [InlineData(0ul, false, false)]
+ public void UpdateContextFromFrame_WasmTransitionFrame_MirrorsNativeTransitionBlock(ulong returnAddress, bool hasStackPointer, bool expectDerivedIP)
+ {
+ MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false };
+ const uint FunctionTableIndex = 5;
+ const ulong MinVirtualIP = 0x8001_0000;
+ const uint FunctionBeginAddress = 0x100;
+ const uint LocalVirtualIPHalf = 3;
+
+ MockFramedMethodFrame? framedMethodFrame = null;
+ ulong r2rFrame = 0;
+ ulong transitionBlock = 0;
+ TestPlaceholderTarget target = CreateTarget(
+ wasmArch,
+ threadBuilder => threadBuilder.AddThread(1, 1234),
+ frameBuilder => framedMethodFrame = frameBuilder.AddFramedMethodFrame(methodDescPtr: 0),
+ runtimeArchitecture: RuntimeInfoArchitecture.Wasm,
+ configureTarget: targetBuilder =>
+ {
+ TargetTestHelpers helpers = targetBuilder.MemoryBuilder.TargetTestHelpers;
+ MockMemorySpace.BumpAllocator allocator = targetBuilder.MemoryBuilder.CreateAllocator(0x0020_0000, 0x0020_4000);
+ AddWasmR2RFunction(targetBuilder, allocator, FunctionTableIndex, MinVirtualIP, FunctionBeginAddress);
+ r2rFrame = AddWasmR2RFrameRecord(helpers, allocator, FunctionTableIndex, LocalVirtualIPHalf);
+ transitionBlock = AddWasmTransitionBlock(helpers, allocator, returnAddress, hasStackPointer ? r2rFrame : 0);
+ helpers.WritePointer(
+ framedMethodFrame!.Memory.Span.Slice(framedMethodFrame.Layout.GetField("TransitionBlockPtr").Offset, helpers.PointerSize),
+ transitionBlock);
+ targetBuilder.AddTypes(CreateWasmTransitionBlockTypes(helpers.PointerSize));
+ });
+
+ ContextHolder context = new();
+ FrameHelpers frameHelpers = new(target);
+ Data.Frame frame = target.ProcessedData.GetOrAdd(framedMethodFrame!.Address);
+ frameHelpers.UpdateContextFromFrame(frame, context);
+
+ ulong expectedIP = expectDerivedIP ? MinVirtualIP + FunctionBeginAddress + LocalVirtualIPHalf * 2 : returnAddress;
+ bool callerIsR2R = hasStackPointer && expectedIP != 0;
+ Assert.Equal(expectedIP, context.InstructionPointer.Value);
+ Assert.Equal(expectedIP, frameHelpers.GetReturnAddress(frame).Value);
+ Assert.Equal(callerIsR2R ? r2rFrame : transitionBlock + 2 * sizeof(uint), context.StackPointer.Value);
+ Assert.Equal(callerIsR2R ? r2rFrame : 0ul, context.FramePointer.Value);
+ }
+
+ // A funclet's frame pointer is its establishing method's frame, not its own record (native
+ // GetWasmFramePointerFromStackPointer). Here the funclet was invoked by the VM through
+ // CallFuncletWith[out]Throwable, so a TERMINATE_R2R_STACK_WALK frame carrying the establishing
+ // frame pointer sits directly above it. Both frame kinds that seed from an R2R stack pointer
+ // (a transition helper's TransitionBlock and an R2R inlined P/Invoke) must report that pointer.
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void UpdateContextFromFrame_WasmFuncletCaller_ReportsEstablishingFramePointer(bool transitionFrame)
+ {
+ MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false };
+ const uint ParentIndex = 5;
+ const uint FuncletIndex = 6;
+ const ulong MinVirtualIP = 0x8001_0000;
+ const uint ParentBegin = 0x100;
+ const uint FuncletVipHalf = 0x21;
+ const byte FuncletFrameSize = 16; // AlignUp(2 * TARGET_POINTER_SIZE, STACK_ALIGN) on wasm32
+ const ulong EstablishingFp = 0x0021_0F00;
+
+ MockFramedMethodFrame? framedMethodFrame = null;
+ MockInlinedCallFrame? inlinedCallFrame = null;
+ ulong funcletFrame = 0;
+ TestPlaceholderTarget target = CreateTarget(
+ wasmArch,
+ threadBuilder => threadBuilder.AddThread(1, 1234),
+ frameBuilder =>
+ {
+ if (transitionFrame)
+ framedMethodFrame = frameBuilder.AddFramedMethodFrame(methodDescPtr: 0);
+ else
+ inlinedCallFrame = frameBuilder.AddInlinedCallFrame(callerReturnAddress: 1, datum: 0);
+ },
+ runtimeArchitecture: RuntimeInfoArchitecture.Wasm,
+ configureTarget: targetBuilder =>
+ {
+ TargetTestHelpers helpers = targetBuilder.MemoryBuilder.TargetTestHelpers;
+ MockMemorySpace.BumpAllocator allocator = targetBuilder.MemoryBuilder.CreateAllocator(0x0020_0000, 0x0020_4000);
+ AddWasmR2RFunctions(targetBuilder, allocator, ParentIndex, MinVirtualIP,
+ [(ParentBegin, 0x30), (0x8000_0000 | (ParentBegin + 0x40), FuncletFrameSize)]);
+
+ // The funclet's record, then the VM's terminator frame: [TERMINATE_R2R_STACK_WALK, establishing FP].
+ MockMemorySpace.HeapFragment stack = allocator.Allocate(FuncletFrameSize + 2 * sizeof(uint), "LinearStack");
+ helpers.Write(stack.Data.AsSpan(0, sizeof(uint)), FuncletIndex);
+ helpers.Write(stack.Data.AsSpan(4, sizeof(uint)), FuncletVipHalf);
+ helpers.Write(stack.Data.AsSpan(FuncletFrameSize, sizeof(uint)), 1u);
+ helpers.WritePointer(stack.Data.AsSpan(FuncletFrameSize + sizeof(uint), helpers.PointerSize), EstablishingFp);
+ funcletFrame = stack.Address;
+
+ if (transitionFrame)
+ {
+ ulong transitionBlock = AddWasmTransitionBlock(helpers, allocator, returnAddress: 0, stackPointer: funcletFrame);
+ helpers.WritePointer(
+ framedMethodFrame!.Memory.Span.Slice(framedMethodFrame.Layout.GetField("TransitionBlockPtr").Offset, helpers.PointerSize),
+ transitionBlock);
+ targetBuilder.AddTypes(CreateWasmTransitionBlockTypes(helpers.PointerSize));
+ }
+ else
+ {
+ inlinedCallFrame!.CallSiteSP = funcletFrame;
+ }
+ });
+
+ ContextHolder context = new();
+ FrameHelpers frameHelpers = new(target);
+ ulong frameAddress = transitionFrame ? framedMethodFrame!.Address : inlinedCallFrame!.Address;
+ frameHelpers.UpdateContextFromFrame(target.ProcessedData.GetOrAdd(frameAddress), context);
+
+ // Funclet virtual IPs are relative to the controlling method's base.
+ Assert.Equal(MinVirtualIP + ParentBegin + FuncletVipHalf * 2, context.InstructionPointer.Value);
+ Assert.Equal(funcletFrame, context.StackPointer.Value);
+ Assert.Equal(EstablishingFp, context.FramePointer.Value);
+ }
+
+ // R2R code that enters an interpreted method through a portable-entry-point thunk leaves an
+ // InterpreterFrame whose TransitionBlock records only the R2R caller's linear-stack pointer.
+ // When the interpreted chain is exhausted, the walk must continue into that R2R caller rather
+ // than reporting a native marker at the end of the TransitionBlock and skipping the R2R frames.
+ // Each R2R frame's frame pointer is recomputed from its own stack pointer as the walk unwinds.
+ [Fact]
+ public void CreateStackWalk_WasmInterpreterFrameEnteredFromR2R_ContinuesIntoR2RCaller()
+ {
+ MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false };
+ const ulong InterpIp1 = 0x0005_1000;
+ const ulong InterpIp2 = 0x0005_2000;
+ const uint FunctionTableIndex = 5;
+ const ulong MinVirtualIP = 0x8001_0000;
+ const uint FunctionBeginAddress = 0x100;
+ const uint LocalVirtualIPHalf = 3;
+ const uint OuterLocalVirtualIPHalf = 5;
+ const byte R2RFrameSize = 16;
+ const ulong R2RCallerIp = MinVirtualIP + FunctionBeginAddress + LocalVirtualIPHalf * 2;
+ const ulong OuterR2RCallerIp = MinVirtualIP + FunctionBeginAddress + OuterLocalVirtualIPHalf * 2;
+
+ MockThread? thread = null;
+ MockFrameBuilder? frames = null;
+ ulong imcfLeaf = 0;
+ ulong r2rFrame = 0;
+ TestPlaceholderTarget target = CreateTarget(
+ wasmArch,
+ threadBuilder => thread = threadBuilder.AddThread(1, 1234),
+ frameBuilder =>
+ {
+ frames = frameBuilder;
+ ulong imcfRoot = frameBuilder.AddInterpMethodContextFrame(parentPtr: 0, ip: InterpIp2, stack: 0x0006_2000).Address;
+ imcfLeaf = frameBuilder.AddInterpMethodContextFrame(parentPtr: imcfRoot, ip: InterpIp1, stack: 0x0006_1000).Address;
+ },
+ runtimeArchitecture: RuntimeInfoArchitecture.Wasm,
+ configureTarget: targetBuilder =>
+ {
+ TargetTestHelpers helpers = targetBuilder.MemoryBuilder.TargetTestHelpers;
+ int pointerSize = helpers.PointerSize;
+ MockMemorySpace.BumpAllocator allocator = targetBuilder.MemoryBuilder.CreateAllocator(0x0020_0000, 0x0020_4000);
+ AddWasmR2RFunction(targetBuilder, allocator, FunctionTableIndex, MinVirtualIP, FunctionBeginAddress, R2RFrameSize);
+ AddWasmNullDebugger(targetBuilder, allocator);
+
+ // Two R2R frame records, each frame size apart, then a TERMINATE_R2R_STACK_WALK
+ // marker, so the walk ends after the outer caller.
+ MockMemorySpace.HeapFragment r2rStack = allocator.Allocate(2 * R2RFrameSize + 4, "R2RLinearStack");
+ helpers.Write(r2rStack.Data.AsSpan(0, sizeof(uint)), FunctionTableIndex);
+ helpers.Write(r2rStack.Data.AsSpan(4, sizeof(uint)), LocalVirtualIPHalf);
+ helpers.Write(r2rStack.Data.AsSpan(R2RFrameSize, sizeof(uint)), FunctionTableIndex);
+ helpers.Write(r2rStack.Data.AsSpan(R2RFrameSize + 4, sizeof(uint)), OuterLocalVirtualIPHalf);
+ helpers.Write(r2rStack.Data.AsSpan(2 * R2RFrameSize, sizeof(uint)), 1u);
+ r2rFrame = r2rStack.Address;
+
+ ulong transitionBlock = AddWasmTransitionBlock(helpers, allocator, returnAddress: 0, stackPointer: r2rFrame);
+
+ Layout fmfLayout = frames!.FramedMethodFrameLayout;
+ int topOffset = fmfLayout.Size;
+ int isFaultingOffset = topOffset + pointerSize;
+ Dictionary interpreterFrameFields = new(TargetTestHelpers.CreateTypeInfo(fmfLayout).Fields)
+ {
+ [nameof(Data.InterpreterFrame.TopInterpMethodContextFrame)] = new() { Offset = topOffset },
+ [nameof(Data.InterpreterFrame.IsFaulting)] = new() { Offset = isFaultingOffset },
+ };
+ MockMemorySpace.HeapFragment interpreterFrame = allocator.Allocate((ulong)(isFaultingOffset + pointerSize), "InterpreterFrame");
+ MockFramedMethodFrame fmf = fmfLayout.Create(interpreterFrame);
+ fmf.Identifier = MockFrameBuilder.InterpreterFrameIdentifierValue;
+ fmf.Next = uint.MaxValue;
+ helpers.WritePointer(interpreterFrame.Data.AsSpan(fmfLayout.Fields.Single(f => f.Name == nameof(Data.FramedMethodFrame.TransitionBlockPtr)).Offset, pointerSize), transitionBlock);
+ helpers.WritePointer(interpreterFrame.Data.AsSpan(topOffset, pointerSize), imcfLeaf);
+ thread!.Frame = interpreterFrame.Address;
+
+ targetBuilder
+ .AddTypes(CreateWasmTransitionBlockTypes(pointerSize))
+ .AddTypes(new Dictionary
+ {
+ [DataType.InterpreterFrame] = new() { Fields = interpreterFrameFields, Size = (uint)(isFaultingOffset + pointerSize) },
+ });
+ },
+ executionManager: CreateInterpreterExecutionManager([InterpIp1, InterpIp2], managedIps: [R2RCallerIp, OuterR2RCallerIp]));
+
+ IStackWalk stackWalk = target.Contracts.StackWalk;
+ ThreadData threadData = target.Contracts.Thread.GetThreadData(new TargetPointer(thread!.Address));
+ IStackDataFrameHandle[] walked = stackWalk.CreateStackWalk(threadData).Take(16).ToArray();
+
+ (ulong Ip, ulong Sp, ulong Fp)[] frameless = walked
+ .Where(f => f.State == StackWalkState.Frameless)
+ .Select(f => (stackWalk.GetInstructionPointer(f).Value, stackWalk.GetStackPointer(f).Value, stackWalk.GetContextFramePointer(f).Value))
+ .ToArray();
+
+ Assert.Equal([InterpIp1, InterpIp2, R2RCallerIp, OuterR2RCallerIp], frameless.Select(f => f.Ip));
+ Assert.Equal((r2rFrame, r2rFrame), (frameless[2].Sp, frameless[2].Fp));
+ Assert.Equal((r2rFrame + R2RFrameSize, r2rFrame + R2RFrameSize), (frameless[3].Sp, frameless[3].Fp));
+ Assert.True(walked.Length <= 7, $"Walk did not terminate: {walked.Length} frames");
+ }
+
+ private static IExecutionManager CreateInterpreterExecutionManager(ulong[] interpreterIps, ulong[]? managedIps = null)
{
Mock executionManager = new();
executionManager
.Setup(em => em.GetCodeBlockHandle(It.IsAny()))
- .Returns((TargetCodePointer ip) => interpreterIps.Contains(ip.Value) ? new CodeBlockHandle(new TargetPointer(ip.Value)) : null);
+ .Returns((TargetCodePointer ip) => interpreterIps.Contains(ip.Value) || (managedIps?.Contains(ip.Value) ?? false)
+ ? new CodeBlockHandle(new TargetPointer(ip.Value))
+ : null);
executionManager
.Setup(em => em.GetCodeKind(It.IsAny()))
.Returns((TargetCodePointer ip) => interpreterIps.Contains(ip.Value) ? CodeKind.Interpreter : default);
return executionManager.Object;
}
+ // WASM TransitionBlock: m_ReturnAddress, then m_StackPointer. Matches the browser-wasm data
+ // descriptor, where ArgumentRegisters and FirstGCRefMapSlot are sizeof(TransitionBlock).
+ private static Dictionary CreateWasmTransitionBlockTypes(int pointerSize)
+ => new()
+ {
+ [DataType.TransitionBlock] = new()
+ {
+ Fields = new Dictionary
+ {
+ [nameof(Data.TransitionBlock.ReturnAddress)] = new() { Offset = 0 },
+ [nameof(Data.TransitionBlock.StackPointer)] = new() { Offset = pointerSize },
+ [nameof(Data.TransitionBlock.CalleeSavedRegisters)] = new() { Offset = 0 },
+ [nameof(Data.TransitionBlock.ArgumentRegisters)] = new() { Offset = 2 * pointerSize },
+ [nameof(Data.TransitionBlock.FirstGCRefMapSlot)] = new() { Offset = 2 * pointerSize },
+ },
+ Size = (uint)(2 * pointerSize),
+ },
+ [DataType.CalleeSavedRegisters] = new() { Fields = new Dictionary(), Size = 0 },
+ };
+
+ private static ulong AddWasmTransitionBlock(TargetTestHelpers helpers, MockMemorySpace.BumpAllocator allocator, ulong returnAddress, ulong stackPointer)
+ {
+ int pointerSize = helpers.PointerSize;
+ MockMemorySpace.HeapFragment transitionBlock = allocator.Allocate((ulong)(2 * pointerSize), "TransitionBlock");
+ helpers.WritePointer(transitionBlock.Data.AsSpan(0, pointerSize), returnAddress);
+ helpers.WritePointer(transitionBlock.Data.AsSpan(pointerSize, pointerSize), stackPointer);
+ return transitionBlock.Address;
+ }
+
+ // An R2R linear-stack frame record: the function-table index, then the function-local virtual
+ // IP / 2 (WASM_STACKFRAME_FUNCTION_INDEX_OFFSET / WASM_STACKFRAME_VIRTUALIP_OFFSET).
+ private static ulong AddWasmR2RFrameRecord(TargetTestHelpers helpers, MockMemorySpace.BumpAllocator allocator, uint functionTableIndex, uint localVirtualIPHalf)
+ {
+ MockMemorySpace.HeapFragment frame = allocator.Allocate(8, "R2RShadowFrame");
+ helpers.Write(frame.Data.AsSpan(0, sizeof(uint)), functionTableIndex);
+ helpers.Write(frame.Data.AsSpan(4, sizeof(uint)), localVirtualIPHalf);
+ return frame.Address;
+ }
+
// WASM advertises the Debugger contract, but the in-process debugger is not built there, so
// g_pDebugger stays null.
private static void AddWasmNullDebugger(TestPlaceholderTarget.Builder targetBuilder, MockMemorySpace.BumpAllocator allocator)
@@ -1345,7 +1592,19 @@ private static void AddWasmR2RFunction(
MockMemorySpace.BumpAllocator allocator,
uint functionTableIndex,
ulong minVirtualIP,
- uint functionBeginAddress)
+ uint functionBeginAddress,
+ byte frameSize = 0)
+ => AddWasmR2RFunctions(targetBuilder, allocator, functionTableIndex, minVirtualIP, [(functionBeginAddress, frameSize)]);
+
+ // Registers one R2R module whose RUNTIME_FUNCTIONs occupy consecutive function-table indices
+ // starting at minFunctionTableIndex. BeginAddress bit 31 marks a funclet; a frame size of 0
+ // leaves the unwind data unset.
+ private static void AddWasmR2RFunctions(
+ TestPlaceholderTarget.Builder targetBuilder,
+ MockMemorySpace.BumpAllocator allocator,
+ uint minFunctionTableIndex,
+ ulong minVirtualIP,
+ (uint BeginAddress, byte FrameSize)[] functions)
{
MockTarget.Architecture arch = targetBuilder.MemoryBuilder.TargetTestHelpers.Arch;
TargetTestHelpers helpers = targetBuilder.MemoryBuilder.TargetTestHelpers;
@@ -1364,21 +1623,33 @@ private static void AddWasmR2RFunction(
new(nameof(Data.FunctionTableIndexRangeSection.Next), DataType.pointer),
]);
- MockMemorySpace.HeapFragment runtimeFunction = allocator.Allocate(runtimeFunctionLayout.Stride, "RuntimeFunction");
- helpers.Write(runtimeFunction.Data.AsSpan(runtimeFunctionLayout.Fields[nameof(Data.RuntimeFunction.BeginAddress)].Offset, sizeof(uint)), functionBeginAddress);
+ MockMemorySpace.HeapFragment runtimeFunctions = allocator.Allocate(runtimeFunctionLayout.Stride * (ulong)functions.Length, "RuntimeFunctions");
+ for (int i = 0; i < functions.Length; i++)
+ {
+ Span entry = runtimeFunctions.Data.AsSpan(i * (int)runtimeFunctionLayout.Stride, (int)runtimeFunctionLayout.Stride);
+ helpers.Write(entry.Slice(runtimeFunctionLayout.Fields[nameof(Data.RuntimeFunction.BeginAddress)].Offset, sizeof(uint)), functions[i].BeginAddress);
+ if (functions[i].FrameSize != 0)
+ {
+ // Unwind data is the ULEB128 frame size, addressed as LoadedImageBase (0 here) + UnwindData.
+ Assert.True(functions[i].FrameSize < 0x80);
+ MockMemorySpace.HeapFragment unwindData = allocator.Allocate(1, "UnwindData");
+ unwindData.Data[0] = functions[i].FrameSize;
+ helpers.Write(entry.Slice(runtimeFunctionLayout.Fields[nameof(Data.RuntimeFunction.UnwindData)].Offset, sizeof(uint)), (uint)unwindData.Address);
+ }
+ }
MockReadyToRunInfo r2rInfo = r2rInfoLayout.Create(allocator.Allocate((ulong)r2rInfoLayout.Size, "ReadyToRunInfo"));
r2rInfo.CompositeInfo = r2rInfo.Address;
- r2rInfo.NumRuntimeFunctions = 1;
- r2rInfo.RuntimeFunctions = runtimeFunction.Address;
+ r2rInfo.NumRuntimeFunctions = (uint)functions.Length;
+ r2rInfo.RuntimeFunctions = runtimeFunctions.Address;
r2rInfo.MinVirtualIP = minVirtualIP;
MockLoaderModule module = moduleLayout.Create(allocator.Allocate((ulong)moduleLayout.Size, "Module"));
module.ReadyToRunInfo = r2rInfo.Address;
MockMemorySpace.HeapFragment section = allocator.Allocate(rangeSectionLayout.Stride, "FunctionTableIndexRangeSection");
- helpers.Write(section.Data.AsSpan(rangeSectionLayout.Fields[nameof(Data.FunctionTableIndexRangeSection.MinFunctionTableIndex)].Offset, sizeof(uint)), functionTableIndex);
- helpers.Write(section.Data.AsSpan(rangeSectionLayout.Fields[nameof(Data.FunctionTableIndexRangeSection.NumRuntimeFunctions)].Offset, sizeof(uint)), 1u);
+ helpers.Write(section.Data.AsSpan(rangeSectionLayout.Fields[nameof(Data.FunctionTableIndexRangeSection.MinFunctionTableIndex)].Offset, sizeof(uint)), minFunctionTableIndex);
+ helpers.Write(section.Data.AsSpan(rangeSectionLayout.Fields[nameof(Data.FunctionTableIndexRangeSection.NumRuntimeFunctions)].Offset, sizeof(uint)), (uint)functions.Length);
helpers.WritePointer(section.Data.AsSpan(rangeSectionLayout.Fields[nameof(Data.FunctionTableIndexRangeSection.R2RModule)].Offset, helpers.PointerSize), module.Address);
MockMemorySpace.HeapFragment listSlot = allocator.Allocate((ulong)helpers.PointerSize, "FunctionTableIndexRangeListSlot");
diff --git a/src/native/managed/cdac/tests/UnitTests/WasmUnwinderTests.cs b/src/native/managed/cdac/tests/UnitTests/WasmUnwinderTests.cs
index cd215f35489e87..2622df8713a3a7 100644
--- a/src/native/managed/cdac/tests/UnitTests/WasmUnwinderTests.cs
+++ b/src/native/managed/cdac/tests/UnitTests/WasmUnwinderTests.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers;
using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.Wasm;
using Microsoft.Diagnostics.DataContractReader.TestInfrastructure;
using Xunit;
@@ -27,6 +28,13 @@ private sealed class FakeWasmR2RInfo : IWasmR2RInfo
{
public Dictionary VirtualIpBases { get; } = new();
public Dictionary UnwindData { get; } = new();
+ public HashSet Funclets { get; } = new();
+
+ public bool TryIsFunclet(uint functionTableIndex, out bool isFunclet)
+ {
+ isFunclet = Funclets.Contains(functionTableIndex);
+ return VirtualIpBases.ContainsKey(functionTableIndex) || UnwindData.ContainsKey(functionTableIndex) || isFunclet;
+ }
public bool TryGetVirtualIPBase(uint functionTableIndex, out ulong baseVirtualIP)
=> VirtualIpBases.TryGetValue(functionTableIndex, out baseVirtualIP);
@@ -264,4 +272,295 @@ public void TryUnwindOneFrame_MalformedUleb128_Throws()
}
private const uint StackWalkSentinelIndirect = 0;
+
+ // ---------------------------------------------------------------------------------------
+ // Funclet stack walks over linear-stack layouts that mirror what the producers emit:
+ // * RyuJIT funclet prolog (genFuncletProlog, src/coreclr/jit/codegenwasm.cpp): an unwindable
+ // funclet moves its own $sp down by AlignUp(2 * pointer, STACK_ALIGN) (16 on wasm32), stores
+ // its own function table index at $sp[0], and its unwind blob encodes that 16-byte frame.
+ // * fgWasmVirtualIP (src/coreclr/jit/fgwasm.cpp): a funclet stores its virtual IP at $sp[4] of
+ // its own frame; virtual IPs are relative to the controlling method, so the funclet's table
+ // index resolves to the parent's base virtual IP.
+ // * genCallFinally (codegenwasm.cpp): a non-exceptional finally is called with the parent's
+ // current $sp and FP; WasmRegAlloc::AllocateFramePointer (regallocwasm.cpp) makes the
+ // funclet's FP local that parent FP.
+ // * genLclHeap (codegenwasm.cpp): after localloc, $sp[0] == STACK_WALK_INDIRECT_TO_FRAMEPOINTER
+ // and $sp[pointer] == FP.
+ // * CallFuncletWith[out]Throwable (src/coreclr/vm/wasm/helpers.cpp): the VM pushes a 16-byte
+ // frame holding TERMINATE_R2R_STACK_WALK at +0 and the establishing FP at
+ // TERMINATE_R2R_STACK_WALK_FP_OFFSET (one pointer), then calls the funclet with that $sp.
+ // Expected frame pointers follow native GetWasmFramePointerFromStackPointer, which reports a
+ // funclet's logical FP: the frame base of the establishing method.
+ // ---------------------------------------------------------------------------------------
+
+ private const ulong LinearStackBase = 0x0001_0000;
+ private const int LinearStackSize = 0x1000;
+ private const ulong ProducerMinVirtualIP = 0x8001_0000;
+ private const uint WasmFuncletFlag = 0x8000_0000;
+
+ // Function table indices are assigned consecutively per R2R module; a method's funclets
+ // immediately follow it, which is what lets a funclet index walk back to its parent.
+ private const uint ParentIndex = 100;
+ private const uint OuterFuncletIndex = 101;
+ private const uint InnerFuncletIndex = 102;
+ private const uint CalleeIndex = 103;
+
+ private const uint ParentBegin = 0x100;
+ private const uint CalleeBegin = 0x200;
+
+ private const uint ParentFrameSize = 0x30;
+ private const uint FuncletFrameSize = 16; // AlignUp(2 * TARGET_POINTER_SIZE, STACK_ALIGN) on wasm32
+ private const uint CalleeFrameSize = 0x20;
+
+ private const uint ParentVipHalf = 0x05;
+ private const uint OuterFuncletVipHalf = 0x21;
+ private const uint InnerFuncletVipHalf = 0x29;
+ private const uint CalleeVipHalf = 0x03;
+
+ private const ulong ParentIp = ProducerMinVirtualIP + ParentBegin + ParentVipHalf * 2;
+ private const ulong OuterFuncletIp = ProducerMinVirtualIP + ParentBegin + OuterFuncletVipHalf * 2;
+ private const ulong InnerFuncletIp = ProducerMinVirtualIP + ParentBegin + InnerFuncletVipHalf * 2;
+ private const ulong CalleeIp = ProducerMinVirtualIP + CalleeBegin + CalleeVipHalf * 2;
+
+ // The parent method's frame base, near the top of the modeled linear stack. Its own caller
+ // slot holds TERMINATE_R2R_STACK_WALK (e.g. it was entered from the interpreter).
+ private const ulong ParentFp = LinearStackBase + 0xF00;
+
+ private sealed class LinearStack
+ {
+ private readonly TargetTestHelpers _helpers = new(WasmArch);
+ public byte[] Data { get; } = new byte[LinearStackSize];
+
+ private Span At(ulong address, int length) => Data.AsSpan((int)(address - LinearStackBase), length);
+
+ public void Record(ulong address, uint functionIndex, uint vipHalf)
+ {
+ _helpers.Write(At(address, sizeof(uint)), functionIndex);
+ _helpers.Write(At(address + 4, sizeof(uint)), vipHalf);
+ }
+
+ public void Terminator(ulong address, ulong establishingFp = 0)
+ {
+ _helpers.Write(At(address, sizeof(uint)), 1u);
+ _helpers.WritePointer(At(address + (ulong)_helpers.PointerSize, _helpers.PointerSize), establishingFp);
+ }
+
+ public void LocallocSlot(ulong address, ulong framePointer)
+ {
+ _helpers.Write(At(address, sizeof(uint)), 0u);
+ _helpers.WritePointer(At(address + (ulong)_helpers.PointerSize, _helpers.PointerSize), framePointer);
+ }
+ }
+
+ // Parent root method, the two funclets it owns, and an unrelated callee root method, in one
+ // R2R module, registered through FunctionTableIndexRangeList exactly as the runtime does.
+ private static TestPlaceholderTarget CreateProducerLayoutTarget(LinearStack stack)
+ {
+ TestPlaceholderTarget.Builder targetBuilder = new(WasmArch);
+ TargetTestHelpers helpers = targetBuilder.MemoryBuilder.TargetTestHelpers;
+ MockMemorySpace.BumpAllocator allocator = targetBuilder.MemoryBuilder.CreateAllocator(0x0020_0000, 0x0020_4000);
+
+ (uint BeginAddress, uint FrameSize)[] functions =
+ [
+ (ParentBegin, ParentFrameSize),
+ (WasmFuncletFlag | (ParentBegin + 0x40), FuncletFrameSize),
+ (WasmFuncletFlag | (ParentBegin + 0x50), FuncletFrameSize),
+ (CalleeBegin, CalleeFrameSize),
+ ];
+
+ int hashMapStride = MockHashMap.CreateLayout(WasmArch).Size;
+ var moduleLayout = MockLoaderModule.CreateLayout(WasmArch);
+ var r2rInfoLayout = MockReadyToRunInfo.CreateLayout(WasmArch, hashMapStride, isWasm: true);
+ TargetTestHelpers.LayoutResult runtimeFunctionLayout = helpers.LayoutFields([
+ new(nameof(Data.RuntimeFunction.BeginAddress), DataType.uint32),
+ new(nameof(Data.RuntimeFunction.UnwindData), DataType.uint32),
+ ]);
+ TargetTestHelpers.LayoutResult rangeSectionLayout = helpers.LayoutFields([
+ new(nameof(Data.FunctionTableIndexRangeSection.MinFunctionTableIndex), DataType.uint32),
+ new(nameof(Data.FunctionTableIndexRangeSection.NumRuntimeFunctions), DataType.uint32),
+ new(nameof(Data.FunctionTableIndexRangeSection.R2RModule), DataType.pointer),
+ new(nameof(Data.FunctionTableIndexRangeSection.Next), DataType.pointer),
+ ]);
+
+ MockMemorySpace.HeapFragment runtimeFunctions = allocator.Allocate(runtimeFunctionLayout.Stride * (ulong)functions.Length, "RuntimeFunctions");
+ for (int i = 0; i < functions.Length; i++)
+ {
+ // Unwind data is the ULEB128 fixed frame size, addressed as LoadedImageBase (0) + UnwindData.
+ Assert.True(functions[i].FrameSize < 0x80);
+ MockMemorySpace.HeapFragment unwindData = allocator.Allocate(1, "UnwindData");
+ unwindData.Data[0] = (byte)functions[i].FrameSize;
+
+ Span entry = runtimeFunctions.Data.AsSpan(i * (int)runtimeFunctionLayout.Stride, (int)runtimeFunctionLayout.Stride);
+ helpers.Write(entry.Slice(runtimeFunctionLayout.Fields[nameof(Data.RuntimeFunction.BeginAddress)].Offset, sizeof(uint)), functions[i].BeginAddress);
+ helpers.Write(entry.Slice(runtimeFunctionLayout.Fields[nameof(Data.RuntimeFunction.UnwindData)].Offset, sizeof(uint)), (uint)unwindData.Address);
+ }
+
+ MockReadyToRunInfo r2rInfo = r2rInfoLayout.Create(allocator.Allocate((ulong)r2rInfoLayout.Size, "ReadyToRunInfo"));
+ r2rInfo.CompositeInfo = r2rInfo.Address;
+ r2rInfo.NumRuntimeFunctions = (uint)functions.Length;
+ r2rInfo.RuntimeFunctions = runtimeFunctions.Address;
+ r2rInfo.MinVirtualIP = ProducerMinVirtualIP;
+
+ MockLoaderModule module = moduleLayout.Create(allocator.Allocate((ulong)moduleLayout.Size, "Module"));
+ module.ReadyToRunInfo = r2rInfo.Address;
+
+ MockMemorySpace.HeapFragment section = allocator.Allocate(rangeSectionLayout.Stride, "FunctionTableIndexRangeSection");
+ helpers.Write(section.Data.AsSpan(rangeSectionLayout.Fields[nameof(Data.FunctionTableIndexRangeSection.MinFunctionTableIndex)].Offset, sizeof(uint)), ParentIndex);
+ helpers.Write(section.Data.AsSpan(rangeSectionLayout.Fields[nameof(Data.FunctionTableIndexRangeSection.NumRuntimeFunctions)].Offset, sizeof(uint)), (uint)functions.Length);
+ helpers.WritePointer(section.Data.AsSpan(rangeSectionLayout.Fields[nameof(Data.FunctionTableIndexRangeSection.R2RModule)].Offset, helpers.PointerSize), module.Address);
+
+ MockMemorySpace.HeapFragment listSlot = allocator.Allocate((ulong)helpers.PointerSize, "FunctionTableIndexRangeListSlot");
+ helpers.WritePointer(listSlot.Data.AsSpan(), section.Address);
+
+ targetBuilder.MemoryBuilder.AddHeapFragment(new MockMemorySpace.HeapFragment { Address = LinearStackBase, Data = stack.Data, Name = "LinearStack" });
+ targetBuilder
+ .AddTypes(new Dictionary
+ {
+ [DataType.RuntimeFunction] = new() { Fields = runtimeFunctionLayout.Fields, Size = runtimeFunctionLayout.Stride },
+ [DataType.ReadyToRunInfo] = TargetTestHelpers.CreateTypeInfo(r2rInfoLayout),
+ [DataType.Module] = TargetTestHelpers.CreateTypeInfo(moduleLayout),
+ [DataType.FunctionTableIndexRangeSection] = new() { Fields = rangeSectionLayout.Fields, Size = rangeSectionLayout.Stride },
+ })
+ .AddGlobals((Constants.Globals.FunctionTableIndexRangeList, listSlot.Address));
+ return targetBuilder.Build();
+ }
+
+ // The walk starts in a root method called from inside the innermost funclet (for example
+ // a breakpoint or Debugger.Break in a method called from a catch/finally body).
+ private static WasmContext CalleeContext(ulong calleeSp) => new()
+ {
+ StackPointer = new TargetPointer(calleeSp),
+ InstructionPointer = new TargetCodePointer(CalleeIp),
+ FramePointer = new TargetPointer(calleeSp),
+ };
+
+ private static void AssertFrame(WasmContext context, ulong sp, ulong ip, ulong fp, string frame)
+ {
+ Assert.True(sp == context.StackPointer.Value, $"{frame}: SP 0x{context.StackPointer.Value:x}, expected 0x{sp:x}");
+ Assert.True(ip == context.InstructionPointer.Value, $"{frame}: IP 0x{context.InstructionPointer.Value:x}, expected 0x{ip:x}");
+ Assert.True(fp == context.FramePointer.Value, $"{frame}: FP 0x{context.FramePointer.Value:x}, expected 0x{fp:x}");
+ }
+
+ // The walk leaves R2R code at a TERMINATE_R2R_STACK_WALK frame; no R2R caller is reported.
+ private static void AssertLeftR2R(WasmContext context)
+ => Assert.Equal(TargetCodePointer.Null, context.InstructionPointer);
+
+ ///
+ /// Non-exceptional finally: the parent calls the finally funclet directly (genCallFinally)
+ /// with its own $sp and FP, so the funclet's 16-byte frame sits immediately below the parent's
+ /// $sp. When , the parent's $sp is a localloc slot that
+ /// indirects to its FP, and the funclet frame sits below that slot.
+ ///
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void Unwind_ProducerLayout_FinallyCalledByParent_ReportsParentFramePointer(bool parentUsedLocalloc)
+ {
+ ulong parentSp = parentUsedLocalloc ? ParentFp - 0x40 : ParentFp;
+ ulong funcletSp = parentSp - FuncletFrameSize;
+ ulong calleeSp = funcletSp - CalleeFrameSize;
+
+ LinearStack stack = new();
+ stack.Terminator(ParentFp + ParentFrameSize);
+ stack.Record(ParentFp, ParentIndex, ParentVipHalf);
+ if (parentUsedLocalloc)
+ stack.LocallocSlot(parentSp, ParentFp);
+ stack.Record(funcletSp, OuterFuncletIndex, OuterFuncletVipHalf);
+ stack.Record(calleeSp, CalleeIndex, CalleeVipHalf);
+ Target target = CreateProducerLayoutTarget(stack);
+
+ WasmContext context = CalleeContext(calleeSp);
+
+ context.Unwind(target);
+ AssertFrame(context, funcletSp, OuterFuncletIp, ParentFp, "finally funclet");
+
+ context.Unwind(target);
+ AssertFrame(context, parentSp, ParentIp, ParentFp, "parent");
+
+ context.Unwind(target);
+ AssertLeftR2R(context);
+ }
+
+ ///
+ /// Catch, finally, fault or filter invoked by the VM (EECodeManager::CallFunclet ->
+ /// CallFuncletWith[out]Throwable). The funclet's frame sits directly below the synthetic
+ /// TERMINATE_R2R_STACK_WALK frame, which carries the establishing FP; the establishing frame
+ /// itself is far above, past native VM frames. A filter runs during the first pass, so the
+ /// R2R frames of the try body that threw are still live between the establishing frame and
+ /// the terminator (); they must not be visited.
+ ///
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void Unwind_ProducerLayout_FuncletInvokedByVM_ReportsEstablishingFramePointer(bool throwingFramesStillLive)
+ {
+ ulong terminatorSp = LinearStackBase + 0x800;
+ ulong funcletSp = terminatorSp - FuncletFrameSize;
+ ulong calleeSp = funcletSp - CalleeFrameSize;
+
+ LinearStack stack = new();
+ stack.Terminator(ParentFp + ParentFrameSize);
+ stack.Record(ParentFp, ParentIndex, ParentVipHalf);
+ if (throwingFramesStillLive)
+ stack.Record(ParentFp - CalleeFrameSize, CalleeIndex, CalleeVipHalf);
+ stack.Terminator(terminatorSp, ParentFp);
+ stack.Record(funcletSp, OuterFuncletIndex, OuterFuncletVipHalf);
+ stack.Record(calleeSp, CalleeIndex, CalleeVipHalf);
+ Target target = CreateProducerLayoutTarget(stack);
+
+ WasmContext context = CalleeContext(calleeSp);
+
+ context.Unwind(target);
+ AssertFrame(context, funcletSp, OuterFuncletIp, ParentFp, "VM-invoked funclet");
+
+ context.Unwind(target);
+ AssertLeftR2R(context);
+ }
+
+ ///
+ /// A finally nested in another funclet (for example try/finally inside a catch) is called
+ /// directly by the outer funclet with the outer funclet's $sp and its FP local, which is
+ /// already the establishing FP. Both funclets report the establishing method's frame base,
+ /// whether the outer funclet was called by the parent or invoked by the VM.
+ ///
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void Unwind_ProducerLayout_NestedFinally_ReportsEstablishingFramePointer(bool outerInvokedByVM)
+ {
+ ulong outerCallerSp = outerInvokedByVM ? LinearStackBase + 0x800 : ParentFp;
+ ulong outerSp = outerCallerSp - FuncletFrameSize;
+ ulong innerSp = outerSp - FuncletFrameSize;
+ ulong calleeSp = innerSp - CalleeFrameSize;
+
+ LinearStack stack = new();
+ stack.Terminator(ParentFp + ParentFrameSize);
+ stack.Record(ParentFp, ParentIndex, ParentVipHalf);
+ if (outerInvokedByVM)
+ stack.Terminator(outerCallerSp, ParentFp);
+ stack.Record(outerSp, OuterFuncletIndex, OuterFuncletVipHalf);
+ stack.Record(innerSp, InnerFuncletIndex, InnerFuncletVipHalf);
+ stack.Record(calleeSp, CalleeIndex, CalleeVipHalf);
+ Target target = CreateProducerLayoutTarget(stack);
+
+ WasmContext context = CalleeContext(calleeSp);
+
+ context.Unwind(target);
+ AssertFrame(context, innerSp, InnerFuncletIp, ParentFp, "inner finally");
+
+ context.Unwind(target);
+ AssertFrame(context, outerSp, OuterFuncletIp, ParentFp, "outer funclet");
+
+ context.Unwind(target);
+ if (outerInvokedByVM)
+ {
+ AssertLeftR2R(context);
+ }
+ else
+ {
+ AssertFrame(context, ParentFp, ParentIp, ParentFp, "parent");
+ context.Unwind(target);
+ AssertLeftR2R(context);
+ }
+ }
}