File: Contracts\StackWalk\FrameHandling\FrameHelpers.cs
Web Access
Project: Microsoft.Diagnostics.DataContractReader.Contracts.csproj (Microsoft.Diagnostics.DataContractReader.Contracts)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using Microsoft.Diagnostics.DataContractReader.Contracts;
using Microsoft.Diagnostics.DataContractReader.Data;
using Microsoft.Diagnostics.DataContractReader.RuntimeTypeSystemHelpers;

namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers;

internal enum FrameType
{
    Unknown,

    InlinedCallFrame,
    SoftwareExceptionFrame,

    /* TransitionFrame Types */
    FramedMethodFrame,
    PInvokeCalliFrame,
    PrestubMethodFrame,
    StubDispatchFrame,
    CallCountingHelperFrame,
    ExternalMethodFrame,
    DynamicHelperFrame,
    InterpreterFrame,

    FuncEvalFrame,

    /* ResumableFrame Types */
    ResumableFrame,
    RedirectedThreadFrame,

    FaultingExceptionFrame,

    HijackFrame,

    TailCallFrame,

    /* Other Frame Types not handled by the iterator */
    ProtectValueClassFrame,
    DebuggerClassInitMarkFrame,
    DebuggerExitFrame,
    DebuggerU2MCatchHandlerFrame,
    ExceptionFilterFrame,
}

/// <summary>
/// Helpers for inspecting and operating on a single capital-F <see cref="Data.Frame"/>.
/// These do not depend on iterator state — given a frame (or frame pointer/identifier) they
/// return information about that one frame or use it to update a context.
/// <see cref="FrameIterator"/> drives the chain traversal and uses these helpers to
/// classify and decode each frame as it walks.
/// </summary>
internal sealed class FrameHelpers
{
    private const ulong InlinedCallFrameMarkerMask = 1;
    private readonly Target _target;

    public FrameHelpers(Target target)
    {
        _target = target;
    }

    public string GetFrameName(TargetPointer frameIdentifier)
    {
        FrameType frameType = GetFrameType(frameIdentifier);
        if (frameType == FrameType.Unknown)
        {
            return string.Empty;
        }
        return frameType.ToString();
    }

    public FrameType GetFrameType(TargetPointer frameIdentifier)
    {
        foreach (FrameType frameType in Enum.GetValues<FrameType>())
        {
            if (_target.TryReadGlobalPointer(frameType.ToString() + "Identifier", out TargetPointer? id))
            {
                if (frameIdentifier == id)
                {
                    return frameType;
                }
            }
        }

        return FrameType.Unknown;
    }

    /// <summary>
    /// Resolves a frame's GCRefMap from its import slot, matching native FindGCRefMap (frames.cpp).
    /// </summary>
    public TargetPointer FindGCRefMap(TargetPointer indirection)
    {
        if (indirection == TargetPointer.Null)
            return TargetPointer.Null;

        TargetPointer zapModule = _target.Contracts.ExecutionManager.FindReadyToRunModule(indirection);
        if (zapModule == TargetPointer.Null)
            return TargetPointer.Null;

        Data.Module module = _target.ProcessedData.GetOrAdd<Data.Module>(zapModule);
        if (module.ReadyToRunInfo == TargetPointer.Null)
            return TargetPointer.Null;

        Data.ReadyToRunInfo r2rInfo = _target.ProcessedData.GetOrAdd<Data.ReadyToRunInfo>(module.ReadyToRunInfo);
        if (r2rInfo.ImportSections == TargetPointer.Null || r2rInfo.NumImportSections == 0)
            return TargetPointer.Null;

        ulong imageBase = r2rInfo.LoadedImageBase.Value;
        if (indirection.Value < imageBase)
            return TargetPointer.Null;
        ulong diff = indirection.Value - imageBase;
        if (diff > uint.MaxValue)
            return TargetPointer.Null;
        uint rva = (uint)diff;

        const int ImportSectionSize = 20;
        const int SectionVAOffset = 0;
        const int SectionSizeOffset = 4;
        const int FlagsOffset = 8;
        const int EntrySizeOffset = 11;
        const int SignaturesOffset = 12;
        const int AuxiliaryDataOffset = 16;
        const ushort GCRefMapFlag = 0x0008;

        TargetPointer sectionsBase = r2rInfo.ImportSections;
        for (uint i = 0; i < r2rInfo.NumImportSections; i++)
        {
            TargetPointer sectionAddr = new(sectionsBase.Value + i * ImportSectionSize);
            uint sectionVA = _target.Read<uint>(sectionAddr + SectionVAOffset);
            uint sectionSize = _target.Read<uint>(sectionAddr + SectionSizeOffset);

            if (rva >= sectionVA && rva < sectionVA + sectionSize)
            {
                byte entrySize = _target.Read<byte>(sectionAddr + EntrySizeOffset);
                if (entrySize == 0)
                    return TargetPointer.Null;

                uint index = (rva - sectionVA) / entrySize;
                if ((_target.Read<ushort>(sectionAddr + FlagsOffset) & GCRefMapFlag) != 0)
                {
                    uint signaturesRva = _target.Read<uint>(sectionAddr + SignaturesOffset);
                    TargetPointer signatureEntry = new(imageBase + signaturesRva + index * sizeof(uint));
                    uint mapRva = _target.Read<uint>(signatureEntry);
                    return new TargetPointer(imageBase + mapRva);
                }

                uint auxDataRva = _target.Read<uint>(sectionAddr + AuxiliaryDataOffset);
                if (auxDataRva == 0)
                    return TargetPointer.Null;

                TargetPointer gcRefMapBase = new(imageBase + auxDataRva);

                const uint GCREFMAP_LOOKUP_STRIDE = 1024;
                uint lookupIndex = index / GCREFMAP_LOOKUP_STRIDE;
                uint remaining = index % GCREFMAP_LOOKUP_STRIDE;

                uint lookupOffset = _target.Read<uint>(new TargetPointer(gcRefMapBase.Value + lookupIndex * 4));
                TargetPointer p = new(gcRefMapBase.Value + lookupOffset);

                while (remaining > 0)
                {
                    while ((_target.Read<byte>(p) & 0x80) != 0)
                        p = new(p.Value + 1);
                    p = new(p.Value + 1);
                    remaining--;
                }

                return p;
            }
        }

        return TargetPointer.Null;
    }

