File: Contracts\StackWalk\Context\ARM64\ARM64Unwinder.cs
Web Access
Project: src\src\runtime\src\native\managed\cdac\Microsoft.Diagnostics.DataContractReader.Contracts\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.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using static Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.ARM64Context;

namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.ARM64;

internal class ARM64Unwinder(Target target)
{
    #region Constants

    /// <summary>
    /// This table describes the size of each unwind code, in bytes, for unwind codes
    /// in the range 0xE0-0xFF.
    /// </summary>
    private static ReadOnlySpan<byte> UnwindCodeSizeTable =>
    [
        4, 1, 2, 1, 1, 1, 1, 3,
        1, 1, 1, 1, 1, 1, 1, 1,
        1, 1, 1, 1, 1, 1, 1, 1,
        2, 3, 4, 5, 1, 1, 1, 1,
    ];

    /// <summary>
    // This table describes the number of instructions represented by each unwind
    // code in the range 0xE0-0xFF.
    /// </summary>
    private static ReadOnlySpan<byte> UnwindCodeInstructionCountTable =>
    [
        1, 1, 1, 1, 1, 1, 1, 1,    // 0xE0-0xE7
        0,                         // 0xE8 - MSFT_OP_TRAP_FRAME
        0,                         // 0xE9 - MSFT_OP_MACHINE_FRAME
        0,                         // 0xEA - MSFT_OP_CONTEXT
        0,                         // 0xEB - MSFT_OP_EC_CONTEXT / MSFT_OP_RET_TO_GUEST (unused)
        0,                         // 0xEC - MSFT_OP_CLEAR_UNWOUND_TO_CALL
        0,                         // 0XED - MSFT_OP_RET_TO_GUEST_LEAF (unused)
        0, 0,                      // 0xEE-0xEF
        0, 0, 0, 0, 0, 0, 0, 0,    // 0xF0-0xF7
        1, 1, 1, 1, 1, 1, 1, 1,    // 0xF8-0xFF
    ];

    #endregion

    private readonly Target _target = target;
    private readonly IExecutionManager _eman = target.Contracts.ExecutionManager;

    #region Entrypoint

    public bool Unwind(ref ARM64Context context)
    {
        if (_eman.GetCodeBlockHandle(context.InstructionPointer.Value) is not CodeBlockHandle cbh)
            return false;

        TargetPointer imageBase = _eman.GetUnwindInfoBaseAddress(cbh);
        TargetPointer unwindInfoAddr = _eman.GetUnwindInfo(cbh);

        if (unwindInfoAddr == TargetPointer.Null)
            return false;

        Data.RuntimeFunction functionEntry = _target.ProcessedData.GetOrAdd<Data.RuntimeFunction>(unwindInfoAddr);

        ulong startingPc = context.Pc;
        ulong startingSp = context.Sp;

        bool status = VirtualUnwind(ref context, imageBase, functionEntry);

        //
        // If we fail the unwind, clear the PC to 0. This is recognized by
        // many callers as a failure, given that RtlVirtualUnwind does not
        // return a status code.
        //

        if (!status)
        {
            context.Pc = 0;
        }

        // PC == 0 means unwinding is finished.
        // Same if no forward progress is made
        if (context.Pc == 0 || (startingPc == context.Pc && startingSp == context.Sp))
            return false;

        return true;
    }

    #endregion
    #region Unwinder

    private bool VirtualUnwind(ref ARM64Context context, TargetPointer imageBase, Data.RuntimeFunction functionEntry)
    {
        // FunctionEntry could be null if the function is a pure leaf/trivial function.
        // This is not a valid case for managed code and not handled here.

        bool status = true;
        uint controlPcRva;
        uint unwindType = functionEntry.UnwindData & 0x3u;

        //
        // Unwind type 3 refers to a chained record. The top 30 bits of the
        // unwind data contains the RVA of the parent pdata record.
        //
        if (unwindType == 3)
        {
            if ((functionEntry.UnwindData & 4) == 0)
            {
                functionEntry = _target.ProcessedData.GetOrAdd<Data.RuntimeFunction>(imageBase + functionEntry.UnwindData - 3);
                unwindType = functionEntry.UnwindData & 3u;

                UnwinderAssert(unwindType != 3);

                controlPcRva = functionEntry.BeginAddress;

            }
            else
            {
                // unsupported version
                return false;
            }

        }
        else
        {
            controlPcRva = (uint)(context.Pc - imageBase);
        }

        //
        // Identify the compact .pdata format versus the full .pdata+.xdata format.
        //
        if (unwindType != 0)
        {
            // managed code does not use compact .pdata format.
            UnwinderAssert(false, "Compact .pdata format is not currently supported.");
        }
        else
        {

            status = VirtualUnwindFull(ref context, controlPcRva, imageBase, functionEntry);
        }

        return status;
    }

