From 2cd405f576905ef7d15f498e28fb6706110f9eac Mon Sep 17 00:00:00 2001 From: rcj1 Date: Mon, 15 Jun 2026 13:29:41 -0700 Subject: [PATCH 1/7] add Hijack API --- docs/design/datacontracts/Debugger.md | 22 +++ docs/design/datacontracts/Thread.md | 20 +++ src/coreclr/debug/daccess/cdac.cpp | 17 ++- src/coreclr/vm/cdacstress.cpp | 4 +- .../vm/datadescriptor/datadescriptor.inc | 2 + src/coreclr/vm/exinfo.h | 4 + .../Contracts/IDebugger.cs | 1 + .../Contracts/IThread.cs | 5 +- .../Target.cs | 8 ++ .../Contracts/Debugger_1.cs | 29 ++++ .../Contracts/IntegerArgPlacer.cs | 88 ++++++++++++ .../Contracts/StackPusher.cs | 73 ++++++++++ .../StackWalk/Context/AMD64Context.cs | 3 + .../StackWalk/Context/ARM64Context.cs | 3 + .../Contracts/StackWalk/Context/ARMContext.cs | 2 + .../StackWalk/Context/ContextHolder.cs | 1 + .../Context/IPlatformAgnosticContext.cs | 7 + .../StackWalk/Context/IPlatformContext.cs | 7 + .../StackWalk/Context/LoongArch64Context.cs | 2 + .../StackWalk/Context/RISCV64Context.cs | 2 + .../Contracts/StackWalk/Context/X86Context.cs | 3 + .../Contracts/Thread_1.cs | 5 +- .../Data/ExceptionInfo.cs | 4 + .../Dbi/DacDbiImpl.cs | 126 +++++++++++++++++- .../ContractDescriptorTarget.cs | 21 ++- src/native/managed/cdac/inc/cdac_reader.h | 2 + .../mscordaccore_universal/Entrypoints.cs | 54 ++++++++ .../managed/cdac/scripts/DumpHelpers.cs | 1 + .../cdac/tests/DataGenerator/TestTarget.cs | 1 + .../ContractDescriptorBuilder.cs | 2 +- .../tests/TestInfrastructure/DumpTestBase.cs | 2 + .../TestPlaceholderTarget.cs | 1 + .../UnitTests/ClrDataExceptionStateTests.cs | 10 +- .../cdac/tests/UnitTests/DebuggerTests.cs | 27 ++++ .../MockDescriptors/MockDescriptors.Thread.cs | 16 +++ .../tests/UnitTests/PlatformContextTests.cs | 40 ++++++ .../cdac/tests/UnitTests/ThreadTests.cs | 52 ++++++++ src/tools/StressLogAnalyzer/src/Program.cs | 1 + 38 files changed, 657 insertions(+), 11 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/IntegerArgPlacer.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackPusher.cs diff --git a/docs/design/datacontracts/Debugger.md b/docs/design/datacontracts/Debugger.md index c9764df281834f..8c2d6d22c684fa 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 GetHijackAddress(); ``` ## Version 1 @@ -188,4 +189,25 @@ HijackKind GetHijackKind(TargetCodePointer controlPC) } return HijackKind.None; } + +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 */); +} ``` 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 af5f8362af37bd..7cf9f391edc09b 100644 --- a/src/coreclr/vm/cdacstress.cpp +++ b/src/coreclr/vm/cdacstress.cpp @@ -425,8 +425,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 d8ad288c1e1af2..3927ca03574789 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..5d961708428b2e 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 GetHijackAddress() => 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_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger_1.cs index 94a3ebea153095..fb8c3d5dc0d9eb 100644 --- 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 @@ -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.Diagnostics.CodeAnalysis; + namespace Microsoft.Diagnostics.DataContractReader.Contracts; internal readonly struct Debugger_1 : IDebugger @@ -149,4 +151,31 @@ HijackKind IDebugger.GetHijackKind(TargetCodePointer controlPC) } return HijackKind.None; } + + TargetPointer IDebugger.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; + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/IntegerArgPlacer.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/IntegerArgPlacer.cs new file mode 100644 index 00000000000000..6f8b5a733456a1 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/IntegerArgPlacer.cs @@ -0,0 +1,88 @@ +// 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 Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +public 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], arch); + } + + // 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, RuntimeInfoArchitecture arch) + { + if ((arch is RuntimeInfoArchitecture.X86 or RuntimeInfoArchitecture.Arm) && value.Value > uint.MaxValue) + { + throw new InvalidOperationException($"Cannot set register '{register}' to value {value.Value} on x86 context: 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/StackPusher.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackPusher.cs new file mode 100644 index 00000000000000..e0b80fa3b3e3be --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackPusher.cs @@ -0,0 +1,73 @@ +// 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 Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +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/StackWalk/Context/AMD64Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64Context.cs index 5ce1ed5ec6467c..8b1b2b8ae00481 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 fecff344c0aa10..8f1119d17561e5 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 1bec70d46057c6..833b699247029a 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,8 @@ public void Unwind(Target target) unwinder.Unwind(ref this); } + public void UnsetSingleStepFlag() => throw new NotSupportedException("ARM uses emulated single-stepping; there is no hardware single-step flag in the context."); + 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 41e777840d5253..9acf9382ab95e0 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 f7d210a6de2df2..9b7119981d1a39 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 @@ -31,6 +31,13 @@ public interface IPlatformAgnosticContext 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 a4debfc3b641d0..f3bbaba48df03c 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 e76d7ba37411ef..1f455bfefda1a7 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() => throw new NotSupportedException("LoongArch64 uses emulated single-stepping; there is no hardware single-step flag in the context."); + 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 b8f84d959983a9..1fb83cb2e1bdcd 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,8 @@ public void Unwind(Target target) unwinder.Unwind(ref this); } + public void UnsetSingleStepFlag() => throw new NotSupportedException("RISCV64 uses emulated single-stepping; there is no hardware single-step flag in the context."); + 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 735e889c798032..9e6aa2e05a4d7d 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..12592aa01e3657 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 @@ -12,6 +12,10 @@ internal sealed partial class ExceptionInfo : IData [Field] public TargetPointer StackLowBound { get; } [Field] public TargetPointer StackHighBound { get; } + // The OS EXCEPTION_RECORD and CONTEXT the OS exception dispatcher pushed for this exception. + [Field] public TargetPointer ExceptionRecord { get; } + [Field] public TargetPointer ContextRecord { get; } + // Only present on Windows platforms [Field] public TargetPointer? ExceptionWatsonBucketTrackerBuckets { get; } [Field] public byte PassNumber { 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 0a34ff6f9ed546..9980f16ccdc07d 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 @@ -685,8 +685,132 @@ public int MarkDebuggerAttached(Interop.BOOL fAttached) return hr; } + // Maximum number of exception parameters in an OS EXCEPTION_RECORD (EXCEPTION_MAXIMUM_PARAMETERS). + private const int ExceptionMaximumParameters = 15; + + // 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(nint pExceptionRecord) + { + int numberParametersOffset = sizeof(uint) + sizeof(uint) + (2 * _target.PointerSize); + return (uint)*(int*)(pExceptionRecord + numberParametersOffset); + } + + private void WriteExceptionRecordHelper(TargetPointer remotePtr, nint pExceptionRecord) + { + uint numberParameters = ReadExceptionRecordNumberParameters(pExceptionRecord); + int cbSize = ExceptionRecordHeaderSize() + ((int)numberParameters * _target.PointerSize); + _target.WriteBuffer(remotePtr.Value, new Span((void*)pExceptionRecord, cbSize)); + } + 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/rcj1/runtime/blob/eacf48a66e67ba40bfca35674b9623c634bfdddc/src/coreclr/debug/daccess/dacdbiimpl.cpp#L4909 for more algorithm detail. + int hr = HResults.S_OK; + try + { + TargetPointer pfnHijackFunction = _target.Contracts.Debugger.GetHijackAddress(); + if (pfnHijackFunction == TargetPointer.Null) + throw Marshal.GetExceptionForHR(CorDbgHResults.CORDBG_E_NOTREADY)!; + + // 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)!; + ctx.FillFromBuffer(contextBuffer); + + // If the caller requested it, copy back the original (pre-hijack) context. + if (pOriginalContext != 0) + { + if (cbSizeContext != ctx.Size) + throw Marshal.GetExceptionForHR(HResults.E_INVALIDARG)!; + contextBuffer.AsSpan().CopyTo(new Span((void*)pOriginalContext, (int)cbSizeContext)); + } + + if (_target.Contracts.RuntimeInfo.GetTargetArchitecture() is RuntimeInfoArchitecture.X64 or RuntimeInfoArchitecture.X86 or RuntimeInfoArchitecture.Arm64) + ctx.UnsetSingleStepFlag(); + + TargetPointer sp = ctx.StackPointer; + TargetPointer espContext = TargetPointer.Null; + TargetPointer espRecord = TargetPointer.Null; + + if (vmThread != 0) + { + 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, pRecord); + 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 (pRecord != 0) + { + int fullRecordSize = ExceptionRecordHeaderSize() + (ExceptionMaximumParameters * _target.PointerSize); + espRecord = StackPusher.Push(_target, ref sp, new Span((void*)pRecord, fullRecordSize), align: true); + } + } + + if (pRemoteContextAddr is not null) + *pRemoteContextAddr = espContext.Value; + + // 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((ulong)pUserData), + ]; + IntegerArgPlacer.PlaceArgs(_target, ctx, ref sp, args); + + ctx.StackPointer = sp; + ctx.InstructionPointer = pfnHijackFunction; + + // Commit the modified context to the thread. + if (!_target.TrySetThreadContext(dwThreadId, ctx.GetBytes())) + 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 4c0dbef321d223..55bf1a1c5a4d27 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, @@ -95,6 +126,7 @@ private static unsafe int Init( } } }, + setThreadContextDelegate, allocDelegate, [Contracts.CoreCLRContracts.Register], out ContractDescriptorTarget? target)) @@ -313,6 +345,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 411d7203087de4..573c9b6a28e41d 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"), [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"), [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..9f87653602c7ad 100644 --- a/src/native/managed/cdac/tests/UnitTests/DebuggerTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/DebuggerTests.cs @@ -518,4 +518,31 @@ public void GetHijackKind_ReturnsNoneWhenTableEmpty(MockTarget.Architecture arch Assert.Equal(HijackKind.None, debugger.GetHijackKind(new TargetCodePointer(0x10_0080))); } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetHijackAddress_ReturnsUnhandledExceptionStart(MockTarget.Architecture arch) + { + // Index 0 is the unhandled-exception hijack; GetHijackAddress returns its start address. + (ulong Start, ulong Size)[] ranges = + [ + (0x10_0000, 0x100), + (0x20_0000, 0x100), + ]; + Target target = BuildTargetWithHijackTable(arch, ranges); + IDebugger debugger = target.Contracts.Debugger; + + Assert.Equal(0x10_0000ul, debugger.GetHijackAddress().Value); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetHijackAddress_ReturnsNullWhenTableEmpty(MockTarget.Architecture arch) + { + // FEATURE_HIJACK off / uninitialized: MaxHijackFunctions == 0, no array. + Target target = BuildTargetWithHijackTable(arch, []); + IDebugger debugger = target.Contracts.Debugger; + + Assert.Equal(TargetPointer.Null, debugger.GetHijackAddress()); + } } 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..cce147676d971a 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,42 @@ 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_Throws_OnEmulatedSingleStepArches(IPlatformAgnosticContext ctx) + { + Assert.Throws(ctx.UnsetSingleStepFlag); + } } \ 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, From 4c5b8c54517b478d8c954fcb55273269c829a276 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Tue, 23 Jun 2026 14:31:35 -0700 Subject: [PATCH 2/7] code review --- .../Contracts/IntegerArgPlacer.cs | 2 +- .../Dbi/DacDbiImpl.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/IntegerArgPlacer.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/IntegerArgPlacer.cs index 6f8b5a733456a1..da1e493ccad08e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/IntegerArgPlacer.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/IntegerArgPlacer.cs @@ -78,7 +78,7 @@ private static void SetRegisterChecked(IPlatformAgnosticContext ctx, string regi { if ((arch is RuntimeInfoArchitecture.X86 or RuntimeInfoArchitecture.Arm) && value.Value > uint.MaxValue) { - throw new InvalidOperationException($"Cannot set register '{register}' to value {value.Value} on x86 context: value exceeds 32-bit range."); + throw new InvalidOperationException($"Cannot set register '{register}' to value {value.Value} on {arch} context: value exceeds 32-bit range."); } if (!ctx.TrySetRegister(register, value)) { 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 52f74b635c65cf..b2cb35030de375 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 @@ -717,7 +717,7 @@ public int Hijack(ulong vmThread, uint dwThreadId, nint pRecord, nint pOriginalC { // 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/rcj1/runtime/blob/eacf48a66e67ba40bfca35674b9623c634bfdddc/src/coreclr/debug/daccess/dacdbiimpl.cpp#L4909 for more algorithm detail. + // 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 { @@ -799,7 +799,7 @@ public int Hijack(ulong vmThread, uint dwThreadId, nint pRecord, nint pOriginalC IntegerArgPlacer.PlaceArgs(_target, ctx, ref sp, args); ctx.StackPointer = sp; - ctx.InstructionPointer = pfnHijackFunction; + ctx.InstructionPointer = new TargetCodePointer(pfnHijackFunction.Value); // Commit the modified context to the thread. if (!_target.TrySetThreadContext(dwThreadId, ctx.GetBytes())) From b436506956121db6f4e55520bd718cb08dfc7c00 Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Tue, 23 Jun 2026 14:42:24 -0700 Subject: [PATCH 3/7] Update ExceptionInfo.cs --- .../Data/ExceptionInfo.cs | 2 -- 1 file changed, 2 deletions(-) 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 12592aa01e3657..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,8 +11,6 @@ internal sealed partial class ExceptionInfo : IData [Field] public uint ExceptionFlags { get; } [Field] public TargetPointer StackLowBound { get; } [Field] public TargetPointer StackHighBound { get; } - - // The OS EXCEPTION_RECORD and CONTEXT the OS exception dispatcher pushed for this exception. [Field] public TargetPointer ExceptionRecord { get; } [Field] public TargetPointer ContextRecord { get; } From 199bbf11a33f26e138a218177d25c488c187cad0 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Wed, 24 Jun 2026 13:22:17 -0700 Subject: [PATCH 4/7] code review --- docs/design/datacontracts/Debugger.md | 7 ++ .../Contracts/IDebugger.cs | 1 + .../Contracts/Debugger_1.cs | 91 +++++++++++++++++++ .../Contracts/IntegerArgPlacer.cs | 88 ------------------ .../Dbi/DacDbiImpl.cs | 6 +- 5 files changed, 102 insertions(+), 91 deletions(-) delete mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/IntegerArgPlacer.cs diff --git a/docs/design/datacontracts/Debugger.md b/docs/design/datacontracts/Debugger.md index 8c2d6d22c684fa..aea7e2b8fba08a 100644 --- a/docs/design/datacontracts/Debugger.md +++ b/docs/design/datacontracts/Debugger.md @@ -29,6 +29,7 @@ TargetPointer GetDebuggerControlBlockAddress(); void EnableGCNotificationEvents(bool fEnable); HijackKind GetHijackKind(TargetCodePointer controlPC); TargetPointer GetHijackAddress(); +void PlaceExceptionHijackWorkerArguments(byte[] context, ref TargetPointer sp, ReadOnlySpan args); ``` ## Version 1 @@ -210,4 +211,10 @@ TargetPointer GetHijackAddress() TargetPointer entryAddress = rgHijack + (ulong)(UnhandledExceptionHijackIndex * stride); return target.ReadPointer(entryAddress + /* MemoryRange::StartAddress offset */); } + +void PlaceExceptionHijackWorkerArguments(byte[] context, ref TargetPointer sp, ReadOnlySpan args) +{ + // Places the arguments to ExceptionHijackWorker as dictated by the native ABI. + // Mutates stack pointer and context as necessary. +} ``` 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 5d961708428b2e..87534e06c81813 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 @@ -29,6 +29,7 @@ public interface IDebugger : IContract void EnableGCNotificationEvents(bool fEnable) => throw new NotImplementedException(); HijackKind GetHijackKind(TargetCodePointer controlPC) => throw new NotImplementedException(); TargetPointer GetHijackAddress() => throw new NotImplementedException(); + void PlaceExceptionHijackWorkerArguments(byte[] context, ref TargetPointer sp, ReadOnlySpan args) => throw new NotImplementedException(); } public readonly struct Debugger : IDebugger 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 index fb8c3d5dc0d9eb..ffce6f08f15e5f 100644 --- 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 @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics.CodeAnalysis; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; namespace Microsoft.Diagnostics.DataContractReader.Contracts; @@ -178,4 +180,93 @@ private bool TryGetHijackFunctionRange(uint index, [NotNullWhen(true)] out Data. range = _target.ProcessedData.GetOrAdd(entryAddress); return true; } + + void IDebugger.PlaceExceptionHijackWorkerArguments(byte[] context, ref TargetPointer sp, ReadOnlySpan args) + { + IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(_target); + ctx.FillFromBuffer(context); + IntegerArgPlacer.PlaceArgs(_target, ctx, ref sp, args); + ctx.GetBytes().AsSpan().CopyTo(context); + } + + 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/IntegerArgPlacer.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/IntegerArgPlacer.cs deleted file mode 100644 index da1e493ccad08e..00000000000000 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/IntegerArgPlacer.cs +++ /dev/null @@ -1,88 +0,0 @@ -// 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 Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; - -namespace Microsoft.Diagnostics.DataContractReader.Contracts; - -public 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], arch); - } - - // 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, RuntimeInfoArchitecture arch) - { - if ((arch is RuntimeInfoArchitecture.X86 or RuntimeInfoArchitecture.Arm) && value.Value > uint.MaxValue) - { - throw new InvalidOperationException($"Cannot set register '{register}' to value {value.Value} on {arch} context: 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.Legacy/Dbi/DacDbiImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs index b2cb35030de375..3c8e1213562c7a 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 @@ -787,8 +787,6 @@ public int Hijack(ulong vmThread, uint dwThreadId, nint pRecord, nint pOriginalC if (pRemoteContextAddr is not null) *pRemoteContextAddr = espContext.Value; - // 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), @@ -796,7 +794,9 @@ public int Hijack(ulong vmThread, uint dwThreadId, nint pRecord, nint pOriginalC new TargetNUInt((uint)reason), new TargetNUInt((ulong)pUserData), ]; - IntegerArgPlacer.PlaceArgs(_target, ctx, ref sp, args); + byte[] ctxBytes = ctx.GetBytes(); + _target.Contracts.Debugger.PlaceExceptionHijackWorkerArguments(ctxBytes, ref sp, args); + ctx.FillFromBuffer(ctxBytes); ctx.StackPointer = sp; ctx.InstructionPointer = new TargetCodePointer(pfnHijackFunction.Value); From 811f438830999caa2cd2f3c6fdb8a1660c3bbaf6 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Tue, 30 Jun 2026 12:10:41 -0700 Subject: [PATCH 5/7] code review/refactor --- docs/design/datacontracts/Debugger.md | 5 +- .../Contracts/IDebugger.cs | 2 +- .../Contracts/Debugger_1.cs | 96 ++++++++++- .../StackWalk/Context/AMD64Context.cs | 1 + .../StackWalk/Context/ARM64Context.cs | 1 + .../Contracts/StackWalk/Context/ARMContext.cs | 1 + .../StackWalk/Context/ContextHolder.cs | 1 + .../Context/IPlatformAgnosticContext.cs | 2 + .../StackWalk/Context/IPlatformContext.cs | 2 + .../StackWalk/Context/LoongArch64Context.cs | 1 + .../StackWalk/Context/RISCV64Context.cs | 1 + .../Contracts/StackWalk/Context/X86Context.cs | 1 + .../Dbi/DacDbiImpl.cs | 103 +++--------- .../cdac/tests/UnitTests/DebuggerTests.cs | 158 ++++++++++++++++++ 14 files changed, 288 insertions(+), 87 deletions(-) diff --git a/docs/design/datacontracts/Debugger.md b/docs/design/datacontracts/Debugger.md index aea7e2b8fba08a..030a19512eb6fe 100644 --- a/docs/design/datacontracts/Debugger.md +++ b/docs/design/datacontracts/Debugger.md @@ -29,7 +29,7 @@ TargetPointer GetDebuggerControlBlockAddress(); void EnableGCNotificationEvents(bool fEnable); HijackKind GetHijackKind(TargetCodePointer controlPC); TargetPointer GetHijackAddress(); -void PlaceExceptionHijackWorkerArguments(byte[] context, ref TargetPointer sp, ReadOnlySpan args); +TargetPointer PrepareExceptionHijack(byte[] context, TargetPointer vmThread, byte[]? exceptionRecord, int reason, TargetPointer userData) ``` ## Version 1 @@ -212,8 +212,9 @@ TargetPointer GetHijackAddress() return target.ReadPointer(entryAddress + /* MemoryRange::StartAddress offset */); } -void PlaceExceptionHijackWorkerArguments(byte[] context, ref TargetPointer sp, ReadOnlySpan args) +TargetPointer PrepareExceptionHijack(byte[] context, TargetPointer vmThread, byte[]? exceptionRecord, int reason, TargetPointer userData) { + // 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/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugger.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugger.cs index 87534e06c81813..4808279a3233c9 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 @@ -29,7 +29,7 @@ public interface IDebugger : IContract void EnableGCNotificationEvents(bool fEnable) => throw new NotImplementedException(); HijackKind GetHijackKind(TargetCodePointer controlPC) => throw new NotImplementedException(); TargetPointer GetHijackAddress() => throw new NotImplementedException(); - void PlaceExceptionHijackWorkerArguments(byte[] context, ref TargetPointer sp, ReadOnlySpan args) => 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.Contracts/Contracts/Debugger_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger_1.cs index ffce6f08f15e5f..12c86d5e86d695 100644 --- 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 @@ -2,7 +2,10 @@ // 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; @@ -181,12 +184,103 @@ private bool TryGetHijackFunctionRange(uint index, [NotNullWhen(true)] out Data. return true; } - void IDebugger.PlaceExceptionHijackWorkerArguments(byte[] context, ref TargetPointer sp, ReadOnlySpan args) + // 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 = ((IDebugger)this).GetHijackAddress(); + if (pfnHijackFunction == TargetPointer.Null) + throw Marshal.GetExceptionForHR(CorDbgHResults.CORDBG_E_NOTREADY)!; + IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(_target); ctx.FillFromBuffer(context); + + if (ctx.SupportsSingleStep) + 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 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 4603d6f1f1f2ec..10e36921d53bc7 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 @@ -66,6 +66,7 @@ public void Unwind(Target target) // Clears the x64 hardware trace flag (EFLAGS.TF, bit 0x100). public void UnsetSingleStepFlag() => EFlags &= ~0x100; + public bool SupportsSingleStep => true; public bool TrySetRegister(string name, TargetNUInt value) { 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 8016c0ac1765a2..42ca6810ab3cce 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 @@ -72,6 +72,7 @@ public void Unwind(Target target) // Clears the AArch64 hardware single-step flag (CPSR.SS, bit 0x00200000). public void UnsetSingleStepFlag() => Cpsr &= ~0x00200000u; + public bool SupportsSingleStep => true; public bool TrySetRegister(string name, TargetNUInt value) { 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 9871836803c21d..e6035f57df2063 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 @@ -65,6 +65,7 @@ public void Unwind(Target target) } public void UnsetSingleStepFlag() => throw new NotSupportedException("ARM uses emulated single-stepping; there is no hardware single-step flag in the context."); + public bool SupportsSingleStep => false; public bool TrySetRegister(string name, TargetNUInt value) { 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 01457bab25b56a..6b16756d4e4828 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 @@ -53,6 +53,7 @@ public unsafe byte[] GetBytes() public void Clear() => Context = default; public void Unwind(Target target) => Context.Unwind(target); public void UnsetSingleStepFlag() => Context.UnsetSingleStepFlag(); + public bool SupportsSingleStep => Context.SupportsSingleStep; 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 fc311d9f22d744..a6b55c61f41a4a 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,6 +29,8 @@ 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 bool SupportsSingleStep { get; } + public abstract void Unwind(Target target); /// 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 2e71424e3d2191..e04f7216e329f9 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 @@ -18,6 +18,8 @@ public interface IPlatformContext uint RawContextFlags { get; set; } + bool SupportsSingleStep { get; } + void Unwind(Target target); /// 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 a152ba1c2f1e90..acef2302b95891 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 @@ -69,6 +69,7 @@ public void Unwind(Target target) } public void UnsetSingleStepFlag() => throw new NotSupportedException("LoongArch64 uses emulated single-stepping; there is no hardware single-step flag in the context."); + public bool SupportsSingleStep => false; public bool TrySetRegister(string name, TargetNUInt value) { 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 d7b0f0303f6237..0b8992715d94fa 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 @@ -69,6 +69,7 @@ public void Unwind(Target target) } public void UnsetSingleStepFlag() => throw new NotSupportedException("RISCV64 uses emulated single-stepping; there is no hardware single-step flag in the context."); + public bool SupportsSingleStep => false; public bool TrySetRegister(string name, TargetNUInt value) { 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 08fac2a6f5b3ea..943a8c66294c39 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 @@ -73,6 +73,7 @@ public void Unwind(Target target) // Clears the x86 hardware trace flag (EFLAGS.TF, bit 0x100). public void UnsetSingleStepFlag() => EFlags &= ~0x100u; + public bool SupportsSingleStep => true; public bool TrySetRegister(string name, TargetNUInt value) { 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 3c8e1213562c7a..675c83eb542f74 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 @@ -685,32 +685,17 @@ public int MarkDebuggerAttached(Interop.BOOL fAttached) return hr; } - // Maximum number of exception parameters in an OS EXCEPTION_RECORD (EXCEPTION_MAXIMUM_PARAMETERS). + // EXCEPTION_MAXIMUM_PARAMETERS from the Windows SDK. private const int ExceptionMaximumParameters = 15; - // 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() + // 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 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(nint pExceptionRecord) - { - int numberParametersOffset = sizeof(uint) + sizeof(uint) + (2 * _target.PointerSize); - return (uint)*(int*)(pExceptionRecord + numberParametersOffset); - } - - private void WriteExceptionRecordHelper(TargetPointer remotePtr, nint pExceptionRecord) - { - uint numberParameters = ReadExceptionRecordNumberParameters(pExceptionRecord); - int cbSize = ExceptionRecordHeaderSize() + ((int)numberParameters * _target.PointerSize); - _target.WriteBuffer(remotePtr.Value, new Span((void*)pExceptionRecord, cbSize)); + 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) @@ -721,88 +706,40 @@ public int Hijack(ulong vmThread, uint dwThreadId, nint pRecord, nint pOriginalC int hr = HResults.S_OK; try { - TargetPointer pfnHijackFunction = _target.Contracts.Debugger.GetHijackAddress(); - if (pfnHijackFunction == TargetPointer.Null) - throw Marshal.GetExceptionForHR(CorDbgHResults.CORDBG_E_NOTREADY)!; - // 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)!; - ctx.FillFromBuffer(contextBuffer); // If the caller requested it, copy back the original (pre-hijack) context. if (pOriginalContext != 0) { - if (cbSizeContext != ctx.Size) + if (cbSizeContext != contextBuffer.Length) throw Marshal.GetExceptionForHR(HResults.E_INVALIDARG)!; contextBuffer.AsSpan().CopyTo(new Span((void*)pOriginalContext, (int)cbSizeContext)); } - if (_target.Contracts.RuntimeInfo.GetTargetArchitecture() is RuntimeInfoArchitecture.X64 or RuntimeInfoArchitecture.X86 or RuntimeInfoArchitecture.Arm64) - ctx.UnsetSingleStepFlag(); - - TargetPointer sp = ctx.StackPointer; - TargetPointer espContext = TargetPointer.Null; - TargetPointer espRecord = TargetPointer.Null; - - if (vmThread != 0) + byte[]? recordBytes = null; + if (pRecord != 0) { - 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, pRecord); - espRecord = espOSRecord; - - sp = espOSContext < espOSRecord ? espOSContext : espOSRecord; - } - } + int recordSize = ExceptionRecordFullSize(_target.PointerSize); + recordBytes = new byte[recordSize]; + new ReadOnlySpan((void*)pRecord, recordSize).CopyTo(recordBytes); } - // 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 (pRecord != 0) - { - int fullRecordSize = ExceptionRecordHeaderSize() + (ExceptionMaximumParameters * _target.PointerSize); - espRecord = StackPusher.Push(_target, ref sp, new Span((void*)pRecord, fullRecordSize), align: true); - } - } + TargetPointer espContext = _target.Contracts.Debugger.PrepareExceptionHijack( + contextBuffer, + new TargetPointer(vmThread), + recordBytes, + reason, + new TargetPointer((ulong)pUserData)); if (pRemoteContextAddr is not null) *pRemoteContextAddr = espContext.Value; - ReadOnlySpan args = - [ - new TargetNUInt(espContext.Value), - new TargetNUInt(espRecord.Value), - new TargetNUInt((uint)reason), - new TargetNUInt((ulong)pUserData), - ]; - byte[] ctxBytes = ctx.GetBytes(); - _target.Contracts.Debugger.PlaceExceptionHijackWorkerArguments(ctxBytes, ref sp, args); - ctx.FillFromBuffer(ctxBytes); - - ctx.StackPointer = sp; - ctx.InstructionPointer = new TargetCodePointer(pfnHijackFunction.Value); - // Commit the modified context to the thread. - if (!_target.TrySetThreadContext(dwThreadId, ctx.GetBytes())) + if (!_target.TrySetThreadContext(dwThreadId, contextBuffer)) throw Marshal.GetExceptionForHR(HResults.E_FAIL)!; } catch (System.Exception ex) diff --git a/src/native/managed/cdac/tests/UnitTests/DebuggerTests.cs b/src/native/managed/cdac/tests/UnitTests/DebuggerTests.cs index 9f87653602c7ad..289c09f21c0771 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; @@ -545,4 +546,161 @@ public void GetHijackAddress_ReturnsNullWhenTableEmpty(MockTarget.Architecture a Assert.Equal(TargetPointer.Null, debugger.GetHijackAddress()); } + + // ----------------------------------------------------------------------- + // 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); + } } From b6d690cd69c3e0062efb81e15b7450bd840f01b3 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Tue, 30 Jun 2026 13:43:39 -0700 Subject: [PATCH 6/7] code review --- docs/design/datacontracts/Debugger.md | 4 +- .../Contracts/IDebugger.cs | 1 - .../Contracts/Debugger_1.cs | 366 ------------------ .../Contracts/StackPusher.cs | 73 ---- .../StackWalk/Context/AMD64Context.cs | 1 - .../StackWalk/Context/ARM64Context.cs | 1 - .../Contracts/StackWalk/Context/ARMContext.cs | 4 +- .../StackWalk/Context/ContextHolder.cs | 1 - .../Context/IPlatformAgnosticContext.cs | 1 - .../StackWalk/Context/IPlatformContext.cs | 2 - .../StackWalk/Context/LoongArch64Context.cs | 3 +- .../StackWalk/Context/RISCV64Context.cs | 4 +- .../Contracts/StackWalk/Context/X86Context.cs | 1 - .../cdac/tests/UnitTests/DebuggerTests.cs | 27 -- .../tests/UnitTests/PlatformContextTests.cs | 8 +- 15 files changed, 11 insertions(+), 486 deletions(-) delete mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger_1.cs delete mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackPusher.cs diff --git a/docs/design/datacontracts/Debugger.md b/docs/design/datacontracts/Debugger.md index 030a19512eb6fe..6923882643d8e8 100644 --- a/docs/design/datacontracts/Debugger.md +++ b/docs/design/datacontracts/Debugger.md @@ -28,7 +28,6 @@ void SetSendExceptionsOutsideOfJMC(bool sendExceptionsOutsideOfJMC); TargetPointer GetDebuggerControlBlockAddress(); void EnableGCNotificationEvents(bool fEnable); HijackKind GetHijackKind(TargetCodePointer controlPC); -TargetPointer GetHijackAddress(); TargetPointer PrepareExceptionHijack(byte[] context, TargetPointer vmThread, byte[]? exceptionRecord, int reason, TargetPointer userData) ``` @@ -191,7 +190,7 @@ HijackKind GetHijackKind(TargetCodePointer controlPC) return HijackKind.None; } -TargetPointer GetHijackAddress() +private TargetPointer GetHijackAddress() { // Returns the start address of the unhandled-exception hijack function // (index UnhandledExceptionHijackIndex == 0 in the RgHijackFunction array). @@ -214,6 +213,7 @@ TargetPointer GetHijackAddress() 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/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugger.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugger.cs index 4808279a3233c9..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,7 +28,6 @@ public interface IDebugger : IContract TargetPointer GetDebuggerControlBlockAddress() => throw new NotImplementedException(); void EnableGCNotificationEvents(bool fEnable) => throw new NotImplementedException(); HijackKind GetHijackKind(TargetCodePointer controlPC) => throw new NotImplementedException(); - TargetPointer GetHijackAddress() => throw new NotImplementedException(); TargetPointer PrepareExceptionHijack(byte[] context, TargetPointer vmThread, byte[]? exceptionRecord, int reason, TargetPointer userData) => throw new NotImplementedException(); } 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 12c86d5e86d695..00000000000000 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger_1.cs +++ /dev/null @@ -1,366 +0,0 @@ -// 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; - } - - TargetPointer IDebugger.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 = ((IDebugger)this).GetHijackAddress(); - if (pfnHijackFunction == TargetPointer.Null) - throw Marshal.GetExceptionForHR(CorDbgHResults.CORDBG_E_NOTREADY)!; - - IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(_target); - ctx.FillFromBuffer(context); - - if (ctx.SupportsSingleStep) - 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/StackPusher.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackPusher.cs deleted file mode 100644 index e0b80fa3b3e3be..00000000000000 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackPusher.cs +++ /dev/null @@ -1,73 +0,0 @@ -// 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 Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; - -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/StackWalk/Context/AMD64Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64Context.cs index 10e36921d53bc7..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 @@ -66,7 +66,6 @@ public void Unwind(Target target) // Clears the x64 hardware trace flag (EFLAGS.TF, bit 0x100). public void UnsetSingleStepFlag() => EFlags &= ~0x100; - public bool SupportsSingleStep => true; public bool TrySetRegister(string name, TargetNUInt value) { 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 42ca6810ab3cce..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 @@ -72,7 +72,6 @@ public void Unwind(Target target) // Clears the AArch64 hardware single-step flag (CPSR.SS, bit 0x00200000). public void UnsetSingleStepFlag() => Cpsr &= ~0x00200000u; - public bool SupportsSingleStep => true; public bool TrySetRegister(string name, TargetNUInt value) { 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 e6035f57df2063..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,9 +64,7 @@ public void Unwind(Target target) unwinder.Unwind(ref this); } - public void UnsetSingleStepFlag() => throw new NotSupportedException("ARM uses emulated single-stepping; there is no hardware single-step flag in the context."); - public bool SupportsSingleStep => false; - + 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 6b16756d4e4828..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 @@ -53,7 +53,6 @@ public unsafe byte[] GetBytes() public void Clear() => Context = default; public void Unwind(Target target) => Context.Unwind(target); public void UnsetSingleStepFlag() => Context.UnsetSingleStepFlag(); - public bool SupportsSingleStep => Context.SupportsSingleStep; 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 a6b55c61f41a4a..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,7 +29,6 @@ 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 bool SupportsSingleStep { get; } public abstract void Unwind(Target target); 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 e04f7216e329f9..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 @@ -18,8 +18,6 @@ public interface IPlatformContext uint RawContextFlags { get; set; } - bool SupportsSingleStep { get; } - void Unwind(Target target); /// 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 acef2302b95891..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,8 +68,7 @@ public void Unwind(Target target) unwinder.Unwind(ref this); } - public void UnsetSingleStepFlag() => throw new NotSupportedException("LoongArch64 uses emulated single-stepping; there is no hardware single-step flag in the context."); - public bool SupportsSingleStep => false; + public void UnsetSingleStepFlag() {} public bool TrySetRegister(string name, TargetNUInt value) { 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 0b8992715d94fa..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,9 +68,7 @@ public void Unwind(Target target) unwinder.Unwind(ref this); } - public void UnsetSingleStepFlag() => throw new NotSupportedException("RISCV64 uses emulated single-stepping; there is no hardware single-step flag in the context."); - public bool SupportsSingleStep => false; - + 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 943a8c66294c39..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 @@ -73,7 +73,6 @@ public void Unwind(Target target) // Clears the x86 hardware trace flag (EFLAGS.TF, bit 0x100). public void UnsetSingleStepFlag() => EFlags &= ~0x100u; - public bool SupportsSingleStep => true; public bool TrySetRegister(string name, TargetNUInt value) { diff --git a/src/native/managed/cdac/tests/UnitTests/DebuggerTests.cs b/src/native/managed/cdac/tests/UnitTests/DebuggerTests.cs index 289c09f21c0771..a0a9aea33e6c72 100644 --- a/src/native/managed/cdac/tests/UnitTests/DebuggerTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/DebuggerTests.cs @@ -520,33 +520,6 @@ public void GetHijackKind_ReturnsNoneWhenTableEmpty(MockTarget.Architecture arch Assert.Equal(HijackKind.None, debugger.GetHijackKind(new TargetCodePointer(0x10_0080))); } - [Theory] - [ClassData(typeof(MockTarget.StdArch))] - public void GetHijackAddress_ReturnsUnhandledExceptionStart(MockTarget.Architecture arch) - { - // Index 0 is the unhandled-exception hijack; GetHijackAddress returns its start address. - (ulong Start, ulong Size)[] ranges = - [ - (0x10_0000, 0x100), - (0x20_0000, 0x100), - ]; - Target target = BuildTargetWithHijackTable(arch, ranges); - IDebugger debugger = target.Contracts.Debugger; - - Assert.Equal(0x10_0000ul, debugger.GetHijackAddress().Value); - } - - [Theory] - [ClassData(typeof(MockTarget.StdArch))] - public void GetHijackAddress_ReturnsNullWhenTableEmpty(MockTarget.Architecture arch) - { - // FEATURE_HIJACK off / uninitialized: MaxHijackFunctions == 0, no array. - Target target = BuildTargetWithHijackTable(arch, []); - IDebugger debugger = target.Contracts.Debugger; - - Assert.Equal(TargetPointer.Null, debugger.GetHijackAddress()); - } - // ----------------------------------------------------------------------- // PrepareExceptionHijack // ----------------------------------------------------------------------- diff --git a/src/native/managed/cdac/tests/UnitTests/PlatformContextTests.cs b/src/native/managed/cdac/tests/UnitTests/PlatformContextTests.cs index cce147676d971a..3adbf371674f94 100644 --- a/src/native/managed/cdac/tests/UnitTests/PlatformContextTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/PlatformContextTests.cs @@ -180,8 +180,12 @@ public static IEnumerable EmulatedSingleStepContexts() [Theory] [MemberData(nameof(EmulatedSingleStepContexts))] - public void UnsetSingleStepFlag_Throws_OnEmulatedSingleStepArches(IPlatformAgnosticContext ctx) + public void UnsetSingleStepFlag_IsNoOp_OnEmulatedSingleStepArches(IPlatformAgnosticContext ctx) { - Assert.Throws(ctx.UnsetSingleStepFlag); + byte[] before = ctx.GetBytes(); + + ctx.UnsetSingleStepFlag(); + + Assert.Equal(before, ctx.GetBytes()); } } \ No newline at end of file From 69c5db50d388e4b7b500df326d16fc868ffd2e66 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Tue, 30 Jun 2026 14:23:00 -0700 Subject: [PATCH 7/7] add --- .../Contracts/Debugger/Debugger_1.cs | 365 ++++++++++++++++++ .../Contracts/Debugger/StackPusher.cs | 72 ++++ 2 files changed, 437 insertions(+) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger/Debugger_1.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger/StackPusher.cs 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); + } + +}