diff --git a/docs/design/datacontracts/Debugger.md b/docs/design/datacontracts/Debugger.md index c9764df281834f..6923882643d8e8 100644 --- a/docs/design/datacontracts/Debugger.md +++ b/docs/design/datacontracts/Debugger.md @@ -28,6 +28,7 @@ void SetSendExceptionsOutsideOfJMC(bool sendExceptionsOutsideOfJMC); TargetPointer GetDebuggerControlBlockAddress(); void EnableGCNotificationEvents(bool fEnable); HijackKind GetHijackKind(TargetCodePointer controlPC); +TargetPointer PrepareExceptionHijack(byte[] context, TargetPointer vmThread, byte[]? exceptionRecord, int reason, TargetPointer userData) ``` ## Version 1 @@ -188,4 +189,33 @@ HijackKind GetHijackKind(TargetCodePointer controlPC) } return HijackKind.None; } + +private TargetPointer GetHijackAddress() +{ + // Returns the start address of the unhandled-exception hijack function + // (index UnhandledExceptionHijackIndex == 0 in the RgHijackFunction array). + if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress)) + return TargetPointer.Null; + + TargetPointer rgHijack = target.ReadPointer( + debuggerAddress + /* Debugger::RgHijackFunction offset */); + if (rgHijack == TargetPointer.Null) + return TargetPointer.Null; + + uint maxHijackFunctions = target.ReadGlobal("MaxHijackFunctions"); + if (UnhandledExceptionHijackIndex >= maxHijackFunctions) + return TargetPointer.Null; + + uint stride = // Size of one MemoryRange entry + TargetPointer entryAddress = rgHijack + (ulong)(UnhandledExceptionHijackIndex * stride); + return target.ReadPointer(entryAddress + /* MemoryRange::StartAddress offset */); +} + +TargetPointer PrepareExceptionHijack(byte[] context, TargetPointer vmThread, byte[]? exceptionRecord, int reason, TargetPointer userData) +{ + // Finds hijack address via GetHijackAddress. + // Writes the exception record and context into the target stack as necessary. + // Places the arguments to ExceptionHijackWorker as dictated by the native ABI. + // Mutates stack pointer and context as necessary. +} ``` diff --git a/docs/design/datacontracts/Thread.md b/docs/design/datacontracts/Thread.md index df765d3825551f..7419e7f978abaa 100644 --- a/docs/design/datacontracts/Thread.md +++ b/docs/design/datacontracts/Thread.md @@ -56,6 +56,9 @@ record struct ThreadData ( bool IsInteropDebuggingHijacked; TargetPointer DebuggerFilterContext; TargetPointer GCFrame; + bool IsExceptionInProgress; + TargetPointer OSExceptionRecord; + TargetPointer OSExceptionContextRecord; ); ``` @@ -103,6 +106,8 @@ The contract additionally depends on these data descriptors | `ExceptionInfo` | `PreviousNestedInfo` | Pointer to previous nested exception info | | `ExceptionInfo` | `ThrownObjectHandle` | Pointer to exception object handle | | `ExceptionInfo` | `ExceptionWatsonBucketTrackerBuckets` | Pointer to Watson unhandled buckets on non-Unix | +| `ExceptionInfo` | `ExceptionRecord` | Pointer to the OS `EXCEPTION_RECORD` the OS dispatcher pushed for this exception | +| `ExceptionInfo` | `ContextRecord` | Pointer to the OS `CONTEXT` the OS dispatcher pushed for this exception | | `GCAllocContext` | `Pointer` | GC allocation pointer | | `GCAllocContext` | `Limit` | Allocation limit pointer | | `GCAllocContext` | `AllocBytes` | Number of bytes allocated on SOH by this context | @@ -228,6 +233,18 @@ ThreadData GetThreadData(TargetPointer address) } ulong threadLinkoffset = ... // offset from Thread data descriptor + + // The OS-pushed EXCEPTION_RECORD / CONTEXT are reachable through the current + // exception tracker (ExInfo). When there is no exception in progress the tracker + // pointer is null. + bool isExceptionInProgress = exceptionTrackerAddr != TargetPointer.Null; + TargetPointer osExceptionRecord = isExceptionInProgress + ? target.ReadPointer(exceptionTrackerAddr + /* ExceptionInfo::ExceptionRecord offset */) + : TargetPointer.Null; + TargetPointer osExceptionContextRecord = isExceptionInProgress + ? target.ReadPointer(exceptionTrackerAddr + /* ExceptionInfo::ContextRecord offset */) + : TargetPointer.Null; + return new ThreadData( Id: target.Read(address + /* Thread::Id offset */), OSId: target.ReadNUInt(address + /* Thread::OSId offset */), @@ -240,6 +257,9 @@ ThreadData GetThreadData(TargetPointer address) FirstNestedException : firstNestedException, NextThread: target.ReadPointer(address + /* Thread::LinkNext offset */) - threadLinkOffset; GCFrame: target.ReadPointer(address + /* Thread::GCFrame offset */), + IsExceptionInProgress: isExceptionInProgress, + OSExceptionRecord: osExceptionRecord, + OSExceptionContextRecord: osExceptionContextRecord, ); } diff --git a/src/coreclr/debug/daccess/cdac.cpp b/src/coreclr/debug/daccess/cdac.cpp index 0ea7990fd880cb..ed81d0cd0bc3b0 100644 --- a/src/coreclr/debug/daccess/cdac.cpp +++ b/src/coreclr/debug/daccess/cdac.cpp @@ -79,6 +79,21 @@ namespace return S_OK; } + int WriteThreadContext(uint32_t threadId, uint32_t contextSize, const uint8_t* contextBuffer, void* context) + { + ICorDebugDataTarget* target = reinterpret_cast(context); + ICorDebugMutableDataTarget* mutableTarget = nullptr; + HRESULT hr = target->QueryInterface(__uuidof(ICorDebugMutableDataTarget), (void**)&mutableTarget); + if (FAILED(hr)) + return hr; + hr = mutableTarget->SetThreadContext(threadId, contextSize, contextBuffer); + mutableTarget->Release(); + if (FAILED(hr)) + return hr; + + return S_OK; + } + int AllocVirtualCallback(uint32_t size, uint64_t* allocatedAddress, void* context) { ICorDebugDataTarget* target = reinterpret_cast(context); @@ -119,7 +134,7 @@ CDAC CDAC::Create(uint64_t descriptorAddr, ICorDebugDataTarget* target, IUnknown target2->Release(); intptr_t handle; - if (init(descriptorAddr, &ReadFromTargetCallback, &WriteToTargetCallback, &ReadThreadContext, allocCallback, target, &handle) != 0) + if (init(descriptorAddr, &ReadFromTargetCallback, &WriteToTargetCallback, &ReadThreadContext, &WriteThreadContext, allocCallback, target, &handle) != 0) { ::FreeLibrary(cdacLib); return {}; diff --git a/src/coreclr/vm/cdacstress.cpp b/src/coreclr/vm/cdacstress.cpp index 810dc57672f262..f8360adcbc5d99 100644 --- a/src/coreclr/vm/cdacstress.cpp +++ b/src/coreclr/vm/cdacstress.cpp @@ -470,8 +470,8 @@ void CdacStressPolicy::Initialize() // Get the address of the contract descriptor in our own process uint64_t descriptorAddr = reinterpret_cast(&DotNetRuntimeContractDescriptor); - // Initialize the cDAC reader with in-process callbacks (no alloc_virtual for in-process stress) - if (init(descriptorAddr, &ReadFromTargetCallback, &WriteToTargetCallback, &ReadThreadContextCallback, nullptr, nullptr, &s_cdacHandle) != 0) + // Initialize the cDAC reader with in-process callbacks (no write_thread_context or alloc_virtual for in-process stress) + if (init(descriptorAddr, &ReadFromTargetCallback, &WriteToTargetCallback, &ReadThreadContextCallback, nullptr, nullptr, nullptr, &s_cdacHandle) != 0) { CDAC_ERR("cdac_reader_init failed (descriptorAddr=0x%llx).\n", (unsigned long long)descriptorAddr); diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 065f950ab7b3c5..c76f7e903446ee 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -153,6 +153,8 @@ CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, ThrownObject, offsetof(ExInfo, m_excep CDAC_TYPE_FIELD(ExceptionInfo, T_UINT32, ExceptionFlags, cdac_data::ExceptionFlagsValue) CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, StackLowBound, cdac_data::StackLowBound) CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, StackHighBound, cdac_data::StackHighBound) +CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, ExceptionRecord, cdac_data::ExceptionRecord) +CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, ContextRecord, cdac_data::ContextRecord) #ifndef TARGET_UNIX CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, ExceptionWatsonBucketTrackerBuckets, cdac_data::ExceptionWatsonBucketTrackerBuckets) #endif // TARGET_UNIX diff --git a/src/coreclr/vm/exinfo.h b/src/coreclr/vm/exinfo.h index 1b1db4c1b9d122..bda9282f0b667f 100644 --- a/src/coreclr/vm/exinfo.h +++ b/src/coreclr/vm/exinfo.h @@ -357,6 +357,10 @@ struct cdac_data static constexpr size_t StackHighBound = offsetof(ExInfo, m_ScannedStackRange) + offsetof(ExInfo::StackRange, m_sfHighBound); static constexpr size_t ExceptionFlagsValue = offsetof(ExInfo, m_ExceptionFlags.m_flags); + static constexpr size_t ExceptionRecord = offsetof(ExInfo, m_ptrs) + + offsetof(ExInfo::DAC_EXCEPTION_POINTERS, ExceptionRecord); + static constexpr size_t ContextRecord = offsetof(ExInfo, m_ptrs) + + offsetof(ExInfo::DAC_EXCEPTION_POINTERS, ContextRecord); #ifndef TARGET_UNIX static constexpr size_t ExceptionWatsonBucketTrackerBuckets = offsetof(ExInfo, m_WatsonBucketTracker) + offsetof(EHWatsonBucketTracker, m_WatsonUnhandledInfo.m_pUnhandledBuckets); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugger.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugger.cs index 2c4e24180e5128..253c7faff878b2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugger.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugger.cs @@ -28,6 +28,7 @@ public interface IDebugger : IContract TargetPointer GetDebuggerControlBlockAddress() => throw new NotImplementedException(); void EnableGCNotificationEvents(bool fEnable) => throw new NotImplementedException(); HijackKind GetHijackKind(TargetCodePointer controlPC) => throw new NotImplementedException(); + TargetPointer PrepareExceptionHijack(byte[] context, TargetPointer vmThread, byte[]? exceptionRecord, int reason, TargetPointer userData) => throw new NotImplementedException(); } public readonly struct Debugger : IDebugger diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs index 32abf6cfd7aec5..cd49f7e5071961 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs @@ -63,7 +63,10 @@ public record struct ThreadData( TargetPointer ThreadHandle, bool IsInteropDebuggingHijacked, TargetPointer DebuggerFilterContext, - TargetPointer GCFrame); + TargetPointer GCFrame, + bool IsExceptionInProgress, + TargetPointer OSExceptionRecord, + TargetPointer OSExceptionContextRecord); public interface IThread : IContract { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs index b091152bcc7a66..1456316dc93bce 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs @@ -36,6 +36,14 @@ public abstract class Target /// true if successful, false otherwise public abstract bool TryGetThreadContext(ulong threadId, uint contextFlags, Span buffer); + /// + /// Sets the context of the given thread from the supplied buffer + /// + /// The identifier of the thread whose context is to be set. The identifier is defined by the operating system. + /// Buffer containing the new thread context. The contents must be a platform context structure of the size expected by the target. + /// true if successful, false otherwise + public abstract bool TrySetThreadContext(ulong threadId, ReadOnlySpan context); + /// /// Reads a well-known global pointer value from the target process /// diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger/Debugger_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger/Debugger_1.cs new file mode 100644 index 00000000000000..2d9751adaca52b --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger/Debugger_1.cs @@ -0,0 +1,365 @@ +// 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(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(addr.Value); + } + + void IDebugger.MarkDebuggerAttachPending() + { + TargetPointer addr = _target.ReadGlobalPointer(Constants.Globals.CORDebuggerControlFlags); + uint currentFlags = _target.Read(addr.Value); + _target.Write(addr.Value, currentFlags | (uint)DebuggerControlFlag_1.PendingAttach); + } + + void IDebugger.MarkDebuggerAttached(bool fAttached) + { + TargetPointer addr = _target.ReadGlobalPointer(Constants.Globals.CORDebuggerControlFlags); + uint currentFlags = _target.Read(addr.Value); + if (fAttached) + { + _target.Write(addr.Value, currentFlags | (uint)DebuggerControlFlag_1.Attached); + } + else + { + _target.Write(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(addr.Value.Value) != 0; + } + return false; + } + + void IDebugger.RequestSyncAtEvent() + { + if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress)) + return; + + Data.Debugger debugger = _target.ProcessedData.GetOrAdd(debuggerAddress); + debugger.WriteRSRequestedSync(1); + } + + void IDebugger.SetSendExceptionsOutsideOfJMC(bool sendExceptionsOutsideOfJMC) + { + if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress)) + return; + + Data.Debugger debugger = _target.ProcessedData.GetOrAdd(debuggerAddress); + debugger.WriteSendExceptionsOutsideOfJMC(sendExceptionsOutsideOfJMC ? 1 : 0); + } + + TargetPointer IDebugger.GetDebuggerControlBlockAddress() + { + if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress)) + return TargetPointer.Null; + + Data.Debugger debugger = _target.ProcessedData.GetOrAdd(debuggerAddress); + TargetPointer rcThread = debugger.RCThread; + if (rcThread == TargetPointer.Null) + return TargetPointer.Null; + + Data.DebuggerRCThread debuggerRcThread = _target.ProcessedData.GetOrAdd(rcThread); + return debuggerRcThread.DCB; + } + + void IDebugger.EnableGCNotificationEvents(bool fEnable) + { + if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress)) + return; + + Data.Debugger debugger = _target.ProcessedData.GetOrAdd(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(debuggerAddress); + if (debugger.RgHijackFunction == TargetPointer.Null) + return HijackKind.None; + + uint maxHijackFunctions = _target.ReadGlobal(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(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(debuggerAddress); + if (debugger.RgHijackFunction == TargetPointer.Null) + return false; + + uint maxHijackFunctions = _target.ReadGlobal(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(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 record) + { + int numberParametersOffset = sizeof(uint) + sizeof(uint) + (2 * _target.PointerSize); + ReadOnlySpan 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 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 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."); + } + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger/StackPusher.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger/StackPusher.cs new file mode 100644 index 00000000000000..65cc1b4e1cd17a --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger/StackPusher.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +public static class StackPusher +{ + public static TargetPointer Push(Target target, ref TargetPointer sp, Span bytes, bool align) + { + if (align) + { + AlignStackPointer(target, ref sp); + } + + sp = new TargetPointer(sp.Value - (ulong)bytes.Length); + + if (align) + { + AlignStackPointer(target, ref sp); + } + + target.WriteBuffer(sp.Value, bytes); + return sp; + } + + public static TargetPointer PushSlot(Target target, ref TargetPointer sp, TargetNUInt value, bool align) + { + if (align) + { + AlignStackPointer(target, ref sp); + } + + sp = new TargetPointer(sp.Value - (ulong)target.PointerSize); + + if (align) + { + AlignStackPointer(target, ref sp); + } + + target.WriteNUInt(sp.Value, value); + return sp; + } + + private static uint GetStackAlignment(Target target) + { + RuntimeInfoArchitecture arch = target.Contracts.RuntimeInfo.GetTargetArchitecture(); + RuntimeInfoOperatingSystem os = target.Contracts.RuntimeInfo.GetTargetOperatingSystem(); + + return arch switch + { + // Windows x86 (cdecl/stdcall) only requires 4-byte SP alignment. + // System V i386 ABI requires 16-byte alignment at call sites. + RuntimeInfoArchitecture.X86 => os == RuntimeInfoOperatingSystem.Windows ? 4u : 16u, + RuntimeInfoArchitecture.X64 => 16, + RuntimeInfoArchitecture.Arm => 8, // AAPCS32: 8-byte aligned at public interfaces + RuntimeInfoArchitecture.Arm64 => 16, + RuntimeInfoArchitecture.LoongArch64 => 16, + RuntimeInfoArchitecture.RiscV64 => 16, + _ => throw new NotSupportedException($"StackPusher does not know the stack alignment for architecture '{arch}'."), + }; + } + + private static void AlignStackPointer(Target target, ref TargetPointer sp) + { + uint alignment = GetStackAlignment(target); + ulong mask = ~((ulong)alignment - 1UL); + sp = new TargetPointer(sp.Value & mask); + } + +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger_1.cs deleted file mode 100644 index 94a3ebea153095..00000000000000 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger_1.cs +++ /dev/null @@ -1,152 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -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(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(addr.Value); - } - - void IDebugger.MarkDebuggerAttachPending() - { - TargetPointer addr = _target.ReadGlobalPointer(Constants.Globals.CORDebuggerControlFlags); - uint currentFlags = _target.Read(addr.Value); - _target.Write(addr.Value, currentFlags | (uint)DebuggerControlFlag_1.PendingAttach); - } - - void IDebugger.MarkDebuggerAttached(bool fAttached) - { - TargetPointer addr = _target.ReadGlobalPointer(Constants.Globals.CORDebuggerControlFlags); - uint currentFlags = _target.Read(addr.Value); - if (fAttached) - { - _target.Write(addr.Value, currentFlags | (uint)DebuggerControlFlag_1.Attached); - } - else - { - _target.Write(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(addr.Value.Value) != 0; - } - return false; - } - - void IDebugger.RequestSyncAtEvent() - { - if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress)) - return; - - Data.Debugger debugger = _target.ProcessedData.GetOrAdd(debuggerAddress); - debugger.WriteRSRequestedSync(1); - } - - void IDebugger.SetSendExceptionsOutsideOfJMC(bool sendExceptionsOutsideOfJMC) - { - if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress)) - return; - - Data.Debugger debugger = _target.ProcessedData.GetOrAdd(debuggerAddress); - debugger.WriteSendExceptionsOutsideOfJMC(sendExceptionsOutsideOfJMC ? 1 : 0); - } - - TargetPointer IDebugger.GetDebuggerControlBlockAddress() - { - if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress)) - return TargetPointer.Null; - - Data.Debugger debugger = _target.ProcessedData.GetOrAdd(debuggerAddress); - TargetPointer rcThread = debugger.RCThread; - if (rcThread == TargetPointer.Null) - return TargetPointer.Null; - - Data.DebuggerRCThread debuggerRcThread = _target.ProcessedData.GetOrAdd(rcThread); - return debuggerRcThread.DCB; - } - - void IDebugger.EnableGCNotificationEvents(bool fEnable) - { - if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress)) - return; - - Data.Debugger debugger = _target.ProcessedData.GetOrAdd(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(debuggerAddress); - if (debugger.RgHijackFunction == TargetPointer.Null) - return HijackKind.None; - - uint maxHijackFunctions = _target.ReadGlobal(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(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; - } -} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64Context.cs index 9cc8a53983890a..4603d6f1f1f2ec 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64Context.cs @@ -64,6 +64,9 @@ public void Unwind(Target target) unwinder.Unwind(ref this); } + // Clears the x64 hardware trace flag (EFLAGS.TF, bit 0x100). + public void UnsetSingleStepFlag() => EFlags &= ~0x100; + public bool TrySetRegister(string name, TargetNUInt value) { if (name.Equals("cs", StringComparison.OrdinalIgnoreCase)) { Cs = (ushort)value.Value; return true; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64Context.cs index db38995c2f7a54..8016c0ac1765a2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64Context.cs @@ -70,6 +70,9 @@ public void Unwind(Target target) unwinder.Unwind(ref this); } + // Clears the AArch64 hardware single-step flag (CPSR.SS, bit 0x00200000). + public void UnsetSingleStepFlag() => Cpsr &= ~0x00200000u; + public bool TrySetRegister(string name, TargetNUInt value) { if (name.Equals("cpsr", StringComparison.OrdinalIgnoreCase)) { Cpsr = (uint)value.Value; return true; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARMContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARMContext.cs index d13c3f6ead4490..cde8ee0c0ce6f4 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARMContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARMContext.cs @@ -64,6 +64,7 @@ public void Unwind(Target target) unwinder.Unwind(ref this); } + public void UnsetSingleStepFlag() { } public bool TrySetRegister(string name, TargetNUInt value) { if (name.Equals("r0", StringComparison.OrdinalIgnoreCase)) { R0 = (uint)value.Value; return true; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ContextHolder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ContextHolder.cs index 6358c537268061..01457bab25b56a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ContextHolder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ContextHolder.cs @@ -52,6 +52,7 @@ public unsafe byte[] GetBytes() public IPlatformAgnosticContext Clone() => new ContextHolder() { Context = Context }; public void Clear() => Context = default; public void Unwind(Target target) => Context.Unwind(target); + public void UnsetSingleStepFlag() => Context.UnsetSingleStepFlag(); public bool TrySetRegister(string fieldName, TargetNUInt value) => Context.TrySetRegister(fieldName, value); public bool TryReadRegister(string fieldName, out TargetNUInt value) => Context.TryReadRegister(fieldName, out value); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs index fc384bf929027b..3e45d381ecbc0d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs @@ -29,8 +29,16 @@ public interface IPlatformAgnosticContext public abstract bool TryReadRegister(string fieldName, out TargetNUInt value); public abstract bool TrySetRegister(int number, TargetNUInt value); public abstract bool TryReadRegister(int number, out TargetNUInt value); + public abstract void Unwind(Target target); + /// + /// Clears the hardware single-step (trace) flag in the context, if the architecture + /// supports a hardware single-step flag. Architectures that emulate single-stepping + /// throw . + /// + public abstract void UnsetSingleStepFlag(); + public static IPlatformAgnosticContext GetContextForPlatform(Target target) { IRuntimeInfo runtimeInfo = target.Contracts.RuntimeInfo; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformContext.cs index 24da7a13513827..2e71424e3d2191 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformContext.cs @@ -20,6 +20,13 @@ public interface IPlatformContext void Unwind(Target target); + /// + /// Clears the hardware single-step (trace) flag in the context, if the architecture + /// supports a hardware single-step flag. Architectures that emulate single-stepping + /// throw . + /// + void UnsetSingleStepFlag(); + bool TrySetRegister(string name, TargetNUInt value); bool TryReadRegister(string name, out TargetNUInt value); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/LoongArch64Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/LoongArch64Context.cs index 9f182801ebd609..bf0081fa0033b7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/LoongArch64Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/LoongArch64Context.cs @@ -68,6 +68,8 @@ public void Unwind(Target target) unwinder.Unwind(ref this); } + public void UnsetSingleStepFlag() {} + public bool TrySetRegister(string name, TargetNUInt value) { if (name.Equals("r0", StringComparison.OrdinalIgnoreCase)) { R0 = value.Value; return true; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RISCV64Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RISCV64Context.cs index b81fdf4447fb91..f829c897e1c5c2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RISCV64Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RISCV64Context.cs @@ -68,6 +68,7 @@ public void Unwind(Target target) unwinder.Unwind(ref this); } + public void UnsetSingleStepFlag() {} public bool TrySetRegister(string name, TargetNUInt value) { if (name.Equals("zero", StringComparison.OrdinalIgnoreCase)) { return false; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86Context.cs index db1afdc2dd5bba..08fac2a6f5b3ea 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86Context.cs @@ -71,6 +71,9 @@ public void Unwind(Target target) unwinder.Unwind(ref this); } + // Clears the x86 hardware trace flag (EFLAGS.TF, bit 0x100). + public void UnsetSingleStepFlag() => EFlags &= ~0x100u; + public bool TrySetRegister(string name, TargetNUInt value) { if (name.Equals("dr0", StringComparison.OrdinalIgnoreCase)) { Dr0 = (uint)value.Value; return true; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs index 55e0314844fe9f..677b8c8154b84b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs @@ -151,7 +151,10 @@ ThreadData IThread.GetThreadData(TargetPointer threadPointer) thread.ThreadHandle, thread.InteropDebuggingHijacked != 0, thread.DebuggerFilterContext, - thread.GCFrame); + thread.GCFrame, + address != TargetPointer.Null, + exceptionInfo?.ExceptionRecord ?? TargetPointer.Null, + exceptionInfo?.ContextRecord ?? TargetPointer.Null); } void IThread.GetThreadAllocContext(TargetPointer threadPointer, out long allocBytes, out long allocBytesLoh) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs index ef97eeee601b14..2781671b37a428 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs @@ -11,6 +11,8 @@ internal sealed partial class ExceptionInfo : IData [Field] public uint ExceptionFlags { get; } [Field] public TargetPointer StackLowBound { get; } [Field] public TargetPointer StackHighBound { get; } + [Field] public TargetPointer ExceptionRecord { get; } + [Field] public TargetPointer ContextRecord { get; } // Only present on Windows platforms [Field] public TargetPointer? ExceptionWatsonBucketTrackerBuckets { get; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs index ff3b2b3cf4aa95..d81aaf00297c34 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs @@ -698,8 +698,69 @@ public int MarkDebuggerAttached(Interop.BOOL fAttached) return hr; } + // EXCEPTION_MAXIMUM_PARAMETERS from the Windows SDK. + private const int ExceptionMaximumParameters = 15; + + // Full size of a native EXCEPTION_RECORD for the target's pointer size: the header + // (two DWORDs + two pointers + NumberParameters DWORD, aligned up to the pointer size) + // followed by the fixed ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS] array. + private static int ExceptionRecordFullSize(int ptrSize) + { + int unaligned = sizeof(uint) + sizeof(uint) + ptrSize + ptrSize + sizeof(uint); + int header = (unaligned + (ptrSize - 1)) & ~(ptrSize - 1); + return header + (ExceptionMaximumParameters * ptrSize); + } + public int Hijack(ulong vmThread, uint dwThreadId, nint pRecord, nint pOriginalContext, uint cbSizeContext, int reason, nint pUserData, ulong* pRemoteContextAddr) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.Hijack(vmThread, dwThreadId, pRecord, pOriginalContext, cbSizeContext, reason, pUserData, pRemoteContextAddr) : HResults.E_NOTIMPL; + { + // Hijack mutates live target state (it writes to the thread's stack and sets the thread context). + // It therefore cannot be cross-checked against the legacy implementation in DEBUG builds. + // See https://github.com/dotnet/runtime/blob/0d1a20fb14109f277df06ebee3f83c964f9dcc61/src/coreclr/debug/daccess/dacdbiimpl.cpp#L4907 for more algorithm detail. + int hr = HResults.S_OK; + try + { + // Read the thread's current context. + IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(_target); + byte[] contextBuffer = new byte[ctx.Size]; + if (!_target.TryGetThreadContext(dwThreadId, ctx.AllContextFlags, contextBuffer)) + throw Marshal.GetExceptionForHR(HResults.E_FAIL)!; + + // If the caller requested it, copy back the original (pre-hijack) context. + if (pOriginalContext != 0) + { + if (cbSizeContext != contextBuffer.Length) + throw Marshal.GetExceptionForHR(HResults.E_INVALIDARG)!; + contextBuffer.AsSpan().CopyTo(new Span((void*)pOriginalContext, (int)cbSizeContext)); + } + + byte[]? recordBytes = null; + if (pRecord != 0) + { + int recordSize = ExceptionRecordFullSize(_target.PointerSize); + recordBytes = new byte[recordSize]; + new ReadOnlySpan((void*)pRecord, recordSize).CopyTo(recordBytes); + } + + TargetPointer espContext = _target.Contracts.Debugger.PrepareExceptionHijack( + contextBuffer, + new TargetPointer(vmThread), + recordBytes, + reason, + new TargetPointer((ulong)pUserData)); + + if (pRemoteContextAddr is not null) + *pRemoteContextAddr = espContext.Value; + + // Commit the modified context to the thread. + if (!_target.TrySetThreadContext(dwThreadId, contextBuffer)) + throw Marshal.GetExceptionForHR(HResults.E_FAIL)!; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + return hr; + } public int EnumerateThreads(delegate* unmanaged fpCallback, nint pUserData) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs index 633484f17e2578..d0efe07cabba36 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs @@ -57,6 +57,7 @@ private readonly struct Configuration public delegate int ReadFromTargetDelegate(ulong address, Span bufferToFill); public delegate int WriteToTargetDelegate(ulong address, Span bufferToWrite); public delegate int GetTargetThreadContextDelegate(uint threadId, uint contextFlags, Span bufferToFill); + public delegate int SetTargetThreadContextDelegate(uint threadId, ReadOnlySpan context); public delegate int AllocVirtualDelegate(ulong size, out ulong allocatedAddress); private static readonly UTF8Encoding strictUTF8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); @@ -69,6 +70,7 @@ private readonly struct Configuration /// A callback to read memory blocks at a given address from the target /// A callback to write memory blocks at a given address to the target /// A callback to fetch a thread's context + /// A callback to set a thread's context /// A callback to allocate virtual memory in the target /// Registration actions that populate the contract registry (e.g., ) /// The target object. @@ -78,11 +80,12 @@ public static bool TryCreate( ReadFromTargetDelegate readFromTarget, WriteToTargetDelegate writeToTarget, GetTargetThreadContextDelegate getThreadContext, + SetTargetThreadContextDelegate setThreadContext, AllocVirtualDelegate allocVirtual, Action[] contractRegistrations, [NotNullWhen(true)] out ContractDescriptorTarget? target) { - DataTargetDelegates dataTargetDelegates = new DataTargetDelegates(readFromTarget, writeToTarget, getThreadContext, allocVirtual); + DataTargetDelegates dataTargetDelegates = new DataTargetDelegates(readFromTarget, writeToTarget, getThreadContext, setThreadContext, allocVirtual); if (TryReadContractDescriptor( contractDescriptor, dataTargetDelegates, @@ -104,6 +107,7 @@ public static bool TryCreate( /// A callback to read memory blocks at a given address from the target /// A callback to write memory blocks at a given address to the target /// A callback to fetch a thread's context + /// A callback to set a thread's context /// A callback to allocate virtual memory in the target /// Whether the target is little-endian /// The size of a pointer in bytes in the target process. @@ -115,6 +119,7 @@ public static ContractDescriptorTarget Create( ReadFromTargetDelegate readFromTarget, WriteToTargetDelegate writeToTarget, GetTargetThreadContextDelegate getThreadContext, + SetTargetThreadContextDelegate setThreadContext, AllocVirtualDelegate allocVirtual, bool isLittleEndian, int pointerSize, @@ -127,7 +132,7 @@ public static ContractDescriptorTarget Create( ContractDescriptor = contractDescriptor, PointerData = globalPointerValues }, - new DataTargetDelegates(readFromTarget, writeToTarget, getThreadContext, allocVirtual), + new DataTargetDelegates(readFromTarget, writeToTarget, getThreadContext, setThreadContext, allocVirtual), contractRegistrations ?? []); } @@ -415,6 +420,13 @@ public override bool TryGetThreadContext(ulong threadId, uint contextFlags, Span return hr == 0; } + public override bool TrySetThreadContext(ulong threadId, ReadOnlySpan context) + { + // Underlying API only supports 32-bit thread IDs, mask off top 32 bits + int hr = _dataTargetDelegates.SetThreadContext((uint)(threadId & uint.MaxValue), context); + return hr == 0; + } + /// /// Read a value from the target in target endianness /// @@ -932,6 +944,7 @@ private readonly struct DataTargetDelegates( ReadFromTargetDelegate readFromTarget, WriteToTargetDelegate writeToTarget, GetTargetThreadContextDelegate getThreadContext, + SetTargetThreadContextDelegate setThreadContext, AllocVirtualDelegate allocVirtual) { public int ReadFromTarget(ulong address, Span buffer) @@ -946,6 +959,10 @@ public int GetThreadContext(uint threadId, uint contextFlags, Span buffer) { return getThreadContext(threadId, contextFlags, buffer); } + public int SetThreadContext(uint threadId, ReadOnlySpan context) + { + return setThreadContext(threadId, context); + } public int WriteToTarget(ulong address, Span buffer) { return writeToTarget(address, buffer); diff --git a/src/native/managed/cdac/inc/cdac_reader.h b/src/native/managed/cdac/inc/cdac_reader.h index fab54ab2c28aae..7dbc2d32386f3f 100644 --- a/src/native/managed/cdac/inc/cdac_reader.h +++ b/src/native/managed/cdac/inc/cdac_reader.h @@ -14,6 +14,7 @@ extern "C" // read_from_target: a callback that reads memory from the target process // write_to_target: a callback that writes memory to the target process // read_thread_context: a callback that reads the context of a thread in the target process +// write_thread_context: optional callback that sets the context of a thread in the target process (may be NULL) // alloc_virtual: optional callback that allocates memory in the target process (may be NULL) // read_context: a context pointer that will be passed to callbacks // handle: returned opaque the handle to the reader. This should be passed to other functions in this API. @@ -22,6 +23,7 @@ int cdac_reader_init( int(*read_from_target)(uint64_t, uint8_t*, uint32_t, void*), int(*write_to_target)(uint64_t, const uint8_t*, uint32_t, void*), int(*read_thread_context)(uint32_t, uint32_t, uint32_t, uint8_t*, void*), + int(*write_thread_context)(uint32_t, uint32_t, const uint8_t*, void*), int(*alloc_virtual)(uint32_t, uint64_t*, void*), void* read_context, /*out*/ intptr_t* handle); diff --git a/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs b/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs index 8a0a0f3f31f5f3..26fd7bdec35152 100644 --- a/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs +++ b/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs @@ -18,6 +18,7 @@ private static unsafe int Init( delegate* unmanaged readFromTarget, delegate* unmanaged writeToTarget, delegate* unmanaged readThreadContext, + delegate* unmanaged writeThreadContext, delegate* unmanaged allocVirtual, void* delegateContext, IntPtr* handle) @@ -52,6 +53,36 @@ private static unsafe int Init( }; } + // Build the setThreadContext delegate if the caller provided a callback + ContractDescriptorTarget.SetTargetThreadContextDelegate setThreadContextDelegate = + (uint threadId, ReadOnlySpan context) => HResults.E_NOTIMPL; + + if (writeThreadContext != null) + { + setThreadContextDelegate = (uint threadId, ReadOnlySpan context) => + { + const nuint RequiredAlignment = 16; + fixed (byte* contextPtr = context) + { + if (((nuint)contextPtr & (RequiredAlignment - 1)) == 0) + { + return writeThreadContext(threadId, (uint)context.Length, contextPtr, delegateContext); + } + + byte* alignedBuffer = (byte*)NativeMemory.AlignedAlloc((nuint)context.Length, RequiredAlignment); + try + { + context.CopyTo(new Span(alignedBuffer, context.Length)); + return writeThreadContext(threadId, (uint)context.Length, alignedBuffer, delegateContext); + } + finally + { + NativeMemory.AlignedFree(alignedBuffer); + } + } + }; + } + // TODO: [cdac] Better error code/details if (!ContractDescriptorTarget.TryCreate( descriptor, @@ -96,6 +127,7 @@ private static unsafe int Init( } } }, + setThreadContextDelegate, allocDelegate, [Contracts.CoreCLRContracts.Register], out ContractDescriptorTarget? target)) @@ -314,6 +346,28 @@ private static unsafe int CLRDataCreateInstanceCore(Guid* pIID, IntPtr /*ICLRDat return dataTarget.GetThreadContext(threadId, contextFlags, (uint)bufferToFill.Length, bufferPtr); } }, + (threadId, context) => + { + const nuint RequiredAlignment = 16; + fixed (byte* contextPtr = context) + { + if (((nuint)contextPtr & (RequiredAlignment - 1)) == 0) + { + return dataTarget.SetThreadContext(threadId, (uint)context.Length, contextPtr); + } + + byte* alignedBuffer = (byte*)NativeMemory.AlignedAlloc((nuint)context.Length, RequiredAlignment); + try + { + context.CopyTo(new Span(alignedBuffer, context.Length)); + return dataTarget.SetThreadContext(threadId, (uint)context.Length, alignedBuffer); + } + finally + { + NativeMemory.AlignedFree(alignedBuffer); + } + } + }, allocVirtual, [Contracts.CoreCLRContracts.Register], out ContractDescriptorTarget? target)) diff --git a/src/native/managed/cdac/scripts/DumpHelpers.cs b/src/native/managed/cdac/scripts/DumpHelpers.cs index fb1bb0acdf967c..e4b1c93d7e7326 100644 --- a/src/native/managed/cdac/scripts/DumpHelpers.cs +++ b/src/native/managed/cdac/scripts/DumpHelpers.cs @@ -54,6 +54,7 @@ public static ContractDescriptorTarget CreateCdacTarget(DataTarget dt) (ulong address, Span buffer) => -1, (uint threadId, uint contextFlags, Span buffer) => dt.DataReader.GetThreadContext(threadId, contextFlags, buffer) ? 0 : -1, + (uint threadId, ReadOnlySpan context) => -1, [CoreCLRContracts.Register], out ContractDescriptorTarget? target)) { diff --git a/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs b/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs index b823054d3b0220..ef928551ebb8e7 100644 --- a/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs +++ b/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs @@ -256,6 +256,7 @@ public override bool TryReadGlobalPointer(string name, [NotNullWhen(true)] out T public override TargetPointer ReadPointerFromSpan(ReadOnlySpan bytes) => throw new NotImplementedException(); public override bool IsAlignedToPointerSize(TargetPointer pointer) => throw new NotImplementedException(); public override bool TryGetThreadContext(ulong threadId, uint contextFlags, Span buffer) => throw new NotImplementedException(); + public override bool TrySetThreadContext(ulong threadId, ReadOnlySpan context) => throw new NotImplementedException(); // --- Stub ContractRegistry ------------------------------------- diff --git a/src/native/managed/cdac/tests/TestInfrastructure/ContractDescriptor/ContractDescriptorBuilder.cs b/src/native/managed/cdac/tests/TestInfrastructure/ContractDescriptor/ContractDescriptorBuilder.cs index 93ca07995d1b8f..e525d16c6eb865 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/ContractDescriptor/ContractDescriptorBuilder.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/ContractDescriptor/ContractDescriptorBuilder.cs @@ -209,6 +209,6 @@ public bool TryCreateTarget(DescriptorBuilder descriptor, [NotNullWhen(true)] ou _created = true; ulong contractDescriptorAddress = descriptor.CreateSubDescriptor(ContractDescriptorAddr, JsonDescriptorAddr, ContractPointerDataAddr); MockMemorySpace.MemoryContext memoryContext = GetMemoryContext(); - return ContractDescriptorTarget.TryCreate(contractDescriptorAddress, memoryContext.ReadFromTarget, memoryContext.WriteToTarget, (_, _, _) => throw new NotImplementedException("Tests do not provide GetTargetThreadContext"), (ulong _, out ulong _) => throw new NotImplementedException("Tests do not provide AllocVirtual"), contractRegistrations ?? [Contracts.CoreCLRContracts.Register], out target); + return ContractDescriptorTarget.TryCreate(contractDescriptorAddress, memoryContext.ReadFromTarget, memoryContext.WriteToTarget, (_, _, _) => throw new NotImplementedException("Tests do not provide GetTargetThreadContext"), (_, _) => throw new NotImplementedException("Tests do not provide SetTargetThreadContext"), (ulong _, out ulong _) => throw new NotImplementedException("Tests do not provide AllocVirtual"), contractRegistrations ?? [Contracts.CoreCLRContracts.Register], out target); } } diff --git a/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs b/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs index e3eaca7b45050e..c98afbe8b41c96 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs @@ -113,6 +113,7 @@ protected void InitializeDumpTest(TestConfiguration config, string debuggeeName, _host.ReadFromTarget, writeToTarget: static (_, _) => -1, _host.GetThreadContext, + setThreadContext: static (_, _) => -1, allocVirtual: static (ulong _, out ulong _) => throw new NotImplementedException("Dump tests do not provide AllocVirtual"), [Contracts.CoreCLRContracts.Register], out _target); @@ -138,6 +139,7 @@ protected void InitializeDumpTestFromPath(string dumpPath) _host.ReadFromTarget, writeToTarget: static (_, _) => -1, _host.GetThreadContext, + setThreadContext: static (_, _) => -1, allocVirtual: static (ulong _, out ulong _) => throw new NotImplementedException("Dump tests do not provide AllocVirtual"), [Contracts.CoreCLRContracts.Register], out _target); diff --git a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs index 12561ffd8676a7..d80b4c51694415 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs @@ -547,6 +547,7 @@ public override bool TryGetTypeInfo(string typeName, out Target.TypeInfo info) => _typeInfoCache.TryGetValue(typeName, out info); public override bool TryGetThreadContext(ulong threadId, uint contextFlags, Span bufferToFill) => throw new NotImplementedException(); + public override bool TrySetThreadContext(ulong threadId, ReadOnlySpan context) => throw new NotImplementedException(); public override Target.IDataCache ProcessedData => _dataCache; public override ContractRegistry Contracts => _contractRegistry; diff --git a/src/native/managed/cdac/tests/UnitTests/ClrDataExceptionStateTests.cs b/src/native/managed/cdac/tests/UnitTests/ClrDataExceptionStateTests.cs index cf0e8004afaf98..015c8ed19f4feb 100644 --- a/src/native/managed/cdac/tests/UnitTests/ClrDataExceptionStateTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ClrDataExceptionStateTests.cs @@ -94,7 +94,10 @@ private static (TestPlaceholderTarget Target, IXCLRDataTask Task) CreateTargetWi ThreadHandle: TargetPointer.Null, IsInteropDebuggingHijacked: false, DebuggerFilterContext: TargetPointer.Null, - GCFrame: TargetPointer.Null)); + GCFrame: TargetPointer.Null, + IsExceptionInProgress: false, + OSExceptionRecord: TargetPointer.Null, + OSExceptionContextRecord: TargetPointer.Null)); var target = new TestPlaceholderTarget.Builder(arch) .UseReader((ulong _, Span _) => -1) @@ -479,7 +482,10 @@ private static (IXCLRDataTask Task, string ExpectedMessage) CreateTargetWithLast ThreadHandle: TargetPointer.Null, IsInteropDebuggingHijacked: false, DebuggerFilterContext: TargetPointer.Null, - GCFrame: TargetPointer.Null)); + GCFrame: TargetPointer.Null, + IsExceptionInProgress: false, + OSExceptionRecord: TargetPointer.Null, + OSExceptionContextRecord: TargetPointer.Null)); var mockException = new Mock(); mockException.Setup(e => e.GetExceptionData(exceptionObjectAddr)).Returns(new ExceptionData( diff --git a/src/native/managed/cdac/tests/UnitTests/DebuggerTests.cs b/src/native/managed/cdac/tests/UnitTests/DebuggerTests.cs index 42963531cdc295..a0a9aea33e6c72 100644 --- a/src/native/managed/cdac/tests/UnitTests/DebuggerTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/DebuggerTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; using Xunit; @@ -518,4 +519,161 @@ public void GetHijackKind_ReturnsNoneWhenTableEmpty(MockTarget.Architecture arch Assert.Equal(HijackKind.None, debugger.GetHijackKind(new TargetCodePointer(0x10_0080))); } + + // ----------------------------------------------------------------------- + // PrepareExceptionHijack + // ----------------------------------------------------------------------- + + public static IEnumerable HijackArches() + { + MockTarget.Architecture le64 = new() { IsLittleEndian = true, Is64Bit = true }; + MockTarget.Architecture le32 = new() { IsLittleEndian = true, Is64Bit = false }; + yield return [le64, "x64", "windows"]; + yield return [le64, "x64", "unix"]; + yield return [le64, "arm64", "unix"]; + yield return [le32, "x86", "windows"]; + yield return [le32, "arm", "unix"]; + } + + private static int ExceptionRecordSize(int ptrSize) + { + int unaligned = sizeof(uint) + sizeof(uint) + ptrSize + ptrSize + sizeof(uint); + int header = (unaligned + (ptrSize - 1)) & ~(ptrSize - 1); + return header + (15 * ptrSize); + } + + private static string[] IntegerArgRegisters(string targetArch, bool isWindows) => targetArch switch + { + "x86" => [], + "x64" => isWindows ? ["rcx", "rdx", "r8", "r9"] : ["rdi", "rsi", "rdx", "rcx", "r8", "r9"], + "arm" => ["r0", "r1", "r2", "r3"], + "arm64" => ["x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7"], + "loongarch64" or "riscv64" => ["a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7"], + _ => throw new NotSupportedException(targetArch), + }; + + private static TestPlaceholderTarget BuildHijackTarget( + MockTarget.Architecture arch, + string targetArch, + string os, + ulong hijackStart, + out ulong stackBase, + out ulong stackSize) + { + TargetTestHelpers helpers = new(arch); + var builder = new TestPlaceholderTarget.Builder(arch); + MockMemorySpace.BumpAllocator allocator = builder.MemoryBuilder.CreateAllocator(0x1_0000, 0x100_0000); + + TargetTestHelpers.LayoutResult debuggerLayout = GetDebuggerLayout(helpers); + TargetTestHelpers.LayoutResult memoryRangeLayout = GetMemoryRangeLayout(helpers); + builder.AddTypes(new Dictionary + { + [DataType.Debugger] = new() { Fields = debuggerLayout.Fields, Size = debuggerLayout.Stride }, + [DataType.MemoryRange] = new() { Fields = memoryRangeLayout.Fields, Size = memoryRangeLayout.Stride }, + }); + + // Single hijack entry at index 0 (the unhandled-exception hijack). + MockMemorySpace.HeapFragment rgFrag = allocator.Allocate(memoryRangeLayout.Stride, "RgHijackFunction"); + helpers.WritePointer(rgFrag.Data.AsSpan(memoryRangeLayout.Fields[nameof(Data.MemoryRange.StartAddress)].Offset, helpers.PointerSize), hijackStart); + helpers.WriteNUInt(rgFrag.Data.AsSpan(memoryRangeLayout.Fields[nameof(Data.MemoryRange.Size)].Offset, helpers.PointerSize), new TargetNUInt(0x100)); + + MockMemorySpace.HeapFragment debuggerFrag = allocator.Allocate(debuggerLayout.Stride, "Debugger"); + helpers.WritePointer( + debuggerFrag.Data.AsSpan(debuggerLayout.Fields[nameof(Data.Debugger.RgHijackFunction)].Offset, helpers.PointerSize), + rgFrag.Address); + + MockMemorySpace.HeapFragment debuggerPtrFrag = allocator.Allocate((ulong)helpers.PointerSize, "g_pDebugger"); + helpers.WritePointer(debuggerPtrFrag.Data, debuggerFrag.Address); + + // A writable region that the hijack setup uses as the target thread's stack. + stackSize = 0x4000; + MockMemorySpace.HeapFragment stackFrag = allocator.Allocate(stackSize, "Stack"); + stackBase = stackFrag.Address; + + builder.AddGlobals( + (Constants.Globals.Debugger, debuggerPtrFrag.Address), + (Constants.Globals.MaxHijackFunctions, (ulong)1)); + builder.AddGlobalStrings( + (Constants.Globals.Architecture, targetArch), + (Constants.Globals.OperatingSystem, os)); + builder.AddContract(version: "c1"); + builder.AddContract(version: "c1"); + + return builder.Build(); + } + + [Theory] + [MemberData(nameof(HijackArches))] + public void PrepareExceptionHijack_EditsContextAndStack(MockTarget.Architecture arch, string targetArch, string os) + { + const ulong HijackStart = 0x55_0000; + const ulong OriginalIp = 0x1_2340; + const int Reason = 7; + TargetPointer userData = new(0xCAFEF00D); + + Target target = BuildHijackTarget(arch, targetArch, os, HijackStart, out ulong stackBase, out ulong stackSize); + IDebugger debugger = target.Contracts.Debugger; + + int ptrSize = target.PointerSize; + ulong originalSp = stackBase + stackSize - 0x200; + + IPlatformAgnosticContext seed = IPlatformAgnosticContext.GetContextForPlatform(target); + uint contextSize = seed.Size; + seed.StackPointer = new TargetPointer(originalSp); + seed.InstructionPointer = new TargetCodePointer(OriginalIp); + byte[] contextBuffer = seed.GetBytes(); + + int recordSize = ExceptionRecordSize(ptrSize); + byte[] recordBytes = new byte[recordSize]; + for (int i = 0; i < recordSize; i++) + recordBytes[i] = (byte)(0x80 + (i % 0x40)); + + TargetPointer espContext = debugger.PrepareExceptionHijack(contextBuffer, TargetPointer.Null, recordBytes, Reason, userData); + + // The saved CONTEXT lands within the stack, below the original SP. + Assert.True(espContext.Value >= stackBase && espContext.Value < originalSp); + + // The pushed CONTEXT preserves the original IP and SP for the worker to restore. + byte[] savedBytes = new byte[contextSize]; + target.ReadBuffer(espContext.Value, savedBytes); + IPlatformAgnosticContext saved = IPlatformAgnosticContext.GetContextForPlatform(target); + saved.FillFromBuffer(savedBytes); + Assert.Equal(OriginalIp, saved.InstructionPointer.Value); + Assert.Equal(originalSp, saved.StackPointer.Value); + + // The committed CONTEXT jumps to the hijack worker with a descended SP. + IPlatformAgnosticContext final = IPlatformAgnosticContext.GetContextForPlatform(target); + final.FillFromBuffer(contextBuffer); + Assert.Equal(HijackStart, final.InstructionPointer.Value); + Assert.True(final.StackPointer.Value < espContext.Value); + + // Worker arguments are (espContext, espRecord, reason, userData). + string[] argRegs = IntegerArgRegisters(targetArch, os == "windows"); + TargetPointer espRecord; + if (argRegs.Length > 0) + { + Assert.True(final.TryReadRegister(argRegs[0], out TargetNUInt arg0)); + Assert.True(final.TryReadRegister(argRegs[1], out TargetNUInt arg1)); + Assert.True(final.TryReadRegister(argRegs[2], out TargetNUInt arg2)); + Assert.True(final.TryReadRegister(argRegs[3], out TargetNUInt arg3)); + Assert.Equal(espContext.Value, arg0.Value); + Assert.Equal((ulong)Reason, arg2.Value); + Assert.Equal(userData.Value, arg3.Value); + espRecord = new TargetPointer(arg1.Value); + } + else + { + ulong sp = final.StackPointer.Value; + Assert.Equal(espContext.Value, target.ReadPointer(sp).Value); + espRecord = target.ReadPointer(sp + (ulong)ptrSize); + Assert.Equal((ulong)Reason, target.ReadPointer(sp + (ulong)(2 * ptrSize)).Value); + Assert.Equal(userData.Value, target.ReadPointer(sp + (ulong)(3 * ptrSize)).Value); + } + + // espRecord points at the pushed EXCEPTION_RECORD bytes. + Assert.True(espRecord.Value >= stackBase && espRecord.Value < espContext.Value); + byte[] pushedRecord = new byte[recordSize]; + target.ReadBuffer(espRecord.Value, pushedRecord); + Assert.Equal(recordBytes, pushedRecord); + } } diff --git a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Thread.cs b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Thread.cs index cb948c27c552fe..0e229e91ea22e2 100644 --- a/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Thread.cs +++ b/src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Thread.cs @@ -20,6 +20,8 @@ internal sealed class MockExceptionInfo : TypedView private const string CallerOfActualHandlerFrameFieldName = "CallerOfActualHandlerFrame"; private const string ClauseForCatchHandlerStartPCFieldName = "ClauseForCatchHandlerStartPC"; private const string ClauseForCatchHandlerEndPCFieldName = "ClauseForCatchHandlerEndPC"; + private const string ExceptionRecordFieldName = "ExceptionRecord"; + private const string ContextRecordFieldName = "ContextRecord"; public static Layout CreateLayout(MockTarget.Architecture architecture) => new SequentialLayoutBuilder("ExceptionInfo", architecture) @@ -35,6 +37,8 @@ public static Layout CreateLayout(MockTarget.Architecture arc .AddPointerField(CallerOfActualHandlerFrameFieldName) .AddUInt32Field(ClauseForCatchHandlerStartPCFieldName) .AddUInt32Field(ClauseForCatchHandlerEndPCFieldName) + .AddPointerField(ExceptionRecordFieldName) + .AddPointerField(ContextRecordFieldName) .Build(); public ulong ThrownObject @@ -42,6 +46,18 @@ public ulong ThrownObject get => ReadPointerField(ThrownObjectFieldName); set => WritePointerField(ThrownObjectFieldName, value); } + + public ulong ExceptionRecord + { + get => ReadPointerField(ExceptionRecordFieldName); + set => WritePointerField(ExceptionRecordFieldName, value); + } + + public ulong ContextRecord + { + get => ReadPointerField(ContextRecordFieldName); + set => WritePointerField(ContextRecordFieldName, value); + } } internal sealed class MockGCAllocContext : TypedView diff --git a/src/native/managed/cdac/tests/UnitTests/PlatformContextTests.cs b/src/native/managed/cdac/tests/UnitTests/PlatformContextTests.cs index 052e7c0f003a7e..3adbf371674f94 100644 --- a/src/native/managed/cdac/tests/UnitTests/PlatformContextTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/PlatformContextTests.cs @@ -1,6 +1,8 @@ // 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 Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; using Xunit; @@ -144,4 +146,46 @@ public void RISCV64_ZeroRegister_ReadReturnsZero_WriteReturnsFalse() Assert.Equal(0UL, value.Value); Assert.False(ctx.TrySetRegister(0, new TargetNUInt(0xDEAD))); } + + /// + /// Architectures with a hardware single-step (trace) flag, the register that holds it, + /// the trace-flag bit, and an unrelated bit that must be preserved by UnsetSingleStepFlag. + /// + public static IEnumerable HardwareSingleStepContexts() + { + yield return [new ContextHolder(), "eflags", 0x100UL, 0x202UL]; + yield return [new ContextHolder(), "eflags", 0x100UL, 0x202UL]; + yield return [new ContextHolder(), "cpsr", 0x0020_0000UL, 0x4000_0000UL]; + } + + [Theory] + [MemberData(nameof(HardwareSingleStepContexts))] + public void UnsetSingleStepFlag_ClearsTraceFlag_PreservesOtherBits( + IPlatformAgnosticContext ctx, string flagRegister, ulong traceFlagBit, ulong otherBits) + { + Assert.True(ctx.TrySetRegister(flagRegister, new TargetNUInt(traceFlagBit | otherBits))); + + ctx.UnsetSingleStepFlag(); + + Assert.True(ctx.TryReadRegister(flagRegister, out TargetNUInt value)); + Assert.Equal(otherBits, value.Value); + } + + public static IEnumerable EmulatedSingleStepContexts() + { + yield return [new ContextHolder()]; + yield return [new ContextHolder()]; + yield return [new ContextHolder()]; + } + + [Theory] + [MemberData(nameof(EmulatedSingleStepContexts))] + public void UnsetSingleStepFlag_IsNoOp_OnEmulatedSingleStepArches(IPlatformAgnosticContext ctx) + { + byte[] before = ctx.GetBytes(); + + ctx.UnsetSingleStepFlag(); + + Assert.Equal(before, ctx.GetBytes()); + } } \ No newline at end of file diff --git a/src/native/managed/cdac/tests/UnitTests/ThreadTests.cs b/src/native/managed/cdac/tests/UnitTests/ThreadTests.cs index 7e5ef29a2909e2..2177671829ec9d 100644 --- a/src/native/managed/cdac/tests/UnitTests/ThreadTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ThreadTests.cs @@ -352,6 +352,58 @@ public void GetThreadData_LastThrownObjectHandle_NoActiveException(MockTarget.Ar Assert.Equal(lastThrownHandle, data.LastThrownObjectHandle); } + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetThreadData_ExceptionInProgress_ExposesOSRecords(MockTarget.Architecture arch) + { + // When an exception is in progress (non-null tracker), GetThreadData exposes the + // OS EXCEPTION_RECORD and CONTEXT pointers stored on the current ExInfo. + MockThread? thread = null; + TargetPointer osExceptionRecord = new(0xAAAA_0001); + TargetPointer osContextRecord = new(0xBBBB_0001); + + TestPlaceholderTarget target = CreateTarget( + arch, + threadBuilder => + { + thread = threadBuilder.AddThread(1, 1234); + MockExceptionInfo exceptionInfo = threadBuilder.GetExceptionInfo(thread); + exceptionInfo.ExceptionRecord = (ulong)osExceptionRecord; + exceptionInfo.ContextRecord = (ulong)osContextRecord; + }); + + IThread contract = target.Contracts.Thread; + ThreadData data = contract.GetThreadData(new TargetPointer(thread!.Address)); + + Assert.True(data.IsExceptionInProgress); + Assert.Equal(osExceptionRecord, data.OSExceptionRecord); + Assert.Equal(osContextRecord, data.OSExceptionContextRecord); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetThreadData_NoExceptionInProgress_OSRecordsNull(MockTarget.Architecture arch) + { + // When there is no current exception tracker, no exception is in progress and the + // OS record pointers are null. + MockThread? thread = null; + + TestPlaceholderTarget target = CreateTarget( + arch, + threadBuilder => + { + thread = threadBuilder.AddThread(1, 1234); + thread.ExceptionTracker = 0; + }); + + IThread contract = target.Contracts.Thread; + ThreadData data = contract.GetThreadData(new TargetPointer(thread!.Address)); + + Assert.False(data.IsExceptionInProgress); + Assert.Equal(TargetPointer.Null, data.OSExceptionRecord); + Assert.Equal(TargetPointer.Null, data.OSExceptionContextRecord); + } + [Theory] [ClassData(typeof(MockTarget.StdArch))] public void GetThreadData_ThreadHandle(MockTarget.Architecture arch) diff --git a/src/tools/StressLogAnalyzer/src/Program.cs b/src/tools/StressLogAnalyzer/src/Program.cs index b7195a218f4afb..f15459c68557fd 100644 --- a/src/tools/StressLogAnalyzer/src/Program.cs +++ b/src/tools/StressLogAnalyzer/src/Program.cs @@ -494,6 +494,7 @@ ContractDescriptorTarget CreateTarget() => ContractDescriptorTarget.Create( (address, buffer) => ReadFromMemoryMappedLog(address, buffer, header), (address, buffer) => throw new NotImplementedException("StressLogAnalyzer does not provide WriteToTarget implementation"), (threadId, contextFlags, bufferToFill) => throw new NotImplementedException("StressLogAnalyzer does not provide GetTargetThreadContext implementation"), + (threadId, context) => throw new NotImplementedException("StressLogAnalyzer does not provide SetTargetThreadContext implementation"), (ulong size, out ulong allocatedAddress) => throw new NotImplementedException("StressLogAnalyzer does not provide AllocVirtual implementation"), true, nuint.Size,