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
50 changes: 50 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 that contains the IP of a ReadyToRun frameless frame.
WasmFunctionIdentity GetWasmFunctionIdentity(IStackDataFrameHandle stackDataFrameHandle);
```

```csharp
public readonly struct WasmFunctionIdentity
{
public uint FunctionTableIndex { get; init; } // runtime-global function-table index of the function
public TargetPointer Module { get; init; } // module that registered the R2R image containing the function
public uint RuntimeFunctionIndex { get; init; } // RUNTIME_FUNCTION index of the function within its image
public bool IsFunclet { get; init; } // a funclet is a function of its own, distinct from its method's root
}
```

## Version 1
Expand Down Expand Up @@ -719,6 +732,43 @@ 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` resolves the WebAssembly function that contains the frame's instruction pointer. `IExecutionManager.TryGetUnwindInfo` maps the virtual IP to the `RUNTIME_FUNCTION` of that function, which is the funclet's own entry when the IP is in a funclet. The entry's address is then matched against the `RUNTIME_FUNCTION` table of each image registered in `FunctionTableIndexRangeList`, giving the module that registered the image, the entry's index within the image, its runtime-global function-table index (`MinFunctionTableIndex` plus that index), and whether it is a funclet (bit 31 of `BeginAddress`). The result does not depend on how the frame record encodes its identity. For a composite image the module is the one that loaded the image, not necessarily the method's own module. It throws `PlatformNotSupportedException` on other architectures, and `InvalidOperationException` unless the frame is `Frameless` and its IP belongs to a registered R2R image (interpreter code has no unwind info). 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 function-table index.

```csharp
WasmFunctionIdentity GetWasmFunctionIdentity(IStackDataFrameHandle frame)
{
if (RuntimeInfo.GetTargetArchitecture() != RuntimeInfoArchitecture.Wasm)
throw new PlatformNotSupportedException();
if (frame.State != StackWalkState.Frameless)
throw new InvalidOperationException();

// The virtual IP selects the function's own RUNTIME_FUNCTION, including a funclet's.
if (!ExecutionManager.TryGetUnwindInfo(frame.Context.IP, out TargetPointer runtimeFunction, out _))
throw new InvalidOperationException();

// Find the registered image whose RUNTIME_FUNCTION table contains the entry.
foreach (FunctionTableIndexRangeSection section in FunctionTableIndexRangeList)
{
ReadyToRunInfo image = section.R2RModule.ReadyToRunInfo;
uint offset = runtimeFunction - image.RuntimeFunctions;
if (runtimeFunction >= image.RuntimeFunctions && offset % sizeof(RuntimeFunction) == 0
&& offset / sizeof(RuntimeFunction) < image.NumRuntimeFunctions)
{
uint index = offset / sizeof(RuntimeFunction);
return new WasmFunctionIdentity
{
FunctionTableIndex = section.MinFunctionTableIndex + index,
Module = section.R2RModule,
RuntimeFunctionIndex = index,
IsFunclet = (image.RuntimeFunctions[index].BeginAddress & 0x80000000) != 0,
};
}
}

throw new InvalidOperationException();
}
```