    public TargetPointer GetGCRefMapFromMethodDesc(TargetPointer methodDescPtr)
    {
        if (methodDescPtr == TargetPointer.Null)
            return TargetPointer.Null;

        Data.MethodDesc methodDesc = _target.ProcessedData.GetOrAdd<Data.MethodDesc>(methodDescPtr);
        return methodDesc.CodeData != TargetPointer.Null
            ? _target.ProcessedData.GetOrAdd<Data.MethodDescCodeData>(methodDesc.CodeData).GCRefMap ?? TargetPointer.Null
            : TargetPointer.Null;
    }

    public TargetPointer GetGCRefMapForFrame(TargetPointer frameAddress)
    {
        FrameType kind = GetFrameType(_target.ProcessedData.GetOrAdd<Data.Frame>(frameAddress).Identifier);
        if (kind is not (FrameType.PrestubMethodFrame or FrameType.CallCountingHelperFrame or FrameType.StubDispatchFrame))
            return TargetPointer.Null;
        TargetPointer method = GetMethodDescPtr(frameAddress);
        if (method == TargetPointer.Null)
            return TargetPointer.Null;
        if (!_target.TryGetTypeInfo(nameof(DataType.MethodDesc), out _))
            return TargetPointer.Null;

        bool supportsVirtualMaps = !_target.Contracts.TryGetContract<IStackWalk>(out IStackWalk? stackWalk) ||
            stackWalk is not StackWalk_1 implementation || implementation.Version >= 2;
        if (!supportsVirtualMaps && kind is FrameType.StubDispatchFrame)
            return TargetPointer.Null;
        Data.FramedMethodFrame frame = _target.ProcessedData.GetOrAdd<Data.FramedMethodFrame>(frameAddress);
        // Marking saves hits and misses before relocating roots. A cold diagnostic walk
        // can still inspect the receiver; a completed miss must never retry that lookup.
        if (supportsVirtualMaps && frame.CachedGCRefMap is TargetPointer cachedMap && cachedMap != TargetPointer.Null)
        {
            TargetPointer noMap = new(_target.PointerSize == 8 ? ulong.MaxValue : uint.MaxValue);
            return cachedMap == noMap ? TargetPointer.Null : cachedMap;
        }
        TargetPointer cached = GetGCRefMapFromMethodDesc(method);
        if (!supportsVirtualMaps || !_target.TryGetTypeInfo(nameof(DataType.MethodTable), out _))
            return cached;

        IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem;
        MethodDescHandle md = rts.GetMethodDescHandle(method);
        MethodGCRefMapLookup lookup = new(_target, this);
        bool dispatch = kind is FrameType.StubDispatchFrame;
        TargetPointer map = lookup.Find(md, TargetPointer.Null, dispatch);
        if (map != TargetPointer.Null)
            return map;
        if (rts.GetGenericMethodInstantiation(md).Length != 0 ||
            (!dispatch && rts.GetGenericContextLoc(md) is GenericContextLoc.InstArgMethodDesc or GenericContextLoc.InstArgMethodTable))
            return TargetPointer.Null;

        TargetPointer owner = rts.GetMethodTable(md);
        Data.MethodDesc data = _target.ProcessedData.GetOrAdd<Data.MethodDesc>(method);
        if (((MethodDescFlags_1.MethodDescFlags)data.Flags & MethodDescFlags_1.MethodDescFlags.Static) != 0 ||
            !CanReadReceiver(md, owner) || (!dispatch && rts.IsValueType(rts.GetTypeHandle(owner)) && !rts.IsUnboxingStub(md)))
            return TargetPointer.Null;

        Data.TransitionBlock tb = _target.ProcessedData.GetOrAdd<Data.TransitionBlock>(frame.TransitionBlockPtr);
        TargetPointer thisSlot = tb.ArgumentRegisters;
        if (_target.Contracts.RuntimeInfo.GetTargetArchitecture() is RuntimeInfoArchitecture.X86)
            thisSlot += (uint)_target.PointerSize;
        TargetPointer thisObject = _target.ReadPointer(thisSlot);
        if (thisObject == TargetPointer.Null)
            return TargetPointer.Null;
        TargetPointer receiver = _target.Contracts.Object.GetMethodTableAddress(thisObject);
        return lookup.Find(md, receiver, dispatch);
    }