    private bool VirtualUnwindFull(
        ref ARM64Context context,
        uint controlPcRva,
        TargetPointer imageBase,
        Data.RuntimeFunction functionEntry)
    {
        //
        // Unless a special frame is encountered, assume that any unwinding
        // will return us to the return address of a call and set the flag
        // appropriately (it will be cleared again if the special cases apply).
        //
        context.ContextFlags |= (uint)ContextFlagsValues.CONTEXT_UNWOUND_TO_CALL;

        //
        // By default, unwinding is done by popping to the LR, then copying
        // that LR to the PC. However, some special opcodes require different
        // behavior.
        //
        bool finalPcFromLr = true;

        //
        // Fetch the header word from the .xdata blob
        //

        TargetPointer unwindDataPtr = imageBase + functionEntry.UnwindData;
        uint headerWord = _target.Read<uint>(unwindDataPtr);
        unwindDataPtr += 4;

        //
        // Verify the version before we do anything else
        //

        if (((headerWord >> 18) & 3) != 0)
        {
            // unsupported version
            return false;
        }

        uint functionLength = headerWord & 0x3ffff;
        uint offsetInFunction = (controlPcRva - functionEntry.BeginAddress) / 4;

        //
        // Determine the number of epilog scope records and the maximum number
        // of unwind codes.
        //
        uint unwindWords = (headerWord >> 27) & 31;
        uint epilogScopeCount = (headerWord >> 22) & 31;
        if (epilogScopeCount == 0 && unwindWords == 0)
        {
            epilogScopeCount = _target.Read<uint>(unwindDataPtr);
            unwindDataPtr += 4;
            unwindWords = (epilogScopeCount >> 16) & 0xff;
            epilogScopeCount &= 0xffff;
        }

        uint unwindIndex = 0;
        if ((headerWord & (1 << 21)) != 0)
        {
            unwindIndex = epilogScopeCount;
            epilogScopeCount = 0;
        }

        //
        // Exception data is not supported in this implementation and is not used by managed code.
        // If it were, it should be extracted here.
        //

        //
        // Unless we are in a prolog/epilog, we execute the unwind codes
        // that immediately follow the epilog scope list.
        //

        TargetPointer unwindCodePtr = unwindDataPtr + 4 * epilogScopeCount;
        TargetPointer unwindCodesEndPtr = unwindCodePtr + 4 * unwindWords;
        uint skipWords = 0;

        //
        // If we're near the start of the function, and this function has a prolog,
        // compute the size of the prolog from the unwind codes. If we're in the
        // midst of it, we still execute starting at unwind code index 0, but we may
        // need to skip some to account for partial execution of the prolog.
        //
        // N.B. As an optimization here, note that each byte of unwind codes can
        //      describe at most one 32-bit instruction. Thus, the largest prologue
        //      that could possibly be described by UnwindWords (which is 4 * the
        //      number of unwind code bytes) is 4 * UnwindWords words. If
        //      OffsetInFunction is larger than this value, it is guaranteed to be
        //      in the body of the function.
        //
        uint scopeSize;
        if (offsetInFunction < 4 * unwindWords)
        {
            scopeSize = ComputeScopeSize(unwindCodePtr, unwindCodesEndPtr, isEpilog: false);

            if (offsetInFunction < scopeSize)
            {
                skipWords = scopeSize - offsetInFunction;
            }
        }

        if (skipWords > 0)
        {
            // Found that we are in the middle of a prolog, no need to check for epilog scopes
        }

        //
        // We're not in the prolog, now check to see if we are in the epilog.
        // In the simple case, the 'E' bit is set indicating there is a single
        // epilog that lives at the end of the function. If we're near the end
        // of the function, compute the actual size of the epilog from the
        // unwind codes. If we're in the midst of it, adjust the unwind code
        // pointer to the start of the codes and determine how many we need to skip.
        //
        // N.B. Similar to the prolog case above, the maximum number of halfwords
        //      that an epilog can cover is limited by UnwindWords. In the epilog
        //      case, however, the starting index within the unwind code table is
        //      non-zero, and so the maximum number of unwind codes that can pertain
        //      to an epilog is (UnwindWords * 4 - UnwindIndex), thus further
        //      constraining the bounds of the epilog.
        //
        else if ((headerWord & (1 << 21)) != 0)
        {
            if (offsetInFunction + (4 * unwindWords - unwindIndex) >= functionLength)
            {
                scopeSize = ComputeScopeSize(unwindCodePtr + unwindIndex, unwindCodesEndPtr, isEpilog: true);
                uint scopeStart = functionLength - scopeSize;

                //
                // N.B. This code assumes that no handleable exceptions can occur in
                //      the prolog or in a chained shrink-wrapping prolog region.
                //
                if (offsetInFunction >= scopeStart)
                {
                    unwindCodePtr += unwindIndex;
                    skipWords = offsetInFunction - scopeStart;
                }
            }
        }

        //
        // In the multiple-epilog case, we scan forward to see if we are within
        // shooting distance of any of the epilogs. If we are, we compute the
        // actual size of the epilog from the unwind codes and proceed like the
        // simple case above.
        //
        else
        {
            for (uint scopeNum = 0; scopeNum < epilogScopeCount; scopeNum++)
            {
                headerWord = _target.Read<uint>(unwindDataPtr);
                unwindDataPtr += 4;

                //
                // The scope records are stored in order. If we hit a record that
                // starts after our current position, we must not be in an epilog.
                //
                uint scopeStart = headerWord & 0x3ffff;
                if (offsetInFunction < scopeStart)
                    break;

                unwindIndex = headerWord >> 22;
                if (offsetInFunction < scopeStart + (4 * unwindWords - unwindIndex))
                {
                    scopeSize = ComputeScopeSize(unwindCodePtr + unwindIndex, unwindCodesEndPtr, isEpilog: true);

                    if (offsetInFunction < scopeStart + scopeSize)
                    {
                        unwindCodePtr += unwindIndex;
                        skipWords = offsetInFunction - scopeStart;
                        break;
                    }
                }
            }
        }

        //
        // Skip over unwind codes until we account for the number of halfwords
        // to skip.
        //
        while (unwindCodePtr < unwindCodesEndPtr && skipWords > 0)
        {
            byte curCode = _target.Read<byte>(unwindCodePtr);
            if (OPCODE_IS_END(curCode))
                break;

            unwindCodePtr += GetUnwindCodeSize(curCode);
            skipWords--;
        }

        //
        // Now execute codes until we hit the end.
        //
        bool status = true;
        uint accumulatedSaveNexts = 0;
        while (unwindCodePtr < unwindCodesEndPtr && status)
        {
            byte curCode = _target.Read<byte>(unwindCodePtr);
            unwindCodePtr += 1;

            //
            // alloc_s (000xxxxx): allocate small stack with size < 1024 (2^5 * 16)
            //
            if (curCode <= 0x1f)
            {
                if (accumulatedSaveNexts != 0)
                {
                    // invalid sequence
                    return false;
                }
                context.Sp += 16u * (curCode & 0x1fu);
            }

            //
            // save_r19r20_x (001zzzzz): save <r19,r20> pair at [sp-#Z*8]!, pre-indexed offset >= -248
            //
            else if (curCode <= 0x3f)
            {
                status = RestoreRegisterRange(
                            ref context,
                            -8 * (curCode & 0x1f),
                            19,
                            2 + (2 * accumulatedSaveNexts));
                accumulatedSaveNexts = 0;
            }

            //
            // save_fplr (01zzzzzz): save <r29,lr> pair at [sp+#Z*8], offset <= 504
            //
            else if (curCode <= 0x7f)
            {
                if (accumulatedSaveNexts != 0)
                {
                    // invalid sequence
                    return false;
                }
                status = RestoreRegisterRange(
                            ref context,
                            8 * (curCode & 0x3f),
                            29,
                            2);
            }

            //
            // save_fplr_x (10zzzzzz): save <r29,lr> pair at [sp-(#Z+1)*8]!, pre-indexed offset >= -512
            //
            else if (curCode <= 0xbf)
            {
                if (accumulatedSaveNexts != 0)
                {
                    // invalid sequence
                    return false;
                }
                status = RestoreRegisterRange(
                            ref context,
                            -8 * ((curCode & 0x3f) + 1),
                            29,
                            2);
            }

            //
            // alloc_m (11000xxx|xxxxxxxx): allocate large stack with size < 32k (2^11 * 16).
            //
            else if (curCode <= 0xc7)
            {
                if (accumulatedSaveNexts != 0)
                {
                    // invalid sequence
                    return false;
                }
                context.Sp += 16u * ((curCode & 7u) << 8);
                context.Sp += 16u * _target.Read<byte>(unwindCodePtr);
                unwindCodePtr++;
            }

            //
            // save_regp (110010xx|xxzzzzzz): save r(19+#X) pair at [sp+#Z*8], offset <= 504
            //
            else if (curCode <= 0xcb)
            {
                byte nextCode = _target.Read<byte>(unwindCodePtr);
                unwindCodePtr++;
                status = RestoreRegisterRange(
                            ref context,
                            8 * (nextCode & 0x3f),
                            19u + ((curCode & 3u) << 2) + (uint)(nextCode >>> 6),
                            2 + (2 * accumulatedSaveNexts));
                accumulatedSaveNexts = 0;
            }

            //
            // save_regp_x (110011xx|xxzzzzzz): save pair r(19+#X) at [sp-(#Z+1)*8]!, pre-indexed offset >= -512
            //
            else if (curCode <= 0xcf)
            {
                byte nextCode = _target.Read<byte>(unwindCodePtr);
                unwindCodePtr++;
                status = RestoreRegisterRange(
                            ref context,
                            -8 * ((nextCode & 0x3f) + 1),
                            19u + ((curCode & 3u) << 2) + (uint)(nextCode >>> 6),
                            2 + (2 * accumulatedSaveNexts));
                accumulatedSaveNexts = 0;
            }

            //
            // save_reg (110100xx|xxzzzzzz): save reg r(19+#X) at [sp+#Z*8], offset <= 504
            //
            else if (curCode <= 0xd3)
            {
                if (accumulatedSaveNexts != 0)
                {
                    // invalid sequence
                    return false;
                }
                byte nextCode = _target.Read<byte>(unwindCodePtr);
                unwindCodePtr++;
                status = RestoreRegisterRange(
                            ref context,
                            8 * (nextCode & 0x3f),
                            19u + ((curCode & 3u) << 2) + (uint)(nextCode >> 6),
                            1);
            }

            //
            // save_reg_x (1101010x|xxxzzzzz): save reg r(19+#X) at [sp-(#Z+1)*8]!, pre-indexed offset >= -256
            //
            else if (curCode <= 0xd5)
            {
                if (accumulatedSaveNexts != 0)
                {
                    // invalid sequence
                    return false;
                }
                byte nextCode = _target.Read<byte>(unwindCodePtr);
                unwindCodePtr++;
                status = RestoreRegisterRange(
                            ref context,
                            -8 * ((nextCode & 0x1f) + 1),
                            19u + ((curCode & 1u) << 3) + (uint)(nextCode >>> 5),
                            1);
            }

            //
            // save_lrpair (1101011x|xxzzzzzz): save pair <r19+2*#X,lr> at [sp+#Z*8], offset <= 504
            //
            else if (curCode <= 0xd7)
            {
                if (accumulatedSaveNexts != 0)
                {
                    // invalid sequence
                    return false;
                }
                byte nextCode = _target.Read<byte>(unwindCodePtr);
                unwindCodePtr++;
                status = RestoreRegisterRange(
                            ref context,
                            8 * (nextCode & 0x3f),
                            19u + 2 * (((curCode & 1u) << 2) + (uint)(nextCode >>> 6)),
                            1);
                if (status)
                {
                    RestoreRegisterRange(
                            ref context,
                            8 * (nextCode & 0x3f) + 8,
                            30,
                            1);
                }
            }

            //
            // save_fregp (1101100x|xxzzzzzz): save pair d(8+#X) at [sp+#Z*8], offset <= 504
            //
            else if (curCode <= 0xd9)
            {
                byte nextCode = _target.Read<byte>(unwindCodePtr);
                unwindCodePtr++;
                status = RestoreFpRegisterRange(
                            ref context,
                            8 * (nextCode & 0x3f),
                            8u + ((curCode & 1u) << 2) + (uint)(nextCode >>> 6),
                            2 + (2 * accumulatedSaveNexts));
                accumulatedSaveNexts = 0;
            }

            //
            // save_fregp_x (1101101x|xxzzzzzz): save pair d(8+#X), at [sp-(#Z+1)*8]!, pre-indexed offset >= -512
            //
            else if (curCode <= 0xdb)
            {
                byte nextCode = _target.Read<byte>(unwindCodePtr);
                unwindCodePtr++;
                status = RestoreFpRegisterRange(
                            ref context,
                            -8 * ((nextCode & 0x3f) + 1),
                            8u + ((curCode & 1u) << 2) + (uint)(nextCode >>> 6),
                            2 + (2 * accumulatedSaveNexts));
                accumulatedSaveNexts = 0;
            }

            //
            // save_freg (1101110x|xxzzzzzz): save reg d(9+#X) at [sp+#Z*8], offset <= 504
            //
            else if (curCode <= 0xdd)
            {
                if (accumulatedSaveNexts != 0)
                {
                    // invalid sequence
                    return false;
                }
                byte nextCode = _target.Read<byte>(unwindCodePtr);
                unwindCodePtr++;
                status = RestoreFpRegisterRange(
                            ref context,
                            8 * (nextCode & 0x3f),
                            8u + ((curCode & 1u) << 2) + (uint)(nextCode >>> 6),
                            1);
            }

            //
            // save_freg_x (11011110|xxxzzzzz): save reg d(8+#X) at [sp-(#Z+1)*8]!, pre-indexed offset >= -256
            //
            else if (curCode == 0xde)
            {
                if (accumulatedSaveNexts != 0)
                {
                    // invalid sequence
                    return false;
                }
                byte nextCode = _target.Read<byte>(unwindCodePtr);
                unwindCodePtr++;
                status = RestoreFpRegisterRange(
                            ref context,
                            -8 * ((nextCode & 0x1f) + 1),
                            8 + (uint)(nextCode >>> 5),
                            1);
            }

            //
            // alloc_l (11100000|xxxxxxxx|xxxxxxxx|xxxxxxxx): allocate large stack with size < 256M
            //
            else if (curCode == 0xe0)
            {
                if (accumulatedSaveNexts != 0)
                {
                    // invalid sequence
                    return false;
                }
                context.Sp += 16u * ((uint)_target.Read<byte>(unwindCodePtr) << 16);
                unwindCodePtr++;
                context.Sp += 16 * ((uint)_target.Read<byte>(unwindCodePtr) << 8);
                unwindCodePtr++;
                context.Sp += 16 * (uint)_target.Read<byte>(unwindCodePtr);
                unwindCodePtr++;
            }

            //
            // set_fp (11100001): set up r29: with: mov r29,sp
            //
            else if (curCode == 0xe1)
            {
                if (accumulatedSaveNexts != 0)
                {
                    // invalid sequence
                    return false;
                }
                context.Sp = context.Fp;
            }

            //
            // add_fp (11100010|xxxxxxxx): set up r29 with: add r29,sp,#x*8
            //
            else if (curCode == 0xe2)
            {
                if (accumulatedSaveNexts != 0)
                {
                    // invalid sequence
                    return false;
                }
                context.Sp = context.Fp - 8u * _target.Read<byte>(unwindCodePtr);
                unwindCodePtr++;
            }

            //
            // nop (11100011): no unwind operation is required
            //
            else if (curCode == 0xe3)
            {
                if (accumulatedSaveNexts != 0)
                {
                    // invalid sequence
                    return false;
                }
            }

            //
            // end (11100100): end of unwind code
            //
            else if (curCode == 0xe4)
            {
                if (accumulatedSaveNexts != 0)
                {
                    // invalid sequence
                    return false;
                }

                break;
            }

            //
            // end_c (11100101): end of unwind code in current chained scope.
            //          Continue unwinding parent scope.
            //
            else if (curCode == 0xe5)
            {
                // no-op
            }

            //
            // save_next_pair (11100110): save next non-volatile Int or FP register pair.
            //
            else if (curCode == 0xe6)
            {
                accumulatedSaveNexts += 1;
            }

            //
            //      11100111 ' 0pxrrrrr ' ffoooooo
            //      p: 0/1 - single/pair
            //      x: 0/1 - positive offset / negative offset with writeback
            //      r: register number
            //      f: 00/01/10 - X / D / Q
            //      o: offset * 16 for x=1 or p=1 or f=Q / else offset * 8
            //
            else if (curCode == 0xe7)
            {
                byte val2 = _target.Read<byte>(unwindCodePtr);
                unwindCodePtr += 1;
                byte val1 = _target.Read<byte>(unwindCodePtr);
                unwindCodePtr += 1;
                SaveAnyUnwindCode op = new SaveAnyUnwindCode(val1, val2);

                //
                // save_next_pair only permited for pairs.
                //
                if ((op.p == 0) && accumulatedSaveNexts != 0)
                {
                    // invalid sequence
                    return false;
                }

                if (op.fixedOp != 0)
                {
                    // invalid sequence
                    return false;
                }

                int spOffset = op.o + op.x;
                spOffset *= ((op.x == 1) || (op.f == 2) || (op.p == 1)) ? 16 : 8;
                spOffset *= op.x == 1 ? -1 : 1;
                uint regCount = 1u + op.p + (2u * accumulatedSaveNexts);
                switch (op.f)
                {
                    case 0:
                        status = RestoreRegisterRange(
                                    ref context,
                                    spOffset,
                                    op.r,
                                    regCount);
                        break;

                    case 1:
                        status = RestoreFpRegisterRange(
                                    ref context,
                                    spOffset,
                                    op.r,
                                    regCount);
                        break;

                    case 2:
                        status = RestoreSimdRegisterRange(
                                    ref context,
                                    spOffset,
                                    op.r,
                                    regCount);
                        break;

                    default:
                        // invalid sequence
                        return false;
                }

                accumulatedSaveNexts = 0;
            }

            //
            // custom_0 (111010xx): restore custom structure
            //
            else if (curCode >= 0xe8 && curCode <= 0xec)
            {
                if (accumulatedSaveNexts != 0)
                {
                    // invalid sequence
                    return false;
                }

                status = UnwindCustom(ref context, curCode);
                finalPcFromLr = false;
            }

            //
            // pac (11111100): function has pointer authentication
            //
            else if (curCode == 0xfc)
            {
                if (accumulatedSaveNexts != 0)
                {
                    // invalid sequence
                    return false;
                }

                //
                // TODO: Implement support for UnwindFlags RTL_VIRTUAL_UNWIND2_VALIDATE_PAC.
                //
            }

            //
            // future/nop: the following ranges represent encodings reserved for
            //      future extension. They are treated as a nop and, therefore, no
            //      unwind action is taken.
            //
            //      11111000|yyyyyyyy
            //      11111001|yyyyyyyy|yyyyyyyy
            //      11111010|yyyyyyyy|yyyyyyyy|yyyyyyyy
            //      11111011|yyyyyyyy|yyyyyyyy|yyyyyyyy|yyyyyyyy
            //      111111xx
            //
            else if (curCode >= 0xf8)
            {
                if (accumulatedSaveNexts != 0)
                {
                    // invalid sequence
                    return false;
                }

                if (curCode <= 0xfb)
                {
                    unwindCodePtr += 1u + (curCode & 0x3u);
                }
            }

            //
            // Anything else is invalid
            //
            else
            {
                // invalid sequence
                return false;
            }
        }

        //
        // If we succeeded, post-process the results a bit
        //
        if (status)
        {

            //
            // Since we always POP to the LR, recover the final PC from there, unless
            // it was overwritten due to a special case custom unwinding operation.
            // Also set the establisher frame equal to the final stack pointer.
            //
            if (finalPcFromLr)
            {
                context.Pc = context.Lr;
            }
        }

        return status;
    }

