From 77a1c5f98a26576236a22451e7874354c418031f Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 8 Oct 2026 17:33:11 +0200 Subject: [PATCH 1/3] [cDAC][wasm] Expose the WebAssembly function identity of R2R frames Add IStackWalk.GetWasmFunctionIdentity. For a ReadyToRun frameless frame it returns the raw runtime-global function-table index from the frame record and, when that index is in a registered range, the module that registered the image, the image-relative RUNTIME_FUNCTION index, and whether the function is a funclet. A WebAssembly engine's function index is module-local, so an out-of-process consumer (for example a debugger over the Chrome DevTools Protocol) needs the module and image-relative index to find the engine function; the raw index alone is not enough. The API rejects non-ReadyToRun and non-frameless frames before reading the stack, because interpreter frames and native markers can hold bytes that look like an R2R frame record. WasmFunctionTableIndexLookup's funclet check now goes through the same resolution. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/StackWalk.md | 15 +++ .../Contracts/IStackWalk.cs | 25 ++++ .../Helpers/WasmFunctionTableIndexLookup.cs | 18 ++- .../StackWalk/Context/Wasm/WasmR2RInfo.cs | 7 ++ .../StackWalk/Context/Wasm/WasmUnwinder.cs | 14 +++ .../Contracts/StackWalk/StackWalk_1.cs | 31 +++++ .../cdac/tests/UnitTests/StackWalkTests.cs | 111 +++++++++++++++++- .../cdac/tests/UnitTests/WasmR2RInfoTests.cs | 97 +++++++++++++++ 8 files changed, 315 insertions(+), 3 deletions(-) diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 659cb19799244d..247880985f7b59 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,8 @@ 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. + `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). From eb9853390a723fa9a6619fa0c2be4f9ddfbc7383 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 8 Oct 2026 18:02:00 +0200 Subject: [PATCH 2/3] [cDAC][wasm] Add pseudocode for GetWasmFunctionIdentity Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/StackWalk.md | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 247880985f7b59..3165fd10a5f04e 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -722,6 +722,35 @@ If no Frame in the chain produces a usable context (thread is not running manage `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`. From 325c1f66eb3dc9e522b507a62c58d46d054d9c7e Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 19 Oct 2026 17:39:34 -0500 Subject: [PATCH 3/3] Simplify WasmFunctionTableIndexLookup.TryGetFunctionIdentity Use GetReadyToRunInfo(section) like the other walkers and drop the ReadyToRunInfo null guard. A function-table index range is registered by ReadyToRunInfo::RegisterVirtualIPRange, which Module runs through GetReadyToRunInfo() during eager fixups, so the module's ReadyToRunInfo is already set when the range is published. The only field assigned after publication is MinVirtualIP, which the walkers that read it already check. Drop the redundant NumRuntimeFunctions > 0 check, and shorten the helper summary; the composite-image caveat stays on WasmFunctionIdentity.Module and in StackWalk.md. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Helpers/WasmFunctionTableIndexLookup.cs | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) 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 ad59d54fb4087e..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 @@ -157,10 +157,9 @@ public bool TryGetCoreLibVirtualIPBase(uint runtimeFunctionIndex, out ulong base } /// - /// Maps a RUNTIME_FUNCTION address to the registered R2R image that contains it. Returns - /// the module that registered the image (for a composite image, the module that loaded it, not - /// necessarily the method's own module), the entry's index within the image, its runtime-global - /// function-table index, and whether the entry is a funclet. + /// 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, @@ -185,22 +184,17 @@ public bool TryGetFunctionIdentity( return false; Data.FunctionTableIndexRangeSection section = _target.ProcessedData.GetOrAdd(current); - Data.Module sectionModule = _target.ProcessedData.GetOrAdd(section.R2RModule); - // A range is published before its module is fully set up; it cannot contain the entry. - if (sectionModule.ReadyToRunInfo != TargetPointer.Null) + Data.ReadyToRunInfo r2rInfo = GetReadyToRunInfo(section); + if (runtimeFunctionAddress >= r2rInfo.RuntimeFunctions) { - Data.ReadyToRunInfo r2rInfo = _target.ProcessedData.GetOrAdd(sectionModule.ReadyToRunInfo); - if (r2rInfo.NumRuntimeFunctions > 0 && runtimeFunctionAddress >= r2rInfo.RuntimeFunctions) + ulong offset = runtimeFunctionAddress.Value - r2rInfo.RuntimeFunctions.Value; + if (offset % entrySize == 0 && offset / entrySize < r2rInfo.NumRuntimeFunctions) { - 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; - } + runtimeFunctionIndex = (uint)(offset / entrySize); + functionTableIndex = section.MinFunctionTableIndex + runtimeFunctionIndex; + module = section.R2RModule; + isFunclet = _runtimeFunctions.IsFunclet(GetRuntimeFunction(r2rInfo, runtimeFunctionIndex)); + return true; } }