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
44 changes: 44 additions & 0 deletions docs/design/datacontracts/StackWalk.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,19 @@ TargetPointer GetFuncletRootId(
// Returns the exact generic instantiation context token for the current frameless managed frame,
// or TargetPointer.Null if it can't be recovered.
TargetPointer GetExactGenericArgsToken(IStackDataFrameHandle stackDataFrameHandle);

// WASM only: identifies the WebAssembly function of a ReadyToRun frameless frame.
WasmFunctionIdentity GetWasmFunctionIdentity(IStackDataFrameHandle stackDataFrameHandle);
```

```csharp
public readonly struct WasmFunctionIdentity
{
public uint FunctionTableIndex { get; init; } // raw runtime-global function-table index in the frame record
public TargetPointer? Module { get; init; } // module that registered the R2R image; null if unregistered
public uint? RuntimeFunctionIndex { get; init; } // RUNTIME_FUNCTION index within the image; null if unregistered
public bool? IsFunclet { get; init; } // null if unregistered
}
```

## Version 1
Expand Down Expand Up @@ -707,6 +720,37 @@ If no Frame in the chain produces a usable context (thread is not running manage

`GetFuncletRootId` returns the caller stack pointer for a non-funclet frame and sets `parentNativeOffset` to zero. For a funclet, it performs a secondary stack walk that skips intervening funclets and returns the caller stack pointer and relative native offset of the parent method frame. If that parent cannot be located because it has already been unwound, the method returns the current frame's caller stack pointer and sets `parentNativeOffset` to zero.

`GetWasmFunctionIdentity` reads the function-table index from the WASM R2R frame record at the frame's SP and resolves it through `FunctionTableIndexRangeList` to the module that registered the image, the `RUNTIME_FUNCTION` index within the image, and whether that function is a funclet (bit 31 of `BeginAddress`). For a composite image the module is the one that loaded the image, not necessarily the method's own module. When the index is not in a registered range, only `FunctionTableIndex` is set. It throws `PlatformNotSupportedException` on other architectures, and `InvalidOperationException` unless the frame is `Frameless` with `CodeKind.ReadyToRun`, because native markers and interpreter frames can hold bytes that resemble a frame record. A WebAssembly engine's function index is module-local, so a consumer maps `Module` and `RuntimeFunctionIndex` through the image's element section rather than using the raw function-table index.

```csharp
WasmFunctionIdentity GetWasmFunctionIdentity(IStackDataFrameHandle frame)
{
if (RuntimeInfo.GetTargetArchitecture() != RuntimeInfoArchitecture.Wasm)
throw new PlatformNotSupportedException();
if (frame.State != StackWalkState.Frameless || ExecutionManager.GetCodeKind(frame.Context.IP) != CodeKind.ReadyToRun)
throw new InvalidOperationException();

TargetPointer frameBase = /* frame base of the R2R frame at frame.Context.SP; InvalidOperationException if none */;
uint index = target.Read<uint>(frameBase + FunctionIndexOffset);

// Walk FunctionTableIndexRangeList for the section with
// MinFunctionTableIndex <= index < MinFunctionTableIndex + NumRuntimeFunctions.
FunctionTableIndexRangeSection? section = /* lookup */;
if (section is null)
return new WasmFunctionIdentity { FunctionTableIndex = index };

uint runtimeFunctionIndex = index - section.MinFunctionTableIndex;
RuntimeFunction function = /* section.R2RModule's ReadyToRunInfo RUNTIME_FUNCTION table */[runtimeFunctionIndex];
return new WasmFunctionIdentity
{
FunctionTableIndex = index,
Module = section.R2RModule,
RuntimeFunctionIndex = runtimeFunctionIndex,
IsFunclet = (function.BeginAddress & 0x80000000) != 0,
};
}
```

`GetExactGenericArgsToken` recovers the exact generic instantiation context for the current frameless managed frame, mirroring native `CrawlFrame::GetExactGenericArgsToken`. It returns `TargetPointer.Null` unless the frame is `Frameless`, has a `MethodDesc`, and that method is shared by generic instantiations (`GetGenericContextLoc != None`). When applicable it:

1. Selects the platform GC-info decoder, or the interpreter GC-info decoder for `CodeKind.Interpreter`, and requests the location through `IGCInfo.TryGetGenericContextStorage`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,30 @@ public record struct DebuggerEvalData(
uint MethodToken,
TargetPointer AssemblyPtr);

/// <summary>
/// Identifies the WebAssembly function represented by a ReadyToRun stack frame.
/// </summary>
/// <remarks>
/// <see cref="FunctionTableIndex"/> is the raw runtime-global function-table index stored in the
/// frame. A WebAssembly engine's function index is module-local, so consumers must use
/// <see cref="Module"/> and <see cref="RuntimeFunctionIndex"/> with the image's element section to
/// find the engine function; the raw index is not an engine function index.
/// </remarks>
public readonly struct WasmFunctionIdentity
{
/// <summary>The raw runtime-global function-table index stored in the frame.</summary>
public uint FunctionTableIndex { get; init; }
/// <summary>
/// The module that registered the R2R image containing the function, or null when the index is
/// not registered. For a composite image this is the module that loaded the image.
/// </summary>
public TargetPointer? Module { get; init; }
/// <summary>The RUNTIME_FUNCTION index within the image, or null when unresolved.</summary>
public uint? RuntimeFunctionIndex { get; init; }
/// <summary>Whether the function is a funclet, or null when unresolved.</summary>
public bool? IsFunclet { get; init; }
}

public interface IStackWalk : IContract
{
static string IContract.Name => nameof(StackWalk);
Expand All @@ -106,6 +130,7 @@ public interface IStackWalk : IContract
byte[] GetContext(ThreadData threadData, ThreadContextSource contextSource, uint contextFlags) => throw new NotImplementedException();
TargetPointer GetFuncletRootId(IStackDataFrameHandle stackDataFrameHandle, out uint parentNativeOffset) => throw new NotImplementedException();
TargetPointer GetExactGenericArgsToken(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException();
WasmFunctionIdentity GetWasmFunctionIdentity(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException();
}

public struct StackWalk : IStackWalk
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,30 @@ public bool TryGetVirtualIPBase(uint functionTableIndex, out ulong baseVirtualIP

// Mirrors ExecutionManager::IsFuncletFunctionIndex.
public bool TryIsFunclet(uint functionTableIndex, out bool isFunclet)
=> TryGetFunctionIdentity(functionTableIndex, out _, out _, out isFunclet);

/// <summary>
/// Resolves a function-table index to the module that registered its R2R image, the
/// RUNTIME_FUNCTION index within that image, and whether it is a funclet. For a composite
/// image the module is the one that loaded the image, not necessarily the method's module.
/// </summary>
public bool TryGetFunctionIdentity(
uint functionTableIndex,
out TargetPointer module,
out uint runtimeFunctionIndex,
out bool isFunclet)
{
module = TargetPointer.Null;
runtimeFunctionIndex = 0;
isFunclet = false;
Data.FunctionTableIndexRangeSection? section = FindSection(functionTableIndex);
if (section is null)
return false;

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,11 @@ public bool TryGetUnwindData(uint functionTableIndex, out TargetPointer unwindDa

public bool TryIsFunclet(uint functionTableIndex, out bool isFunclet)
=> _lookup.TryIsFunclet(functionTableIndex, out isFunclet);

public bool TryGetFunctionIdentity(
uint functionTableIndex,
out TargetPointer module,
out uint runtimeFunctionIndex,
out bool isFunclet)
=> _lookup.TryGetFunctionIdentity(functionTableIndex, out module, out runtimeFunctionIndex, out isFunclet);
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,20 @@ public TargetCodePointer GetVirtualIP(TargetPointer sp)
return new TargetCodePointer(baseVirtualIP + functionLocalVirtualIP);
}

/// <summary>
/// Returns the function-table index recorded in the R2R frame at <paramref name="sp"/>.
/// Returns false when there is no R2R frame there.
/// </summary>
public bool TryGetFunctionIndex(TargetPointer sp, out uint functionIndex)
{
functionIndex = 0;
if (!TryGetFramePointer(sp, out TargetPointer frameBase))
return false;

functionIndex = _target.Read<uint>(frameBase.Value + FunctionIndexOffset);
return true;
}

/// <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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Reflection.Metadata.Ecma335;
using System.Runtime.InteropServices;
using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers;
using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.Wasm;
using Microsoft.Diagnostics.DataContractReader.Data;
using System.Linq;

Expand Down Expand Up @@ -1111,6 +1112,36 @@ TargetPointer IStackWalk.GetContextFramePointer(IStackDataFrameHandle stackDataF
return handle.Context.FramePointer;
}

WasmFunctionIdentity IStackWalk.GetWasmFunctionIdentity(IStackDataFrameHandle stackDataFrameHandle)
{
if (_target.Contracts.RuntimeInfo.GetTargetArchitecture() != RuntimeInfoArchitecture.Wasm)
throw new PlatformNotSupportedException("WASM function identity is only available on WASM targets.");

StackDataFrameHandle handle = AssertCorrectHandle(stackDataFrameHandle);
// Native markers and interpreter frames can hold bytes that resemble an R2R frame record,
// so reject them before reading the stack.
if (handle.State != StackWalkState.Frameless ||
_eman.GetCodeKind(handle.Context.InstructionPointer) != CodeKind.ReadyToRun)
{
throw new InvalidOperationException("WASM function identity is only available for ReadyToRun frameless frames.");
}

WasmR2RInfo r2rInfo = new(_target);
if (!new WasmUnwinder(_target, r2rInfo).TryGetFunctionIndex(handle.Context.StackPointer, out uint functionTableIndex))
throw new InvalidOperationException("The stack frame does not contain a WASM R2R function-table index.");

if (!r2rInfo.TryGetFunctionIdentity(functionTableIndex, out TargetPointer module, out uint runtimeFunctionIndex, out bool isFunclet))
return new WasmFunctionIdentity { FunctionTableIndex = functionTableIndex };

return new WasmFunctionIdentity
{
FunctionTableIndex = functionTableIndex,
Module = module,
RuntimeFunctionIndex = runtimeFunctionIndex,
IsFunclet = isFunclet,
};
}

TargetPointer IStackWalk.GetStackPointer(IStackDataFrameHandle stackDataFrameHandle)
{
StackDataFrameHandle handle = AssertCorrectHandle(stackDataFrameHandle);
Expand Down
111 changes: 109 additions & 2 deletions src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1436,6 +1436,111 @@ public void UpdateContextFromFrame_WasmFuncletCaller_ReportsEstablishingFramePoi
// 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.
/// <summary>
/// <c>GetWasmFunctionIdentity</c> resolves each ReadyToRun frame of a real walk to the module that
/// registered the image, the image-relative RUNTIME_FUNCTION index, and the funclet flag. It
/// rejects interpreter frames, whose stack bytes are not R2R frame records.
/// </summary>
[Fact]
public void GetWasmFunctionIdentity_WalkedFrames_ResolveModuleRuntimeFunctionAndFunclet()
{
MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false };
const ulong InterpIp = 0x0005_1000;
const uint ParentIndex = 5;
const uint FuncletIndex = 6;
const ulong MinVirtualIP = 0x8001_0000;
const uint ParentBegin = 0x100;
const uint FuncletVipHalf = 0x21;
const uint ParentVipHalf = 0x05;
const byte FuncletFrameSize = 16;
const ulong FuncletIp = MinVirtualIP + ParentBegin + FuncletVipHalf * 2;
const ulong ParentIp = MinVirtualIP + ParentBegin + ParentVipHalf * 2;

MockThread? thread = null;
MockFrameBuilder? frames = null;
ulong imcf = 0;
ulong funcletFrame = 0;
TestPlaceholderTarget target = CreateTarget(
wasmArch,
threadBuilder => thread = threadBuilder.AddThread(1, 1234),
frameBuilder =>
{
frames = frameBuilder;
MockInterpMethodContextFrame interpFrame = frameBuilder.AddInterpMethodContextFrame(parentPtr: 0, ip: InterpIp, stack: 0x0006_1000);
// The interpreter frame's SP is this InterpMethodContextFrame. Make its first word a
// registered R2R function-table index so only the code-kind check rejects it.
interpFrame.StartIp = ParentIndex;
imcf = interpFrame.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);
AddWasmR2RFunctions(targetBuilder, allocator, ParentIndex, MinVirtualIP,
[(ParentBegin, 0x30), (0x8000_0000 | (ParentBegin + 0x40), FuncletFrameSize)]);
AddWasmNullDebugger(targetBuilder, allocator);

// A finally funclet called by its parent: funclet record, parent record, then a terminator.
MockMemorySpace.HeapFragment stack = allocator.Allocate(FuncletFrameSize + 0x30 + 4, "R2RLinearStack");
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)), ParentIndex);
helpers.Write(stack.Data.AsSpan(FuncletFrameSize + 4, sizeof(uint)), ParentVipHalf);
helpers.Write(stack.Data.AsSpan(FuncletFrameSize + 0x30, sizeof(uint)), 1u);
funcletFrame = stack.Address;

ulong transitionBlock = AddWasmTransitionBlock(helpers, allocator, returnAddress: 0, stackPointer: funcletFrame);

Layout<MockFramedMethodFrame> fmfLayout = frames!.FramedMethodFrameLayout;
int topOffset = fmfLayout.Size;
int isFaultingOffset = topOffset + pointerSize;
Dictionary<string, Target.FieldInfo> 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), imcf);
thread!.Frame = interpreterFrame.Address;

targetBuilder
.AddTypes(CreateWasmTransitionBlockTypes(pointerSize))
.AddTypes(new Dictionary<DataType, Target.TypeInfo>
{
[DataType.InterpreterFrame] = new() { Fields = interpreterFrameFields, Size = (uint)(isFaultingOffset + pointerSize) },
});
},
executionManager: CreateInterpreterExecutionManager([InterpIp], managedIps: [FuncletIp, ParentIp], readyToRunIps: [FuncletIp, ParentIp]));

IStackWalk stackWalk = target.Contracts.StackWalk;
ThreadData threadData = target.Contracts.Thread.GetThreadData(new TargetPointer(thread!.Address));
IStackDataFrameHandle[] frameless = stackWalk.CreateStackWalk(threadData).Take(16)
.Where(f => f.State == StackWalkState.Frameless)
.ToArray();
Assert.Equal([InterpIp, FuncletIp, ParentIp], frameless.Select(f => stackWalk.GetInstructionPointer(f).Value));

Assert.Throws<InvalidOperationException>(() => stackWalk.GetWasmFunctionIdentity(frameless[0]));

WasmFunctionIdentity funclet = stackWalk.GetWasmFunctionIdentity(frameless[1]);
WasmFunctionIdentity parent = stackWalk.GetWasmFunctionIdentity(frameless[2]);

Assert.Equal(FuncletIndex, funclet.FunctionTableIndex);
Assert.Equal(1u, funclet.RuntimeFunctionIndex);
Assert.True(funclet.IsFunclet);
Assert.Equal(ParentIndex, parent.FunctionTableIndex);
Assert.Equal(0u, parent.RuntimeFunctionIndex);
Assert.False(parent.IsFunclet);
Assert.NotNull(funclet.Module);
Assert.NotEqual(TargetPointer.Null, funclet.Module.Value);
Assert.Equal(funclet.Module, parent.Module);
}

[Fact]
public void CreateStackWalk_WasmInterpreterFrameEnteredFromR2R_ContinuesIntoR2RCaller()
{
Expand Down Expand Up @@ -1525,7 +1630,7 @@ public void CreateStackWalk_WasmInterpreterFrameEnteredFromR2R_ContinuesIntoR2RC
Assert.True(walked.Length <= 7, $"Walk did not terminate: {walked.Length} frames");
}

private static IExecutionManager CreateInterpreterExecutionManager(ulong[] interpreterIps, ulong[]? managedIps = null)
private static IExecutionManager CreateInterpreterExecutionManager(ulong[] interpreterIps, ulong[]? managedIps = null, ulong[]? readyToRunIps = null)
{
Mock<IExecutionManager> executionManager = new();
executionManager
Expand All @@ -1535,7 +1640,9 @@ private static IExecutionManager CreateInterpreterExecutionManager(ulong[] inter
: null);
executionManager
.Setup(em => em.GetCodeKind(It.IsAny<TargetCodePointer>()))
.Returns((TargetCodePointer ip) => interpreterIps.Contains(ip.Value) ? CodeKind.Interpreter : default);
.Returns((TargetCodePointer ip) => interpreterIps.Contains(ip.Value)
? CodeKind.Interpreter
: (readyToRunIps?.Contains(ip.Value) ?? false) ? CodeKind.ReadyToRun : default);
return executionManager.Object;
}

Expand Down
Loading
Loading