File: Contracts\GCInfo\X86\GCArgTable.cs
Web Access
Project: 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.Collections.Generic;

namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers.X86;

public class GCArgTable
{
    private const uint byref_OFFSET_FLAG = 0x1;

    private readonly Target _target;
    private readonly InfoHdr _header;

    public Dictionary<int, List<BaseGcTransition>> Transitions { get; private set; } = [];

    public GCArgTable(Target target, InfoHdr header, TargetPointer argTablePtr)
    {
        _target = target;
        _header = header;

        TargetPointer offset = argTablePtr;
        if (header.Interruptible)
        {
            GetTransitionsFullyInterruptible(ref offset);
        }
        else if (_header.EbpFrame)
        {
            GetTransitionsEbpFrame(ref offset);
        }
        else
        {
            GetTransitionsNoEbp(ref offset);
        }
    }

    private void AddNewTransition(BaseGcTransition transition)
    {
        if (!Transitions.TryGetValue(transition.CodeOffset, out List<BaseGcTransition>? value))
        {
            value = [];
            Transitions[transition.CodeOffset] = value;
        }

        value.Add(transition);
    }

    private void ArgEncoding(ref uint isPop, ref uint argOffs, ref uint argCnt, ref uint curOffs, ref bool isThis, ref bool iptr)
    {
        if (isPop != 0)
        {
            // A Pop of 0, means little-delta

            if (argOffs != 0)
            {
                AddNewTransition(new GcTransitionPointer((int)curOffs, argOffs, argCnt - argOffs, Action.POP, _header.EbpFrame));
            }
        }
        else
        {
            AddNewTransition(new GcTransitionPointer((int)curOffs, argOffs, argOffs + 1, Action.PUSH, _header.EbpFrame, isThis, iptr));
            isThis = false;
            iptr = false;
        }
    }

    private static RegMask ThreeBitEncodingToRegMask(byte val) =>
        (val & 0x7) switch
        {
            0x0 => RegMask.EAX,
            0x1 => RegMask.ECX,
            0x2 => RegMask.EDX,
            0x3 => RegMask.EBX,
            0x4 => RegMask.ESP,
            0x5 => RegMask.EBP,
            0x6 => RegMask.ESI,
            0x7 => RegMask.EDI,
            _ => throw new ArgumentOutOfRangeException(nameof(val), $"Not expected register value: {val}"),
        };

    private static RegMask TwoBitEncodingToRegMask(byte val) =>
        (val & 0x3) switch
        {
            0x0 => RegMask.EDI,
            0x1 => RegMask.ESI,
            0x2 => RegMask.EBX,
            0x3 => RegMask.EBP,
            _ => throw new ArgumentOutOfRangeException(nameof(val), $"Not expected register value: {val}"),
        };