    private bool CanReadReceiver(MethodDescHandle method, TargetPointer owner)
    {
        IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem;
        Data.MethodDesc data = _target.ProcessedData.GetOrAdd<Data.MethodDesc>(method.Address);
        if ((data.Flags & (ushort)MethodDescFlags_1.MethodDescFlags.ClassificationMask) == (ushort)MethodClassification.Array)
            return !rts.IsArrayMethod(method, out ArrayFunctionType function) || function is not ArrayFunctionType.Constructor;
        if (!rts.IsString(rts.GetTypeHandle(owner)))
            return true;

        uint token = rts.GetMethodToken(method);
        if ((token & 0x00ffffff) == 0)
            return false;
        ModuleHandle module = _target.Contracts.Loader.GetModuleHandleFromModulePtr(rts.GetModule(rts.GetTypeHandle(owner)));
        MetadataReader? metadata = _target.Contracts.EcmaMetadata.GetMetadata(module);
        // Without String metadata, the saved argument could belong to a constructor's
        // static factory rather than be a receiver.
        if (metadata is null)
            return false;
        MethodDefinition definition = metadata.GetMethodDefinition((MethodDefinitionHandle)MetadataTokens.Handle((int)token));
        return (definition.Attributes & MethodAttributes.RTSpecialName) == 0 || !metadata.StringComparer.Equals(definition.Name, ".ctor");
    }

    public bool ThisIsInterior(TargetPointer frameAddress, bool fallback)
    {
        FrameType kind = GetFrameType(_target.ProcessedData.GetOrAdd<Data.Frame>(frameAddress).Identifier);
        if (kind is FrameType.StubDispatchFrame)
            return false;
        if (kind is not (FrameType.PrestubMethodFrame or FrameType.CallCountingHelperFrame))
            return fallback;
        IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem;
        MethodDescHandle md = rts.GetMethodDescHandle(GetMethodDescPtr(frameAddress));
        return rts.IsValueType(rts.GetTypeHandle(rts.GetMethodTable(md))) && !rts.IsUnboxingStub(md);
    }

    public TargetPointer GetMethodDescPtr(TargetPointer framePtr)
    {
        Data.Frame frame = _target.ProcessedData.GetOrAdd<Data.Frame>(framePtr);
        FrameType frameType = GetFrameType(frame.Identifier);
        switch (frameType)
        {
            case FrameType.FramedMethodFrame:
            case FrameType.DynamicHelperFrame:
            case FrameType.ExternalMethodFrame:
            case FrameType.PrestubMethodFrame:
            case FrameType.CallCountingHelperFrame:
                Data.FramedMethodFrame framedMethodFrame = _target.ProcessedData.GetOrAdd<Data.FramedMethodFrame>(frame.Address);
                return framedMethodFrame.MethodDescPtr;
            case FrameType.InterpreterFrame:
                // InterpreterFrame is constructed with pMD=NULL in native.
                // The interpreted methods are yielded as frameless frames via
                // interpreter virtual unwind, not through the Frame's MethodDesc.
                return TargetPointer.Null;
            case FrameType.PInvokeCalliFrame:
                return TargetPointer.Null;
            case FrameType.StubDispatchFrame:
                Data.StubDispatchFrame stubDispatchFrame = _target.ProcessedData.GetOrAdd<Data.StubDispatchFrame>(frame.Address);
                if (stubDispatchFrame.MethodDescPtr != TargetPointer.Null)
                {
                    return stubDispatchFrame.MethodDescPtr;
                }
                else if (stubDispatchFrame.RepresentativeMTPtr != TargetPointer.Null)
                {
                    IRuntimeTypeSystem rtsContract = _target.Contracts.RuntimeTypeSystem;
                    ITypeHandle mtHandle = rtsContract.GetTypeHandle(stubDispatchFrame.RepresentativeMTPtr);
                    return rtsContract.GetMethodDescForSlot(mtHandle, (ushort)stubDispatchFrame.RepresentativeSlot);
                }
                else
                {
                    return TargetPointer.Null;
                }
            case FrameType.InlinedCallFrame:
                Data.InlinedCallFrame inlinedCallFrame = _target.ProcessedData.GetOrAdd<Data.InlinedCallFrame>(frame.Address);
                if (InlinedCallFrameHasActiveCall(inlinedCallFrame) && InlinedCallFrameHasFunction(inlinedCallFrame))
                    return inlinedCallFrame.Datum & ~InlinedCallFrameMarkerMask;
                else
                    return TargetPointer.Null;
            default:
                return TargetPointer.Null;
        }
    }

