File: Contracts\Debugger\Debugger_1.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.Buffers.Binary;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers;

namespace Microsoft.Diagnostics.DataContractReader.Contracts;

internal readonly struct Debugger_1 : IDebugger
{
    private enum DebuggerControlFlag_1 : uint
    {
        PendingAttach = 0x0100,
        Attached = 0x0200,
    }
    private const uint UnhandledExceptionHijackIndex = 0;

    private readonly Target _target;

    internal Debugger_1(Target target)
    {
        _target = target;
    }

    private bool TryGetDebuggerAddress(out TargetPointer debuggerAddress)
    {
        debuggerAddress = TargetPointer.Null;

        TargetPointer debuggerPtrPtr = _target.ReadGlobalPointer(Constants.Globals.Debugger);
        if (debuggerPtrPtr == TargetPointer.Null)
            return false;

        debuggerAddress = _target.ReadPointer(debuggerPtrPtr);
        return debuggerAddress != TargetPointer.Null;
    }

    bool IDebugger.TryGetDebuggerData(out DebuggerData data)
    {
        data = default;
        if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress))
            return false;

        Data.Debugger debugger = _target.ProcessedData.GetOrAdd<Data.Debugger>(debuggerAddress);
        data = new DebuggerData(debugger.LeftSideInitialized != 0, debugger.Defines, debugger.MDStructuresVersion);
        return true;
    }

    int IDebugger.GetAttachStateFlags()
    {
        TargetPointer addr = _target.ReadGlobalPointer(Constants.Globals.CLRJitAttachState);
        return (int)_target.Read<uint>(addr.Value);
    }

    void IDebugger.MarkDebuggerAttachPending()
    {
        TargetPointer addr = _target.ReadGlobalPointer(Constants.Globals.CORDebuggerControlFlags);
        uint currentFlags = _target.Read<uint>(addr.Value);
        _target.Write<uint>(addr.Value, currentFlags | (uint)DebuggerControlFlag_1.PendingAttach);
    }

    void IDebugger.MarkDebuggerAttached(bool fAttached)
    {
        TargetPointer addr = _target.ReadGlobalPointer(Constants.Globals.CORDebuggerControlFlags);
        uint currentFlags = _target.Read<uint>(addr.Value);
        if (fAttached)
        {
            _target.Write<uint>(addr.Value, currentFlags | (uint)DebuggerControlFlag_1.Attached);
        }
        else
        {
            _target.Write<uint>(addr.Value, currentFlags & ~((uint)DebuggerControlFlag_1.Attached | (uint)DebuggerControlFlag_1.PendingAttach));
        }
    }

    bool IDebugger.MetadataUpdatesApplied()
    {
        if (_target.TryReadGlobalPointer(Constants.Globals.MetadataUpdatesApplied, out TargetPointer? addr))
        {
            return _target.Read<byte>(addr.Value.Value) != 0;
        }
        return false;
    }

    void IDebugger.RequestSyncAtEvent()
    {
        if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress))
            return;

        Data.Debugger debugger = _target.ProcessedData.GetOrAdd<Data.Debugger>(debuggerAddress);
        debugger.WriteRSRequestedSync(1);
    }

    void IDebugger.SetSendExceptionsOutsideOfJMC(bool sendExceptionsOutsideOfJMC)
    {
        if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress))
            return;

        Data.Debugger debugger = _target.ProcessedData.GetOrAdd<Data.Debugger>(debuggerAddress);
        debugger.WriteSendExceptionsOutsideOfJMC(sendExceptionsOutsideOfJMC ? 1 : 0);
    }

    TargetPointer IDebugger.GetDebuggerControlBlockAddress()
    {
        if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress))
            return TargetPointer.Null;

        Data.Debugger debugger = _target.ProcessedData.GetOrAdd<Data.Debugger>(debuggerAddress);
        TargetPointer rcThread = debugger.RCThread;
        if (rcThread == TargetPointer.Null)
            return TargetPointer.Null;

        Data.DebuggerRCThread debuggerRcThread = _target.ProcessedData.GetOrAdd<Data.DebuggerRCThread>(rcThread);
        return debuggerRcThread.DCB;
    }

    void IDebugger.EnableGCNotificationEvents(bool fEnable)
    {
        if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress))
            return;

        Data.Debugger debugger = _target.ProcessedData.GetOrAdd<Data.Debugger>(debuggerAddress);
        debugger.WriteGCNotificationEventsEnabled(fEnable ? 1 : 0);
    }

    HijackKind IDebugger.GetHijackKind(TargetCodePointer controlPC)
    {
        if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress))
            return HijackKind.None;

        Data.Debugger debugger = _target.ProcessedData.GetOrAdd<Data.Debugger>(debuggerAddress);
        if (debugger.RgHijackFunction == TargetPointer.Null)
            return HijackKind.None;

        uint maxHijackFunctions = _target.ReadGlobal<uint>(Constants.Globals.MaxHijackFunctions);
        if (maxHijackFunctions == 0)
            return HijackKind.None;

        Target.TypeInfo memoryRangeTypeInfo = _target.GetTypeInfo(DataType.MemoryRange);
        uint stride = memoryRangeTypeInfo.Size!.Value;

        for (uint i = 0; i < maxHijackFunctions; i++)
        {
            TargetPointer entryAddress = debugger.RgHijackFunction + (ulong)(i * stride);
            Data.MemoryRange entry = _target.ProcessedData.GetOrAdd<Data.MemoryRange>(entryAddress);

            ulong start = entry.StartAddress.Value;
            ulong end = start + entry.Size.Value;
            if (controlPC.Value >= start && controlPC.Value < end)
            {
                return i == UnhandledExceptionHijackIndex ? HijackKind.UnhandledException : HijackKind.Other;
            }
        }
        return HijackKind.None;
    }

    private TargetPointer GetHijackAddress()
    {
        return TryGetHijackFunctionRange(UnhandledExceptionHijackIndex, out Data.MemoryRange? range)
            ? range.StartAddress
            : TargetPointer.Null;
    }

    private bool TryGetHijackFunctionRange(uint index, [NotNullWhen(true)] out Data.MemoryRange? range)
    {
        range = null;
        if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress))
            return false;

        Data.Debugger debugger = _target.ProcessedData.GetOrAdd<Data.Debugger>(debuggerAddress);
        if (debugger.RgHijackFunction == TargetPointer.Null)
            return false;

        uint maxHijackFunctions = _target.ReadGlobal<uint>(Constants.Globals.MaxHijackFunctions);
        if (index >= maxHijackFunctions)
            return false;

        uint stride = _target.GetTypeInfo(DataType.MemoryRange).Size!.Value;
        TargetPointer entryAddress = debugger.RgHijackFunction + (ulong)(index * stride);
        range = _target.ProcessedData.GetOrAdd<Data.MemoryRange>(entryAddress);
        return true;
    }

    // offsetof(EXCEPTION_RECORD, ExceptionInformation) for the target's pointer size.
    // Layout: ExceptionCode (DWORD) + ExceptionFlags (DWORD) + ExceptionRecord (ptr) +
    //         ExceptionAddress (ptr) + NumberParameters (DWORD), then ExceptionInformation[]
    //         aligned up to the pointer size.
    private int ExceptionRecordHeaderSize()
    {
        int ptrSize = _target.PointerSize;
        int unaligned = sizeof(uint) + sizeof(uint) + ptrSize + ptrSize + sizeof(uint);
        return (unaligned + (ptrSize - 1)) & ~(ptrSize - 1);
    }

    // EXCEPTION_RECORD::NumberParameters lives after the two leading DWORDs and the two pointers.
    private uint ReadExceptionRecordNumberParameters(ReadOnlySpan<byte> record)
    {
        int numberParametersOffset = sizeof(uint) + sizeof(uint) + (2 * _target.PointerSize);
        ReadOnlySpan<byte> slice = record.Slice(numberParametersOffset, sizeof(uint));
        return _target.IsLittleEndian
            ? BinaryPrimitives.ReadUInt32LittleEndian(slice)
            : BinaryPrimitives.ReadUInt32BigEndian(slice);
    }

    private void WriteExceptionRecordHelper(TargetPointer remotePtr, byte[] record)
    {
        uint numberParameters = ReadExceptionRecordNumberParameters(record);
        int cbSize = ExceptionRecordHeaderSize() + ((int)numberParameters * _target.PointerSize);
        _target.WriteBuffer(remotePtr.Value, record.AsSpan(0, cbSize));
    }

    TargetPointer IDebugger.PrepareExceptionHijack(byte[] context, TargetPointer vmThread, byte[]? exceptionRecord, int reason, TargetPointer userData)
    {
        TargetPointer pfnHijackFunction = GetHijackAddress();
        if (pfnHijackFunction == TargetPointer.Null)
            throw Marshal.GetExceptionForHR(CorDbgHResults.CORDBG_E_NOTREADY)!;

        IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(_target);
        ctx.FillFromBuffer(context);

        ctx.UnsetSingleStepFlag();

        TargetPointer sp = ctx.StackPointer;
        TargetPointer espContext = TargetPointer.Null;
        TargetPointer espRecord = TargetPointer.Null;

        if (vmThread != TargetPointer.Null)
        {
            ThreadData threadData = _target.Contracts.Thread.GetThreadData(vmThread);
            if (threadData.IsExceptionInProgress)
            {
                TargetPointer espOSContext = threadData.OSExceptionContextRecord;
                TargetPointer espOSRecord = threadData.OSExceptionRecord;
                if (espOSContext < sp)
                {
                    _target.WriteBuffer(espOSContext.Value, ctx.GetBytes());
                    espContext = espOSContext;

                    // We should have an EXCEPTION_RECORD if we're hijacked at an exception.
                    WriteExceptionRecordHelper(espOSRecord, exceptionRecord!);
                    espRecord = espOSRecord;

                    sp = espOSContext < espOSRecord ? espOSContext : espOSRecord;
                }
            }
        }

        // If we didn't reuse the OS stack space, push fresh structures at the leaf of the stack.
        if (espContext == TargetPointer.Null)
        {
            Debug.Assert(espRecord == TargetPointer.Null);

            espContext = StackPusher.Push(_target, ref sp, ctx.GetBytes(), align: true);

            // If the caller didn't pass an exception record, we're not hijacking at an
            // exception and pass null for the record argument.
            if (exceptionRecord is not null)
            {
                espRecord = StackPusher.Push(_target, ref sp, exceptionRecord, align: true);
            }
        }

        // Set up the arguments for the hijack worker:
        //   void ExceptionHijackWorker(CONTEXT* pContext, EXCEPTION_RECORD* pRecord, EHijackReason reason, void* pData)
        ReadOnlySpan<TargetNUInt> args =
        [
            new TargetNUInt(espContext.Value),
            new TargetNUInt(espRecord.Value),
            new TargetNUInt((uint)reason),
            new TargetNUInt(userData.Value),
        ];
        IntegerArgPlacer.PlaceArgs(_target, ctx, ref sp, args);

        ctx.StackPointer = sp;
        ctx.InstructionPointer = new TargetCodePointer(pfnHijackFunction.Value);

        ctx.GetBytes().AsSpan().CopyTo(context);

        return espContext;
    }

    private static class IntegerArgPlacer
    {
        // Places integer arguments into the appropriate registers and stack slots for the native ABI.
        public static void PlaceArgs(Target target, IPlatformAgnosticContext ctx, ref TargetPointer sp, ReadOnlySpan<TargetNUInt> args)
        {
            RuntimeInfoArchitecture arch = target.Contracts.RuntimeInfo.GetTargetArchitecture();
            RuntimeInfoOperatingSystem os = target.Contracts.RuntimeInfo.GetTargetOperatingSystem();
            CallingConvention cc = GetCallingConvention(arch, os);
            int regCount = Math.Min(args.Length, cc.IntegerArgRegisters.Length);

            // Place register args.
            for (int i = 0; i < regCount; i++)
            {
                SetRegisterChecked(ctx, cc.IntegerArgRegisters[i], args[i], target.PointerSize);
            }

            // Push stack-passed args (those beyond the register slots) right-to-left
            for (int i = args.Length - 1; i >= regCount; i--)
            {
                StackPusher.PushSlot(target, ref sp, args[i], align: false);
            }

            // Reserve home / shadow space (Windows x64 only).
            if (cc.HomeSpaceSlotCount > 0)
            {
                sp = new TargetPointer(sp.Value - (ulong)(cc.HomeSpaceSlotCount * target.PointerSize));
            }
        }

        private readonly record struct CallingConvention(
            string[] IntegerArgRegisters,
            int HomeSpaceSlotCount);

        private static CallingConvention GetCallingConvention(RuntimeInfoArchitecture arch, RuntimeInfoOperatingSystem os)
        {
            switch (arch)
            {
                case RuntimeInfoArchitecture.X86:
                    // cdecl / stdcall: all args on the stack, no register args, no home space.
                    return new CallingConvention([], HomeSpaceSlotCount: 0);

                case RuntimeInfoArchitecture.X64 when os == RuntimeInfoOperatingSystem.Windows:
                    // Microsoft x64 calling convention: 4 integer arg regs + always 32 bytes
                    // of caller-allocated home / shadow space.
                    return new CallingConvention(["rcx", "rdx", "r8", "r9"], HomeSpaceSlotCount: 4);

                case RuntimeInfoArchitecture.X64:
                    // System V AMD64 ABI (Linux, macOS, *BSD, ...): 6 integer arg regs, no home space.
                    return new CallingConvention(["rdi", "rsi", "rdx", "rcx", "r8", "r9"], HomeSpaceSlotCount: 0);

                case RuntimeInfoArchitecture.Arm:
                    // AAPCS32: r0..r3.
                    return new CallingConvention(["r0", "r1", "r2", "r3"], HomeSpaceSlotCount: 0);

                case RuntimeInfoArchitecture.Arm64:
                    // AAPCS64: x0..x7.
                    return new CallingConvention(["x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7"], HomeSpaceSlotCount: 0);

                case RuntimeInfoArchitecture.LoongArch64:
                case RuntimeInfoArchitecture.RiscV64:
                    // LoongArch and RISC-V calling conventions: a0..a7.
                    return new CallingConvention(["a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7"], HomeSpaceSlotCount: 0);

                default:
                    throw new NotSupportedException($"IntegerArgPlacer.PlaceArgs does not support architecture '{arch}'.");
            }
        }

        private static void SetRegisterChecked(IPlatformAgnosticContext ctx, string register, TargetNUInt value, int pointerSize)
        {
            if (pointerSize == 4 && value.Value > uint.MaxValue)
            {
                throw new InvalidOperationException($"Cannot set register '{register}' to value {value.Value}: value exceeds 32-bit range.");
            }
            if (!ctx.TrySetRegister(register, value))
            {
                throw new InvalidOperationException($"Failed to set register '{register}' on context.");
            }
        }
    }
}