    private unsafe bool UnwindCustom(
        ref ARM64Context context,
        byte customCode)
    {
        ulong startingSp = context.Sp;

        switch (customCode)
        {
            //
            // Trap frame case
            //
            case 0xE8: // MSFT_OP_TRAP_FRAME:
            {
                //
                // Restore X0-X18, and D0-D7
                //
                TargetPointer sourceAddress = startingSp + /*offsetof(ARM64_KTRAP_FRAME, X)*/ 0x0A0;
                for (uint regIndex = 0; regIndex < 19; regIndex++)
                {
                    SetRegister(ref context, regIndex, _target.Read<ulong>(sourceAddress));
                    sourceAddress += sizeof(ulong);
                }

                sourceAddress = startingSp + /*offsetof(ARM64_KTRAP_FRAME, VfpState)*/ 0x010;
                TargetPointer vfpStateAddress = _target.Read<ulong>(sourceAddress);
                if (vfpStateAddress != 0)
                {
                    sourceAddress = vfpStateAddress + /*offsetof(KARM64_VFP_STATE, Fpcr)*/ 0x08;
                    uint Fpcr = _target.Read<uint>(sourceAddress);
                    sourceAddress = vfpStateAddress + /*offsetof(KARM64_VFP_STATE, Fpcr)*/ 0x0C;
                    uint Fpsr = _target.Read<uint>(sourceAddress);
                    if (Fpcr != uint.MaxValue && Fpsr != uint.MaxValue)
                    {
                        context.Fpcr = Fpcr;
                        context.Fpsr = Fpsr;

                        sourceAddress = vfpStateAddress + /*offsetof(KARM64_VFP_STATE, V)*/ 0x10;
                        for (uint regIndex = 0; regIndex < 32; regIndex++)
                        {
                            context.V[regIndex * 2] = _target.Read<ulong>(sourceAddress);
                            context.V[(regIndex * 2) + 1] = _target.Read<ulong>(sourceAddress + 8);
                            sourceAddress += 2 * sizeof(ulong);
                        }
                    }
                }

                //
                // Restore R11, R12, SP, LR, PC, and the status registers
                //
                sourceAddress = startingSp + /*offsetof(ARM64_KTRAP_FRAME, Spsr)*/ 0x090;
                context.Cpsr = _target.Read<uint>(sourceAddress);

                sourceAddress = startingSp + /*offsetof(ARM64_KTRAP_FRAME, Sp)*/ 0x098;
                context.Sp = _target.Read<ulong>(sourceAddress);

                sourceAddress = startingSp + /*offsetof(ARM64_KTRAP_FRAME, Lr)*/ 0x138;
                context.Lr = _target.Read<ulong>(sourceAddress);

                sourceAddress = startingSp + /*offsetof(ARM64_KTRAP_FRAME, Fp)*/ 0x140;
                context.Fp = _target.Read<ulong>(sourceAddress);

                sourceAddress = startingSp + /*offsetof(ARM64_KTRAP_FRAME, Pc)*/ 0x148;
                context.Pc = _target.Read<ulong>(sourceAddress);

                //
                // Clear the unwound-to-call flag
                //
                context.ContextFlags &= (uint)~ContextFlagsValues.CONTEXT_UNWOUND_TO_CALL;
                break;
            }

            //
            // Machine frame case
            //
            case 0xE9:  // MSFT_OP_MACHINE_FRAME:
            {
                //
                // Restore the SP and PC, and clear the unwound-to-call flag
                //
                context.Sp = _target.Read<ulong>(startingSp + 0);
                context.Pc = _target.Read<ulong>(startingSp + 8);
                context.ContextFlags &= (uint)~ContextFlagsValues.CONTEXT_UNWOUND_TO_CALL;
                break;
            }

            //
            // Context case
            //
            case 0xEA:  // MSFT_OP_CONTEXT:
            {
                //
                // Restore X0-X28, and D0-D31
                //
                TargetPointer sourceAddress = startingSp + (uint)Marshal.OffsetOf<ARM64Context>(nameof(ARM64Context.X0));
                for (uint regIndex = 0; regIndex < 29; regIndex++)
                {
                    SetRegister(ref context, regIndex, _target.Read<ulong>(sourceAddress));
                    sourceAddress += sizeof(ulong);
                }

                sourceAddress = startingSp + (uint)Marshal.OffsetOf<ARM64Context>(nameof(ARM64Context.V));
                for (uint regIndex = 0; regIndex < 32; regIndex++)
                {
                    context.V[regIndex * 2] = _target.Read<ulong>(sourceAddress);
                    context.V[(regIndex * 2) + 1] = _target.Read<ulong>(sourceAddress + 8);
                    sourceAddress += 2 * sizeof(ulong);
                }

                //
                // Restore SP, LR, PC, and the status registers
                //
                sourceAddress = startingSp + (uint)Marshal.OffsetOf<ARM64Context>(nameof(ARM64Context.Cpsr));
                context.Cpsr = _target.Read<uint>(sourceAddress);

                sourceAddress = startingSp + (uint)Marshal.OffsetOf<ARM64Context>(nameof(ARM64Context.Fp));
                context.Fp = _target.Read<ulong>(sourceAddress);

                sourceAddress = startingSp + (uint)Marshal.OffsetOf<ARM64Context>(nameof(ARM64Context.Lr));
                context.Lr = _target.Read<ulong>(sourceAddress);

                sourceAddress = startingSp + (uint)Marshal.OffsetOf<ARM64Context>(nameof(ARM64Context.Sp));
                context.Sp = _target.Read<ulong>(sourceAddress);

                sourceAddress = startingSp + (uint)Marshal.OffsetOf<ARM64Context>(nameof(ARM64Context.Pc));
                context.Pc = _target.Read<ulong>(sourceAddress);

                sourceAddress = startingSp + (uint)Marshal.OffsetOf<ARM64Context>(nameof(ARM64Context.Fpcr));
                context.Fpcr = _target.Read<uint>(sourceAddress);

                sourceAddress = startingSp + (uint)Marshal.OffsetOf<ARM64Context>(nameof(ARM64Context.Fpsr));
                context.Fpsr = _target.Read<uint>(sourceAddress);

                //
                // Inherit the unwound-to-call flag from this context
                //
                sourceAddress = startingSp + (uint)Marshal.OffsetOf<ARM64Context>(nameof(ARM64Context.ContextFlags));
                context.ContextFlags &= (uint)~ContextFlagsValues.CONTEXT_UNWOUND_TO_CALL;
                context.ContextFlags |=
                                _target.Read<uint>(sourceAddress) & (uint)ContextFlagsValues.CONTEXT_UNWOUND_TO_CALL;
                break;
            }

            case 0xEB:  // MSFT_OP_EC_CONTEXT:
                // NOTE: for .NET, the arm64ec context restoring is not implemented
                UnwinderAssert(false);
                return false;

            case 0xec:  // MSFT_OP_CLEAR_UNWOUND_TO_CALL
                context.ContextFlags &= (uint)~ContextFlagsValues.CONTEXT_UNWOUND_TO_CALL;
                context.Pc = context.Lr;
                break;

            default:
                return false;
        }

        return true;
    }