    /// <summary>
    /// Updates <paramref name="context"/> based on <paramref name="frame"/>'s type, replicating
    /// the per-frame context update performed by the native stack walker before it yields a
    /// Frame (or transitions to Frameless for InterpreterFrame).
    /// </summary>
    public void UpdateContextFromFrame(Data.Frame frame, IPlatformAgnosticContext context)
    {
        switch (GetFrameType(frame.Identifier))
        {
            case FrameType.InlinedCallFrame:
                Data.InlinedCallFrame inlinedCallFrame = _target.ProcessedData.GetOrAdd<Data.InlinedCallFrame>(frame.Address);
                GetFrameHandler(context).HandleInlinedCallFrame(inlinedCallFrame);
                return;

            case FrameType.SoftwareExceptionFrame:
                Data.SoftwareExceptionFrame softwareExceptionFrame = _target.ProcessedData.GetOrAdd<Data.SoftwareExceptionFrame>(frame.Address);
                GetFrameHandler(context).HandleSoftwareExceptionFrame(softwareExceptionFrame);
                return;

            // TransitionFrame type frames
            case FrameType.FramedMethodFrame:
            case FrameType.PInvokeCalliFrame:
            case FrameType.PrestubMethodFrame:
            case FrameType.StubDispatchFrame:
            case FrameType.CallCountingHelperFrame:
            case FrameType.ExternalMethodFrame:
            case FrameType.DynamicHelperFrame:
                // FrameMethodFrame is the base type for all transition Frames
                Data.FramedMethodFrame framedMethodFrame = _target.ProcessedData.GetOrAdd<Data.FramedMethodFrame>(frame.Address);
                GetFrameHandler(context).HandleTransitionFrame(framedMethodFrame);
                return;

            case FrameType.InterpreterFrame:
                {
                    // Mirrors native InterpreterFrame::SetContextToInterpMethodContextFrame
                    // (frames.cpp). Sets context to the top InterpMethodContextFrame so the
                    // walker transitions to Frameless and yields each interpreted method
                    // individually via virtual unwind.
                    Data.InterpreterFrame interpreterFrame = _target.ProcessedData.GetOrAdd<Data.InterpreterFrame>(frame.Address);
                    SetContextToInterpMethodContextFrame(context, interpreterFrame);
                    return;
                }

            case FrameType.FuncEvalFrame:
                Data.FuncEvalFrame funcEvalFrame = _target.ProcessedData.GetOrAdd<Data.FuncEvalFrame>(frame.Address);
                GetFrameHandler(context).HandleFuncEvalFrame(funcEvalFrame);
                return;

            // ResumableFrame type frames
            case FrameType.ResumableFrame:
            case FrameType.RedirectedThreadFrame:
                Data.ResumableFrame resumableFrame = _target.ProcessedData.GetOrAdd<Data.ResumableFrame>(frame.Address);
                GetFrameHandler(context).HandleResumableFrame(resumableFrame);
                return;

            case FrameType.FaultingExceptionFrame:
                Data.FaultingExceptionFrame faultingExceptionFrame = _target.ProcessedData.GetOrAdd<Data.FaultingExceptionFrame>(frame.Address);
                GetFrameHandler(context).HandleFaultingExceptionFrame(faultingExceptionFrame);
                return;

            case FrameType.HijackFrame:
                Data.HijackFrame hijackFrame = _target.ProcessedData.GetOrAdd<Data.HijackFrame>(frame.Address);
                GetFrameHandler(context).HandleHijackFrame(hijackFrame);
                return;
            case FrameType.TailCallFrame:
                Data.TailCallFrame tailCallFrame = _target.ProcessedData.GetOrAdd<Data.TailCallFrame>(frame.Address);
                GetFrameHandler(context).HandleTailCallFrame(tailCallFrame);
                return;
            default:
                // Unknown Frame type. This could either be a Frame that we don't know how to handle,
                // or a Frame that does not update the context.
                return;
        }
    }

    /// <summary>
    /// Returns the return address recorded in <paramref name="transitionBlock"/>. On WASM, a
    /// transition helper called from R2R code may store only the caller's linear-stack pointer;
    /// the return address is then that frame's R2R virtual IP, matching the lazy computation in
    /// native <c>FramedMethodFrame::GetTransitionBlock_Impl</c>.
    /// </summary>
    public TargetCodePointer GetTransitionBlockReturnAddress(Data.TransitionBlock transitionBlock)
    {
        if (transitionBlock.ReturnAddress == TargetCodePointer.Null
            && transitionBlock.StackPointer is TargetPointer stackPointer
            && stackPointer != TargetPointer.Null)
        {
            Wasm.WasmUnwinder unwinder = new(_target, new Wasm.WasmR2RInfo(_target));
            return unwinder.GetVirtualIP(stackPointer);
        }

        return transitionBlock.ReturnAddress;
    }

