diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 659cb19799244d..3165fd10a5f04e 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 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 @@ -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(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`. 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..d5ec945ab3b279 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 represented by a ReadyToRun stack frame. +/// +/// +/// 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 +/// and with the image's element section to +/// find the engine function; the raw index is not an engine function index. +/// +public readonly struct WasmFunctionIdentity +{ + /// The raw runtime-global function-table index stored in the frame. + public uint FunctionTableIndex { get; init; } + /// + /// 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. + /// + public TargetPointer? Module { get; init; } + /// The RUNTIME_FUNCTION index within the image, or null when unresolved. + public uint? RuntimeFunctionIndex { get; init; } + /// Whether the function is a funclet, or null when unresolved. + 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 5589f8ee8c365e..2a508943a5cd54 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 @@ -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); + + /// + /// 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. + /// + 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; } 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 b2823fa028d096..64e6808fe2ff72 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 @@ -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); } 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 8ac71e0d66542f..a623d55d53f108 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 @@ -138,6 +138,20 @@ public TargetCodePointer GetVirtualIP(TargetPointer sp) return new TargetCodePointer(baseVirtualIP + functionLocalVirtualIP); } + /// + /// Returns the function-table index recorded in the R2R frame at . + /// Returns false when there is no R2R frame there. + /// + public bool TryGetFunctionIndex(TargetPointer sp, out uint functionIndex) + { + functionIndex = 0; + if (!TryGetFramePointer(sp, out TargetPointer frameBase)) + return false; + + functionIndex = _target.Read(frameBase.Value + FunctionIndexOffset); + return true; + } + /// /// Advances by one R2R frame and produces the caller's virtual IP, /// mirroring WasmUnwindStackFrameCore. Returns false when the R2R walk terminates 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..f0e0843646ddd7 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.Contracts.StackWalkHelpers.Wasm; using Microsoft.Diagnostics.DataContractReader.Data; using System.Linq; @@ -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); diff --git a/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs b/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs index 7c692ad791b0bf..180ac0ca85463b 100644 --- a/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs @@ -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. + /// + /// GetWasmFunctionIdentity 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. + /// + [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 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: [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(() => 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() { @@ -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 executionManager = new(); executionManager @@ -1535,7 +1640,9 @@ private static IExecutionManager CreateInterpreterExecutionManager(ulong[] inter : null); executionManager .Setup(em => em.GetCodeKind(It.IsAny())) - .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; } diff --git a/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs b/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs index 4554ee3d5fa69e..55d0828f1b91d3 100644 --- a/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/WasmR2RInfoTests.cs @@ -120,6 +120,103 @@ public void EmptyRangeList_ResolvesToNoSection() Assert.False(info.TryGetUnwindData(FunctionTableIndex, out _)); } + /// + /// Function-table indices are runtime-global, but RUNTIME_FUNCTION indices are image-relative. + /// Two registered images with distinct index ranges must each resolve to their own module and + /// local index; an unregistered index resolves to nothing. + /// + [Theory] + [InlineData(10u, 0, 0u, false)] + [InlineData(12u, 0, 2u, true)] + [InlineData(20u, 1, 0u, false)] + [InlineData(21u, 1, 1u, true)] + public void TryGetFunctionIdentity_ResolvesOwningImageAndLocalIndex( + uint functionTableIndex, int expectedImage, uint expectedRuntimeFunctionIndex, bool expectedFunclet) + { + (TestPlaceholderTarget target, ulong[] modules) = CreateTwoImageTarget( + (10, [0x100, 0x200, 0x8000_0240]), + (20, [0x100, 0x8000_0180])); + WasmR2RInfo info = new(target); + + Assert.True(info.TryGetFunctionIdentity(functionTableIndex, out TargetPointer module, out uint runtimeFunctionIndex, out bool isFunclet)); + Assert.Equal(modules[expectedImage], module.Value); + Assert.Equal(expectedRuntimeFunctionIndex, runtimeFunctionIndex); + Assert.Equal(expectedFunclet, isFunclet); + + Assert.False(info.TryGetFunctionIdentity(15, out module, out _, out _)); + Assert.Equal(TargetPointer.Null, module); + } + + private static (TestPlaceholderTarget Target, ulong[] Modules) 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 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]); + } + + 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); + } + // 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).