`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 that contains the instruction pointer of a ReadyToRun stack frame.
/// </summary>
/// <remarks>
/// A WebAssembly engine's function index is module-local, so a consumer uses <see cref="Module"/>
/// and <see cref="RuntimeFunctionIndex"/> with the image's element section to find the engine
/// function. <see cref="FunctionTableIndex"/> is the runtime-global index of the same function.
/// A funclet is a function of its own, distinct from its method's root function.
/// </remarks>
public readonly struct WasmFunctionIdentity
{
/// <summary>The runtime-global function-table index of the function.</summary>
public uint FunctionTableIndex { get; init; }
/// <summary>
/// The module that registered the R2R image containing the function. For a composite image this
/// is the module that loaded the image, not necessarily the method's own module.
/// </summary>
public TargetPointer Module { get; init; }
/// <summary>The RUNTIME_FUNCTION index of the function within its image.</summary>
public uint RuntimeFunctionIndex { get; init; }
/// <summary>Whether the function is a funclet rather than a method's root function.</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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the need for this new API?

We currently have APIs to access the Module, check if a code region is a funclet, and get the RUNTIME_FUNCTION address. If there is a reason we need the index, I believe that should be done in IExecutionManager.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, it isn't needed. The pieces already exist: GetCodeBlockHandle, IsFunclet, GetUnwindInfo / TryGetUnwindInfo for the RUNTIME_FUNCTION, and the module from the frame's MethodDesc. The CDP consumer that motivated this matches cDAC frames to engine frames by stack pointer and already decodes R2R RUNTIME_FUNCTION indices on its own, so it doesn't use this API either. Closing this PR. If an image-relative index turns out to be needed later, it will go in IExecutionManager as you suggest.

Note

This reply was generated with GitHub Copilot.

Auto-replied by the GitHub Copilot app

}

public struct StackWalk : IStackWalk
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,51 @@ public bool TryGetCoreLibVirtualIPBase(uint runtimeFunctionIndex, out ulong base
return false;
}

/// <summary>
/// Maps a <c>RUNTIME_FUNCTION</c> address to the registered image that contains it, returning
/// the registering module, the entry's index within the image, its function-table index, and
/// whether it is a funclet.
/// </summary>
public bool TryGetFunctionIdentity(
TargetPointer runtimeFunctionAddress,
out TargetPointer module,
out uint runtimeFunctionIndex,
out uint functionTableIndex,
out bool isFunclet)
{
module = TargetPointer.Null;
runtimeFunctionIndex = 0;
functionTableIndex = 0;
isFunclet = false;
if (!_target.TryReadGlobalPointer(Constants.Globals.FunctionTableIndexRangeList, out TargetPointer? listHeadSlot))
return false;

ulong entrySize = Data.RuntimeFunction.GetSize(_target);
TargetPointer current = _target.ReadPointer(listHeadSlot.Value);
HashSet<TargetPointer> visited = new();
while (current != TargetPointer.Null)
{
if (visited.Count == MaxFunctionTableIndexRangeNodes || !visited.Add(current))
return false;

Data.FunctionTableIndexRangeSection section = _target.ProcessedData.GetOrAdd<Data.FunctionTableIndexRangeSection>(current);
Data.ReadyToRunInfo r2rInfo = GetReadyToRunInfo(section);
if (runtimeFunctionAddress >= r2rInfo.RuntimeFunctions)
{
ulong offset = runtimeFunctionAddress.Value - r2rInfo.RuntimeFunctions.Value;
if (offset % entrySize == 0 && offset / entrySize < r2rInfo.NumRuntimeFunctions)
{
runtimeFunctionIndex = (uint)(offset / entrySize);
functionTableIndex = section.MinFunctionTableIndex + runtimeFunctionIndex;
module = section.R2RModule;
isFunclet = _runtimeFunctions.IsFunclet(GetRuntimeFunction(r2rInfo, runtimeFunctionIndex));
return true;
}
}

current = section.Next;
}

return false;
}
}
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.ExecutionManagerHelpers;
using Microsoft.Diagnostics.DataContractReader.Data;
using System.Linq;

Expand Down Expand Up @@ -1111,6 +1112,37 @@ 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);
if (handle.State != StackWalkState.Frameless)
throw new InvalidOperationException("WASM function identity is only available for frameless frames.");

// The virtual IP selects the function's own RUNTIME_FUNCTION, including a funclet's.
// Interpreter code has no unwind info, so it is rejected here.
if (!_eman.TryGetUnwindInfo(handle.Context.InstructionPointer, out TargetPointer runtimeFunction, out _) ||
!new WasmFunctionTableIndexLookup(_target).TryGetFunctionIdentity(
runtimeFunction,
out TargetPointer module,
out uint runtimeFunctionIndex,
out uint functionTableIndex,
out bool isFunclet))
{
throw new InvalidOperationException("The instruction pointer does not belong to a registered WASM R2R image.");
}

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

TargetPointer IStackWalk.GetStackPointer(IStackDataFrameHandle stackDataFrameHandle)
{
StackDataFrameHandle handle = AssertCorrectHandle(stackDataFrameHandle);
Expand Down
Loading
Loading