    /// <summary>
    /// Returns the return address for <paramref name="frame"/>, matching native Frame::GetReturnAddress().
    /// Returns TargetCodePointer.Null if the Frame has no return address (e.g., non-active ICF,
    /// base Frame types, FuncEvalFrame during exception eval).
    /// </summary>
    public TargetCodePointer GetReturnAddress(Data.Frame frame)
    {
        FrameType frameType = GetFrameType(frame.Identifier);
        switch (frameType)
        {
            // InlinedCallFrame: returns 0 if inactive, else m_pCallerReturnAddress
            case FrameType.InlinedCallFrame:
                Data.InlinedCallFrame icf = _target.ProcessedData.GetOrAdd<Data.InlinedCallFrame>(frame.Address);
                return InlinedCallFrameHasActiveCall(icf) ? icf.CallerReturnAddress : TargetCodePointer.Null;

            // TransitionFrame types: read return address from the transition block
            case FrameType.FramedMethodFrame:
            case FrameType.PInvokeCalliFrame:
            case FrameType.PrestubMethodFrame:
            case FrameType.StubDispatchFrame:
            case FrameType.CallCountingHelperFrame:
            case FrameType.ExternalMethodFrame:
            case FrameType.DynamicHelperFrame:
                Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd<Data.FramedMethodFrame>(frame.Address);
                Data.TransitionBlock tb = _target.ProcessedData.GetOrAdd<Data.TransitionBlock>(fmf.TransitionBlockPtr);
                if (frameType == FrameType.StubDispatchFrame &&
                    _target.Contracts.RuntimeInfo.GetTargetArchitecture() == RuntimeInfoArchitecture.X86 &&
                    GetMethodDescPtr(frame.Address) == TargetPointer.Null)
                {
                    Data.StubDispatchFrame dispatchFrame = _target.ProcessedData.GetOrAdd<Data.StubDispatchFrame>(frame.Address);
                    if (FindGCRefMap(dispatchFrame.Indirection) == TargetPointer.Null)
                        return new TargetCodePointer((uint)tb.ReturnAddress - X86FrameHandler.CallInstructionSize);
                }
                return GetTransitionBlockReturnAddress(tb);

            // SoftwareExceptionFrame: stored m_ReturnAddress
            case FrameType.SoftwareExceptionFrame:
                Data.SoftwareExceptionFrame sef = _target.ProcessedData.GetOrAdd<Data.SoftwareExceptionFrame>(frame.Address);
                return sef.ReturnAddress;

            // ResumableFrame / RedirectedThreadFrame: RIP from captured context
            case FrameType.ResumableFrame:
            case FrameType.RedirectedThreadFrame:
            {
                Data.ResumableFrame rf = _target.ProcessedData.GetOrAdd<Data.ResumableFrame>(frame.Address);
                IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(_target);
                ctx.ReadFromAddress(_target, rf.TargetContextPtr);
                return ctx.InstructionPointer;
            }

            // FaultingExceptionFrame: RIP from embedded context
            case FrameType.FaultingExceptionFrame:
            {
                Data.FaultingExceptionFrame fef = _target.ProcessedData.GetOrAdd<Data.FaultingExceptionFrame>(frame.Address);
                IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(_target);
                ctx.ReadFromAddress(_target, fef.TargetContext);
                return ctx.InstructionPointer;
            }

            // HijackFrame: stored m_ReturnAddress
            case FrameType.HijackFrame:
                Data.HijackFrame hf = _target.ProcessedData.GetOrAdd<Data.HijackFrame>(frame.Address);
                return hf.ReturnAddress;

            // TailCallFrame: stored m_ReturnAddress
            case FrameType.TailCallFrame:
                Data.TailCallFrame tcf = _target.ProcessedData.GetOrAdd<Data.TailCallFrame>(frame.Address);
                return tcf.ReturnAddress;

            // FuncEvalFrame: returns 0 during exception eval
            case FrameType.FuncEvalFrame:
                Data.FuncEvalFrame funcEval = _target.ProcessedData.GetOrAdd<Data.FuncEvalFrame>(frame.Address);
                Data.DebuggerEval dbgEval = _target.ProcessedData.GetOrAdd<Data.DebuggerEval>(funcEval.DebuggerEvalPtr);
                if (!dbgEval.EvalUsesHijack)
                    return TargetCodePointer.Null;
                return funcEval.ReturnAddress;

            // Base Frame and unknown types: return 0 (matches native Frame::GetReturnAddressPtr_Impl)
            default:
                return TargetCodePointer.Null;
        }
    }