    /// <summary>
    /// based on <a href="https://github.com/dotnet/runtime/blob/main/src/coreclr/gcdump/i386/gcdumpx86.cpp">GCDump::DumpGCTable</a>
    /// </summary>
    private void GetTransitionsFullyInterruptible(ref TargetPointer offset)
    {
        uint argCnt = 0;
        bool isThis = false;
        bool iptr = false;
        uint curOffs = 0;

        while (true)
        {
            uint isPop;
            uint argOffs;
            uint val = _target.Read<byte>(offset++);

            if ((val & 0x80) == 0)
            {
                /* A small 'regPtr' encoding */

                curOffs += val & 0x7;

                Action isLive = Action.LIVE;
                if ((val & 0x40) == 0)
                    isLive = Action.DEAD;
                AddNewTransition(new GcTransitionRegister((int)curOffs, ThreeBitEncodingToRegMask((byte)((val >> 3) & 7)), isLive, isThis, iptr));

                isThis = false;
                iptr = false;
                continue;
            }

            /* This is probably an argument push/pop */

            argOffs = (val & 0x38) >> 3;

            /* 6 [110] and 7 [111] are reserved for other encodings */

            if (argOffs < 6)
            {
                /* A small argument encoding */

                curOffs += val & 0x07;
                isPop = val & 0x40;

                ArgEncoding(ref isPop, ref argOffs, ref argCnt, ref curOffs, ref isThis, ref iptr);

                continue;
            }
            else if (argOffs == 6)
            {
                if ((val & 0x40) != 0)
                {
                    curOffs += (((val & 0x07) + 1) << 3);
                }
                else
                {
                    // non-ptr arg push

                    curOffs += (val & 0x07);
                    argCnt++;
                    AddNewTransition(new GcTransitionPointer((int)curOffs, argOffs, argCnt, Action.PUSH, _header.EbpFrame, false, false, false));
                }

                continue;
            }

            // argOffs was 7 [111] which is reserved for the larger encodings
            switch (val)
            {
                case 0xFF:
                    return;
                case 0xBC:
                    isThis = true;
                    break;
                case 0xBF:
                    iptr = true;
                    break;
                case 0xB8:
                    val = _target.GCDecodeUnsigned(ref offset);
                    curOffs += val;
                    break;
                case 0xF8:
                case 0xFC:
                    isPop = val & 0x04;
                    argOffs = _target.GCDecodeUnsigned(ref offset);
                    ArgEncoding(ref isPop, ref argOffs, ref argCnt, ref curOffs, ref isThis, ref iptr);
                    break;
                case 0xFD:
                    argOffs = _target.GCDecodeUnsigned(ref offset);
                    AddNewTransition(new GcTransitionPointer((int)curOffs, argOffs, argCnt, Action.KILL, _header.EbpFrame));
                    break;
                case 0xF9:
                    argOffs = _target.GCDecodeUnsigned(ref offset);
                    AddNewTransition(new StackDepthTransition((int)curOffs, (int)argOffs));
                    argCnt += argOffs;
                    break;
                default:
                    throw new BadImageFormatException($"Unexpected special code {val}");
            }
        }
    }

    /// <summary>
    /// based on <a href="https://github.com/dotnet/runtime/blob/main/src/coreclr/gcdump/i386/gcdumpx86.cpp">GCDump::DumpGCTable</a>
    /// </summary>
    private void GetTransitionsEbpFrame(ref TargetPointer offset)
    {
        uint curOffs = 0;
        while (true)
        {
            uint argMask = 0, byrefArgMask = 0;
            uint regMask, byrefRegMask = 0;

            uint argCnt = 0;
            TargetPointer argOffset = offset;
            uint argTabSize;

            uint val, nxt;

            // Get the next byte and check for a 'special' entry
            uint encType = _target.Read<byte>(offset++);
            GcTransitionCall? transition;

            switch (encType)
            {
                default:
                    // A tiny or small call entry
                    val = encType;

                    if ((val & 0x80) == 0x00)
                    {
                        if ((val & 0x0F) != 0)
                        {
                            // A tiny call entry

                            curOffs += (val & 0x0F);
                            regMask = (val & 0x70) >> 4;
                            argMask = 0;
                        }
                        else
                        {
                            // "This pointer liveness encoding" (val & 0x80 == 0 && val & 0x0F == 0):
                            // metadata for which callee-saved register holds the 'this' pointer
                            // at the next call site. Native (gc_unwind_x86.inl ~line 970) does NOT
                            // record a call entry here -- it only sets thisPtrReg. Adding a spurious
                            // GcTransitionCall at the current curOffs would overwrite the real
                            // call site's CallRegisters during EnumerateLiveSlots (since the
                            // partial-EBP decoder may emit the this-ptr tag at the same curOffs
                            // as a real call site), so we just consume the byte and continue.
                            continue;
                        }
                    }
                    else
                    {
                        // A small call entry
                        curOffs += (val & 0x7F);
                        val = _target.Read<byte>(offset++);
                        regMask = val >> 5;
                        argMask = val & 0x1F;
                    }
                    break;

                case 0xFD:  // medium encoding
                    argMask = _target.Read<byte>(offset++);
                    val = _target.Read<byte>(offset++);
                    argMask |= (val & 0xF0) << 4;
                    nxt = _target.Read<byte>(offset++);
                    curOffs += (val & 0x0F) + ((nxt & 0x1F) << 4);
                    regMask = nxt >> 5;                   // EBX,ESI,EDI
                    break;

                case 0xF9:  // medium encoding with byrefs
                    curOffs += _target.Read<byte>(offset++);
                    val = _target.Read<byte>(offset++);
                    argMask = val & 0x1F;
                    regMask = val >> 5;
                    val = _target.Read<byte>(offset++);
                    byrefArgMask = val & 0x1F;
                    byrefRegMask = val >> 5;
                    break;
                case 0xFE:  // large encoding
                case 0xFA:  // large encoding with byrefs
                    val = _target.Read<byte>(offset++);
                    regMask = val & 0x7;
                    byrefRegMask = val >> 4;

                    curOffs += _target.Read<uint>(offset);
                    offset += 4;
                    argMask = _target.Read<uint>(offset);
                    offset += 4;

                    if (encType == 0xFA) // read byrefArgMask
                    {
                        byrefArgMask = _target.Read<uint>(offset);
                        offset += 4;
                    }
                    break;
                case 0xFB:  // huge encoding
                    val = _target.Read<byte>(offset++);
                    regMask = val & 0x7;
                    byrefRegMask = val >> 4;
                    // Code delta is 32-bit and is added to curOffs (mirrors `scanOffs +=` in
                    // native gc_unwind_x86.inl scanArgRegTable case 0xFB).
                    curOffs += _target.Read<uint>(offset);
                    offset += 4;
                    argCnt = _target.Read<uint>(offset);
                    offset += 4;
                    argTabSize = _target.Read<uint>(offset);
                    offset += 4;
                    argOffset = offset;
                    offset += argTabSize;
                    break;
                case 0xFF:
                    return;
            }

            /*
                Here we have the following values:

                curOffs      ...    the code offset of the call
                regMask      ...    mask of live pointer register variables
                argMask      ...    bitmask of pushed pointer arguments
                byrefRegMask ...    byref qualifier for regMask
                byrefArgMask ...    byrer qualifier for argMask
            */
            transition = new GcTransitionCall((int)curOffs, _header.EbpFrame, regMask, byrefRegMask);
            AddNewTransition(transition);

            if (argCnt != 0)
            {
                do
                {
                    val = _target.GCDecodeUnsigned(ref argOffset);

                    uint stkOffs = val & ~byref_OFFSET_FLAG;
                    uint lowBit = val & byref_OFFSET_FLAG;
                    transition.PtrArgs.Add(new GcTransitionCall.PtrArg(stkOffs, lowBit));
                }
                while (--argCnt > 0);
            }
            else
            {
                transition.ArgMask = argMask;
                if (byrefArgMask != 0)
                    transition.IArgs = byrefArgMask;
            }
        }
    }