    #endregion
    #region Helpers

    private uint ComputeScopeSize(
        TargetPointer unwindCodePtr,
        TargetPointer unwindCodesEndPtr,
        bool isEpilog)
    {
        uint scopeSize = 0;
        byte opcode;

        //
        // Iterate through the unwind codes until we hit an end marker.
        // While iterating, accumulate the total scope size.
        //

        while (unwindCodePtr < unwindCodesEndPtr)
        {
            opcode = _target.Read<byte>(unwindCodePtr);
            if (OPCODE_IS_END(opcode))
                break;

            unwindCodePtr += GetUnwindCodeSize(opcode);
            scopeSize += GetUnwindCodeScopeSize(opcode);
        }

        //
        // Epilogs have one extra instruction at the end that needs to be
        // accounted for.
        //
        if (isEpilog)
            scopeSize++;

        return scopeSize;
    }

    private static uint GetUnwindCodeSize(byte unwindCode)
    {
        if (unwindCode < 0xC0)
            return 1;

        if (unwindCode < 0xE0)
            return 2;

        return UnwindCodeSizeTable[unwindCode - 0xE0];
    }

    private static uint GetUnwindCodeScopeSize(byte unwindCode)
    {
        if (unwindCode < 0xE0)
            return 1;

        return UnwindCodeInstructionCountTable[unwindCode - 0xE0];
    }