    /// <summary>
    /// Returns the InternalFrameType (CorDebugInternalFrameType) of the frame at <paramref name="framePtr"/>.
    /// Mirrors the native DacDbiInterfaceImpl::GetInternalFrameType logic.
    /// </summary>
    public InternalFrameType GetInternalFrameType(TargetPointer framePtr)
    {
        Data.Frame frame = _target.ProcessedData.GetOrAdd<Data.Frame>(framePtr);
        FrameType frameType = GetFrameType(frame.Identifier);

        // Mirrors Frame::GetStubFrameType_Impl per subclass in src/coreclr/vm/frames.h.
        switch (frameType)
        {
            case FrameType.FaultingExceptionFrame:
            case FrameType.SoftwareExceptionFrame:
                return InternalFrameType.Exception;

            case FrameType.DebuggerClassInitMarkFrame:
                return InternalFrameType.ClassInit;

            case FrameType.PrestubMethodFrame:
                return InternalFrameType.JitCompilation;

            case FrameType.FuncEvalFrame:
                return InternalFrameType.FuncEval;

            case FrameType.DebuggerU2MCatchHandlerFrame:
                return InternalFrameType.U2M;

            case FrameType.DynamicHelperFrame:
                return InternalFrameType.InternalCall;

            case FrameType.DebuggerExitFrame:
            case FrameType.FramedMethodFrame:
            case FrameType.PInvokeCalliFrame:
            case FrameType.CallCountingHelperFrame:
            case FrameType.ExternalMethodFrame:
            case FrameType.InterpreterFrame:
                return InternalFrameType.M2U;

            // InlinedCallFrame inherits M2U from its _Impl, but the
            // debugger gates the value on FrameHasActiveCall.
            case FrameType.InlinedCallFrame:
                Data.InlinedCallFrame icf = _target.ProcessedData.GetOrAdd<Data.InlinedCallFrame>(frame.Address);
                return InlinedCallFrameHasActiveCall(icf)
                    ? InternalFrameType.M2U
                    : InternalFrameType.None;

            default:
                return InternalFrameType.None;
        }
    }

    /// <summary>
    /// Returns true if the frame at <paramref name="framePtr"/> is an InlinedCallFrame that was
    /// set up by the new exception handling helpers (i.e. its Datum field is tagged with the
    /// ExceptionHandlingHelper marker).
    /// </summary>
    public bool IsExceptionHandlingHelperInlinedCallFrame(TargetPointer framePtr)
    {
        Data.Frame frame = _target.ProcessedData.GetOrAdd<Data.Frame>(framePtr);
        FrameType frameType = GetFrameType(frame.Identifier);
        if (frameType != FrameType.InlinedCallFrame)
            return false;

        Data.InlinedCallFrame icf = _target.ProcessedData.GetOrAdd<Data.InlinedCallFrame>(frame.Address);
        if (!InlinedCallFrameHasActiveCall(icf))
            return false;

        return (icf.Datum.Value & InlinedCallFrameMarkerMask) == InlinedCallFrameMarkerMask;
    }

    private IPlatformFrameHandler GetFrameHandler(IPlatformAgnosticContext context)
    {
        if (TargetArchitectureSupport.IsX86Supported && context is ContextHolder<X86Context> x86Context)
            return new X86FrameHandler(_target, x86Context);

        if (TargetArchitectureSupport.IsX64Supported && context is ContextHolder<AMD64Context> x64Context)
            return new AMD64FrameHandler(_target, x64Context);

        if (TargetArchitectureSupport.IsArmSupported && context is ContextHolder<ARMContext> armContext)
            return new ARMFrameHandler(_target, armContext);

        if (TargetArchitectureSupport.IsArm64Supported && context is ContextHolder<ARM64Context> arm64Context)
            return new ARM64FrameHandler(_target, arm64Context);

        if (TargetArchitectureSupport.IsRiscV64Supported && context is ContextHolder<RISCV64Context> riscV64Context)
            return new RISCV64FrameHandler(_target, riscV64Context);

        if (TargetArchitectureSupport.IsLoongArch64Supported && context is ContextHolder<LoongArch64Context> loongArch64Context)
            return new LoongArch64FrameHandler(_target, loongArch64Context);

        if (TargetArchitectureSupport.IsWasmSupported && context is ContextHolder<WasmContext> wasmContext)
            return new WasmFrameHandler(_target, wasmContext);

        throw new InvalidOperationException($"Unsupported context type {context.GetType()}");
    }

    /// <summary>
    /// Mirrors native <c>InlinedCallFrame::IsInInterpreter</c> (frames.cpp): an active
    /// InlinedCallFrame pushed by the interpreter for a P/Invoke is directly followed by the
    /// owning InterpreterFrame, whose top InterpMethodContextFrame is the ICF's CallSiteSP.
    /// </summary>
    public bool IsInlinedCallFrameInInterpreter(Data.Frame frame)
    {
        if (GetFrameType(frame.Identifier) != FrameType.InlinedCallFrame)
            return false;

        ulong terminator = _target.PointerSize == 8 ? ulong.MaxValue : uint.MaxValue;
        if (frame.Next == TargetPointer.Null || frame.Next.Value == terminator)
            return false;

        Data.Frame next = _target.ProcessedData.GetOrAdd<Data.Frame>(frame.Next);
        if (GetFrameType(next.Identifier) != FrameType.InterpreterFrame)
            return false;

        Data.InlinedCallFrame icf = _target.ProcessedData.GetOrAdd<Data.InlinedCallFrame>(frame.Address);
        Data.InterpreterFrame interpreterFrame = _target.ProcessedData.GetOrAdd<Data.InterpreterFrame>(next.Address);
        return ResolveTopInterpMethodContextFrame(interpreterFrame) == icf.CallSiteSP;
    }

    private static bool InlinedCallFrameHasActiveCall(Data.InlinedCallFrame frame)
    {
        return frame.CallerReturnAddress != TargetCodePointer.Null;
    }

