diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md
index 1923e4ae9db961..1fcd7457d5bce3 100644
--- a/docs/design/datacontracts/StackWalk.md
+++ b/docs/design/datacontracts/StackWalk.md
@@ -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
@@ -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`.
diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs
index f86ca12ee4c699..86845bb519229c 100644
--- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs
+++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs
@@ -84,6 +84,30 @@ public record struct DebuggerEvalData(
uint MethodToken,
TargetPointer AssemblyPtr);
+///
+/// Identifies the WebAssembly function that contains the instruction pointer of a ReadyToRun stack frame.
+///
+///
+/// A WebAssembly engine's function index is module-local, so a consumer uses
+/// and with the image's element section to find the engine
+/// function. is the runtime-global index of the same function.
+/// A funclet is a function of its own, distinct from its method's root function.
+///
+public readonly struct WasmFunctionIdentity
+{
+ /// The runtime-global function-table index of the function.
+ public uint FunctionTableIndex { get; init; }
+ ///
+ /// 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.
+ ///
+ public TargetPointer Module { get; init; }
+ /// The RUNTIME_FUNCTION index of the function within its image.
+ public uint RuntimeFunctionIndex { get; init; }
+ /// Whether the function is a funclet rather than a method's root function.
+ public bool IsFunclet { get; init; }
+}
+
public interface IStackWalk : IContract
{
static string IContract.Name => nameof(StackWalk);
@@ -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
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 fa3d7039d57aeb..959ceadce212e6 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
@@ -156,4 +156,51 @@ public bool TryGetCoreLibVirtualIPBase(uint runtimeFunctionIndex, out ulong base
return false;
}
+ ///
+ /// Maps a RUNTIME_FUNCTION 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.
+ ///
+ 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 visited = new();
+ while (current != TargetPointer.Null)
+ {
+ if (visited.Count == MaxFunctionTableIndexRangeNodes || !visited.Add(current))
+ return false;
+
+ Data.FunctionTableIndexRangeSection section = _target.ProcessedData.GetOrAdd(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;
+ }
}
diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs
index 46f53022ce29a2..fbccc2913d58d0 100644
--- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs
+++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs
@@ -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;
@@ -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);
diff --git a/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs b/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs
index db514222517802..2572b9b90ae07e 100644
--- a/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs
+++ b/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs
@@ -1546,20 +1546,136 @@ public void CreateStackWalk_WasmInterpreterFrameEnteredFromR2R_ContinuesIntoR2RC
Assert.True(walked.Length <= 7, $"Walk did not terminate: {walked.Length} frames");
}
+ // GetWasmFunctionIdentity resolves the function containing each ReadyToRun frame's instruction
+ // pointer, including a funclet's own function, whichever form the root frame identity takes. It
+ // rejects interpreter frames.
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void GetWasmFunctionIdentity_WalkedFrames_ResolveModuleRuntimeFunctionAndFunclet(bool reversePInvokeRoot)
+ {
+ MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false };
+ const ulong InterpIp = 0x0005_1000;
+ const uint ParentIndex = 5;
+ const ulong MinVirtualIP = 0x8001_0000;
+ const uint ParentBegin = 0x100;
+ const uint FuncletVipHalf = 0x21;
+ const uint ParentVipHalf = 0x05;
+ const byte ParentFrameSize = 0x30;
+ const byte FuncletFrameSize = 16;
+ const uint RuntimeFunctionSize = 8;
+ const ulong FuncletIp = MinVirtualIP + ParentBegin + FuncletVipHalf * 2;
+ const ulong ParentIp = MinVirtualIP + ParentBegin + ParentVipHalf * 2;
+
+ MockThread? thread = null;
+ MockFrameBuilder? frames = null;
+ ulong imcf = 0;
+ ulong runtimeFunctions = 0;
+ // The last slot is filled with the Frame handle's IP once it is known, so that only the
+ // frame-state check rejects that handle.
+ ulong[] managedIps = [FuncletIp, ParentIp, 0];
+ TestPlaceholderTarget target = CreateTarget(
+ wasmArch,
+ threadBuilder => thread = threadBuilder.AddThread(1, 1234),
+ frameBuilder =>
+ {
+ frames = frameBuilder;
+ imcf = frameBuilder.AddInterpMethodContextFrame(parentPtr: 0, ip: InterpIp, 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);
+ runtimeFunctions = AddWasmR2RFunctions(targetBuilder, allocator, ParentIndex, MinVirtualIP,
+ [(ParentBegin, ParentFrameSize), (0x8000_0000 | (ParentBegin + 0x40), FuncletFrameSize)]).Value;
+ uint parentIdentity = reversePInvokeRoot
+ ? (ParentIndex << 1) | 1
+ : AddWasmPortableEntryPoint(targetBuilder, allocator, ParentIndex);
+ AddWasmNullDebugger(targetBuilder, allocator);
+
+ // A finally funclet called by its parent: the funclet record carries its establishing
+ // frame (the parent) tagged with 2, then the parent record, then a terminator.
+ MockMemorySpace.HeapFragment stack = allocator.Allocate(FuncletFrameSize + ParentFrameSize + 4, "R2RLinearStack");
+ helpers.Write(stack.Data.AsSpan(0, sizeof(uint)), checked((uint)stack.Address + FuncletFrameSize + 2));
+ helpers.Write(stack.Data.AsSpan(4, sizeof(uint)), FuncletVipHalf);
+ helpers.Write(stack.Data.AsSpan(FuncletFrameSize, sizeof(uint)), parentIdentity);
+ helpers.Write(stack.Data.AsSpan(FuncletFrameSize + 4, sizeof(uint)), ParentVipHalf);
+ helpers.Write(stack.Data.AsSpan(FuncletFrameSize + ParentFrameSize, sizeof(uint)), 1u);
+
+ ulong transitionBlock = AddWasmTransitionBlock(helpers, allocator, returnAddress: 0, stackPointer: stack.Address);
+
+ 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), imcf);
+ thread!.Frame = interpreterFrame.Address;
+
+ targetBuilder
+ .AddTypes(CreateWasmTransitionBlockTypes(pointerSize))
+ .AddTypes(new Dictionary
+ {
+ [DataType.InterpreterFrame] = new() { Fields = interpreterFrameFields, Size = (uint)(isFaultingOffset + pointerSize) },
+ });
+ },
+ executionManager: CreateInterpreterExecutionManager(
+ [InterpIp],
+ managedIps: managedIps,
+ runtimeFunctionForIp: ip => new TargetPointer(runtimeFunctions + (ip == FuncletIp ? RuntimeFunctionSize : 0))));
+
+ IStackWalk stackWalk = target.Contracts.StackWalk;
+ ThreadData threadData = target.Contracts.Thread.GetThreadData(new TargetPointer(thread!.Address));
+ IStackDataFrameHandle[] walked = stackWalk.CreateStackWalk(threadData).Take(16).ToArray();
+ IStackDataFrameHandle[] frameless = walked.Where(f => f.State == StackWalkState.Frameless).ToArray();
+ Assert.Equal([InterpIp, FuncletIp, ParentIp], frameless.Select(f => stackWalk.GetInstructionPointer(f).Value));
+
+ // An interpreter frame has no unwind info, and a Frame is not a frameless frame.
+ Assert.Throws(() => stackWalk.GetWasmFunctionIdentity(frameless[0]));
+ IStackDataFrameHandle frameHandle = walked.First(f => f.State != StackWalkState.Frameless);
+ managedIps[2] = stackWalk.GetInstructionPointer(frameHandle).Value;
+ Assert.Throws(() => stackWalk.GetWasmFunctionIdentity(frameHandle));
+
+ WasmFunctionIdentity funclet = stackWalk.GetWasmFunctionIdentity(frameless[1]);
+ WasmFunctionIdentity parent = stackWalk.GetWasmFunctionIdentity(frameless[2]);
+
+ Assert.Equal(ParentIndex + 1, 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.NotEqual(TargetPointer.Null, funclet.Module);
+ Assert.Equal(funclet.Module, parent.Module);
+ }
+
private sealed class InterpreterExecutionManager : IExecutionManager
{
private readonly ulong[] _interpreterIps;
private readonly ulong[]? _managedIps;
private readonly Func<(TargetPointer RuntimeFunction, TargetPointer BaseAddress)>? _unwindInfo;
+ private readonly Func? _runtimeFunctionForIp;
public InterpreterExecutionManager(
ulong[] interpreterIps,
ulong[]? managedIps,
- Func<(TargetPointer RuntimeFunction, TargetPointer BaseAddress)>? unwindInfo)
+ Func<(TargetPointer RuntimeFunction, TargetPointer BaseAddress)>? unwindInfo,
+ Func? runtimeFunctionForIp = null)
{
_interpreterIps = interpreterIps;
_managedIps = managedIps;
_unwindInfo = unwindInfo;
+ _runtimeFunctionForIp = runtimeFunctionForIp;
}
public CodeBlockHandle? GetCodeBlockHandle(TargetCodePointer ip)
@@ -1575,6 +1691,13 @@ public bool TryGetUnwindInfo(
out TargetPointer runtimeFunction,
out TargetPointer unwindInfoBaseAddress)
{
+ if ((_managedIps?.Contains(controlPC.Value) ?? false) && _runtimeFunctionForIp is not null)
+ {
+ runtimeFunction = _runtimeFunctionForIp(controlPC.Value);
+ unwindInfoBaseAddress = TargetPointer.Null;
+ return runtimeFunction != TargetPointer.Null;
+ }
+
if ((_managedIps?.Contains(controlPC.Value) ?? false) && _unwindInfo is not null)
{
(runtimeFunction, unwindInfoBaseAddress) = _unwindInfo();
@@ -1590,8 +1713,9 @@ public bool TryGetUnwindInfo(
private static IExecutionManager CreateInterpreterExecutionManager(
ulong[] interpreterIps,
ulong[]? managedIps = null,
- Func<(TargetPointer RuntimeFunction, TargetPointer BaseAddress)>? unwindInfo = null)
- => new InterpreterExecutionManager(interpreterIps, managedIps, unwindInfo);
+ Func<(TargetPointer RuntimeFunction, TargetPointer BaseAddress)>? unwindInfo = null,
+ Func? runtimeFunctionForIp = null)
+ => new InterpreterExecutionManager(interpreterIps, managedIps, unwindInfo, runtimeFunctionForIp);
// WASM TransitionBlock: m_ReturnAddress, then m_StackPointer. Matches the browser-wasm data
// descriptor, where ArgumentRegisters and FirstGCRefMapSlot are sizeof(TransitionBlock).
diff --git a/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs b/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs
index 95399213abf502..ea79986dbcba09 100644
--- a/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs
+++ b/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using Microsoft.Diagnostics.DataContractReader.Contracts;
using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.Wasm;
+using Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers;
using Microsoft.Diagnostics.DataContractReader.TestInfrastructure;
using Xunit;
@@ -14,6 +15,7 @@ public class WasmR2RInfoTests
{
// WASM is a 32-bit little-endian target.
private static readonly MockTarget.Architecture WasmArch = new() { IsLittleEndian = true, Is64Bit = false };
+ private const uint RuntimeFunctionSize = 8;
private const uint MinFunctionTableIndex = 5;
private const uint FunctionTableIndex = 5; // localIndex 0
@@ -200,6 +202,128 @@ public void EmptyRangeList_ResolvesToNoSection()
Assert.False(info.TryGetUnwindData(new TargetCodePointer(MinVirtualIP + FunctionBeginAddress), out _));
}
+ ///
+ /// A RUNTIME_FUNCTION address maps to the image that contains it. Function-table indices are
+ /// runtime-global but RUNTIME_FUNCTION indices are image-relative, so two registered images with
+ /// distinct index ranges must each resolve to their own module, local index and global index.
+ ///
+ [Theory]
+ [InlineData(0, 0u, 10u, false)]
+ [InlineData(0, 2u, 12u, true)]
+ [InlineData(1, 0u, 20u, false)]
+ [InlineData(1, 1u, 21u, true)]
+ public void TryGetFunctionIdentity_ResolvesOwningImageAndIndices(
+ int image, uint localIndex, uint expectedFunctionTableIndex, bool expectedFunclet)
+ {
+ (TestPlaceholderTarget target, ulong[] modules, ulong[] runtimeFunctions) = CreateTwoImageTarget(
+ (10, [0x100, 0x200, 0x8000_0240]),
+ (20, [0x100, 0x8000_0180]));
+ WasmFunctionTableIndexLookup lookup = new(target);
+
+ Assert.True(lookup.TryGetFunctionIdentity(
+ new TargetPointer(runtimeFunctions[image] + localIndex * RuntimeFunctionSize),
+ out TargetPointer module,
+ out uint runtimeFunctionIndex,
+ out uint functionTableIndex,
+ out bool isFunclet));
+ Assert.Equal(modules[image], module.Value);
+ Assert.Equal(localIndex, runtimeFunctionIndex);
+ Assert.Equal(expectedFunctionTableIndex, functionTableIndex);
+ Assert.Equal(expectedFunclet, isFunclet);
+ }
+
+ [Theory]
+ [InlineData(0, 3u, 0u)] // one past the end of the first image
+ [InlineData(0, 1u, 4u)] // inside an entry rather than at its start
+ [InlineData(1, 2u, 0u)] // one past the end of the second image
+ public void TryGetFunctionIdentity_AddressOutsideEveryImageEntry_ReturnsFalse(int image, uint localIndex, uint extraBytes)
+ {
+ (TestPlaceholderTarget target, _, ulong[] runtimeFunctions) = CreateTwoImageTarget(
+ (10, [0x100, 0x200, 0x8000_0240]),
+ (20, [0x100, 0x8000_0180]));
+ WasmFunctionTableIndexLookup lookup = new(target);
+
+ Assert.False(lookup.TryGetFunctionIdentity(
+ new TargetPointer(runtimeFunctions[image] + localIndex * RuntimeFunctionSize + extraBytes),
+ out TargetPointer module,
+ out _,
+ out _,
+ out _));
+ Assert.Equal(TargetPointer.Null, module);
+ }
+
+ private static (TestPlaceholderTarget Target, ulong[] Modules, ulong[] RuntimeFunctions) CreateTwoImageTarget(
+ params (uint MinFunctionTableIndex, uint[] BeginAddresses)[] images)
+ {
+ TargetTestHelpers helpers = new(WasmArch);
+ var targetBuilder = new TestPlaceholderTarget.Builder(WasmArch);
+ var allocator = targetBuilder.MemoryBuilder.CreateAllocator(0x0010_0000, 0x0080_0000);
+
+ int hashMapStride = MockHashMap.CreateLayout(WasmArch).Size;
+ var moduleLayout = MockLoaderModule.CreateLayout(WasmArch);
+ var r2rInfoLayout = MockReadyToRunInfo.CreateLayout(WasmArch, hashMapStride, isWasm: true);
+ var runtimeFunctionLayout = helpers.LayoutFields([
+ new("BeginAddress", DataType.uint32),
+ new("UnwindData", DataType.uint32),
+ ]);
+ var rangeSectionLayout = helpers.LayoutFields([
+ new("MinFunctionTableIndex", DataType.uint32),
+ new("NumRuntimeFunctions", DataType.uint32),
+ new("R2RModule", DataType.pointer),
+ new("Next", DataType.pointer),
+ ]);
+
+ ulong[] modules = new ulong[images.Length];
+ ulong[] runtimeFunctionTables = new ulong[images.Length];
+ ulong next = 0;
+ // Build in reverse so the list head is the first image.
+ for (int i = images.Length - 1; i >= 0; i--)
+ {
+ uint[] begins = images[i].BeginAddresses;
+ var runtimeFunctions = allocator.Allocate(runtimeFunctionLayout.Stride * (ulong)begins.Length, "RuntimeFunctions");
+ for (int f = 0; f < begins.Length; f++)
+ {
+ helpers.Write(
+ runtimeFunctions.Data.AsSpan((int)(f * runtimeFunctionLayout.Stride) + runtimeFunctionLayout.Fields["BeginAddress"].Offset, sizeof(uint)),
+ begins[f]);
+ }
+
+ runtimeFunctionTables[i] = runtimeFunctions.Address;
+ MockReadyToRunInfo r2rInfo = r2rInfoLayout.Create(allocator.Allocate((ulong)r2rInfoLayout.Size, "ReadyToRunInfo"));
+ r2rInfo.CompositeInfo = r2rInfo.Address;
+ r2rInfo.NumRuntimeFunctions = (uint)begins.Length;
+ r2rInfo.RuntimeFunctions = runtimeFunctions.Address;
+ r2rInfo.MinVirtualIP = MinVirtualIP;
+
+ MockLoaderModule module = moduleLayout.Create(allocator.Allocate((ulong)moduleLayout.Size, "Module"));
+ module.ReadyToRunInfo = r2rInfo.Address;
+ modules[i] = module.Address;
+
+ var section = allocator.Allocate(rangeSectionLayout.Stride, "FunctionTableIndexRangeSection");
+ var fields = rangeSectionLayout.Fields;
+ helpers.Write(section.Data.AsSpan(fields["MinFunctionTableIndex"].Offset, sizeof(uint)), images[i].MinFunctionTableIndex);
+ helpers.Write(section.Data.AsSpan(fields["NumRuntimeFunctions"].Offset, sizeof(uint)), (uint)begins.Length);
+ helpers.WritePointer(section.Data.AsSpan(fields["R2RModule"].Offset, helpers.PointerSize), module.Address);
+ helpers.WritePointer(section.Data.AsSpan(fields["Next"].Offset, helpers.PointerSize), next);
+ next = section.Address;
+ }
+
+ var slot = allocator.Allocate((uint)helpers.PointerSize, "FunctionTableIndexRangeListSlot");
+ helpers.WritePointer(slot.Data.AsSpan(0, helpers.PointerSize), next);
+
+ TestPlaceholderTarget target = 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(("FunctionTableIndexRangeList", slot.Address))
+ .Build();
+ return (target, modules, runtimeFunctionTables);
+ }
+
// Source: linked merged WASI corerun-composite.wasm (MD5 a978074b91cd722574b308f52c9ca994).
// The MVID-matched System.Private.CoreLib component's MethodDefEntryPoints uniquely maps
// RuntimeFunction[1040] to parameterless System.Exception::.ctor() (token 0x060004AB).