    /// <summary>
    /// Restores a series of integer registers from the stack.
    /// </summary>
    /// <param name="context">ref of the context.</param>
    /// <param name="spOffset">
    /// Specifies a stack offset. Positive values are simply used
    /// as a base offset. Negative values assume a pre-decrement behavior:
    /// a 0 offset is used for restoration, but the absolute value of the
    /// offset is added to the final Sp.
    /// </param>
    /// <param name="firstRegister">Specifies the index of the first register to restore.</param>
    /// <param name="registerCount">Specifies the number of registers to restore.</param>
    private bool RestoreRegisterRange(
        ref ARM64Context context,
        int spOffset,
        uint firstRegister /* in range (0, 30) */,
        uint registerCount /* in range (1, 31 - firstRegister) */)
    {
        if (firstRegister + registerCount > 31)
        {
            // invalid register range
            return false;
        }

        //
        // Compute the source address
        //
        TargetPointer curAddress = context.Sp;
        if (spOffset >= 0)
        {
            curAddress += (uint)spOffset;
        }

        //
        // Restore the registers
        //
        for (uint regIndex = 0; regIndex < registerCount; regIndex++)
        {
            SetRegister(ref context, firstRegister + regIndex, _target.Read<ulong>(curAddress));
            curAddress += 8;
        }
        if (spOffset < 0)
        {
            context.Sp -= (ulong)spOffset;
        }

        return true;
    }