    private bool InlinedCallFrameHasFunction(Data.InlinedCallFrame frame)
    {
        ulong datum = frame.Datum.Value & ~InlinedCallFrameMarkerMask;

        if (!UsesInlinedCallFrameStackSizeSentinel())
        {
            return datum != 0;
        }

        return ((long)datum & ~0xffff) != 0;
    }

    private bool UsesInlinedCallFrameStackSizeSentinel()
    {
        if (_target.PointerSize != sizeof(uint))
        {
            return false;
        }

        if (_target.TryReadGlobalString(Constants.Globals.Architecture, out string? arch)
            && Enum.TryParse(arch, ignoreCase: true, out RuntimeInfoArchitecture runtimeArchitecture))
        {
            return runtimeArchitecture == RuntimeInfoArchitecture.X86;
        }

        return false;
    }

    /// <summary>
    /// Resolves the actual top InterpMethodContextFrame from the hint stored in InterpreterFrame,
    /// replicating InterpreterFrame::GetTopInterpMethodContextFrame() from frames.cpp.
    /// The stored TopInterpMethodContextFrame is only an approximate hint; during dump or native
    /// debugging it may point to a stale frame. This method seeks to the correct top frame using
    /// the Ip field (null = inactive, non-null = active) and the NextPtr/ParentPtr chains.
    /// </summary>
    public TargetPointer ResolveTopInterpMethodContextFrame(Data.InterpreterFrame interpreterFrame)
    {
        TargetPointer hintPtr = interpreterFrame.TopInterpMethodContextFrame;
        if (hintPtr == TargetPointer.Null)
            return TargetPointer.Null;

        Data.InterpMethodContextFrame frame = _target.ProcessedData.GetOrAdd<Data.InterpMethodContextFrame>(hintPtr);
        TargetPointer currentPtr = hintPtr;

        if (frame.Ip != TargetPointer.Null)
        {
            // Active frame — seek upward via NextPtr while next frame is also active
            while (frame.NextPtr != TargetPointer.Null)
            {
                Data.InterpMethodContextFrame next = _target.ProcessedData.GetOrAdd<Data.InterpMethodContextFrame>(frame.NextPtr);
                if (next.Ip == TargetPointer.Null)
                    break;
                currentPtr = frame.NextPtr;
                frame = next;
            }
        }
        else
        {
            // Inactive frame — seek downward via ParentPtr to find first active frame
            while (frame.ParentPtr != TargetPointer.Null && frame.Ip == TargetPointer.Null)
            {
                currentPtr = frame.ParentPtr;
                frame = _target.ProcessedData.GetOrAdd<Data.InterpMethodContextFrame>(currentPtr);
            }
        }

        return currentPtr;
    }

    /// <summary>
    /// Walks the InterpMethodContextFrame chain for an InterpreterFrame,
    /// yielding one context frame pointer per active interpreted method in the call chain.
    /// The TopInterpMethodContextFrame hint is first resolved to the actual top frame
    /// via <see cref="ResolveTopInterpMethodContextFrame"/>, then the ParentPtr chain is walked.
    /// Only active frames (Ip != null) are yielded.
    /// </summary>
    public IEnumerable<TargetPointer> WalkInterpreterFrameChain(TargetPointer frameAddress)
    {
        Data.InterpreterFrame interpFrame = _target.ProcessedData.GetOrAdd<Data.InterpreterFrame>(frameAddress);
        TargetPointer interpMethodFramePtr = ResolveTopInterpMethodContextFrame(interpFrame);
        while (interpMethodFramePtr != TargetPointer.Null)
        {
            Data.InterpMethodContextFrame contextFrame = _target.ProcessedData.GetOrAdd<Data.InterpMethodContextFrame>(interpMethodFramePtr);
            if (contextFrame.Ip != TargetPointer.Null)
                yield return interpMethodFramePtr;
            interpMethodFramePtr = contextFrame.ParentPtr;
        }
    }

    // Matches the Windows CONTEXT_EXCEPTION_ACTIVE flag value. The PAL CONTEXT structures
    // on Linux/macOS use the same bit so a single constant is sufficient across platforms.
    internal const uint ContextExceptionActive = 0x08000000;

    /// <summary>
    /// Mirrors native <c>InterpreterFrame::SetContextToInterpMethodContextFrame</c> (frames.cpp).
    /// </summary>
    public void SetContextToInterpMethodContextFrame(
        IPlatformAgnosticContext context,
        Data.InterpreterFrame interpreterFrame)
    {
        TargetPointer topContextFramePtr = ResolveTopInterpMethodContextFrame(interpreterFrame);
        if (topContextFramePtr == TargetPointer.Null)
            return;

        Data.InterpMethodContextFrame topContextFrame = _target.ProcessedData.GetOrAdd<Data.InterpMethodContextFrame>(topContextFramePtr);
        context.InstructionPointer = new TargetCodePointer((ulong)topContextFrame.Ip);
        context.StackPointer = topContextFramePtr;
        context.FramePointer = topContextFrame.Stack;
        SetFirstArgRegister(context, interpreterFrame.Address);

        uint flags = context.FullContextFlags;
        if (interpreterFrame.IsFaulting)
            flags |= ContextExceptionActive;
        context.RawContextFlags = flags;
    }