    /// <summary>
    /// based on <a href="https://github.com/dotnet/runtime/blob/main/src/coreclr/gcdump/i386/gcdumpx86.cpp">GCDump::DumpGCTable</a>
    /// </summary>
    private void SaveCallTransition(ref TargetPointer offset, uint curOffs, uint callRegMask, bool callPndTab, uint callPndTabCnt, uint callPndMask, ref uint imask)
    {
        uint iregMask, iargMask;
        iregMask = imask & 0xF;
        iargMask = imask >> 4;

        GcTransitionCall transition = new GcTransitionCall((int)curOffs, _header.EbpFrame, callRegMask, iregMask);
        AddNewTransition(transition);

        if (callPndTab)
        {
            for (int i = 0; i < callPndTabCnt; i++)
            {
                uint pndOffs = _target.GCDecodeUnsigned(ref offset);
                transition.PtrArgs.Add(new GcTransitionCall.PtrArg(pndOffs, 0));
            }
        }
        else
        {
            if (callPndMask != 0)
                transition.ArgMask = callPndMask;
            if (iargMask != 0)
                transition.IArgs = iargMask;
        }

        imask /* = lastSkip  */ = 0;
    }

    private void GetTransitionsNoEbp(ref TargetPointer offset)
    {
        uint curOffs = 0;
        uint imask = 0;
        uint lastSkip;

        for (; ; )
        {
            uint val = _target.Read<byte>(offset++);

            if ((val & 0x80) == 0)
            {
                if ((val & 0x40) == 0)
                {
                    if ((val & 0x20) == 0)
                    {
                        // push    000DDDDD          push one item, 5-bit delta
                        curOffs += val & 0x1F;
                        AddNewTransition(new GcTransitionRegister((int)curOffs, RegMask.ESP, Action.PUSH));
                    }
                    else
                    {
                        // push    00100000 [pushCount]     ESP push multiple items
                        uint pushCount = _target.GCDecodeUnsigned(ref offset);
                        AddNewTransition(new GcTransitionRegister((int)curOffs, RegMask.ESP, Action.PUSH, false, false, (int)pushCount));
                    }
                }
                else
                {
                    uint popSize;
                    uint skip;

                    if ((val & 0x3f) == 0)
                    {
                        //
                        // skip    01000000 [Delta]  Skip arbitrary sized delta
                        //
                        skip = _target.GCDecodeUnsigned(ref offset);
                        curOffs += skip;
                    }
                    else
                    {
                        //  pop     01CCDDDD         pop CC items, 4-bit delta
                        popSize = (val & 0x30) >> 4;
                        skip = val & 0x0f;
                        curOffs += skip;

                        if (popSize > 0)
                        {
                            AddNewTransition(new GcTransitionRegister((int)curOffs, RegMask.ESP, Action.POP, false, false, (int)popSize));
                        }
                    }
                }
            }
            else
            {
                uint callArgCnt;
                uint callRegMask;
                bool callPndTab = false;
                uint callPndMask = 0;
                uint callPndTabCnt = 0;

                switch ((val & 0x70) >> 4)
                {
                    default:
                        //
                        // call    1PPPPPPP          Call Pattern, P=[0..79]
                        //
                        CallPattern.DecodeCallPattern((val & 0x7f), out callArgCnt, out callRegMask, out callPndMask, out lastSkip);
                        curOffs += lastSkip;
                        SaveCallTransition(ref offset, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, ref imask);
                        AddNewTransition(new StackDepthTransition((int)curOffs, -(int)callArgCnt));
                        break;

                    case 5:
                        //
                        // call    1101RRRR DDCCCMMM  Call RegMask=RRRR,ArgCnt=CCC,
                        //                        ArgMask=MMM Delta=commonDelta[DD]
                        //
                        callRegMask = val & 0xf;    // EBP,EBX,ESI,EDI
                        val = _target.Read<byte>(offset++);
                        callPndMask = val & 0x7;
                        callArgCnt = (val >> 3) & 0x7;
                        lastSkip = CallPattern.CallCommonDelta[(int)(val >> 6)];
                        curOffs += lastSkip;
                        SaveCallTransition(ref offset, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, ref imask);
                        AddNewTransition(new StackDepthTransition((int)curOffs, -(int)callArgCnt));
                        break;
                    case 6:
                        //
                        // call    1110RRRR [ArgCnt] [ArgMask]
                        //                          Call ArgCnt,RegMask=RRR,ArgMask
                        //
                        callRegMask = val & 0xf;    // EBP,EBX,ESI,EDI
                        callArgCnt = _target.GCDecodeUnsigned(ref offset);
                        callPndMask = _target.GCDecodeUnsigned(ref offset);
                        SaveCallTransition(ref offset, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, ref imask);
                        AddNewTransition(new StackDepthTransition((int)curOffs, -(int)callArgCnt));
                        break;
                    case 7:
                        switch (val & 0x0C)
                        {
                            case 0x00:
                                //  iptr 11110000 [IPtrMask] Arbitrary Interior Pointer Mask
                                imask = _target.GCDecodeUnsigned(ref offset);
                                AddNewTransition(new IPtrMask((int)curOffs, imask));
                                break;

                            case 0x04:
                                AddNewTransition(new CalleeSavedRegister((int)curOffs, TwoBitEncodingToRegMask((byte)(val & 0x3))));
                                break;

                            case 0x08:
                                val = _target.Read<byte>(offset++);
                                callRegMask = val & 0xF;
                                imask = val >> 4;
                                lastSkip = _target.Read<uint>(offset);
                                offset += 4;
                                curOffs += lastSkip;
                                callArgCnt = _target.Read<uint>(offset);
                                offset += 4;
                                callPndTabCnt = _target.Read<uint>(offset);
                                offset += 4;
                                // Skip callPndTabSize - present in encoding but unused by the decoder.
                                offset += 4;
                                callPndTab = true;
                                SaveCallTransition(ref offset, curOffs, callRegMask, callPndTab, callPndTabCnt, callPndMask, ref imask);
                                AddNewTransition(new StackDepthTransition((int)curOffs, -(int)callArgCnt));
                                break;
                            case 0x0C:
                                return;
                            default:
                                throw new BadImageFormatException("Invalid GC encoding");
                        }
                        break;
                }
            }
        }
    }
}