    /// <summary>
    /// Restores a series of floating-point registers from the stack.
    /// </summary>
    /// <param name="context">ref of the context.</param>
    /// <param name="spOffset">
    /// Specifies a stack offset. Positive values are simply used
    /// as a base offset. Negative values assume a pre-decrement behavior:
    /// a 0 offset is used for restoration, but the absolute value of the
    /// offset is added to the final Sp.
    /// </param>
    /// <param name="firstRegister">Specifies the index of the first register to restore.</param>
    /// <param name="registerCount">Specifies the number of registers to restore.</param>
    private unsafe bool RestoreFpRegisterRange(
        ref ARM64Context context,
        int spOffset,
        uint firstRegister,
        uint registerCount)
    {
        if (firstRegister + registerCount > 32)
        {
            // invalid register range
            return false;
        }

        //
        // Compute the source address
        //
        TargetPointer curAddress = context.Sp;
        if (spOffset >= 0)
        {
            curAddress += (uint)spOffset;
        }

        //
        // Restore the registers
        //
        for (uint regIndex = 0; regIndex < registerCount; regIndex++)
        {
            // double register values to only index into the low 64 bits of each 128-bit register
            context.V[(firstRegister + regIndex) * 2] = _target.Read<ulong>(curAddress);
            curAddress += 8;
        }
        if (spOffset < 0)
        {
            context.Sp -= (ulong)spOffset;
        }

        return true;
    }