    /// <summary>
    /// Performs interpreter virtual unwind, matching the native
    /// <c>VirtualUnwindInterpreterCallFrame</c> in eetwain.cpp.
    ///
    /// When unwinding from a frameless interpreter frame, the SP points to the
    /// current InterpMethodContextFrame. We follow pParent to get to the next
    /// interpreted method in the call chain. If pParent is null, the interpreter
    /// chain under the current InterpreterFrame is exhausted, we apply the
    /// InterpreterFrame's transition-block state to restore the context to the
    /// native caller of InterpExecMethod.
    /// </summary>
    public void InterpreterVirtualUnwind(IPlatformAgnosticContext context)
    {
        if (VirtualUnwindInterpreterCallFrame(context))
            return;

        // No active parent: interpreter chain under this InterpreterFrame is exhausted.
        // The owning InterpreterFrame address lives in the first-argument register --
        // populated by SetContextToInterpMethodContextFrame
        TargetPointer interpreterFrame = GetFirstArgRegister(context);
        if (interpreterFrame != TargetPointer.Null)
        {
            ApplyInterpreterFrameTransition(context, interpreterFrame);
        }
    }

    // The SP that native VirtualUnwindInterpreterCallFrame (eetwain.cpp) gives the caller context: the parent
    // InterpMethodContextFrame whenever there is one -- including the zeroed root frame (ip == NULL) of an
    // interpreter activation -- otherwise the owning InterpreterFrame (first-argument register).
    public TargetPointer InterpreterCallerStackPointer(IPlatformAgnosticContext context)
    {
        Data.InterpMethodContextFrame currentFrame = _target.ProcessedData.GetOrAdd<Data.InterpMethodContextFrame>(context.StackPointer);
        return currentFrame.ParentPtr != TargetPointer.Null ? currentFrame.ParentPtr : GetFirstArgRegister(context);
    }

    private bool VirtualUnwindInterpreterCallFrame(IPlatformAgnosticContext context)
    {
        TargetPointer currentFramePtr = context.StackPointer;
        Data.InterpMethodContextFrame currentFrame = _target.ProcessedData.GetOrAdd<Data.InterpMethodContextFrame>(currentFramePtr);

        if (currentFrame.ParentPtr == TargetPointer.Null)
            return false;

        Data.InterpMethodContextFrame parentFrame = _target.ProcessedData.GetOrAdd<Data.InterpMethodContextFrame>(currentFrame.ParentPtr);
        if (parentFrame.Ip == TargetPointer.Null)
            return false;

        context.InstructionPointer = new TargetCodePointer((ulong)parentFrame.Ip);
        context.StackPointer = currentFrame.ParentPtr;
        context.FramePointer = parentFrame.Stack;
        context.RawContextFlags = context.FullContextFlags;
        return true;
    }

    private void ApplyInterpreterFrameTransition(IPlatformAgnosticContext context, TargetPointer interpreterFrameAddress)
    {
        Data.FramedMethodFrame framedMethodFrame = _target.ProcessedData.GetOrAdd<Data.FramedMethodFrame>(interpreterFrameAddress);
        GetFrameHandler(context).HandleTransitionFrame(framedMethodFrame);
    }

    /// <summary>
    /// Returns the first-argument register, which holds the owning InterpreterFrame for a context
    /// in interpreted code (native <c>GetFirstArgReg</c>).
    /// </summary>
    public TargetPointer GetFirstArgRegister(IPlatformAgnosticContext context)
    {
        string registerName = GetFirstArgRegisterName();
        if (!context.TryReadRegister(registerName, out TargetNUInt value))
        {
            throw new InvalidOperationException(
                $"Failed to read first argument register '{registerName}' from the context.");
        }
        return new TargetPointer(value.Value);
    }

    private void SetFirstArgRegister(IPlatformAgnosticContext context, TargetPointer value)
    {
        string registerName = GetFirstArgRegisterName();
        if (!context.TrySetRegister(registerName, new TargetNUInt(value.Value)))
        {
            throw new InvalidOperationException(
                $"Failed to set first argument register '{registerName}' on the context.");
        }
    }

    private string GetFirstArgRegisterName()
    {
        IRuntimeInfo runtimeInfo = _target.Contracts.RuntimeInfo;
        return runtimeInfo.GetTargetArchitecture() switch
        {
            RuntimeInfoArchitecture.X64 =>
                runtimeInfo.GetTargetOperatingSystem() == RuntimeInfoOperatingSystem.Windows ? "rcx" : "rdi",
            RuntimeInfoArchitecture.Arm64 => "x0",
            RuntimeInfoArchitecture.Arm => "r0",
            RuntimeInfoArchitecture.X86 => "ecx",
            RuntimeInfoArchitecture.LoongArch64 => "a0",
            RuntimeInfoArchitecture.RiscV64 => "a0",
            RuntimeInfoArchitecture.Wasm => WasmContext.InterpreterWalkFramePointerRegister,
            var arch => throw new NotSupportedException(
                $"Unsupported architecture for first argument register: {arch}"),
        };
    }
}