    /// <summary>
    /// Restores a series of full SIMD (Q) registers from the stack.
    /// </summary>
    /// <param name="context">ref of the context.</param>
    /// <param name="spOffset">
    /// Specifies a stack offset. Positive values are simply used
    /// as a base offset. Negative values assume a pre-decrement behavior:
    /// a 0 offset is used for restoration, but the absolute value of the
    /// offset is added to the final Sp.
    /// </param>
    /// <param name="firstRegister">Specifies the index of the first register to restore.</param>
    /// <param name="registerCount">Specifies the number of registers to restore.</param>
    private unsafe bool RestoreSimdRegisterRange(
        ref ARM64Context context,
        int spOffset,
        uint firstRegister,
        uint registerCount)
    {
        if (firstRegister + registerCount > 32)
        {
            // invalid register range
            return false;
        }

        //
        // Compute the source address
        //
        TargetPointer curAddress = context.Sp;
        if (spOffset >= 0)
        {
            curAddress += (uint)spOffset;
        }

        //
        // Restore the registers
        //
        for (uint regIndex = 0; regIndex < registerCount; regIndex++)
        {
            // V indexes are 64-bit values of the 128-bit registers
            // double register values to write the low 64 bits of the 128-bit register
            context.V[(firstRegister + regIndex) * 2] = _target.Read<ulong>(curAddress);
            curAddress += 8;
            // double register values + 1 to write the high 64 bits of the 128-bit register
            context.V[((firstRegister + regIndex) * 2) + 1] = _target.Read<ulong>(curAddress);
            curAddress += 8;
        }
        if (spOffset < 0)
        {
            context.Sp -= (ulong)spOffset;
        }

        return true;
    }

    private static void SetRegister(ref ARM64Context context, uint regIndex, ulong value)
    {
        if (!context.TrySetRegister((int)regIndex, new TargetNUInt(value)))
            throw new ArgumentOutOfRangeException(nameof(regIndex));
    }

    private static bool OPCODE_IS_END(byte opcode)
    {
        return (opcode & 0xFE) == 0xE4;
    }

    private static void UnwinderAssert([DoesNotReturnIf(false)] bool condition, string? message = null)
    {
        if (!condition)
        {
            throw new InvalidOperationException(message);
        }
    }

    #endregion
    #region Structs

    private struct SaveAnyUnwindCode(byte val1, byte val2)
    {
        public byte o = (byte)(val1 & 0x3f);
        public byte f = (byte)(val1 >> 6);
        public byte r = (byte)(val2 & 0x1f);
        public byte x = (byte)((val1 >> 5) & 0x1);
        public byte p = (byte)((val1 >> 6) & 0x1);
        public byte fixedOp = (byte)((val1 >> 7) & 0x1);
    }

    #endregion
}