From bc301ae878aa3b453b01d0ffd8c01d9e14d68cdb Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:43:48 -0400 Subject: [PATCH 1/9] Add cDAC interpreter support for stack walking and diagnostics Implement interpreter support in the cDAC diagnostic subsystem: - Add InterpreterFrame data type and frame iteration support - Add interpreter precode resolution (GetInterpreterCodeFromInterpreterPrecodeIfPresent) - Add GetCodeForInterpreterOrJitted API to IRuntimeTypeSystem - Add interpreter RealCodeHeader and EECodeInfo support in ExecutionManager - Support InterpreterMethodInfo, InterpreterRealCodeHeader data descriptors - Update design docs for ExecutionManager, PrecodeStubs, RuntimeTypeSystem - Add unit tests for precode stubs, interpreter frames, SOSDacInterface5 - Add InterpreterStack dump test debuggee and tests --- docs/design/datacontracts/ExecutionManager.md | 40 +- docs/design/datacontracts/PrecodeStubs.md | 54 +++ docs/design/datacontracts/StackWalk.md | 20 + .../vm/datadescriptor/datadescriptor.h | 4 + .../vm/datadescriptor/datadescriptor.inc | 41 ++ src/coreclr/vm/frames.h | 8 + .../Contracts/IExecutionManager.cs | 3 +- .../Contracts/IPrecodeStubs.cs | 8 + .../DataType.cs | 7 + ...cutionManagerCore.InterpreterJitManager.cs | 152 +++++++ .../ExecutionManager/ExecutionManagerCore.cs | 40 +- .../Contracts/PrecodeStubs_1.cs | 5 + .../Contracts/PrecodeStubs_2.cs | 5 + .../Contracts/PrecodeStubs_3.cs | 10 + .../Contracts/PrecodeStubs_Common.cs | 37 ++ .../StackWalk/FrameHandling/FrameIterator.cs | 48 ++- .../Contracts/StackWalk/StackWalk_1.cs | 42 +- .../Data/InterpByteCodeStart.cs | 18 + .../Data/InterpMethod.cs | 18 + .../Data/InterpMethodContextFrame.cs | 20 + .../Data/InterpreterFrame.cs | 18 + .../Data/InterpreterPrecodeData.cs | 20 + .../Data/InterpreterRealCodeHeader.cs | 25 ++ .../MethodValidation.cs | 26 +- .../ClrDataMethodInstance.cs | 3 + .../SOSDacImpl.cs | 20 +- .../Debuggees/Directory.Build.targets | 1 + .../InterpreterStack/InterpreterStack.csproj | 19 + .../Debuggees/InterpreterStack/Program.cs | 51 +++ .../InterpreterStack/Trampoline/Trampoline.cs | 21 + .../Trampoline/Trampoline.csproj | 5 + .../tests/DumpTests/DumpTestStackWalker.cs | 77 +++- .../cdac/tests/DumpTests/DumpTests.targets | 26 +- .../DumpTests/InterpreterStackDumpTests.cs | 137 ++++++ .../ExecutionManager/ExecutionManagerTests.cs | 68 +++ .../managed/cdac/tests/FrameIteratorTests.cs | 405 ++++++++++++++++++ .../managed/cdac/tests/MethodDescTests.cs | 137 +++++- ...iagnostics.DataContractReader.Tests.csproj | 1 + .../MockDescriptors.ExecutionManager.cs | 78 ++++ .../managed/cdac/tests/PrecodeStubsTests.cs | 191 ++++++++- .../cdac/tests/SOSDacInterface5Tests.cs | 12 + 41 files changed, 1842 insertions(+), 79 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.InterpreterJitManager.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpByteCodeStart.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpMethod.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpMethodContextFrame.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpreterFrame.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpreterPrecodeData.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpreterRealCodeHeader.cs create mode 100644 src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/InterpreterStack.csproj create mode 100644 src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/Program.cs create mode 100644 src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/Trampoline/Trampoline.cs create mode 100644 src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/Trampoline/Trampoline.csproj create mode 100644 src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs create mode 100644 src/native/managed/cdac/tests/FrameIteratorTests.cs diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index cf7163569789f9..ae41bf98b6b551 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -161,6 +161,10 @@ Data descriptors used: | `RealCodeHeader` | `DebugInfo` | Pointer to the DebugInfo | | `RealCodeHeader` | `GCInfo` | Pointer to the GCInfo encoding | | `RealCodeHeader` | `EHInfo` | Pointer to the `EE_ILEXCEPTION` containing exception clauses | +| `InterpreterRealCodeHeader` | `MethodDesc` | Pointer to the corresponding `MethodDesc` for interpreter code | +| `InterpreterRealCodeHeader` | `DebugInfo` | Pointer to the DebugInfo for interpreter code | +| `InterpreterRealCodeHeader` | `GCInfo` | Pointer to the GCInfo encoding for interpreter code | +| `InterpreterRealCodeHeader` | `JitEHInfo` | Pointer to the `EE_ILEXCEPTION` containing exception clauses for interpreter code | | `Module` | `ReadyToRunInfo` | Pointer to the `ReadyToRunInfo` for the module | | `ReadyToRunInfo` | `ReadyToRunHeader` | Pointer to the ReadyToRunHeader | | `ReadyToRunInfo` | `CompositeInfo` | Pointer to composite R2R info - or itself for non-composite | @@ -286,6 +290,31 @@ bool GetMethodInfo(TargetPointer rangeSection, TargetCodePointer jittedCodeAddre } ``` +The Interpreter JitManager `GetMethodInfo` uses the same nibble map lookup as the EE JitManager, but reads an `InterpreterRealCodeHeader` instead of a `RealCodeHeader`: + +```csharp +bool GetMethodInfo(TargetPointer rangeSection, TargetCodePointer jittedCodeAddress, [NotNullWhen(true)] out CodeBlock? info) +{ + info = default; + TargetPointer start = // look up jittedCodeAddress in nibble map for rangeSection - see NibbleMap below + if (start == TargetPointer.Null) + return false; + + TargetNUInt relativeOffset = jittedCodeAddress - start; + int codeHeaderOffset = Target.PointerSize; + TargetPointer codeHeaderIndirect = start - codeHeaderOffset; + + // Check if address is in a stub code block + if (codeHeaderIndirect < Target.ReadGlobal("StubCodeBlockLast")) + return false; + + TargetPointer codeHeaderAddress = Target.ReadPointer(codeHeaderIndirect); + Data.InterpreterRealCodeHeader realCodeHeader = // read InterpreterRealCodeHeader at codeHeaderAddress + info = new CodeBlock(jittedCodeAddress, realCodeHeader.MethodDesc, relativeOffset); + return true; +} +``` + The R2R JitManager `GetMethodInfo` finds the runtime function corresponding to an address and maps its entry point pack to a method: ```csharp @@ -384,13 +413,14 @@ public override void GetMethodRegionInfo(RangeSection rangeSection, TargetCodePo ``` -`GetJITType` returns the JIT type by finding the JIT manager for the data range containing the relevant code block. We return `Jit` for the `EEJitManager`, `R2R` for the `R2RJitManager`, and `Unknown` for any other value. +`GetJITType` returns the JIT type by finding the JIT manager for the data range containing the relevant code block. We return `Jit` for the `EEJitManager`, `R2R` for the `R2RJitManager`, `Interpreter` for the `InterpreterJitManager`, and `Unknown` for any other value. ```csharp public enum JitType : uint { Unknown = 0, Jit = 1, - R2R = 2 + R2R = 2, + Interpreter = 3 }; ``` `NonVirtualEntry2MethodDesc` attempts to find a method desc from an entrypoint. If portable entrypoints are enabled, we attempt to read the entrypoint data structure to find the method table. We also attempt to find the method desc from a precode stub. Finally, we attempt to find the method desc using `GetMethodInfo` as described above. @@ -466,6 +496,8 @@ The `GetMethodDesc`, `GetStartAddress`, and `GetRelativeOffset` APIs extract fie * For R2R code (`ReadyToRunJitManager`), a list of sorted `RUNTIME_FUNCTION` are stored on the module's `ReadyToRunInfo`. This is accessed as described above for `GetMethodInfo`. Again, the relevant `RUNTIME_FUNCTION` is found by binary searching the list based on IP. +* For interpreted code (`InterpreterJitManager`), there is no native unwind info. `GetUnwindInfo` returns null. + Unwind info (`RUNTIME_FUNCTION`) use relative addressing. For managed code, these values are relative to the start of the code's containing range in the RangeSectionMap (described below). This could be the beginning of a `CodeHeap` for jitted code or the base address of the loaded image for ReadyToRun code. `GetUnwindInfoBaseAddress` finds this base address for a given `CodeBlockHandle`. @@ -476,6 +508,8 @@ Unwind info (`RUNTIME_FUNCTION`) use relative addressing. For managed code, thes * For R2R code (`ReadyToRunJitManager`) the `DebugInfo` is stored as part of the R2R image. The relevant `ReadyToRunInfo` stores a pointer to the an `ImageDataDirectory` representing the `DebugInfo` directory. Read the `VirtualAddress` of this data directory as a `NativeArray` containing the `DebugInfos`. To find the specific `DebugInfo`, index into the array using the `index` of the beginning of the R2R function as found like in `GetMethodInfo` above. This yields an offset `offset` value relative to the image base. Read the first variable length uint at `imageBase + offset`, `lookBack`. If `lookBack != 0`, return `imageBase + offset - lookback`. Otherwise return `offset + size of reading lookback`. For R2R images, `hasFlagByte` is always `false`. +* For interpreted code (`InterpreterJitManager`), a pointer to the `DebugInfo` is stored on the `InterpreterRealCodeHeader` which is accessed in the same way as the EE JitManager's `GetMethodInfo` (nibble map lookup followed by code header read). `hasFlagByte` is always `false`. + `IExecutionManager.GetGCInfo` gets a pointer to the relevant GCInfo for a `CodeBlockHandle`. The ExecutionManager delegates to the JitManager implementations as the GCInfo is stored differently on jitted and R2R code. * For jitted code (`EEJitManager`) a pointer to the `GCInfo` is stored on the `RealCodeHeader` which is accessed in the same way as `GetMethodInfo` described above. This can simply be returned as is. The `GCInfoVersion` is defined by the runtime global `GCInfoVersion`. @@ -484,6 +518,8 @@ For R2R images, `hasFlagByte` is always `false`. * The `GCInfoVersion` of R2R code is mapped from the R2R MajorVersion and MinorVersion which is read from the ReadyToRunHeader which itself is read from the ReadyToRunInfo (can be found as in GetMethodInfo). The current GCInfoVersion mapping is: * MajorVersion >= 11 and MajorVersion < 15 => 4 +* For interpreted code (`InterpreterJitManager`), a pointer to the `GCInfo` is stored on the `InterpreterRealCodeHeader`, accessed via nibble map lookup as with the EE JitManager. The `GCInfoVersion` is defined by the runtime global `GCInfoVersion`. The GC info is decoded using interpreter-specific decoding (`DecodeInterpreterGCInfo`). + `IExecutionManager.GetFuncletStartAddress` finds the start of the code blocks funclet. This will be different than the methods start address `GetStartAddress` if the current code block is inside of a funclet. To find the funclet start address, we get the unwind info corresponding to the code block using `IExecutionManager.GetUnwindInfo`. We then parse the unwind info to find the begin address (relative to the unwind info base address) and return the unwind info base address + unwind info begin address. diff --git a/docs/design/datacontracts/PrecodeStubs.md b/docs/design/datacontracts/PrecodeStubs.md index b336b433ac2e13..226afc677621f4 100644 --- a/docs/design/datacontracts/PrecodeStubs.md +++ b/docs/design/datacontracts/PrecodeStubs.md @@ -7,6 +7,11 @@ This contract provides support for examining [precode](../coreclr/botr/method-de ```csharp // Gets a pointer to the MethodDesc for a given stub entrypoint TargetPointer GetMethodDescFromStubAddress(TargetCodePointer entryPoint); + + // If the code pointer is an interpreter precode, returns the actual interpreter + // code address (ByteCodeAddr). Otherwise returns the original address unchanged. + // Mirrors GetInterpreterCodeFromInterpreterPrecodeIfPresent in native code (precode.cpp). + TargetCodePointer GetInterpreterCodeFromInterpreterPrecodeIfPresent(TargetCodePointer entryPoint); ``` ## Version 1, 2, and 3 @@ -40,6 +45,10 @@ Data descriptors used: | StubPrecodeData | Type | precise sort of stub precode | | FixupPrecodeData | MethodDesc | pointer to the MethodDesc associated with this fixup precode | | ThisPtrRetBufPrecodeData | MethodDesc | pointer to the MethodDesc associated with the ThisPtrRetBufPrecode (Version 2 only) | +| InterpreterPrecodeData | ByteCodeAddr | pointer to the `InterpByteCodeStart` for the interpreter bytecode (Version 3 only) | +| InterpreterPrecodeData | Type | precode sort byte identifying this as an interpreter precode (Version 3 only) | +| InterpByteCodeStart | Method | pointer to the `InterpMethod` associated with the bytecode | +| InterpMethod | MethodDesc | pointer to the MethodDesc for the interpreted method | arm32 note: the `CodePointerToInstrPointerMask` is used to convert IP values that may include an arm Thumb bit (for example extracted from disassembling a call instruction or from a snapshot of the registers) into an address. On other architectures applying the mask is a no-op. @@ -259,6 +268,22 @@ After the initial precode type is determined, for stub precodes a refined precod } } + // Version 3 only: resolves MethodDesc for interpreter precodes by following + // the InterpreterPrecodeData → InterpByteCodeStart → InterpMethod → MethodDesc chain. + internal sealed class InterpreterPrecode : ValidPrecode + { + internal InterpreterPrecode(TargetPointer instrPointer) : base(instrPointer, KnownPrecodeType.Interpreter) { } + + internal override TargetPointer GetMethodDesc(Target target, Data.PrecodeMachineDescriptor precodeMachineDescriptor) + { + TargetPointer dataAddr = InstrPointer + precodeMachineDescriptor.StubCodePageSize; + Data.InterpreterPrecodeData precodeData = target.ProcessedData.GetOrAdd(dataAddr); + Data.InterpByteCodeStart byteCodeStart = target.ProcessedData.GetOrAdd(precodeData.ByteCodeAddr); + Data.InterpMethod interpMethod = target.ProcessedData.GetOrAdd(byteCodeStart.Method); + return interpMethod.MethodDesc; + } + } + internal TargetPointer CodePointerReadableInstrPointer(TargetCodePointer codePointer) { // Mask off the thumb bit, if we're on arm32, to get the actual instruction pointer @@ -282,6 +307,8 @@ After the initial precode type is determined, for stub precodes a refined precod return new PInvokeImportPrecode(instrPointer); case KnownPrecodeType.ThisPtrRetBuf: return new ThisPtrRetBufPrecode(instrPointer); + case KnownPrecodeType.Interpreter: + return new InterpreterPrecode(instrPointer); default: break; } @@ -295,4 +322,31 @@ After the initial precode type is determined, for stub precodes a refined precod return precode.GetMethodDesc(_target, MachineDescriptor); } + + // Returns the interpreter bytecode address if the entry point is an interpreter precode, + // otherwise returns the original entry point unchanged. + // This method never throws - on any failure, the original address is returned. + TargetCodePointer IPrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(TargetCodePointer entryPoint) + { + try + { + TargetPointer instrPointer = CodePointerReadableInstrPointer(entryPoint); + if (!IsAlignedInstrPointer(instrPointer)) + return entryPoint; + + if (TryGetKnownPrecodeType(instrPointer) is not KnownPrecodeType.Interpreter) + return entryPoint; + + TargetPointer dataAddr = instrPointer + MachineDescriptor.StubCodePageSize; + Data.InterpreterPrecodeData precodeData = // read InterpreterPrecodeData at dataAddr + if (precodeData.ByteCodeAddr == null) + return entryPoint; + + return precodeData.ByteCodeAddr; + } + catch + { + return entryPoint; + } + } ``` diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 9d254591ddb0ac..88d3a540a249cc 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -73,6 +73,9 @@ This contract depends on the following descriptors: | `HijackArgs` (amd64) | `CalleeSavedRegisters` | CalleeSavedRegisters data structure | | `HijackArgs` (amd64 Windows) | `Rsp` | Saved stack pointer | | `HijackArgs` (arm/arm64/x86) | For each register `r` saved in HijackArgs, `r` | Register names associated with stored register values | +| `InterpreterFrame` | `TopInterpMethodContextFrame` | Pointer to the InterpreterFrame's top `InterpMethodContextFrame` | +| `InterpMethodContextFrame` | `StartIp` | Pointer to the `InterpByteCodeStart` for resolving the MethodDesc | +| `InterpMethodContextFrame` | `ParentPtr` | Pointer to the parent `InterpMethodContextFrame` in the call chain (null for outermost frame) | | `ArgumentRegisters` (arm) | For each register `r` saved in ArgumentRegisters, `r` | Register names associated with stored register values | | `CalleeSavedRegisters` | For each callee saved register `r`, `r` | Register names associated with stored register values | | `TailCallFrame` (x86 Windows) | `CalleeSavedRegisters` | CalleeSavedRegisters data structure | @@ -119,6 +122,23 @@ In reality, the actual algorithm is a little more complex fow two reasons. It re If the address of the `frame` is less than the caller's stack pointer, **return the current context**, pop the top Frame from `frameStack`, and **go to step 3**. 3. Unwind `currContext` using the Windows style unwinder. **Return the current context**. +#### Interpreter Frame Expansion + +When the stack walker encounters an `InterpreterFrame`, it expands it into multiple logical frames by walking the `InterpMethodContextFrame.ParentPtr` chain. The runtime maintains a linked list of `InterpMethodContextFrame` nodes representing each interpreted method currently on the call stack within a single `InterpreterFrame`. The `TopInterpMethodContextFrame` field points to the most recently entered interpreted method, and each node's `ParentPtr` points to its caller. + +For each `InterpMethodContextFrame` in the chain, the stack walker yields a separate frame. The `MethodDesc` for each frame is resolved by following: +`InterpMethodContextFrame.StartIp` -> `InterpByteCodeStart.Method` -> `InterpMethod.MethodDesc` + +``` +InterpreterFrame + └─ TopInterpMethodContextFrame -> InterpMethodContextFrame (method C) + └─ ParentPtr -> InterpMethodContextFrame (method B) + └─ ParentPtr -> InterpMethodContextFrame (method A) + └─ ParentPtr -> null +``` + +This produces three frames in order: C, B, A (innermost to outermost). + #### Simple Example diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.h b/src/coreclr/vm/datadescriptor/datadescriptor.h index 36c62393091e66..229284445cef9f 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.h +++ b/src/coreclr/vm/datadescriptor/datadescriptor.h @@ -24,6 +24,10 @@ #include "configure.h" +#ifdef FEATURE_INTERPRETER +#include "interpexec.h" +#endif // FEATURE_INTERPRETER + #include "virtualcallstub.h" #include "../debug/ee/debugger.h" #include "patchpointinfo.h" diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index b7e13c1f8574db..5b2e784aa27f64 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -831,6 +831,40 @@ CDAC_TYPE_FIELD(RealCodeHeader, T_UINT32, NumUnwindInfos, offsetof(RealCodeHeade CDAC_TYPE_FIELD(RealCodeHeader, TYPE(RuntimeFunction), UnwindInfos, offsetof(RealCodeHeader, unwindInfos)) CDAC_TYPE_END(RealCodeHeader) +#ifdef FEATURE_INTERPRETER +CDAC_TYPE_BEGIN(InterpreterRealCodeHeader) +CDAC_TYPE_INDETERMINATE(InterpreterRealCodeHeader) +CDAC_TYPE_FIELD(InterpreterRealCodeHeader, T_POINTER, MethodDesc, offsetof(InterpreterRealCodeHeader, phdrMDesc)) +CDAC_TYPE_FIELD(InterpreterRealCodeHeader, T_POINTER, DebugInfo, offsetof(InterpreterRealCodeHeader, phdrDebugInfo)) +CDAC_TYPE_FIELD(InterpreterRealCodeHeader, T_POINTER, GCInfo, offsetof(InterpreterRealCodeHeader, phdrJitGCInfo)) +CDAC_TYPE_FIELD(InterpreterRealCodeHeader, T_POINTER, JitEHInfo, offsetof(InterpreterRealCodeHeader, phdrJitEHInfo)) +CDAC_TYPE_END(InterpreterRealCodeHeader) + +#ifndef FEATURE_PORTABLE_ENTRYPOINTS +CDAC_TYPE_BEGIN(InterpreterPrecodeData) +CDAC_TYPE_INDETERMINATE(InterpreterPrecodeData) +CDAC_TYPE_FIELD(InterpreterPrecodeData, T_POINTER, ByteCodeAddr, offsetof(::InterpreterPrecodeData, ByteCodeAddr)) +CDAC_TYPE_FIELD(InterpreterPrecodeData, T_UINT8, Type, offsetof(::InterpreterPrecodeData, Type)) +CDAC_TYPE_END(InterpreterPrecodeData) +#endif // !FEATURE_PORTABLE_ENTRYPOINTS + +CDAC_TYPE_BEGIN(InterpByteCodeStart) +CDAC_TYPE_INDETERMINATE(InterpByteCodeStart) +CDAC_TYPE_FIELD(InterpByteCodeStart, T_POINTER, Method, offsetof(InterpByteCodeStart, Method)) +CDAC_TYPE_END(InterpByteCodeStart) + +CDAC_TYPE_BEGIN(InterpMethod) +CDAC_TYPE_INDETERMINATE(InterpMethod) +CDAC_TYPE_FIELD(InterpMethod, T_POINTER, MethodDesc, offsetof(InterpMethod, methodHnd)) +CDAC_TYPE_END(InterpMethod) + +CDAC_TYPE_BEGIN(InterpMethodContextFrame) +CDAC_TYPE_INDETERMINATE(InterpMethodContextFrame) +CDAC_TYPE_FIELD(InterpMethodContextFrame, T_POINTER, StartIp, offsetof(InterpMethodContextFrame, startIp)) +CDAC_TYPE_FIELD(InterpMethodContextFrame, T_POINTER, ParentPtr, offsetof(InterpMethodContextFrame, pParent)) +CDAC_TYPE_END(InterpMethodContextFrame) +#endif // FEATURE_INTERPRETER + CDAC_TYPE_BEGIN(EEExceptionClause) CDAC_TYPE_SIZE(sizeof(EE_ILEXCEPTION_CLAUSE)) CDAC_TYPE_FIELD(EEExceptionClause, T_UINT32, Flags, offsetof(EE_ILEXCEPTION_CLAUSE, Flags)) @@ -962,6 +996,13 @@ CDAC_TYPE_FIELD(FramedMethodFrame, T_POINTER, TransitionBlockPtr, cdac_data::MethodDescPtr) CDAC_TYPE_END(FramedMethodFrame) +#ifdef FEATURE_INTERPRETER +CDAC_TYPE_BEGIN(InterpreterFrame) +CDAC_TYPE_INDETERMINATE(InterpreterFrame) +CDAC_TYPE_FIELD(InterpreterFrame, T_POINTER, TopInterpMethodContextFrame, cdac_data::TopInterpMethodContextFrame) +CDAC_TYPE_END(InterpreterFrame) +#endif // FEATURE_INTERPRETER + CDAC_TYPE_BEGIN(TransitionBlock) CDAC_TYPE_SIZE(sizeof(TransitionBlock)) CDAC_TYPE_FIELD(TransitionBlock, T_POINTER, ReturnAddress, offsetof(TransitionBlock, m_ReturnAddress)) diff --git a/src/coreclr/vm/frames.h b/src/coreclr/vm/frames.h index f3fccab5615efa..07566b028f1293 100644 --- a/src/coreclr/vm/frames.h +++ b/src/coreclr/vm/frames.h @@ -2315,6 +2315,14 @@ class InterpreterFrame : public FramedMethodFrame TADDR m_SP; #endif // TARGET_WASM PTR_Object m_continuation; + + friend struct cdac_data; +}; + +template<> +struct cdac_data +{ + static constexpr size_t TopInterpMethodContextFrame = offsetof(InterpreterFrame, m_pTopInterpMethodContextFrame); }; #endif // FEATURE_INTERPRETER diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs index 0dddd31417e5ad..b89c166197f06f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs @@ -78,7 +78,8 @@ public enum JitType : uint { Unknown = 0, Jit = 1, - R2R = 2 + R2R = 2, + Interpreter = 3 } public interface IExecutionManager : IContract diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IPrecodeStubs.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IPrecodeStubs.cs index aced1d2316dd08..40901457d05528 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IPrecodeStubs.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IPrecodeStubs.cs @@ -9,6 +9,14 @@ public interface IPrecodeStubs : IContract { static string IContract.Name { get; } = nameof(PrecodeStubs); TargetPointer GetMethodDescFromStubAddress(TargetCodePointer entryPoint) => throw new NotImplementedException(); + + /// + /// If the given code pointer is an interpreter precode, returns the actual interpreter code + /// address (ByteCodeAddr). Otherwise returns the original address unchanged. + /// This method never throws; it returns the original address on any failure. + /// Mirrors GetInterpreterCodeFromInterpreterPrecodeIfPresent in native code (precode.cpp). + /// + TargetCodePointer GetInterpreterCodeFromInterpreterPrecodeIfPresent(TargetCodePointer entryPoint) => entryPoint; } public readonly struct PrecodeStubs : IPrecodeStubs diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs index 989db1625b33f9..3b56337e1acbb5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -90,6 +90,10 @@ public enum DataType StubPrecodeData, FixupPrecodeData, ThisPtrRetBufPrecodeData, + InterpreterPrecodeData, + InterpByteCodeStart, + InterpMethod, + InterpMethodContextFrame, Array, SyncBlock, SyncTableEntry, @@ -108,6 +112,7 @@ public enum DataType RangeSectionFragment, RangeSection, RealCodeHeader, + InterpreterRealCodeHeader, CodeHeapListNode, CodeHeap, LoaderCodeHeap, @@ -160,6 +165,8 @@ public enum DataType HijackFrame, TailCallFrame, StubDispatchFrame, + InterpreterFrame, + ComCallWrapper, SimpleComCallWrapper, ComMethodTable, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.InterpreterJitManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.InterpreterJitManager.cs new file mode 100644 index 00000000000000..d41ad0189d113f --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.InterpreterJitManager.cs @@ -0,0 +1,152 @@ +// 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; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +internal partial class ExecutionManagerCore : IExecutionManager +{ + private sealed class InterpreterJitManager : JitManager + { + private readonly INibbleMap _nibbleMap; + + public InterpreterJitManager(Target target, INibbleMap nibbleMap) : base(target) + { + _nibbleMap = nibbleMap; + } + + public override bool GetMethodInfo(RangeSection rangeSection, TargetCodePointer jittedCodeAddress, [NotNullWhen(true)] out CodeBlock? info) + { + info = null; + if (rangeSection.IsRangeList) + return false; + + if (rangeSection.Data is null) + throw new ArgumentException(nameof(rangeSection)); + + TargetPointer codeStart = FindMethodCode(rangeSection, jittedCodeAddress); + if (codeStart == TargetPointer.Null) + return false; + + Debug.Assert(codeStart.Value <= jittedCodeAddress.Value); + TargetNUInt relativeOffset = new TargetNUInt(jittedCodeAddress.Value - codeStart.Value); + + if (!GetInterpreterRealCodeHeader(codeStart, out Data.InterpreterRealCodeHeader? realCodeHeader)) + return false; + + info = new CodeBlock(codeStart.Value, realCodeHeader.MethodDesc, relativeOffset, rangeSection.Data.JitManager); + return true; + } + + public override void GetMethodRegionInfo( + RangeSection rangeSection, + TargetCodePointer jittedCodeAddress, + out uint hotSize, + out TargetPointer coldStart, + out uint coldSize) + { + coldStart = TargetPointer.Null; + coldSize = 0; + + IGCInfo gcInfo = Target.Contracts.GCInfo; + GetGCInfo(rangeSection, jittedCodeAddress, out TargetPointer pGcInfo, out uint gcVersion); + IGCInfoHandle gcInfoHandle = gcInfo.DecodeInterpreterGCInfo(pGcInfo, gcVersion); + hotSize = gcInfo.GetCodeLength(gcInfoHandle); + Debug.Assert(hotSize > 0); + } + + public override TargetPointer GetUnwindInfo(RangeSection rangeSection, TargetCodePointer jittedCodeAddress) + { + // Interpreter code has no native unwind info + return TargetPointer.Null; + } + + public override TargetPointer GetDebugInfo(RangeSection rangeSection, TargetCodePointer jittedCodeAddress, out bool hasFlagByte) + { + hasFlagByte = false; + if (rangeSection.IsRangeList || rangeSection.Data is null) + return TargetPointer.Null; + + TargetPointer codeStart = FindMethodCode(rangeSection, jittedCodeAddress); + if (codeStart == TargetPointer.Null) + return TargetPointer.Null; + + if (!GetInterpreterRealCodeHeader(codeStart, out Data.InterpreterRealCodeHeader? realCodeHeader)) + return TargetPointer.Null; + + return realCodeHeader.DebugInfo; + } + + public override void GetGCInfo(RangeSection rangeSection, TargetCodePointer jittedCodeAddress, out TargetPointer gcInfo, out uint gcVersion) + { + gcInfo = TargetPointer.Null; + gcVersion = 0; + + if (rangeSection.IsRangeList || rangeSection.Data is null) + return; + + TargetPointer codeStart = FindMethodCode(rangeSection, jittedCodeAddress); + if (codeStart == TargetPointer.Null) + return; + + if (!GetInterpreterRealCodeHeader(codeStart, out Data.InterpreterRealCodeHeader? realCodeHeader)) + return; + + gcVersion = Target.ReadGlobal(Constants.Globals.GCInfoVersion); + gcInfo = realCodeHeader.GCInfo; + } + + public override void GetExceptionClauses(RangeSection rangeSection, CodeBlockHandle codeInfoHandle, out TargetPointer startAddr, out TargetPointer endAddr) + { + startAddr = TargetPointer.Null; + endAddr = TargetPointer.Null; + + if (rangeSection.Data is null) + throw new ArgumentException(nameof(rangeSection)); + + TargetPointer codeStart = FindMethodCode(rangeSection, new TargetCodePointer(codeInfoHandle.Address)); + if (!GetInterpreterRealCodeHeader(codeStart, out Data.InterpreterRealCodeHeader? realCodeHeader)) + return; + + if (realCodeHeader.JitEHInfo is null) + return; + + TargetNUInt numEHInfos = Target.ReadNUInt(realCodeHeader.JitEHInfo.Address - (ulong)Target.PointerSize); + startAddr = realCodeHeader.JitEHInfo.Clauses; + endAddr = startAddr + numEHInfos.Value * Target.GetTypeInfo(DataType.EEExceptionClause).Size!.Value; + } + + private TargetPointer FindMethodCode(RangeSection rangeSection, TargetCodePointer jittedCodeAddress) + { + Debug.Assert(rangeSection.Data is not null); + + if (!rangeSection.IsCodeHeap) + throw new InvalidOperationException("RangeSection is not a code heap"); + + TargetPointer heapListAddress = rangeSection.Data.HeapList; + Data.CodeHeapListNode heapListNode = Target.ProcessedData.GetOrAdd(heapListAddress); + return _nibbleMap.FindMethodCode(heapListNode, jittedCodeAddress); + } + + private bool GetInterpreterRealCodeHeader(TargetPointer codeStart, [NotNullWhen(true)] out Data.InterpreterRealCodeHeader? realCodeHeader) + { + realCodeHeader = null; + if (codeStart == TargetPointer.Null) + return false; + + // Same layout as EEJitManager: CodeHeader pointer lives at codeStart - pointerSize + int codeHeaderOffset = Target.PointerSize; + TargetPointer codeHeaderIndirect = new TargetPointer(codeStart - (ulong)codeHeaderOffset); + if (RangeSection.IsStubCodeBlock(Target, codeHeaderIndirect)) + return false; + + TargetPointer codeHeaderAddress = Target.ReadPointer(codeHeaderIndirect); + realCodeHeader = Target.ProcessedData.GetOrAdd(codeHeaderAddress); + return true; + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs index 43e89fbe2237e7..88feb92fb9cba9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs @@ -21,6 +21,7 @@ internal sealed partial class ExecutionManagerCore : IExecutionManager private readonly ExecutionManagerHelpers.RangeSectionMap _rangeSectionMapLookup; private readonly EEJitManager _eeJitManager; private readonly ReadyToRunJitManager _r2rJitManager; + private readonly InterpreterJitManager _interpreterJitManager; public ExecutionManagerCore(Target target, Data.RangeSectionMap topRangeSectionMap) { @@ -30,6 +31,7 @@ public ExecutionManagerCore(Target target, Data.RangeSectionMap topRangeSectionM INibbleMap nibbleMap = T.Create(_target); _eeJitManager = new EEJitManager(_target, nibbleMap); _r2rJitManager = new ReadyToRunJitManager(_target); + _interpreterJitManager = new InterpreterJitManager(_target, nibbleMap); } public void Flush() @@ -60,6 +62,7 @@ private enum RangeSectionFlags : int { CodeHeap = 0x02, RangeList = 0x04, + Interpreter = 0x08, } // Mirrors the native CodeHeap::CodeHeapType enum in codeman.h. @@ -117,6 +120,9 @@ public RangeSection(Data.RangeSection rangeSection) private bool HasFlags(RangeSectionFlags mask) => (Data!.Flags & (int)mask) != 0; internal bool IsRangeList => HasFlags(RangeSectionFlags.RangeList); internal bool IsCodeHeap => HasFlags(RangeSectionFlags.CodeHeap); + internal bool IsInterpreter => HasFlags(RangeSectionFlags.Interpreter); + + internal bool HasR2RModule => Data!.R2RModule != TargetPointer.Null; internal static bool IsStubCodeBlock(Target target, TargetPointer codeHeaderIndirect) { @@ -152,16 +158,20 @@ internal static RangeSection Find(Target target, Data.RangeSectionMap topRangeSe } } - private JitManager GetJitManager(Data.RangeSection rangeSectionData) + private JitManager GetJitManager(RangeSection rangeSection) { - if (rangeSectionData.R2RModule == TargetPointer.Null) + if (rangeSection.IsInterpreter) { - return _eeJitManager; + return _interpreterJitManager; } - else + else if (rangeSection.HasR2RModule) { return _r2rJitManager; } + else + { + return _eeJitManager; + } } private CodeBlock? GetCodeBlock(TargetCodePointer jittedCodeAddress) @@ -171,7 +181,7 @@ private JitManager GetJitManager(Data.RangeSection rangeSectionData) { return null; } - JitManager jitManager = GetJitManager(range.Data); + JitManager jitManager = GetJitManager(range); if (jitManager.GetMethodInfo(range, jittedCodeAddress, out CodeBlock? info)) { return info; @@ -219,7 +229,7 @@ TargetCodePointer IExecutionManager.GetFuncletStartAddress(CodeBlockHandle codeI if (range.Data == null) throw new InvalidOperationException("Unable to get runtime function address"); - JitManager jitManager = GetJitManager(range.Data); + JitManager jitManager = GetJitManager(range); TargetPointer runtimeFunctionPtr = jitManager.GetUnwindInfo(range, codeInfoHandle.Address.Value); if (runtimeFunctionPtr == TargetPointer.Null) @@ -243,7 +253,7 @@ void IExecutionManager.GetMethodRegionInfo(CodeBlockHandle codeInfoHandle, out u if (range.Data == null) throw new InvalidOperationException("Unable to get runtime function address"); - JitManager jitManager = GetJitManager(range.Data); + JitManager jitManager = GetJitManager(range); jitManager.GetMethodRegionInfo(range, codeInfoHandle.Address.Value, out hotSize, out coldStart, out coldSize); } @@ -254,7 +264,7 @@ JitType IExecutionManager.GetJITType(CodeBlockHandle codeInfoHandle) if (range.Data == null) return JitType.Unknown; - JitManager jitManager = GetJitManager(range.Data); + JitManager jitManager = GetJitManager(range); if (jitManager == _eeJitManager) { @@ -264,6 +274,10 @@ JitType IExecutionManager.GetJITType(CodeBlockHandle codeInfoHandle) { return JitType.R2R; } + else if (jitManager == _interpreterJitManager) + { + return JitType.Interpreter; + } else { return JitType.Unknown; @@ -299,7 +313,7 @@ TargetPointer IExecutionManager.NonVirtualEntry2MethodDesc(TargetCodePointer ent } else { - JitManager jitManager = GetJitManager(range.Data); + JitManager jitManager = GetJitManager(range); if (jitManager.GetMethodInfo(range, entrypoint, out CodeBlock? info) && info != null) { return info.MethodDescAddress; @@ -343,7 +357,7 @@ TargetPointer IExecutionManager.GetUnwindInfo(CodeBlockHandle codeInfoHandle) if (range.Data == null) return TargetPointer.Null; - JitManager jitManager = GetJitManager(range.Data); + JitManager jitManager = GetJitManager(range); return jitManager.GetUnwindInfo(range, codeInfoHandle.Address.Value); } @@ -364,7 +378,7 @@ TargetPointer IExecutionManager.GetDebugInfo(CodeBlockHandle codeInfoHandle, out if (range.Data == null) return TargetPointer.Null; - JitManager jitManager = GetJitManager(range.Data); + JitManager jitManager = GetJitManager(range); return jitManager.GetDebugInfo(range, codeInfoHandle.Address.Value, out hasFlagByte); } @@ -377,7 +391,7 @@ void IExecutionManager.GetGCInfo(CodeBlockHandle codeInfoHandle, out TargetPoint if (range.Data == null) return; - JitManager jitManager = GetJitManager(range.Data); + JitManager jitManager = GetJitManager(range); jitManager.GetGCInfo(range, codeInfoHandle.Address.Value, out gcInfo, out gcVersion); } @@ -480,7 +494,7 @@ List IExecutionManager.GetExceptionClauses(CodeBlockHandle if (range.Data == null) return new List(); - JitManager jitManager = GetJitManager(range.Data); + JitManager jitManager = GetJitManager(range); jitManager.GetExceptionClauses(range, codeInfoHandle, out TargetPointer startAddr, out TargetPointer endAddr); bool isR2R = jitManager is ReadyToRunJitManager; DataType clauseType = isR2R ? DataType.R2RExceptionClause : DataType.EEExceptionClause; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_1.cs index 35dac689d5ed99..5a74943702c2e7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_1.cs @@ -28,6 +28,11 @@ public static TargetPointer ThisPtrRetBufPrecode_GetMethodDesc(TargetPointer ins throw new NotImplementedException(); // TODO(cdac) } + public static TargetPointer InterpreterPrecode_GetMethodDesc(TargetPointer instrPointer, Target target, Data.PrecodeMachineDescriptor precodeMachineDescriptor) + { + throw new NotImplementedException(); + } + public static byte StubPrecodeData_GetType(Data.StubPrecodeData_1 stubPrecodeData) { return stubPrecodeData.Type; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_2.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_2.cs index aa888f923c5a76..5353abeea974f1 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_2.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_2.cs @@ -29,6 +29,11 @@ public static TargetPointer ThisPtrRetBufPrecode_GetMethodDesc(TargetPointer ins return thisPtrRetBufPrecodeData.MethodDesc; } + public static TargetPointer InterpreterPrecode_GetMethodDesc(TargetPointer instrPointer, Target target, Data.PrecodeMachineDescriptor precodeMachineDescriptor) + { + throw new NotImplementedException(); + } + public static byte StubPrecodeData_GetType(Data.StubPrecodeData_2 stubPrecodeData) { return stubPrecodeData.Type; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_3.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_3.cs index 2507df5a116205..ac20f74a9ba66a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_3.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_3.cs @@ -26,6 +26,16 @@ public static TargetPointer ThisPtrRetBufPrecode_GetMethodDesc(TargetPointer ins return PrecodeStubs_2_Impl.ThisPtrRetBufPrecode_GetMethodDesc(instrPointer, target, precodeMachineDescriptor); } + public static TargetPointer InterpreterPrecode_GetMethodDesc(TargetPointer instrPointer, Target target, Data.PrecodeMachineDescriptor precodeMachineDescriptor) + { + TargetPointer dataAddr = instrPointer + precodeMachineDescriptor.StubCodePageSize; + Data.InterpreterPrecodeData precodeData = target.ProcessedData.GetOrAdd(dataAddr); + Data.InterpByteCodeStart byteCodeStart = target.ProcessedData.GetOrAdd(precodeData.ByteCodeAddr); + Data.InterpMethod interpMethod = target.ProcessedData.GetOrAdd(byteCodeStart.Method); + + return interpMethod.MethodDesc; + } + public static byte StubPrecodeData_GetType(Data.StubPrecodeData_2 stubPrecodeData) { // Version 3 of this contract behaves just like version 2 diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_Common.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_Common.cs index c8b76481107e5c..b20f795ab4c6ea 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_Common.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_Common.cs @@ -24,6 +24,7 @@ internal interface IPrecodeStubsContractCommonApi public static abstract TargetPointer StubPrecode_GetMethodDesc(TargetPointer instrPointer, Target target, Data.PrecodeMachineDescriptor precodeMachineDescriptor); public static abstract TargetPointer ThisPtrRetBufPrecode_GetMethodDesc(TargetPointer instrPointer, Target target, Data.PrecodeMachineDescriptor precodeMachineDescriptor); public static abstract TargetPointer FixupPrecode_GetMethodDesc(TargetPointer instrPointer, Target target, Data.PrecodeMachineDescriptor precodeMachineDescriptor); + public static abstract TargetPointer InterpreterPrecode_GetMethodDesc(TargetPointer instrPointer, Target target, Data.PrecodeMachineDescriptor precodeMachineDescriptor); public static abstract byte StubPrecodeData_GetType(TStubPrecodeData stubPrecodeData); public static abstract KnownPrecodeType? TryGetKnownPrecodeType(TargetPointer instrPointer, Target target, Data.PrecodeMachineDescriptor precodeMachineDescriptor); } @@ -58,6 +59,16 @@ internal override TargetPointer GetMethodDesc(Target target, Data.PrecodeMachine } } + internal sealed class InterpreterPrecode : ValidPrecode + { + internal InterpreterPrecode(TargetPointer instrPointer) : base(instrPointer, KnownPrecodeType.Interpreter) { } + + internal override TargetPointer GetMethodDesc(Target target, Data.PrecodeMachineDescriptor precodeMachineDescriptor) + { + return TPrecodeStubsImplementation.InterpreterPrecode_GetMethodDesc(InstrPointer, target, precodeMachineDescriptor); + } + } + public sealed class PInvokeImportPrecode : StubPrecode { internal PInvokeImportPrecode(TargetPointer instrPointer) : base(instrPointer, KnownPrecodeType.PInvokeImport) { } @@ -125,6 +136,8 @@ internal ValidPrecode GetPrecodeFromEntryPoint(TargetCodePointer entryPoint) return new PInvokeImportPrecode(instrPointer); case KnownPrecodeType.ThisPtrRetBuf: return new ThisPtrRetBufPrecode(instrPointer); + case KnownPrecodeType.Interpreter: + return new InterpreterPrecode(instrPointer); default: break; } @@ -146,4 +159,28 @@ TargetPointer IPrecodeStubs.GetMethodDescFromStubAddress(TargetCodePointer entry return precode.GetMethodDesc(_target, MachineDescriptor); } + + TargetCodePointer IPrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(TargetCodePointer entryPoint) + { + try + { + TargetPointer instrPointer = CodePointerReadableInstrPointer(entryPoint); + if (!IsAlignedInstrPointer(instrPointer)) + return entryPoint; + + if (TryGetKnownPrecodeType(instrPointer) is not KnownPrecodeType.Interpreter) + return entryPoint; + + TargetPointer dataAddr = instrPointer + MachineDescriptor.StubCodePageSize; + Data.InterpreterPrecodeData precodeData = _target.ProcessedData.GetOrAdd(dataAddr); + if (precodeData.ByteCodeAddr == TargetPointer.Null) + return entryPoint; + + return new TargetCodePointer(precodeData.ByteCodeAddr); + } + catch (VirtualReadException) + { + return entryPoint; + } + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs index 4edd821c203dc9..8ad0910c701bba 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; @@ -22,6 +23,7 @@ internal enum FrameType CallCountingHelperFrame, ExternalMethodFrame, DynamicHelperFrame, + InterpreterFrame, FuncEvalFrame, @@ -41,7 +43,6 @@ internal enum FrameType DebuggerExitFrame, DebuggerU2MCatchHandlerFrame, ExceptionFilterFrame, - InterpreterFrame, } private readonly Target target; @@ -95,6 +96,7 @@ public void UpdateContextFromFrame(IPlatformAgnosticContext context) case FrameType.CallCountingHelperFrame: case FrameType.ExternalMethodFrame: case FrameType.DynamicHelperFrame: + case FrameType.InterpreterFrame: // FrameMethodFrame is the base type for all transition Frames Data.FramedMethodFrame framedMethodFrame = target.ProcessedData.GetOrAdd(CurrentFrame.Address); GetFrameHandler(context).HandleTransitionFrame(framedMethodFrame); @@ -201,9 +203,13 @@ public static TargetPointer GetMethodDescPtr(Target target, TargetPointer frameP case FrameType.ExternalMethodFrame: case FrameType.PrestubMethodFrame: case FrameType.CallCountingHelperFrame: - case FrameType.InterpreterFrame: Data.FramedMethodFrame framedMethodFrame = target.ProcessedData.GetOrAdd(frame.Address); return framedMethodFrame.MethodDescPtr; + case FrameType.InterpreterFrame: + { + Data.InterpreterFrame interpreterFrame = target.ProcessedData.GetOrAdd(frame.Address); + return ResolveMethodDescFromInterpFrame(target, interpreterFrame.TopInterpMethodContextFrame); + } case FrameType.PInvokeCalliFrame: return TargetPointer.Null; case FrameType.StubDispatchFrame: @@ -233,6 +239,44 @@ public static TargetPointer GetMethodDescPtr(Target target, TargetPointer frameP } } + /// + /// Resolves the MethodDesc from a specific InterpMethodContextFrame by following: + /// InterpMethodContextFrame.StartIp -> InterpByteCodeStart.Method -> InterpMethod.methodHnd + /// + internal static TargetPointer ResolveMethodDescFromInterpFrame(Target target, TargetPointer interpMethodFramePtr) + { + if (interpMethodFramePtr == TargetPointer.Null) + return TargetPointer.Null; + + Data.InterpMethodContextFrame contextFrame = target.ProcessedData.GetOrAdd(interpMethodFramePtr); + if (contextFrame.StartIp == TargetPointer.Null) + return TargetPointer.Null; + + Data.InterpByteCodeStart byteCodeStart = target.ProcessedData.GetOrAdd(contextFrame.StartIp); + if (byteCodeStart.Method == TargetPointer.Null) + return TargetPointer.Null; + + Data.InterpMethod interpMethod = target.ProcessedData.GetOrAdd(byteCodeStart.Method); + + return interpMethod.MethodDesc; + } + + /// + /// Walks the InterpMethodContextFrame.ParentPtr chain for an InterpreterFrame, + /// yielding one context frame pointer per interpreted method in the call chain. + /// + internal static IEnumerable WalkInterpreterFrameChain(Target target, TargetPointer frameAddress) + { + Data.InterpreterFrame interpFrame = target.ProcessedData.GetOrAdd(frameAddress); + TargetPointer interpMethodFramePtr = interpFrame.TopInterpMethodContextFrame; + while (interpMethodFramePtr != TargetPointer.Null) + { + yield return interpMethodFramePtr; + Data.InterpMethodContextFrame contextFrame = target.ProcessedData.GetOrAdd(interpMethodFramePtr); + interpMethodFramePtr = contextFrame.ParentPtr; + } + } + public static TargetPointer GetReturnAddress(Target target, TargetPointer framePtr) { Data.Frame frame = target.ProcessedData.GetOrAdd(framePtr); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs index e7a3222806464b..ab8bdda895d217 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs @@ -46,7 +46,8 @@ private record StackDataFrameHandle( TargetPointer FrameAddress, ThreadData ThreadData, bool IsResumableFrame = false, - bool IsActiveFrame = false) : IStackDataFrameHandle + bool IsActiveFrame = false, + TargetPointer InterpContextFramePtr = default) : IStackDataFrameHandle { } private class StackWalkData(IPlatformAgnosticContext context, StackWalkState state, FrameIterator frameIter, ThreadData threadData) @@ -56,7 +57,6 @@ private class StackWalkData(IPlatformAgnosticContext context, StackWalkState sta public FrameIterator FrameIter { get; set; } = frameIter; public ThreadData ThreadData { get; set; } = threadData; - // Track isFirst exactly like native CrawlFrame::isFirst in StackFrameIterator. // Starts true, set false after processing a managed (frameless) frame, // set back to true when encountering a ResumableFrame (FRAME_ATTR_RESUMABLE). @@ -97,11 +97,11 @@ public void AdvanceIsFirst() } } - public StackDataFrameHandle ToDataFrame() + public StackDataFrameHandle ToDataFrame(TargetPointer interpContextFramePtr = default) { bool isResumable = IsCurrentFrameResumable(); bool isActiveFrame = IsFirst && State == StackWalkState.SW_FRAMELESS; - return new(Context.Clone(), State, FrameIter.CurrentFrameAddress, ThreadData, isResumable, isActiveFrame); + return new(Context.Clone(), State, FrameIter.CurrentFrameAddress, ThreadData, isResumable, isActiveFrame, interpContextFramePtr); } } @@ -164,16 +164,40 @@ private IEnumerable CreateStackWalkCore(ThreadData thread stackWalkData.State = StackWalkState.SW_SKIPPED_FRAME; } - yield return stackWalkData.ToDataFrame(); + foreach (StackDataFrameHandle frame in YieldFrames(stackWalkData)) + yield return frame; stackWalkData.AdvanceIsFirst(); while (Next(stackWalkData)) { - yield return stackWalkData.ToDataFrame(); + foreach (StackDataFrameHandle frame in YieldFrames(stackWalkData)) + yield return frame; stackWalkData.AdvanceIsFirst(); } } + /// + /// Yields one or more data frames for the current stack walk position. + /// For InterpreterFrame, walks the InterpMethodContextFrame.pParent chain + /// to yield a separate frame for each interpreted method in the call chain. + /// + private IEnumerable YieldFrames(StackWalkData stackWalkData) + { + if (stackWalkData.State is StackWalkState.SW_FRAME or StackWalkState.SW_SKIPPED_FRAME) + { + TargetPointer frameAddress = stackWalkData.FrameIter.CurrentFrameAddress; + if (frameAddress != TargetPointer.Null + && stackWalkData.FrameIter.GetCurrentFrameType() == FrameIterator.FrameType.InterpreterFrame) + { + foreach (TargetPointer contextFramePtr in FrameIterator.WalkInterpreterFrameChain(_target, frameAddress)) + yield return stackWalkData.ToDataFrame(contextFramePtr); + yield break; + } + } + + yield return stackWalkData.ToDataFrame(); + } + IReadOnlyList IStackWalk.WalkStackReferences(ThreadData threadData) { IEnumerable stackFrames = CreateStackWalkCore(threadData, skipInitialFrames: true); @@ -696,6 +720,12 @@ TargetPointer IStackWalk.GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHa { StackDataFrameHandle handle = AssertCorrectHandle(stackDataFrameHandle); + // If this is a synthetic interpreter chain frame, resolve directly from the specific context frame + if (handle.InterpContextFramePtr != TargetPointer.Null) + { + return FrameIterator.ResolveMethodDescFromInterpFrame(_target, handle.InterpContextFramePtr); + } + // if we are at a capital F Frame, we can get the method desc from the frame TargetPointer framePtr = ((IStackWalk)this).GetFrameAddress(handle); if (framePtr != TargetPointer.Null) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpByteCodeStart.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpByteCodeStart.cs new file mode 100644 index 00000000000000..b9b78726acb6c8 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpByteCodeStart.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal sealed class InterpByteCodeStart : IData +{ + static InterpByteCodeStart IData.Create(Target target, TargetPointer address) + => new InterpByteCodeStart(target, address); + + public InterpByteCodeStart(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.InterpByteCodeStart); + Method = target.ReadPointerField(address, type, nameof(Method)); + } + + public TargetPointer Method { get; init; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpMethod.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpMethod.cs new file mode 100644 index 00000000000000..820ef947cc2a0a --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpMethod.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal sealed class InterpMethod : IData +{ + static InterpMethod IData.Create(Target target, TargetPointer address) + => new InterpMethod(target, address); + + public InterpMethod(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.InterpMethod); + MethodDesc = target.ReadPointerField(address, type, nameof(MethodDesc)); + } + + public TargetPointer MethodDesc { get; init; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpMethodContextFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpMethodContextFrame.cs new file mode 100644 index 00000000000000..5252c284a14628 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpMethodContextFrame.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal sealed class InterpMethodContextFrame : IData +{ + static InterpMethodContextFrame IData.Create(Target target, TargetPointer address) + => new InterpMethodContextFrame(target, address); + + public InterpMethodContextFrame(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.InterpMethodContextFrame); + StartIp = target.ReadPointerField(address, type, nameof(StartIp)); + ParentPtr = target.ReadPointerField(address, type, nameof(ParentPtr)); + } + + public TargetPointer StartIp { get; init; } + public TargetPointer ParentPtr { get; init; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpreterFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpreterFrame.cs new file mode 100644 index 00000000000000..b00a6b1ae1351d --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpreterFrame.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal sealed class InterpreterFrame : IData +{ + static InterpreterFrame IData.Create(Target target, TargetPointer address) + => new InterpreterFrame(target, address); + + public InterpreterFrame(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.InterpreterFrame); + TopInterpMethodContextFrame = target.ReadPointerField(address, type, nameof(TopInterpMethodContextFrame)); + } + + public TargetPointer TopInterpMethodContextFrame { get; init; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpreterPrecodeData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpreterPrecodeData.cs new file mode 100644 index 00000000000000..30ee446857ff9c --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpreterPrecodeData.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal sealed class InterpreterPrecodeData : IData +{ + static InterpreterPrecodeData IData.Create(Target target, TargetPointer address) + => new InterpreterPrecodeData(target, address); + + public InterpreterPrecodeData(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.InterpreterPrecodeData); + ByteCodeAddr = target.ReadPointerField(address, type, nameof(ByteCodeAddr)); + Type = target.ReadField(address, type, nameof(Type)); + } + + public TargetPointer ByteCodeAddr { get; init; } + public byte Type { get; init; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpreterRealCodeHeader.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpreterRealCodeHeader.cs new file mode 100644 index 00000000000000..6a2270ac69f984 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpreterRealCodeHeader.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal sealed class InterpreterRealCodeHeader : IData +{ + static InterpreterRealCodeHeader IData.Create(Target target, TargetPointer address) + => new InterpreterRealCodeHeader(target, address); + + public InterpreterRealCodeHeader(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.InterpreterRealCodeHeader); + MethodDesc = target.ReadPointerField(address, type, nameof(MethodDesc)); + DebugInfo = target.ReadPointerField(address, type, nameof(DebugInfo)); + GCInfo = target.ReadPointerField(address, type, nameof(GCInfo)); + TargetPointer jitEHInfoAddr = target.ReadPointerField(address, type, nameof(JitEHInfo)); + JitEHInfo = jitEHInfoAddr != TargetPointer.Null ? target.ProcessedData.GetOrAdd(jitEHInfoAddr) : null; + } + + public TargetPointer MethodDesc { get; init; } + public TargetPointer DebugInfo { get; init; } + public TargetPointer GCInfo { get; init; } + public EEILException? JitEHInfo { get; init; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodValidation.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodValidation.cs index 74f548675b832b..112dbd23202175 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodValidation.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodValidation.cs @@ -208,18 +208,26 @@ internal bool ValidateMethodDescPointer(TargetPointer methodDescPointer, [NotNul TargetCodePointer jitCodeAddr = GetCodePointer(umd); Contracts.IExecutionManager executionManager = _target.Contracts.ExecutionManager; CodeBlockHandle? codeInfo = executionManager.GetCodeBlockHandle(jitCodeAddr); - if (!codeInfo.HasValue) + if (codeInfo.HasValue) { - return false; - } - TargetPointer methodDesc = executionManager.GetMethodDesc(codeInfo.Value); - if (methodDesc == TargetPointer.Null) - { - return false; + TargetPointer methodDesc = executionManager.GetMethodDesc(codeInfo.Value); + if (methodDesc != methodDescPointer) + { + return false; + } } - if (methodDesc != methodDescPointer) + else { - return false; + // The NativeCodeSlot may point to a precode or portable entry point + // (e.g., interpreter methods with FEATURE_PORTABLE_ENTRYPOINTS). + // Try resolving via precode stubs as a fallback. + // See usage of GetCodeForInterpreterOrJitted in DacValidateMD for more details. + Contracts.IPrecodeStubs precode = _target.Contracts.PrecodeStubs; + TargetPointer methodDesc = precode.GetMethodDescFromStubAddress(jitCodeAddr); + if (methodDesc != methodDescPointer) + { + return false; + } } } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodInstance.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodInstance.cs index 3a8a25284afb21..5c4e1f025c023c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodInstance.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodInstance.cs @@ -288,6 +288,9 @@ int IXCLRDataMethodInstance.GetILAddressMap(uint mapLen, uint* mapNeeded, [In, O try { TargetCodePointer pCode = _target.Contracts.RuntimeTypeSystem.GetNativeCode(_methodDesc); + // Resolve interpreter precode to actual interpreter code address if present. + // Mirrors GetInterpreterCodeFromInterpreterPrecodeIfPresent in daccess.cpp:5631-5694. + pCode = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(pCode); TargetPointer codeStart = pCode.ToAddress(_target); // No debug info exists at all (e.g. ILStubs). diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index d67adff3ab3489..b82b2271efb3f4 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -817,6 +817,7 @@ int ISOSDacInterface.GetCodeHeaderData(ClrDataAddress ip, DacpCodeHeaderData* da { Contracts.JitType.Jit => JitTypes.TYPE_JIT, Contracts.JitType.R2R => JitTypes.TYPE_PJIT, + Contracts.JitType.Interpreter => JitTypes.TYPE_INTERPRETER, _ => JitTypes.TYPE_UNKNOWN, }; @@ -2306,7 +2307,9 @@ int ISOSDacInterface.GetMethodDescData(ClrDataAddress addr, ClrDataAddress ip, D if (nativeCodeAddr != TargetCodePointer.Null) { data->bHasNativeCode = 1; - data->NativeCodeAddr = nativeCodeAddr.ToAddress(_target).ToClrDataAddress(_target); + // Resolve interpreter precode to actual interpreter code address if present. + TargetCodePointer resolvedAddr = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCodeAddr); + data->NativeCodeAddr = resolvedAddr.ToAddress(_target).ToClrDataAddress(_target); } else { @@ -2518,7 +2521,9 @@ private void CopyNativeCodeVersionToReJitData( ILCodeVersionHandle ilCodeVersion = cv.GetILCodeVersion(nativeCodeVersion); pReJitData->rejitID = rejit.GetRejitId(ilCodeVersion).Value; - pReJitData->NativeCodeAddr = cv.GetNativeCode(nativeCodeVersion).Value; + // Resolve interpreter precode to actual interpreter code address if present. + TargetCodePointer nativeCode = cv.GetNativeCode(nativeCodeVersion); + pReJitData->NativeCodeAddr = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode).Value; if (nativeCodeVersion.CodeVersionNodeAddress != activeNativeCodeVersion.CodeVersionNodeAddress || nativeCodeVersion.MethodDescAddress != activeNativeCodeVersion.MethodDescAddress) @@ -5211,19 +5216,20 @@ int ISOSDacInterface5.GetTieredVersions( { r2rImageEnd = r2rImageBase + r2rSize; } - ClrDataAddress r2rImageBaseAddr = r2rImageBase.ToClrDataAddress(_target); - ClrDataAddress r2rImageEndAddr = r2rImageEnd.ToClrDataAddress(_target); bool isEligibleForTieredCompilation = runtimeTypeSystemContract.IsEligibleForTieredCompilation(methodDescHandle); int count = 0; foreach (NativeCodeVersionHandle nativeCodeVersionHandle in codeVersions.GetNativeCodeVersions(methodDescPtr, ilCodeVersionHandle)) { - ClrDataAddress nativeCodeAddr = codeVersions.GetNativeCode(nativeCodeVersionHandle).Value; - nativeCodeAddrs[count].nativeCodeAddr = nativeCodeAddr; + // Resolve interpreter precode to actual interpreter code address if present. + TargetCodePointer nativeCode = codeVersions.GetNativeCode(nativeCodeVersionHandle); + nativeCode = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode); + TargetPointer nativeCodeAddr = nativeCode.ToAddress(_target); + nativeCodeAddrs[count].nativeCodeAddr = nativeCodeAddr.ToClrDataAddress(_target); nativeCodeAddrs[count].nativeCodeVersionNodePtr = nativeCodeVersionHandle.CodeVersionNodeAddress.ToClrDataAddress(_target); - if (r2rImageBaseAddr <= nativeCodeAddr && nativeCodeAddr < r2rImageEndAddr) + if (r2rImageBase <= nativeCodeAddr && nativeCodeAddr < r2rImageEnd) { nativeCodeAddrs[count].optimizationTier = DacpTieredVersionData.OptimizationTier.ReadyToRun; } diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.targets b/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.targets index 60cbea1938b738..aeb120653a986a 100644 --- a/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.targets +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.targets @@ -8,6 +8,7 @@ <_DumpTypeOutput Include="$(MSBuildProjectName)" DumpTypes="$(DumpTypes)" R2RModes="$(R2RModes)" + EnvironmentVariables="$(EnvironmentVariables)" WindowsOnly="$(WindowsOnly)" ProjectPath="$(MSBuildProjectFullPath)" /> diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/InterpreterStack.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/InterpreterStack.csproj new file mode 100644 index 00000000000000..e7dc497211fa3d --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/InterpreterStack.csproj @@ -0,0 +1,19 @@ + + + Full + Jit + + DOTNET_Interpreter=MethodA + false + + + + + + + + + diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/Program.cs new file mode 100644 index 00000000000000..6bf3d049314241 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/Program.cs @@ -0,0 +1,51 @@ +// 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.Runtime.CompilerServices; +using InterpreterStack.Trampoline; + +/// +/// Debuggee for cDAC dump tests — exercises interpreter stack walking with +/// interleaved JIT and interpreter frames. +/// +/// Under DOTNET_Interpreter=MethodA, methods from this assembly that match +/// the filter are interpreted. The call chain routes through JitTrampoline.Bounce +/// (in a separate assembly, always JIT'd) to create two distinct InterpreterFrame +/// regions on the stack: +/// +/// Main (JIT) -> MethodA (interp) -> MethodB (interp) -> [InterpreterFrame 1] +/// -> JitTrampoline.Bounce (JIT) -> MethodC (interp) -> MethodD (interp) -> [InterpreterFrame 2] +/// -> FailFast (JIT) +/// +internal static class Program +{ + private static void Main() + { + MethodA(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void MethodA() + { + MethodB(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void MethodB() + { + JitTrampoline.Bounce(MethodC); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void MethodC() + { + MethodD(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void MethodD() + { + Environment.FailFast("cDAC dump test: InterpreterStack debuggee intentional crash"); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/Trampoline/Trampoline.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/Trampoline/Trampoline.cs new file mode 100644 index 00000000000000..9f0acacd14b181 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/Trampoline/Trampoline.cs @@ -0,0 +1,21 @@ +// 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.Runtime.CompilerServices; + +namespace InterpreterStack.Trampoline; + +/// +/// Provides a JIT'd method in a separate assembly from the debuggee. +/// Since this assembly is NOT in g_interpModule, its methods are always +/// JIT-compiled, creating a gap between two InterpreterFrame regions on the stack. +/// +public static class JitTrampoline +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static void Bounce(Action callback) + { + callback(); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/Trampoline/Trampoline.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/Trampoline/Trampoline.csproj new file mode 100644 index 00000000000000..80f0ccbaf3b827 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/Trampoline/Trampoline.csproj @@ -0,0 +1,5 @@ + + + Library + + diff --git a/src/native/managed/cdac/tests/DumpTests/DumpTestStackWalker.cs b/src/native/managed/cdac/tests/DumpTests/DumpTestStackWalker.cs index bff72c2b2f3b08..ca697f83ef1208 100644 --- a/src/native/managed/cdac/tests/DumpTests/DumpTestStackWalker.cs +++ b/src/native/managed/cdac/tests/DumpTests/DumpTestStackWalker.cs @@ -10,10 +10,19 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; /// -/// A single resolved stack frame, carrying both the method name and the raw -/// MethodDesc pointer so that callers can perform ad-hoc assertions. +/// A single resolved stack frame, carrying the method name, the raw +/// MethodDesc pointer, the runtime Frame name (if this is a capital-F Frame), +/// and the underlying +/// so that callers can perform ad-hoc assertions (e.g. frame type checks). /// -internal readonly record struct ResolvedFrame(string? Name, TargetPointer MethodDescPtr); +/// The resolved method name, or null if unavailable. +/// The raw MethodDesc pointer for this frame. +/// +/// The runtime Frame name (e.g. "InterpreterFrame", "InlinedCallFrame") when this +/// frame has a non-null frame address, or null for native/managed code frames. +/// +/// The underlying stack data frame handle for raw access. +internal readonly record struct ResolvedFrame(string? Name, TargetPointer MethodDescPtr, string? FrameName, IStackDataFrameHandle FrameHandle); /// /// Encapsulates a resolved stack walk for a thread, providing a builder-pattern @@ -94,7 +103,16 @@ public static DumpTestStackWalker Walk(ContractDescriptorTarget target, ThreadDa { TargetPointer methodDescPtr = stackWalk.GetMethodDescPtr(frame); string? name = DumpTestHelpers.GetMethodName(target, methodDescPtr); - frames.Add(new ResolvedFrame(name, methodDescPtr)); + + string? frameName = null; + TargetPointer frameAddress = stackWalk.GetFrameAddress(frame); + if (frameAddress != TargetPointer.Null) + { + TargetPointer frameIdentifier = target.ReadPointer(frameAddress); + frameName = stackWalk.GetFrameName(frameIdentifier); + } + + frames.Add(new ResolvedFrame(name, methodDescPtr, frameName, frame)); } return new DumpTestStackWalker(target, frames); @@ -139,7 +157,8 @@ public DumpTestStackWalker Print(Action? writer = null) string md = f.MethodDescPtr != TargetPointer.Null ? $"0x{(ulong)f.MethodDescPtr:X}" : "null"; - writer($" [{i}] {name} (MethodDesc: {md})"); + string frameInfo = f.FrameName is not null ? $" [{f.FrameName}]" : ""; + writer($" [{i}] {name}{frameInfo} (MethodDesc: {md})"); } return this; @@ -190,6 +209,48 @@ public DumpTestStackWalker ExpectAdjacentFrameWhere(Func pr return this; } + /// + /// Expects a runtime Frame (capital-F) with the given + /// (e.g. "InterpreterFrame", "InlinedCallFrame") after the previous expectation. + /// Gaps between this and the previous expectation are allowed. + /// + public DumpTestStackWalker ExpectRuntimeFrame(string frameName, Action? assert = null) + { + _expectations.Add(new Expectation( + f => string.Equals(f.FrameName, frameName, StringComparison.Ordinal), + $"RuntimeFrame:{frameName}", + adjacent: false, + assert)); + return this; + } + + /// + /// Expects a runtime Frame (capital-F) with the given + /// immediately after the previously matched frame (no gaps allowed). + /// + public DumpTestStackWalker ExpectAdjacentRuntimeFrame(string frameName, Action? assert = null) + { + Assert.True(_expectations.Count > 0, + "ExpectAdjacentRuntimeFrame must follow a prior expectation."); + _expectations.Add(new Expectation( + f => string.Equals(f.FrameName, frameName, StringComparison.Ordinal), + $"RuntimeFrame:{frameName}", + adjacent: true, + assert)); + return this; + } + + /// + /// Asserts that the call stack contains a runtime Frame (capital-F) with the given + /// , regardless of position or order. + /// + public DumpTestStackWalker AssertHasRuntimeFrame(string frameName) + { + Assert.True(_frames.Any(f => string.Equals(f.FrameName, frameName, StringComparison.Ordinal)), + $"Expected runtime frame '{frameName}' not found. Call stack: [{FormatCallStack(_frames)}]"); + return this; + } + /// /// Asserts that the call stack contains a frame with the given /// , regardless of position or order. @@ -257,7 +318,11 @@ public void Verify() } private static string FormatCallStack(List frames) - => string.Join(", ", frames.Select(f => f.Name ?? "")); + => string.Join(", ", frames.Select(f => + { + string name = f.Name ?? ""; + return f.FrameName is not null ? $"{name}[{f.FrameName}]" : name; + })); private sealed class Expectation { diff --git a/src/native/managed/cdac/tests/DumpTests/DumpTests.targets b/src/native/managed/cdac/tests/DumpTests/DumpTests.targets index 1485177bf16027..6c429c2161328a 100644 --- a/src/native/managed/cdac/tests/DumpTests/DumpTests.targets +++ b/src/native/managed/cdac/tests/DumpTests/DumpTests.targets @@ -15,11 +15,15 @@ "Heap;Full" — both heap and full dumps Each debuggee csproj can also set an R2RModes property to control ReadyToRun behavior: - "R2R" — run with ReadyToRun enabled (default) - "Jit" — run with DOTNET_ReadyToRun=0 (force JIT compilation) - "R2R;Jit" — both modes + "R2R" — run with ReadyToRun enabled (default) + "Jit" — run with DOTNET_ReadyToRun=0 (force JIT compilation) + "R2R;Jit" — both R2R and JIT modes Binaries are always compiled with R2R; this property controls runtime behavior only. + Each debuggee csproj can also set an EnvironmentVariables property to pass additional + environment variables when running the debuggee (e.g., DOTNET_Interpreter=MethodA). + Multiple variables are separated by semicolons. + Properties: DumpOutputDir — Where dumps are written (default: artifacts/dumps/cdac/) TestHostDir — Path to the local-build testhost runtime (auto-detected) @@ -113,7 +117,7 @@ + Properties="DebuggeeName=%(_DebuggeeWithDumpTypes.Identity);_DebuggeeDumpTypes=%(_DebuggeeWithDumpTypes.DumpTypes);_DebuggeeR2RModes=%(_DebuggeeWithDumpTypes.R2RModes);_DebuggeeEnvVars=%(_DebuggeeWithDumpTypes.EnvironmentVariables)" /> @@ -179,7 +183,7 @@ + Properties="DebuggeeName=$(DebuggeeName);_DumpTypeName=%(_DumpTypeItem.Identity);_DebuggeeR2RModes=$(_DebuggeeR2RModes);_DebuggeeEnvVars=$(_DebuggeeEnvVars)" /> @@ -199,7 +203,7 @@ + Properties="DebuggeeName=$(DebuggeeName);_MiniDumpType=$(_MiniDumpType);_DumpTypeDirName=$(_DumpTypeDirName);_DumpTypeName=$(_DumpTypeName);_R2RModeName=%(_R2RModeItem.Identity);_DebuggeeEnvVars=$(_DebuggeeEnvVars)" /> @@ -215,7 +219,7 @@ Text="Invalid R2R mode '$(_R2RModeName)' specified for debuggee '$(DebuggeeName)'. Supported values: 'R2R', 'Jit'." /> + Properties="DebuggeeName=$(DebuggeeName);_MiniDumpType=$(_MiniDumpType);_DumpTypeDirName=$(_DumpTypeDirName);_DumpTypeName=$(_DumpTypeName);_R2RValue=$(_R2RValue);_R2RDirName=$(_R2RDirName);_DebuggeeEnvVars=$(_DebuggeeEnvVars);DumpRuntimeVersion=%(DumpRuntimeVersion.Identity)" /> @@ -234,12 +238,12 @@ @@ -262,7 +266,7 @@ + EnvironmentVariables="DOTNET_DbgEnableMiniDump=1;DOTNET_DbgMiniDumpType=$(_MiniDumpType);DOTNET_DbgMiniDumpName=$(_DumpFile);DOTNET_ReadyToRun=$(_R2RValue);$(_DebuggeeEnvVars)" /> @@ -283,7 +287,7 @@ + EnvironmentVariables="DOTNET_DbgEnableMiniDump=1;DOTNET_DbgMiniDumpType=$(_MiniDumpType);DOTNET_DbgMiniDumpName=$(_DumpFile);DOTNET_ReadyToRun=$(_R2RValue);$(_DebuggeeEnvVars)" /> diff --git a/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs new file mode 100644 index 00000000000000..81ff08d12284bf --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Dump-based integration tests for cDAC interpreter support. +/// Uses the InterpreterStack debuggee dump, which has a deterministic call stack: +/// Main -> MethodA -> MethodB -> JitTrampoline.Bounce -> MethodC -> MethodD -> FailFast. +/// Under DOTNET_Interpreter=MethodA, MethodA/B/C/D are interpreted while Main, +/// Bounce, and FailFast remain JIT'd. The trampoline is in a separate assembly +/// so it is NOT in g_interpModule, creating two distinct InterpreterFrame regions +/// on the stack with a JIT'd gap between them. Both InterpreterFrame regions have +/// multiple interpreted methods (pParent chain). +/// +public class InterpreterStackDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "InterpreterStack"; + protected override string DumpType => "full"; + + private void AssertInterpreted(ResolvedFrame f) + { + Assert.Equal("InterpreterFrame", f.FrameName); + + IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; + IExecutionManager executionManager = Target.Contracts.ExecutionManager; + + MethodDescHandle md = rts.GetMethodDescHandle(f.MethodDescPtr); + TargetCodePointer nativeCode = rts.GetNativeCode(md); + Assert.NotEqual(TargetCodePointer.Null, nativeCode); + + // The native code address for interpreter methods is a precode address. + // Resolve it to the actual interpreter code address before looking up the code block. + TargetCodePointer resolvedCode = Target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode); + CodeBlockHandle? codeBlock = executionManager.GetCodeBlockHandle(resolvedCode); + Assert.NotNull(codeBlock); + Assert.Equal(JitType.Interpreter, executionManager.GetJITType(codeBlock.Value)); + } + + private void AssertJitted(ResolvedFrame f) + { + Assert.Null(f.FrameName); + + IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; + IExecutionManager executionManager = Target.Contracts.ExecutionManager; + + MethodDescHandle md = rts.GetMethodDescHandle(f.MethodDescPtr); + TargetCodePointer nativeCode = rts.GetNativeCode(md); + Assert.NotEqual(TargetCodePointer.Null, nativeCode); + CodeBlockHandle? codeBlock = executionManager.GetCodeBlockHandle(nativeCode); + Assert.NotNull(codeBlock); + Assert.Equal(JitType.Jit, executionManager.GetJITType(codeBlock.Value)); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void StackWalk_VerifyInterleavedStackLayout(TestConfiguration config) + { + InitializeDumpTest(config); + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + + // The debuggee routes: Main -> MethodA -> MethodB -> Bounce -> MethodC -> MethodD -> FailFast. + // + // MethodA and MethodB are in InterpreterFrame 1 (interpreted, adjacent via pParent chain). + // Bounce is JIT'd (separate assembly, not in g_interpModule). + // MethodC and MethodD are in InterpreterFrame 2 (interpreted, adjacent via pParent chain). + // Main and FailFast are JIT'd. + // + // This verifies: + // - Full frame ordering + // - Which frames are interpreted vs JIT'd (via FrameName and JitType) + // - Multiple adjacent interpreted frames in each InterpreterFrame (pParent chain walk) + // - Two distinct InterpreterFrame regions separated by JIT'd Bounce + DumpTestStackWalker.Walk(Target, crashingThread) + .ExpectFrame("MethodD", AssertInterpreted) + .ExpectAdjacentFrame("MethodC", AssertInterpreted) + .ExpectFrame("Bounce", AssertJitted) + .ExpectFrame("MethodB", AssertInterpreted) + .ExpectAdjacentFrame("MethodA", AssertInterpreted) + .ExpectFrame("Main", AssertJitted) + .Verify(); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void StackWalk_InterpreterMethodNativeCodeIsPrecode(TestConfiguration config) + { + InitializeDumpTest(config); + IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; + IExecutionManager executionManager = Target.Contracts.ExecutionManager; + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + + DumpTestStackWalker walker = DumpTestStackWalker.Walk(Target, crashingThread); + + // Find the first interpreter method (MethodA/B/C) on the stack. + ResolvedFrame interpFrame = walker.Frames + .First(f => f.Name is "MethodA" or "MethodB" or "MethodC" or "MethodD"); + + MethodDescHandle mdHandle = rts.GetMethodDescHandle(interpFrame.MethodDescPtr); + TargetCodePointer nativeCode = rts.GetNativeCode(mdHandle); + Assert.NotEqual(TargetCodePointer.Null, nativeCode); + + // For interpreter methods, GetCodeBlockHandle returns null because the native code + // slot points to a precode, not a managed code heap entry. + CodeBlockHandle? codeBlock = executionManager.GetCodeBlockHandle(nativeCode); + Assert.Null(codeBlock); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void Thread_CanEnumerateWithInterpreterFrames(TestConfiguration config) + { + InitializeDumpTest(config); + IThread threadContract = Target.Contracts.Thread; + + ThreadStoreData storeData = threadContract.GetThreadStoreData(); + Assert.True(storeData.ThreadCount >= 1, + "Expected at least one thread in the thread store"); + + int threadCount = 0; + TargetPointer currentThreadPtr = storeData.FirstThread; + while (currentThreadPtr != TargetPointer.Null) + { + ThreadData threadData = threadContract.GetThreadData(currentThreadPtr); + threadCount++; + currentThreadPtr = threadData.NextThread; + } + + Assert.True(threadCount >= 1, "Expected at least one thread when walking the list"); + } +} diff --git a/src/native/managed/cdac/tests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdac/tests/ExecutionManager/ExecutionManagerTests.cs index 7f8b5cff5b5603..76de54317c93b5 100644 --- a/src/native/managed/cdac/tests/ExecutionManager/ExecutionManagerTests.cs +++ b/src/native/managed/cdac/tests/ExecutionManager/ExecutionManagerTests.cs @@ -26,6 +26,7 @@ public class ExecutionManagerTests [DataType.LoaderCodeHeap] = TargetTestHelpers.CreateTypeInfo(emBuilder.LoaderCodeHeapLayout), [DataType.HostCodeHeap] = TargetTestHelpers.CreateTypeInfo(emBuilder.HostCodeHeapLayout), [DataType.RealCodeHeader] = TargetTestHelpers.CreateTypeInfo(emBuilder.RealCodeHeaderLayout), + [DataType.InterpreterRealCodeHeader] = TargetTestHelpers.CreateTypeInfo(emBuilder.InterpreterRealCodeHeaderLayout), [DataType.ReadyToRunInfo] = TargetTestHelpers.CreateTypeInfo(emBuilder.ReadyToRunInfoLayout), [DataType.EEJitManager] = TargetTestHelpers.CreateTypeInfo(emBuilder.EEJitManagerLayout), [DataType.Module] = TargetTestHelpers.CreateTypeInfo(emBuilder.ModuleLayout), @@ -667,4 +668,71 @@ public static IEnumerable StdArchAllVersions() } } } + + [Theory] + [MemberData(nameof(StdArchAllVersions))] + public void GetMethodDesc_InterpreterOneMethod(int version, MockTarget.Architecture arch) + { + const ulong codeRangeStart = 0x0a0a_0000u; + const uint codeRangeSize = 0xc000u; + const uint methodSize = 0x200; + + const ulong jitManagerAddress = 0x000b_ff00; + const ulong expectedMethodDescAddress = 0x0101_bbb0; + + ulong methodStart = 0; + + IExecutionManager em = CreateExecutionManagerContract( + version, + arch, + emBuilder => + { + var interpCodeRange = emBuilder.AllocateJittedCodeRange(codeRangeStart, codeRangeSize); + + methodStart = emBuilder.AddInterpretedMethod(interpCodeRange, methodSize, expectedMethodDescAddress).CodeAddress; + + NibbleMapTestBuilderBase nibBuilder = emBuilder.CreateNibbleMap(codeRangeStart, codeRangeSize); + nibBuilder.AllocateCodeChunk(new TargetCodePointer(methodStart), methodSize); + + MockCodeHeapListNode codeHeapListNode = emBuilder.AddCodeHeapListNode(0, codeRangeStart, codeRangeStart + codeRangeSize, codeRangeStart, nibBuilder.NibbleMapFragment.Address); + MockRangeSection rangeSection = emBuilder.AddInterpreterRangeSection(interpCodeRange, jitManagerAddress, codeHeapListNode.Address); + _ = emBuilder.AddRangeSectionFragment(interpCodeRange, rangeSection.Address); + }); + + var eeInfo = em.GetCodeBlockHandle(new TargetCodePointer(methodStart)); + Assert.NotNull(eeInfo); + TargetPointer actualMethodDesc = em.GetMethodDesc(eeInfo.Value); + Assert.Equal(new TargetPointer(expectedMethodDescAddress), actualMethodDesc); + Assert.Equal(JitType.Interpreter, em.GetJITType(eeInfo.Value)); + + eeInfo = em.GetCodeBlockHandle(new TargetCodePointer(methodStart + methodSize / 2)); + Assert.NotNull(eeInfo); + actualMethodDesc = em.GetMethodDesc(eeInfo.Value); + Assert.Equal(new TargetPointer(expectedMethodDescAddress), actualMethodDesc); + } + + [Theory] + [MemberData(nameof(StdArchAllVersions))] + public void GetCodeBlockHandle_InterpreterPrecode_ReturnsNull(int version, MockTarget.Architecture arch) + { + const ulong precodeRangeStart = 0x0b0b_0000u; + const uint precodeRangeSize = 0x1000u; + + IExecutionManager em = CreateExecutionManagerContract( + version, + arch, + emBuilder => + { + var precodeRange = emBuilder.AllocateJittedCodeRange(precodeRangeStart, precodeRangeSize); + MockRangeSection precodeRangeSection = emBuilder.AddRangeListSection(precodeRange); + _ = emBuilder.AddRangeSectionFragment(precodeRange, precodeRangeSection.Address); + }); + + // GetCodeBlockHandle should return null for a precode address. + // Callers are responsible for resolving interpreter precodes via + // PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent before calling GetCodeBlockHandle. + TargetCodePointer precodeAddress = new(precodeRangeStart + 0x100); + var eeInfo = em.GetCodeBlockHandle(precodeAddress); + Assert.Null(eeInfo); + } } diff --git a/src/native/managed/cdac/tests/FrameIteratorTests.cs b/src/native/managed/cdac/tests/FrameIteratorTests.cs new file mode 100644 index 00000000000000..3834a2b6539b81 --- /dev/null +++ b/src/native/managed/cdac/tests/FrameIteratorTests.cs @@ -0,0 +1,405 @@ +// 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; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public class FrameIteratorTests +{ + private record TypeFields + { + public required DataType DataType; + public required TargetTestHelpers.Field[] Fields; + public TypeFields? BaseTypeFields; + } + + private static Dictionary GetTypesForTypeFields(TargetTestHelpers helpers, TypeFields[] typeFields) + { + Dictionary types = new(); + foreach (var toAdd in typeFields) + { + TargetTestHelpers.LayoutResult layout = toAdd.BaseTypeFields is null + ? helpers.LayoutFields(toAdd.Fields) + : helpers.ExtendLayout(toAdd.Fields, GetLayout(helpers, toAdd.BaseTypeFields)); + types[toAdd.DataType] = new Target.TypeInfo() + { + Fields = layout.Fields, + Size = layout.Stride, + }; + } + return types; + + static TargetTestHelpers.LayoutResult GetLayout(TargetTestHelpers helpers, TypeFields typeFields) + { + return typeFields.BaseTypeFields is null + ? helpers.LayoutFields(typeFields.Fields) + : helpers.ExtendLayout(typeFields.Fields, GetLayout(helpers, typeFields.BaseTypeFields)); + } + } + + private static readonly TypeFields FrameFields = new TypeFields() + { + DataType = DataType.Frame, + Fields = + [ + new("_vptr", DataType.pointer), + new(nameof(Data.Frame.Next), DataType.pointer), + ] + }; + + private static readonly TypeFields FramedMethodFrameFields = new TypeFields() + { + DataType = DataType.FramedMethodFrame, + Fields = + [ + new(nameof(Data.FramedMethodFrame.TransitionBlockPtr), DataType.pointer), + new(nameof(Data.FramedMethodFrame.MethodDescPtr), DataType.pointer), + ], + BaseTypeFields = FrameFields + }; + + private static readonly TypeFields InterpreterFrameFields = new TypeFields() + { + DataType = DataType.InterpreterFrame, + Fields = + [ + new(nameof(Data.InterpreterFrame.TopInterpMethodContextFrame), DataType.pointer), + ], + BaseTypeFields = FrameFields + }; + + private static readonly TypeFields InterpMethodContextFrameFields = new TypeFields() + { + DataType = DataType.InterpMethodContextFrame, + Fields = + [ + new(nameof(Data.InterpMethodContextFrame.StartIp), DataType.pointer), + new(nameof(Data.InterpMethodContextFrame.ParentPtr), DataType.pointer), + ] + }; + + private static readonly TypeFields InterpByteCodeStartFields = new TypeFields() + { + DataType = DataType.InterpByteCodeStart, + Fields = + [ + new(nameof(Data.InterpByteCodeStart.Method), DataType.pointer), + ] + }; + + private static readonly TypeFields InterpMethodFields = new TypeFields() + { + DataType = DataType.InterpMethod, + Fields = + [ + new(nameof(Data.InterpMethod.MethodDesc), DataType.pointer), + ] + }; + + private static Dictionary GetTypes(TargetTestHelpers helpers) + { + return GetTypesForTypeFields(helpers, + [ + FrameFields, + FramedMethodFrameFields, + InterpreterFrameFields, + InterpMethodContextFrameFields, + InterpByteCodeStartFields, + InterpMethodFields, + ]); + } + + public static IEnumerable InterpreterFrameArchitectures => + [ + [new MockTarget.Architecture { Is64Bit = true, IsLittleEndian = true }], + [new MockTarget.Architecture { Is64Bit = false, IsLittleEndian = true }], + ]; + + [Theory] + [MemberData(nameof(InterpreterFrameArchitectures))] + public void GetMethodDescPtr_InterpreterFrame_FollowsFullChain(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + Dictionary types = GetTypes(helpers); + + var builder = new TestPlaceholderTarget.Builder(arch) + .AddTypes(types); + + int pointerSize = helpers.PointerSize; + + var alloc = builder.MemoryBuilder.CreateAllocator(0x1000_0000, 0x2000_0000); + + ulong interpreterFrameIdentifierValue = 0xAAAA_1111; + + ulong expectedMethodDesc = 0xDEAD_BEEF; + + var interpMethodFrag = alloc.Allocate((ulong)types[DataType.InterpMethod].Size!, "InterpMethod"); + helpers.WritePointer( + interpMethodFrag.Data.AsSpan(types[DataType.InterpMethod].Fields[nameof(Data.InterpMethod.MethodDesc)].Offset, pointerSize), + expectedMethodDesc); + + var byteCodeStartFrag = alloc.Allocate((ulong)types[DataType.InterpByteCodeStart].Size!, "InterpByteCodeStart"); + helpers.WritePointer( + byteCodeStartFrag.Data.AsSpan(types[DataType.InterpByteCodeStart].Fields[nameof(Data.InterpByteCodeStart.Method)].Offset, pointerSize), + interpMethodFrag.Address); + + var contextFrameFrag = alloc.Allocate((ulong)types[DataType.InterpMethodContextFrame].Size!, "InterpMethodContextFrame"); + helpers.WritePointer( + contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.StartIp)].Offset, pointerSize), + byteCodeStartFrag.Address); + helpers.WritePointer( + contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.ParentPtr)].Offset, pointerSize), + 0); + + int frameNextOffset = types[DataType.Frame].Fields[nameof(Data.Frame.Next)].Offset; + int topContextFrameOffset = types[DataType.InterpreterFrame].Fields[nameof(Data.InterpreterFrame.TopInterpMethodContextFrame)].Offset; + int totalFrameSize = Math.Max(topContextFrameOffset + pointerSize, frameNextOffset + pointerSize); + var frameFrag = alloc.Allocate((ulong)totalFrameSize, "InterpreterFrame"); + + helpers.WritePointer(frameFrag.Data.AsSpan(0, pointerSize), interpreterFrameIdentifierValue); + ulong terminator = arch.Is64Bit ? ulong.MaxValue : uint.MaxValue; + helpers.WritePointer(frameFrag.Data.AsSpan(frameNextOffset, pointerSize), terminator); + helpers.WritePointer(frameFrag.Data.AsSpan(topContextFrameOffset, pointerSize), contextFrameFrag.Address); + + builder.MemoryBuilder.AddHeapFragment(interpMethodFrag); + builder.MemoryBuilder.AddHeapFragment(byteCodeStartFrag); + builder.MemoryBuilder.AddHeapFragment(contextFrameFrag); + builder.MemoryBuilder.AddHeapFragment(frameFrag); + + builder.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); + + var target = builder.Build(); + + TargetPointer result = FrameIterator.GetMethodDescPtr(target, new TargetPointer(frameFrag.Address)); + + Assert.Equal(new TargetPointer(expectedMethodDesc), result); + } + + [Theory] + [MemberData(nameof(InterpreterFrameArchitectures))] + public void GetMethodDescPtr_InterpreterFrame_NullContextFrame_ReturnsNull(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + Dictionary types = GetTypes(helpers); + + var builder = new TestPlaceholderTarget.Builder(arch) + .AddTypes(types); + + int pointerSize = helpers.PointerSize; + var alloc = builder.MemoryBuilder.CreateAllocator(0x1000_0000, 0x2000_0000); + + ulong interpreterFrameIdentifierValue = 0xAAAA_2222; + + int topContextFrameOffset = types[DataType.InterpreterFrame].Fields[nameof(Data.InterpreterFrame.TopInterpMethodContextFrame)].Offset; + int frameNextOffset = types[DataType.Frame].Fields[nameof(Data.Frame.Next)].Offset; + int totalFrameSize = Math.Max(topContextFrameOffset + pointerSize, frameNextOffset + pointerSize); + var frameFrag = alloc.Allocate((ulong)totalFrameSize, "InterpreterFrame"); + + helpers.WritePointer(frameFrag.Data.AsSpan(0, pointerSize), interpreterFrameIdentifierValue); + ulong terminator = arch.Is64Bit ? ulong.MaxValue : uint.MaxValue; + helpers.WritePointer(frameFrag.Data.AsSpan(frameNextOffset, pointerSize), terminator); + helpers.WritePointer(frameFrag.Data.AsSpan(topContextFrameOffset, pointerSize), 0); + + builder.MemoryBuilder.AddHeapFragment(frameFrag); + builder.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); + + var target = builder.Build(); + + TargetPointer result = FrameIterator.GetMethodDescPtr(target, new TargetPointer(frameFrag.Address)); + + Assert.Equal(TargetPointer.Null, result); + } + + [Theory] + [MemberData(nameof(InterpreterFrameArchitectures))] + public void GetMethodDescPtr_InterpreterFrame_NullStartIp_ReturnsNull(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + Dictionary types = GetTypes(helpers); + + var builder = new TestPlaceholderTarget.Builder(arch) + .AddTypes(types); + + int pointerSize = helpers.PointerSize; + var alloc = builder.MemoryBuilder.CreateAllocator(0x1000_0000, 0x2000_0000); + + ulong interpreterFrameIdentifierValue = 0xAAAA_3333; + + var contextFrameFrag = alloc.Allocate((ulong)types[DataType.InterpMethodContextFrame].Size!, "InterpMethodContextFrame"); + helpers.WritePointer( + contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.StartIp)].Offset, pointerSize), + 0); + helpers.WritePointer( + contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.ParentPtr)].Offset, pointerSize), + 0); + + int topContextFrameOffset = types[DataType.InterpreterFrame].Fields[nameof(Data.InterpreterFrame.TopInterpMethodContextFrame)].Offset; + int frameNextOffset = types[DataType.Frame].Fields[nameof(Data.Frame.Next)].Offset; + int totalFrameSize = Math.Max(topContextFrameOffset + pointerSize, frameNextOffset + pointerSize); + var frameFrag = alloc.Allocate((ulong)totalFrameSize, "InterpreterFrame"); + + helpers.WritePointer(frameFrag.Data.AsSpan(0, pointerSize), interpreterFrameIdentifierValue); + ulong terminator = arch.Is64Bit ? ulong.MaxValue : uint.MaxValue; + helpers.WritePointer(frameFrag.Data.AsSpan(frameNextOffset, pointerSize), terminator); + helpers.WritePointer(frameFrag.Data.AsSpan(topContextFrameOffset, pointerSize), contextFrameFrag.Address); + + builder.MemoryBuilder.AddHeapFragment(contextFrameFrag); + builder.MemoryBuilder.AddHeapFragment(frameFrag); + builder.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); + + var target = builder.Build(); + + TargetPointer result = FrameIterator.GetMethodDescPtr(target, new TargetPointer(frameFrag.Address)); + + Assert.Equal(TargetPointer.Null, result); + } + + [Theory] + [MemberData(nameof(InterpreterFrameArchitectures))] + public void GetMethodDescPtr_InterpreterFrame_NullMethod_ReturnsNull(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + Dictionary types = GetTypes(helpers); + + var builder = new TestPlaceholderTarget.Builder(arch) + .AddTypes(types); + + int pointerSize = helpers.PointerSize; + var alloc = builder.MemoryBuilder.CreateAllocator(0x1000_0000, 0x2000_0000); + + ulong interpreterFrameIdentifierValue = 0xAAAA_4444; + + var byteCodeStartFrag = alloc.Allocate((ulong)types[DataType.InterpByteCodeStart].Size!, "InterpByteCodeStart"); + helpers.WritePointer( + byteCodeStartFrag.Data.AsSpan(types[DataType.InterpByteCodeStart].Fields[nameof(Data.InterpByteCodeStart.Method)].Offset, pointerSize), + 0); + + var contextFrameFrag = alloc.Allocate((ulong)types[DataType.InterpMethodContextFrame].Size!, "InterpMethodContextFrame"); + helpers.WritePointer( + contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.StartIp)].Offset, pointerSize), + byteCodeStartFrag.Address); + helpers.WritePointer( + contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.ParentPtr)].Offset, pointerSize), + 0); + + int topContextFrameOffset = types[DataType.InterpreterFrame].Fields[nameof(Data.InterpreterFrame.TopInterpMethodContextFrame)].Offset; + int frameNextOffset = types[DataType.Frame].Fields[nameof(Data.Frame.Next)].Offset; + int totalFrameSize = Math.Max(topContextFrameOffset + pointerSize, frameNextOffset + pointerSize); + var frameFrag = alloc.Allocate((ulong)totalFrameSize, "InterpreterFrame"); + + helpers.WritePointer(frameFrag.Data.AsSpan(0, pointerSize), interpreterFrameIdentifierValue); + ulong terminator = arch.Is64Bit ? ulong.MaxValue : uint.MaxValue; + helpers.WritePointer(frameFrag.Data.AsSpan(frameNextOffset, pointerSize), terminator); + helpers.WritePointer(frameFrag.Data.AsSpan(topContextFrameOffset, pointerSize), contextFrameFrag.Address); + + builder.MemoryBuilder.AddHeapFragment(byteCodeStartFrag); + builder.MemoryBuilder.AddHeapFragment(contextFrameFrag); + builder.MemoryBuilder.AddHeapFragment(frameFrag); + builder.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); + + var target = builder.Build(); + + TargetPointer result = FrameIterator.GetMethodDescPtr(target, new TargetPointer(frameFrag.Address)); + + Assert.Equal(TargetPointer.Null, result); + } + + [Theory] + [MemberData(nameof(InterpreterFrameArchitectures))] + public void ResolveMethodDescFromContextFrame_MultipleContextFrames_ResolvesEach(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + Dictionary types = GetTypes(helpers); + + var builder = new TestPlaceholderTarget.Builder(arch) + .AddTypes(types); + + int pointerSize = helpers.PointerSize; + var alloc = builder.MemoryBuilder.CreateAllocator(0x1000_0000, 0x2000_0000); + + ulong methodDescA = 0xAA00_0001; + ulong methodDescB = 0xBB00_0002; + ulong methodDescC = 0xCC00_0003; + + // Build three independent InterpMethod → InterpByteCodeStart chains + MockMemorySpace.HeapFragment CreateContextChainEntry(ulong methodDesc, ulong parentPtr, out MockMemorySpace.HeapFragment interpMethodFrag, out MockMemorySpace.HeapFragment byteCodeStartFrag) + { + interpMethodFrag = alloc.Allocate((ulong)types[DataType.InterpMethod].Size!, "InterpMethod"); + helpers.WritePointer( + interpMethodFrag.Data.AsSpan(types[DataType.InterpMethod].Fields[nameof(Data.InterpMethod.MethodDesc)].Offset, pointerSize), + methodDesc); + + byteCodeStartFrag = alloc.Allocate((ulong)types[DataType.InterpByteCodeStart].Size!, "InterpByteCodeStart"); + helpers.WritePointer( + byteCodeStartFrag.Data.AsSpan(types[DataType.InterpByteCodeStart].Fields[nameof(Data.InterpByteCodeStart.Method)].Offset, pointerSize), + interpMethodFrag.Address); + + var contextFrameFrag = alloc.Allocate((ulong)types[DataType.InterpMethodContextFrame].Size!, "InterpMethodContextFrame"); + helpers.WritePointer( + contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.StartIp)].Offset, pointerSize), + byteCodeStartFrag.Address); + helpers.WritePointer( + contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.ParentPtr)].Offset, pointerSize), + parentPtr); + + return contextFrameFrag; + } + + // Build chain: C (leaf) → B → A (root, ParentPtr=0) + var contextFrameA = CreateContextChainEntry(methodDescA, 0, out var interpMethodA, out var byteCodeStartA); + var contextFrameB = CreateContextChainEntry(methodDescB, contextFrameA.Address, out var interpMethodB, out var byteCodeStartB); + var contextFrameC = CreateContextChainEntry(methodDescC, contextFrameB.Address, out var interpMethodC, out var byteCodeStartC); + + foreach (var frag in new[] { interpMethodA, byteCodeStartA, contextFrameA, interpMethodB, byteCodeStartB, contextFrameB, interpMethodC, byteCodeStartC, contextFrameC }) + builder.MemoryBuilder.AddHeapFragment(frag); + + var target = builder.Build(); + + // Resolve each context frame individually — verifies the chain links resolve to distinct MethodDescs + Assert.Equal(new TargetPointer(methodDescC), FrameIterator.ResolveMethodDescFromInterpFrame(target, new TargetPointer(contextFrameC.Address))); + Assert.Equal(new TargetPointer(methodDescB), FrameIterator.ResolveMethodDescFromInterpFrame(target, new TargetPointer(contextFrameB.Address))); + Assert.Equal(new TargetPointer(methodDescA), FrameIterator.ResolveMethodDescFromInterpFrame(target, new TargetPointer(contextFrameA.Address))); + + // Verify null terminates correctly + Assert.Equal(TargetPointer.Null, FrameIterator.ResolveMethodDescFromInterpFrame(target, TargetPointer.Null)); + } + + [Theory] + [MemberData(nameof(InterpreterFrameArchitectures))] + public void GetFrameName_InterpreterFrame_ReturnsName(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + Dictionary types = GetTypes(helpers); + + var builder = new TestPlaceholderTarget.Builder(arch) + .AddTypes(types); + + ulong interpreterFrameIdentifierValue = 0xAAAA_5555; + + builder.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); + + var target = builder.Build(); + + string name = FrameIterator.GetFrameName(target, new TargetPointer(interpreterFrameIdentifierValue)); + + Assert.Equal("InterpreterFrame", name); + } + + [Theory] + [MemberData(nameof(InterpreterFrameArchitectures))] + public void GetFrameName_UnknownFrame_ReturnsEmpty(MockTarget.Architecture arch) + { + var builder = new TestPlaceholderTarget.Builder(arch) + .AddTypes(GetTypes(new TargetTestHelpers(arch))); + + var target = builder.Build(); + + string name = FrameIterator.GetFrameName(target, new TargetPointer(0x9999_9999)); + + Assert.Equal(string.Empty, name); + } +} diff --git a/src/native/managed/cdac/tests/MethodDescTests.cs b/src/native/managed/cdac/tests/MethodDescTests.cs index 3de65ac9b148e6..a6f19e702081f9 100644 --- a/src/native/managed/cdac/tests/MethodDescTests.cs +++ b/src/native/managed/cdac/tests/MethodDescTests.cs @@ -66,7 +66,8 @@ private static uint GetMethodDescBaseSize(MockDescriptors.MockMethodDescriptorsB private static IRuntimeTypeSystem CreateRuntimeTypeSystemContract( MockTarget.Architecture arch, Action configure, - Mock? mockExecutionManager = null) + Mock? mockExecutionManager = null, + Mock? mockPrecodeStubs = null) { var targetBuilder = new TestPlaceholderTarget.Builder(arch); MockDescriptors.RuntimeTypeSystem rtsBuilder = new(targetBuilder.MemoryBuilder); @@ -76,6 +77,7 @@ private static IRuntimeTypeSystem CreateRuntimeTypeSystemContract( configure(methodDescBuilder); mockExecutionManager ??= new Mock(); + mockPrecodeStubs ??= new Mock(); var target = targetBuilder .AddTypes(CreateContractTypes(methodDescBuilder)) .AddGlobals(CreateContractGlobals(methodDescBuilder)) @@ -83,6 +85,7 @@ private static IRuntimeTypeSystem CreateRuntimeTypeSystemContract( .AddContract(version: "c1") .AddMockContract(new Mock()) .AddMockContract(mockExecutionManager) + .AddMockContract(mockPrecodeStubs) .Build(); return target.Contracts.RuntimeTypeSystem; } @@ -387,6 +390,21 @@ public static IEnumerable StdArchMethodDescTypeData() } } + public static IEnumerable StdArchNonFCallMethodDescTypeData() + { + foreach (object[] arr in new MockTarget.StdArch()) + { + MockTarget.Architecture arch = (MockTarget.Architecture)arr[0]; + yield return [arch, DataType.MethodDesc]; + yield return [arch, DataType.PInvokeMethodDesc]; + yield return [arch, DataType.EEImplMethodDesc]; + yield return [arch, DataType.ArrayMethodDesc]; + yield return [arch, DataType.InstantiatedMethodDesc]; + yield return [arch, DataType.CLRToCOMCallMethodDesc]; + yield return [arch, DataType.DynamicMethodDesc]; + } + } + [Theory] [MemberData(nameof(StdArchMethodDescTypeData))] public void GetNativeCode_StableEntryPoint_NonVtableSlot(MockTarget.Architecture arch, DataType methodDescType) @@ -638,6 +656,123 @@ public void MethodDescClassificationFlags(MockTarget.Architecture arch) } } + [Theory] + [MemberData(nameof(StdArchMethodDescTypeData))] + public void Validation_NativeCodeSlot_PrecodeFallback(MockTarget.Architecture arch, DataType methodDescType) + { + TargetPointer methodDescAddress = TargetPointer.Null; + TargetCodePointer nativeCode = new TargetCodePointer(0x0789_abc0); + Mock mockExecutionManager = new(); + Mock mockPrecodeStubs = new(); + + IRuntimeTypeSystem rts = CreateRuntimeTypeSystemContract(arch, methodDescBuilder => + { + TargetTestHelpers helpers = methodDescBuilder.Builder.TargetTestHelpers; + TargetPointer methodTable = AddMethodTable(methodDescBuilder.RTSBuilder); + MethodClassification classification = methodDescType switch + { + DataType.MethodDesc => MethodClassification.IL, + DataType.FCallMethodDesc => MethodClassification.FCall, + DataType.PInvokeMethodDesc => MethodClassification.PInvoke, + DataType.EEImplMethodDesc => MethodClassification.EEImpl, + DataType.ArrayMethodDesc => MethodClassification.Array, + DataType.InstantiatedMethodDesc => MethodClassification.Instantiated, + DataType.CLRToCOMCallMethodDesc => MethodClassification.ComInterop, + DataType.DynamicMethodDesc => MethodClassification.Dynamic, + _ => throw new ArgumentOutOfRangeException(nameof(methodDescType)) + }; + + uint methodDescBaseSize = GetMethodDescBaseSize(methodDescBuilder, methodDescType); + uint methodDescSize = methodDescBaseSize + methodDescBuilder.NonVtableSlotSize; + byte chunkSize = (byte)(methodDescSize / methodDescBuilder.MethodDescAlignment); + MockMethodDescChunk chunk = methodDescBuilder.AddMethodDescChunk(string.Empty, chunkSize); + chunk.MethodTable = methodTable.Value; + chunk.Size = chunkSize; + chunk.Count = 1; + + ushort flags = (ushort)((ushort)classification | (ushort)MethodDescFlags_1.MethodDescFlags.HasNonVtableSlot); + MockMethodDesc methodDesc = methodDescType switch + { + DataType.InstantiatedMethodDesc => chunk.GetMethodDescAtChunkIndex(0, methodDescBuilder.InstantiatedMethodDescLayout), + DataType.DynamicMethodDesc => chunk.GetMethodDescAtChunkIndex(0, methodDescBuilder.DynamicMethodDescLayout), + DataType.EEImplMethodDesc or DataType.ArrayMethodDesc => chunk.GetMethodDescAtChunkIndex(0, methodDescBuilder.StoredSigMethodDescLayout), + _ => chunk.GetMethodDescAtChunkIndex(0, methodDescBuilder.MethodDescLayout), + }; + methodDesc.Flags = flags; + methodDesc.Flags3AndTokenRemainder = (ushort)MethodDescFlags_1.MethodDescFlags3.HasStableEntryPoint; + methodDescAddress = new TargetPointer(methodDesc.Address); + helpers.WritePointer( + methodDescBuilder.Builder.BorrowAddressRange(methodDescAddress + methodDescBaseSize, helpers.PointerSize), + nativeCode); + }, mockExecutionManager, mockPrecodeStubs); + + mockExecutionManager.Setup(em => em.GetCodeBlockHandle(nativeCode)).Returns((CodeBlockHandle?)null); + mockPrecodeStubs.Setup(ps => ps.GetMethodDescFromStubAddress(nativeCode)).Returns(methodDescAddress); + + MethodDescHandle handle = rts.GetMethodDescHandle(methodDescAddress); + Assert.NotEqual(TargetPointer.Null, handle.Address); + + TargetCodePointer actualNativeCode = rts.GetNativeCode(handle); + Assert.Equal(nativeCode, actualNativeCode); + } + + [Theory] + [MemberData(nameof(StdArchNonFCallMethodDescTypeData))] + public void Validation_NativeCodeSlot_PrecodeFallback_WrongMethodDesc_Fails(MockTarget.Architecture arch, DataType methodDescType) + { + TargetPointer methodDescAddress = TargetPointer.Null; + TargetCodePointer nativeCode = new TargetCodePointer(0x0789_abc0); + TargetPointer wrongMethodDescAddress = new TargetPointer(0xDEAD_BEEF); + Mock mockExecutionManager = new(); + Mock mockPrecodeStubs = new(); + + IRuntimeTypeSystem rts = CreateRuntimeTypeSystemContract(arch, methodDescBuilder => + { + TargetTestHelpers helpers = methodDescBuilder.Builder.TargetTestHelpers; + TargetPointer methodTable = AddMethodTable(methodDescBuilder.RTSBuilder); + MethodClassification classification = methodDescType switch + { + DataType.MethodDesc => MethodClassification.IL, + DataType.FCallMethodDesc => MethodClassification.FCall, + DataType.PInvokeMethodDesc => MethodClassification.PInvoke, + DataType.EEImplMethodDesc => MethodClassification.EEImpl, + DataType.ArrayMethodDesc => MethodClassification.Array, + DataType.InstantiatedMethodDesc => MethodClassification.Instantiated, + DataType.CLRToCOMCallMethodDesc => MethodClassification.ComInterop, + DataType.DynamicMethodDesc => MethodClassification.Dynamic, + _ => throw new ArgumentOutOfRangeException(nameof(methodDescType)) + }; + + uint methodDescBaseSize = GetMethodDescBaseSize(methodDescBuilder, methodDescType); + uint methodDescSize = methodDescBaseSize + methodDescBuilder.NonVtableSlotSize; + byte chunkSize = (byte)(methodDescSize / methodDescBuilder.MethodDescAlignment); + MockMethodDescChunk chunk = methodDescBuilder.AddMethodDescChunk(string.Empty, chunkSize); + chunk.MethodTable = methodTable.Value; + chunk.Size = chunkSize; + chunk.Count = 1; + + ushort flags = (ushort)((ushort)classification | (ushort)MethodDescFlags_1.MethodDescFlags.HasNonVtableSlot); + MockMethodDesc methodDesc = methodDescType switch + { + DataType.InstantiatedMethodDesc => chunk.GetMethodDescAtChunkIndex(0, methodDescBuilder.InstantiatedMethodDescLayout), + DataType.DynamicMethodDesc => chunk.GetMethodDescAtChunkIndex(0, methodDescBuilder.DynamicMethodDescLayout), + DataType.EEImplMethodDesc or DataType.ArrayMethodDesc => chunk.GetMethodDescAtChunkIndex(0, methodDescBuilder.StoredSigMethodDescLayout), + _ => chunk.GetMethodDescAtChunkIndex(0, methodDescBuilder.MethodDescLayout), + }; + methodDesc.Flags = flags; + methodDesc.Flags3AndTokenRemainder = (ushort)MethodDescFlags_1.MethodDescFlags3.HasStableEntryPoint; + methodDescAddress = new TargetPointer(methodDesc.Address); + helpers.WritePointer( + methodDescBuilder.Builder.BorrowAddressRange(methodDescAddress + methodDescBaseSize, helpers.PointerSize), + nativeCode); + }, mockExecutionManager, mockPrecodeStubs); + + mockExecutionManager.Setup(em => em.GetCodeBlockHandle(nativeCode)).Returns((CodeBlockHandle?)null); + mockPrecodeStubs.Setup(ps => ps.GetMethodDescFromStubAddress(nativeCode)).Returns(wrongMethodDescAddress); + + Assert.Throws(() => rts.GetMethodDescHandle(methodDescAddress)); + } + private static TargetPointer AddMethodTable(MockDescriptors.RuntimeTypeSystem rtsBuilder, ushort numVirtuals = 5) { MockEEClass eeClass = rtsBuilder.AddEEClass(string.Empty); diff --git a/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj b/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj index 5b33a365154275..e0bad8903a2553 100644 --- a/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj +++ b/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj @@ -19,6 +19,7 @@ + diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs index c72126d962f939..2c2b00d222b0dc 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs @@ -316,6 +316,46 @@ public ulong UnwindInfos } } +internal sealed class MockInterpreterRealCodeHeader : TypedView +{ + private const string MethodDescFieldName = "MethodDesc"; + private const string DebugInfoFieldName = "DebugInfo"; + private const string GCInfoFieldName = "GCInfo"; + private const string JitEHInfoFieldName = "JitEHInfo"; + + public static Layout CreateLayout(MockTarget.Architecture architecture) + => new SequentialLayoutBuilder("InterpreterRealCodeHeader", architecture) + .AddPointerField(MethodDescFieldName) + .AddPointerField(DebugInfoFieldName) + .AddPointerField(GCInfoFieldName) + .AddPointerField(JitEHInfoFieldName) + .Build(); + + public ulong MethodDesc + { + get => ReadPointerField(MethodDescFieldName); + set => WritePointerField(MethodDescFieldName, value); + } + + public ulong DebugInfo + { + get => ReadPointerField(DebugInfoFieldName); + set => WritePointerField(DebugInfoFieldName, value); + } + + public ulong GCInfo + { + get => ReadPointerField(GCInfoFieldName); + set => WritePointerField(GCInfoFieldName, value); + } + + public ulong JitEHInfo + { + get => ReadPointerField(JitEHInfoFieldName); + set => WritePointerField(JitEHInfoFieldName, value); + } +} + internal sealed class MockReadyToRunInfo : TypedView { private const string ReadyToRunHeaderFieldName = "ReadyToRunHeader"; @@ -442,6 +482,8 @@ public Memory CodeBytes internal sealed class MockExecutionManagerBuilder { private const uint CodeHeapRangeSectionFlag = 0x02; + private const uint RangeListRangeSectionFlag = 0x04; + private const uint InterpreterRangeSectionFlag = 0x0A; // CodeHeap | Interpreter private const string EEJitManagerGlobalName = "EEJitManagerGlobalPointer"; private const int RangeSectionMapBitsPerLevel = 8; @@ -498,6 +540,7 @@ internal readonly struct JittedCodeRange internal Layout LoaderCodeHeapLayout { get; } internal Layout HostCodeHeapLayout { get; } internal Layout RealCodeHeaderLayout { get; } + internal Layout InterpreterRealCodeHeaderLayout { get; } internal Layout ReadyToRunInfoLayout { get; } internal Layout EEJitManagerLayout { get; } internal Layout ModuleLayout { get; } @@ -552,6 +595,7 @@ internal MockExecutionManagerBuilder(string version, MockMemorySpace.Builder bui LoaderCodeHeapLayout = MockLoaderCodeHeap.CreateLayout(architecture); HostCodeHeapLayout = MockHostCodeHeap.CreateLayout(architecture); RealCodeHeaderLayout = MockRealCodeHeader.CreateLayout(architecture); + InterpreterRealCodeHeaderLayout = MockInterpreterRealCodeHeader.CreateLayout(architecture); ReadyToRunInfoLayout = MockReadyToRunInfo.CreateLayout(architecture, hashMapStride); EEJitManagerLayout = MockEEJitManager.CreateLayout(architecture); ModuleLayout = MockLoaderModule.CreateLayout(architecture); @@ -614,6 +658,26 @@ public MockRangeSection AddReadyToRunRangeSection(JittedCodeRange jittedCodeRang return rangeSection; } + public MockRangeSection AddInterpreterRangeSection(JittedCodeRange jittedCodeRange, ulong jitManagerAddress, ulong codeHeapListNodeAddress) + { + MockRangeSection rangeSection = AllocateAndCreate(RangeSectionLayout, "InterpreterRangeSection", _rangeSectionMapAllocator); + rangeSection.RangeBegin = jittedCodeRange.RangeStart; + rangeSection.RangeEndOpen = jittedCodeRange.RangeEnd; + rangeSection.Flags = InterpreterRangeSectionFlag; + rangeSection.HeapList = codeHeapListNodeAddress; + rangeSection.JitManager = jitManagerAddress; + return rangeSection; + } + + public MockRangeSection AddRangeListSection(JittedCodeRange jittedCodeRange) + { + MockRangeSection rangeSection = AllocateAndCreate(RangeSectionLayout, "RangeListSection", _rangeSectionMapAllocator); + rangeSection.RangeBegin = jittedCodeRange.RangeStart; + rangeSection.RangeEndOpen = jittedCodeRange.RangeEnd; + rangeSection.Flags = RangeListRangeSectionFlag; + return rangeSection; + } + public MockRangeSectionFragment AddRangeSectionFragment(JittedCodeRange jittedCodeRange, ulong rangeSectionAddress) => AddRangeSectionFragment(jittedCodeRange, rangeSectionAddress, insertIntoMap: true); @@ -686,6 +750,20 @@ public MockJittedMethod AddJittedMethod(JittedCodeRange jittedCodeRange, uint co return jittedMethod; } + public MockJittedMethod AddInterpretedMethod(JittedCodeRange jittedCodeRange, uint codeSize, ulong methodDescAddress) + { + MockJittedMethod jittedMethod = AllocateJittedMethod(jittedCodeRange, codeSize, "Interpreter Method Header & Code"); + MockInterpreterRealCodeHeader codeHeader = AllocateAndCreate(InterpreterRealCodeHeaderLayout, "InterpreterRealCodeHeader"); + jittedMethod.CodeHeader = codeHeader.Address; + + codeHeader.MethodDesc = methodDescAddress; + codeHeader.DebugInfo = 0; + codeHeader.GCInfo = 0; + codeHeader.JitEHInfo = 0; + + return jittedMethod; + } + public MockReadyToRunInfo AddReadyToRunInfo(uint[] runtimeFunctions, uint[] hotColdMap) { ulong runtimeFunctionsAddress = _runtimeFunctions.AddRuntimeFunctions(runtimeFunctions); diff --git a/src/native/managed/cdac/tests/PrecodeStubsTests.cs b/src/native/managed/cdac/tests/PrecodeStubsTests.cs index 3d6bfef6dff061..5802b2368cf1dc 100644 --- a/src/native/managed/cdac/tests/PrecodeStubsTests.cs +++ b/src/native/managed/cdac/tests/PrecodeStubsTests.cs @@ -3,6 +3,7 @@ using Xunit; using Moq; +using Microsoft.DotNet.XUnitExtensions; using Microsoft.Diagnostics.DataContractReader.Contracts; using System.Collections.Generic; @@ -172,8 +173,10 @@ public static IEnumerable PrecodeTestDescriptorDataWithContractVersion { foreach (var data in PrecodeTestDescriptorData()) { - yield return new object[]{data[0], "c1"}; // Test v1 of the contract - yield return new object[]{data[0], "c2"}; // Test v2 of the contract + yield return new object[]{data[0], "c1"}; + yield return new object[]{data[0], "c2"}; + yield return new object[]{data[0], "c3"}; + } } @@ -207,6 +210,11 @@ internal class PrecodeBuilder { public CodePointerFlags CodePointerFlags {get; private set;} public string PrecodesVersion { get; } + + // V3-only fields + private byte[]? _v3StubBytes; + private const byte V3InterpreterPrecodeType = 0x06; + public PrecodeBuilder(MockTarget.Architecture arch, string precodesVersion) : this(DefaultAllocationRange, new MockMemorySpace.Builder(new TargetTestHelpers(arch)), precodesVersion) { } public PrecodeBuilder(AllocationRange allocationRange, MockMemorySpace.Builder builder, string precodesVersion, Dictionary? typeInfoCache = null) { @@ -214,22 +222,47 @@ public PrecodeBuilder(AllocationRange allocationRange, MockMemorySpace.Builder b PrecodesVersion = precodesVersion; PrecodeAllocator = builder.CreateAllocator(allocationRange.PrecodeDescriptorStart, allocationRange.PrecodeDescriptorEnd); StubDataPageAllocator = builder.CreateAllocator(allocationRange.StubDataPageStart, allocationRange.StubDataPageEnd); + if (precodesVersion == "c3") + { + _v3StubBytes = new byte[1]; + } Types = typeInfoCache ?? GetTypes(Builder.TargetTestHelpers); } public Dictionary GetTypes(TargetTestHelpers targetTestHelpers) { Dictionary types = new(); - var layout = targetTestHelpers.LayoutFields([ - new(nameof(Data.PrecodeMachineDescriptor.StubCodePageSize), DataType.uint32), - new(nameof(Data.PrecodeMachineDescriptor.OffsetOfPrecodeType), DataType.uint8), - new(nameof(Data.PrecodeMachineDescriptor.ReadWidthOfPrecodeType), DataType.uint8), - new(nameof(Data.PrecodeMachineDescriptor.ShiftOfPrecodeType), DataType.uint8), - new(nameof(Data.PrecodeMachineDescriptor.InvalidPrecodeType), DataType.uint8), - new(nameof(Data.PrecodeMachineDescriptor.StubPrecodeType), DataType.uint8), - new(nameof(Data.PrecodeMachineDescriptor.PInvokeImportPrecodeType), DataType.uint8), - new(nameof(Data.PrecodeMachineDescriptor.FixupPrecodeType), DataType.uint8), - new(nameof(Data.PrecodeMachineDescriptor.ThisPointerRetBufPrecodeType), DataType.uint8), - ]); + TargetTestHelpers.LayoutResult layout; + + if (PrecodesVersion == "c3") + { + layout = targetTestHelpers.LayoutFields([ + new(nameof(Data.PrecodeMachineDescriptor.StubCodePageSize), DataType.uint32), + new(nameof(Data.PrecodeMachineDescriptor.InvalidPrecodeType), DataType.uint8), + new(nameof(Data.PrecodeMachineDescriptor.StubPrecodeType), DataType.uint8), + new(nameof(Data.PrecodeMachineDescriptor.ThisPointerRetBufPrecodeType), DataType.uint8), + new(nameof(Data.PrecodeMachineDescriptor.StubPrecodeSize), DataType.uint8), + new(nameof(Data.PrecodeMachineDescriptor.StubBytes), DataType.uint8, 1u), + new(nameof(Data.PrecodeMachineDescriptor.StubIgnoredBytes), DataType.uint8, 1u), + new(nameof(Data.PrecodeMachineDescriptor.FixupStubPrecodeSize), DataType.uint8), + new(nameof(Data.PrecodeMachineDescriptor.FixupBytes), DataType.uint8, 1u), + new(nameof(Data.PrecodeMachineDescriptor.FixupIgnoredBytes), DataType.uint8, 1u), + new(nameof(Data.PrecodeMachineDescriptor.InterpreterPrecodeType), DataType.uint8), + ]); + } + else + { + layout = targetTestHelpers.LayoutFields([ + new(nameof(Data.PrecodeMachineDescriptor.StubCodePageSize), DataType.uint32), + new(nameof(Data.PrecodeMachineDescriptor.OffsetOfPrecodeType), DataType.uint8), + new(nameof(Data.PrecodeMachineDescriptor.ReadWidthOfPrecodeType), DataType.uint8), + new(nameof(Data.PrecodeMachineDescriptor.ShiftOfPrecodeType), DataType.uint8), + new(nameof(Data.PrecodeMachineDescriptor.InvalidPrecodeType), DataType.uint8), + new(nameof(Data.PrecodeMachineDescriptor.StubPrecodeType), DataType.uint8), + new(nameof(Data.PrecodeMachineDescriptor.PInvokeImportPrecodeType), DataType.uint8), + new(nameof(Data.PrecodeMachineDescriptor.FixupPrecodeType), DataType.uint8), + new(nameof(Data.PrecodeMachineDescriptor.ThisPointerRetBufPrecodeType), DataType.uint8), + ]); + } types[DataType.PrecodeMachineDescriptor] = new Target.TypeInfo() { Fields = layout.Fields, Size = layout.Stride, @@ -261,6 +294,39 @@ public PrecodeBuilder(AllocationRange allocationRange, MockMemorySpace.Builder b Size = layout.Stride, }; } + + if (PrecodesVersion == "c3") + { + layout = targetTestHelpers.LayoutFields([ + new(nameof(Data.InterpreterPrecodeData.Type), DataType.uint8), + new(nameof(Data.InterpreterPrecodeData.ByteCodeAddr), DataType.pointer), + new("Target", DataType.pointer), + ]); + types[DataType.InterpreterPrecodeData] = new Target.TypeInfo() + { + Fields = layout.Fields, + Size = layout.Stride, + }; + + layout = targetTestHelpers.LayoutFields([ + new(nameof(Data.InterpByteCodeStart.Method), DataType.pointer), + ]); + types[DataType.InterpByteCodeStart] = new Target.TypeInfo() + { + Fields = layout.Fields, + Size = layout.Stride, + }; + + layout = targetTestHelpers.LayoutFields([ + new(nameof(Data.InterpMethod.MethodDesc), DataType.pointer), + ]); + types[DataType.InterpMethod] = new Target.TypeInfo() + { + Fields = layout.Fields, + Size = layout.Stride, + }; + } + return types; } @@ -278,12 +344,30 @@ public void AddPlatformMetadata(PrecodeTestDescriptor descriptor) { var fragment = PrecodeAllocator.Allocate((ulong)typeInfo.Size, $"{descriptor.Name} Precode Machine Descriptor"); MachineDescriptorAddress = fragment.Address; Span desc = Builder.BorrowAddressRange(fragment.Address, (int)typeInfo.Size); - Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.ReadWidthOfPrecodeType)].Offset, sizeof(byte)), (byte)descriptor.ReadWidthOfPrecodeType); - Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.OffsetOfPrecodeType)].Offset, sizeof(byte)), (byte)descriptor.OffsetOfPrecodeType); - Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.ShiftOfPrecodeType)].Offset, sizeof(byte)), (byte)descriptor.ShiftOfPrecodeType); - Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.StubCodePageSize)].Offset, sizeof(uint)), descriptor.StubCodePageSize); - Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.StubPrecodeType)].Offset, sizeof(byte)), descriptor.StubPrecode); - Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.ThisPointerRetBufPrecodeType)].Offset, sizeof(byte)), descriptor.ThisPtrRetBufPrecode); + + if (PrecodesVersion == "c3") + { + _v3StubBytes![0] = descriptor.StubPrecode; + Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.StubCodePageSize)].Offset, sizeof(uint)), descriptor.StubCodePageSize); + Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.StubPrecodeType)].Offset, sizeof(byte)), descriptor.StubPrecode); + Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.ThisPointerRetBufPrecodeType)].Offset, sizeof(byte)), descriptor.ThisPtrRetBufPrecode); + Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.InterpreterPrecodeType)].Offset, sizeof(byte)), V3InterpreterPrecodeType); + Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.StubPrecodeSize)].Offset, sizeof(byte)), (byte)_v3StubBytes.Length); + _v3StubBytes.CopyTo(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.StubBytes)].Offset, _v3StubBytes.Length)); + desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.StubIgnoredBytes)].Offset, _v3StubBytes.Length).Fill(0); + Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.FixupStubPrecodeSize)].Offset, sizeof(byte)), (byte)1); + Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.FixupBytes)].Offset, sizeof(byte)), (byte)0xFE); + desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.FixupIgnoredBytes)].Offset, 1).Fill(0); + } + else + { + Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.ReadWidthOfPrecodeType)].Offset, sizeof(byte)), (byte)descriptor.ReadWidthOfPrecodeType); + Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.OffsetOfPrecodeType)].Offset, sizeof(byte)), (byte)descriptor.OffsetOfPrecodeType); + Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.ShiftOfPrecodeType)].Offset, sizeof(byte)), (byte)descriptor.ShiftOfPrecodeType); + Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.StubCodePageSize)].Offset, sizeof(uint)), descriptor.StubCodePageSize); + Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.StubPrecodeType)].Offset, sizeof(byte)), descriptor.StubPrecode); + Builder.TargetTestHelpers.Write(desc.Slice(typeInfo.Fields[nameof(Data.PrecodeMachineDescriptor.ThisPointerRetBufPrecodeType)].Offset, sizeof(byte)), descriptor.ThisPtrRetBufPrecode); + } // FIXME: set the other fields } @@ -299,7 +383,10 @@ public TargetCodePointer AddStubPrecodeEntry(string name, PrecodeTestDescriptor Data = new byte[stubCodeSize], Name = $"Stub code for {name} on {test.Name} with data at 0x{stubDataFragment.Address:x}", }; - test.WritePrecodeType(test.StubPrecode, Builder.TargetTestHelpers, stubCodeFragment.Data); + if (PrecodesVersion == "c3") + _v3StubBytes!.CopyTo(stubCodeFragment.Data.AsSpan()); + else + test.WritePrecodeType(test.StubPrecode, Builder.TargetTestHelpers, stubCodeFragment.Data); Builder.AddHeapFragment(stubCodeFragment); Span stubData = Builder.BorrowAddressRange(stubDataFragment.Address, (int)stubDataTypeInfo.Size); @@ -332,7 +419,10 @@ public TargetCodePointer AddThisPtrRetBufPrecodeEntry(string name, PrecodeTestDe Data = new byte[stubCodeSize], Name = $"Stub code for {name} on {test.Name} with data at 0x{stubDataFragment.Address:x}", }; - test.WritePrecodeType(test.StubPrecode, Builder.TargetTestHelpers, stubCodeFragment.Data); + if (PrecodesVersion == "c3") + _v3StubBytes!.CopyTo(stubCodeFragment.Data.AsSpan()); + else + test.WritePrecodeType(test.StubPrecode, Builder.TargetTestHelpers, stubCodeFragment.Data); Builder.AddHeapFragment(stubCodeFragment); Span thisPtrStubData = Builder.BorrowAddressRange(thisPtrRetBufStubDataFragment.Address, (int)thisPtrRetBufDataTypeInfo.Size); @@ -350,6 +440,43 @@ public TargetCodePointer AddThisPtrRetBufPrecodeEntry(string name, PrecodeTestDe } return address; } + + public TargetCodePointer AddInterpreterPrecodeEntry(string name, TargetPointer methodDesc, uint stubCodePageSize) + { + var interpPrecodeTypeInfo = Types[DataType.InterpreterPrecodeData]; + var interpByteCodeStartTypeInfo = Types[DataType.InterpByteCodeStart]; + var interpMethodTypeInfo = Types[DataType.InterpMethod]; + + MockMemorySpace.HeapFragment interpMethodFragment = StubDataPageAllocator.Allocate((ulong)interpMethodTypeInfo.Size, $"InterpMethod for {name}"); + Builder.AddHeapFragment(interpMethodFragment); + Span interpMethodData = Builder.BorrowAddressRange(interpMethodFragment.Address, (int)interpMethodTypeInfo.Size); + Builder.TargetTestHelpers.WritePointer(interpMethodData.Slice(interpMethodTypeInfo.Fields[nameof(Data.InterpMethod.MethodDesc)].Offset, Builder.TargetTestHelpers.PointerSize), methodDesc); + + MockMemorySpace.HeapFragment byteCodeStartFragment = StubDataPageAllocator.Allocate((ulong)interpByteCodeStartTypeInfo.Size, $"InterpByteCodeStart for {name}"); + Builder.AddHeapFragment(byteCodeStartFragment); + Span byteCodeStartData = Builder.BorrowAddressRange(byteCodeStartFragment.Address, (int)interpByteCodeStartTypeInfo.Size); + Builder.TargetTestHelpers.WritePointer(byteCodeStartData.Slice(interpByteCodeStartTypeInfo.Fields[nameof(Data.InterpByteCodeStart.Method)].Offset, Builder.TargetTestHelpers.PointerSize), interpMethodFragment.Address); + + ulong stubCodeSize = (ulong)Math.Max(_v3StubBytes!.Length, (int)interpPrecodeTypeInfo.Size); + MockMemorySpace.HeapFragment stubDataFragment = StubDataPageAllocator.Allocate(Math.Max((ulong)interpPrecodeTypeInfo.Size, stubCodeSize), $"Interp precode data for {name}"); + Builder.AddHeapFragment(stubDataFragment); + + ulong stubCodeStart = stubDataFragment.Address - stubCodePageSize; + MockMemorySpace.HeapFragment stubCodeFragment = new MockMemorySpace.HeapFragment + { + Address = stubCodeStart, + Data = new byte[stubCodeSize], + Name = $"Interp stub code for {name} at data 0x{stubDataFragment.Address:x}", + }; + _v3StubBytes.CopyTo(stubCodeFragment.Data.AsSpan()); + Builder.AddHeapFragment(stubCodeFragment); + + Span stubData = Builder.BorrowAddressRange(stubDataFragment.Address, (int)interpPrecodeTypeInfo.Size); + Builder.TargetTestHelpers.Write(stubData.Slice(interpPrecodeTypeInfo.Fields[nameof(Data.InterpreterPrecodeData.Type)].Offset, sizeof(byte)), V3InterpreterPrecodeType); + Builder.TargetTestHelpers.WritePointer(stubData.Slice(interpPrecodeTypeInfo.Fields[nameof(Data.InterpreterPrecodeData.ByteCodeAddr)].Offset, Builder.TargetTestHelpers.PointerSize), byteCodeStartFragment.Address); + + return stubCodeFragment.Address; + } } private static Target CreateTarget(PrecodeBuilder precodeBuilder) @@ -401,4 +528,26 @@ public void TestPrecodeStubPrecodeExpectedMethodDesc(PrecodeTestDescriptor test, Assert.Equal(expectedMethodDesc2, actualMethodDesc2); } } + + [ConditionalTheory] + [MemberData(nameof(PrecodeTestDescriptorDataWithContractVersion))] + public void TestInterpreterPrecodeReturnsExpectedMethodDesc(PrecodeTestDescriptor test, string contractVersion) + { + if (contractVersion != "c3") + throw new SkipTestException("Interpreter precodes are only supported in contract version c3 and above."); + + var builder = new PrecodeBuilder(test.Arch, contractVersion); + builder.AddPlatformMetadata(test); + + TargetPointer expectedMethodDesc = new TargetPointer(0xdead_bee0u); + TargetCodePointer interpStub = builder.AddInterpreterPrecodeEntry("Interp 1", expectedMethodDesc, test.StubCodePageSize); + + var target = CreateTarget(builder); + var precodeContract = target.Contracts.PrecodeStubs; + + Assert.NotNull(precodeContract); + + var actualMethodDesc = precodeContract.GetMethodDescFromStubAddress(interpStub); + Assert.Equal(expectedMethodDesc, actualMethodDesc); + } } diff --git a/src/native/managed/cdac/tests/SOSDacInterface5Tests.cs b/src/native/managed/cdac/tests/SOSDacInterface5Tests.cs index 745c1609acfc18..5fd4e20d3ebb39 100644 --- a/src/native/managed/cdac/tests/SOSDacInterface5Tests.cs +++ b/src/native/managed/cdac/tests/SOSDacInterface5Tests.cs @@ -104,12 +104,24 @@ private static ISOSDacInterface5 CreateDac5( .Setup(c => c.GetNativeCodeVersions(s_methodDescAddr, It.IsAny())) .Returns(nativeVersionHandles); + var mockPrecodeStubs = new Mock(); + mockPrecodeStubs + .Setup(p => p.GetInterpreterCodeFromInterpreterPrecodeIfPresent(It.IsAny())) + .Returns((TargetCodePointer ep) => ep); + + var mockPlatformMetadata = new Mock(); + mockPlatformMetadata + .Setup(p => p.GetCodePointerFlags()) + .Returns(default(CodePointerFlags)); + var target = new TestPlaceholderTarget.Builder(arch) .UseReader((_, _) => -1) .AddMockContract(mockCodeVersions) .AddMockContract(mockRts) .AddMockContract(mockLoader) .AddMockContract(mockReJIT) + .AddMockContract(mockPrecodeStubs) + .AddMockContract(mockPlatformMetadata) .Build(); return new SOSDacImpl(target, legacyObj: null); From f56b74175ca54ef8f37266bcf155b10dee0b530d Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:27:56 -0400 Subject: [PATCH 2/9] Add interpreter dump test infrastructure - Build cDAC dump tests with Checked runtime for FEATURE_INTERPRETER - Pass debuggee EnvironmentVariables through Helix dump generation - Fix env var leaking between Helix debuggees - Skip interpreter dump tests when FEATURE_INTERPRETER not enabled --- eng/pipelines/runtime-diagnostics.yml | 6 +++--- .../DumpTests/InterpreterStackDumpTests.cs | 17 +++++++++++++++++ ...gnostics.DataContractReader.DumpTests.csproj | 2 +- .../cdac/tests/DumpTests/cdac-dump-helix.proj | 8 +++++++- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/eng/pipelines/runtime-diagnostics.yml b/eng/pipelines/runtime-diagnostics.yml index f6a3f9215fdd0b..b2e15db0a61e6a 100644 --- a/eng/pipelines/runtime-diagnostics.yml +++ b/eng/pipelines/runtime-diagnostics.yml @@ -262,7 +262,7 @@ extends: - template: /eng/pipelines/common/platform-matrix.yml parameters: jobTemplate: /eng/pipelines/common/global-build-job.yml - buildConfig: release + buildConfig: checked platforms: ${{ parameters.cdacDumpPlatforms }} shouldContinueOnError: true jobParameters: @@ -306,7 +306,7 @@ extends: - template: /eng/pipelines/common/platform-matrix.yml parameters: jobTemplate: /eng/pipelines/common/global-build-job.yml - buildConfig: release + buildConfig: checked platforms: ${{ parameters.cdacDumpPlatforms }} shouldContinueOnError: true jobParameters: @@ -353,7 +353,7 @@ extends: - template: /eng/pipelines/common/platform-matrix.yml parameters: jobTemplate: /eng/pipelines/common/global-build-job.yml - buildConfig: release + buildConfig: checked platforms: ${{ parameters.cdacDumpPlatforms }} shouldContinueOnError: true jobParameters: diff --git a/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs index 81ff08d12284bf..18a7e358422b6d 100644 --- a/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs @@ -1,8 +1,10 @@ // 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.Linq; using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.DotNet.XUnitExtensions; using Xunit; namespace Microsoft.Diagnostics.DataContractReader.DumpTests; @@ -22,6 +24,18 @@ public class InterpreterStackDumpTests : DumpTestBase protected override string DebuggeeName => "InterpreterStack"; protected override string DumpType => "full"; + private void SkipIfInterpreterNotAvailable() + { + try + { + Target.GetTypeInfo(DataType.InterpreterFrame); + } + catch (InvalidOperationException) + { + throw new SkipTestException("Interpreter support not available in this runtime build (FEATURE_INTERPRETER not enabled)."); + } + } + private void AssertInterpreted(ResolvedFrame f) { Assert.Equal("InterpreterFrame", f.FrameName); @@ -61,6 +75,7 @@ private void AssertJitted(ResolvedFrame f) public void StackWalk_VerifyInterleavedStackLayout(TestConfiguration config) { InitializeDumpTest(config); + SkipIfInterpreterNotAvailable(); ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); @@ -91,6 +106,7 @@ public void StackWalk_VerifyInterleavedStackLayout(TestConfiguration config) public void StackWalk_InterpreterMethodNativeCodeIsPrecode(TestConfiguration config) { InitializeDumpTest(config); + SkipIfInterpreterNotAvailable(); IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; IExecutionManager executionManager = Target.Contracts.ExecutionManager; @@ -117,6 +133,7 @@ public void StackWalk_InterpreterMethodNativeCodeIsPrecode(TestConfiguration con public void Thread_CanEnumerateWithInterpreterFrames(TestConfiguration config) { InitializeDumpTest(config); + SkipIfInterpreterNotAvailable(); IThread threadContract = Target.Contracts.Thread; ThreadStoreData storeData = threadContract.GetThreadStoreData(); diff --git a/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj b/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj index 034f33e100abe8..adbdc09ae27a0c 100644 --- a/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj +++ b/src/native/managed/cdac/tests/DumpTests/Microsoft.Diagnostics.DataContractReader.DumpTests.csproj @@ -105,7 +105,7 @@ <_MetadataLines Include="<Project>" /> <_MetadataLines Include=" <ItemGroup>" /> - <_MetadataLines Include="@(_AllDebuggeeMetadata->' <_Debuggee Include="%(Identity)" DumpDir="%(DumpDir)" MiniDumpType="%(MiniDumpType)" R2RDir="%(R2RDir)" R2RValue="%(R2RValue)" />')" /> + <_MetadataLines Include="@(_AllDebuggeeMetadata->' <_Debuggee Include="%(Identity)" DumpDir="%(DumpDir)" MiniDumpType="%(MiniDumpType)" R2RDir="%(R2RDir)" R2RValue="%(R2RValue)" EnvironmentVariables="%(EnvironmentVariables)" />')" /> <_MetadataLines Include=" </ItemGroup>" /> <_MetadataLines Include="</Project>" /> diff --git a/src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj b/src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj index 8b511d859801fc..b0e1564717a03d 100644 --- a/src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj +++ b/src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj @@ -100,21 +100,27 @@ Include="mkdir %25HELIX_WORKITEM_PAYLOAD%25\dumps\local\%(_Debuggee.DumpDir)\%(_Debuggee.R2RDir)\%(_Debuggee.Identity)" /> <_HelixCommandLines Condition="'$(TargetOS)' == 'windows'" Include="echo Generating dump for %(_Debuggee.Identity) (%(_Debuggee.DumpDir)\%(_Debuggee.R2RDir))..." /> + <_HelixCommandLines Condition="'$(TargetOS)' == 'windows' AND '%(_Debuggee.EnvironmentVariables)' != ''" + Include="setlocal" /> <_HelixCommandLines Condition="'$(TargetOS)' == 'windows'" Include="set "DOTNET_DbgMiniDumpType=%(_Debuggee.MiniDumpType)"" /> <_HelixCommandLines Condition="'$(TargetOS)' == 'windows'" Include="set "DOTNET_DbgMiniDumpName=%25HELIX_WORKITEM_PAYLOAD%25\dumps\local\%(_Debuggee.DumpDir)\%(_Debuggee.R2RDir)\%(_Debuggee.Identity)\%(_Debuggee.Identity).dmp"" /> <_HelixCommandLines Condition="'$(TargetOS)' == 'windows'" Include="set "DOTNET_ReadyToRun=%(_Debuggee.R2RValue)"" /> + <_HelixCommandLines Condition="'$(TargetOS)' == 'windows' AND '%(_Debuggee.EnvironmentVariables)' != ''" + Include="set "$([System.String]::new('%(_Debuggee.EnvironmentVariables)').Replace(';', '" %26 set "'))"" /> <_HelixCommandLines Condition="'$(TargetOS)' == 'windows'" Include="%25HELIX_CORRELATION_PAYLOAD%25\dotnet.exe exec %25HELIX_WORKITEM_PAYLOAD%25\debuggees\%(_Debuggee.Identity)\%(_Debuggee.Identity).dll" /> + <_HelixCommandLines Condition="'$(TargetOS)' == 'windows' AND '%(_Debuggee.EnvironmentVariables)' != ''" + Include="endlocal" /> <_HelixCommandLines Condition="'$(TargetOS)' != 'windows'" Include="mkdir -p $HELIX_WORKITEM_PAYLOAD/dumps/local/%(_Debuggee.DumpDir)/%(_Debuggee.R2RDir)/%(_Debuggee.Identity)" /> <_HelixCommandLines Condition="'$(TargetOS)' != 'windows'" Include="echo " Generating dump for %(_Debuggee.Identity) (%(_Debuggee.DumpDir)/%(_Debuggee.R2RDir))..."" /> <_HelixCommandLines Condition="'$(TargetOS)' != 'windows'" - Include="DOTNET_DbgMiniDumpType=%(_Debuggee.MiniDumpType) DOTNET_DbgMiniDumpName=$HELIX_WORKITEM_PAYLOAD/dumps/local/%(_Debuggee.DumpDir)/%(_Debuggee.R2RDir)/%(_Debuggee.Identity)/%(_Debuggee.Identity).dmp DOTNET_ReadyToRun=%(_Debuggee.R2RValue) $HELIX_CORRELATION_PAYLOAD/dotnet exec $HELIX_WORKITEM_PAYLOAD/debuggees/%(_Debuggee.Identity)/%(_Debuggee.Identity).dll || true" /> + Include="$([System.String]::new('%(_Debuggee.EnvironmentVariables)').Replace(';', ' ')) DOTNET_DbgMiniDumpType=%(_Debuggee.MiniDumpType) DOTNET_DbgMiniDumpName=$HELIX_WORKITEM_PAYLOAD/dumps/local/%(_Debuggee.DumpDir)/%(_Debuggee.R2RDir)/%(_Debuggee.Identity)/%(_Debuggee.Identity).dmp DOTNET_ReadyToRun=%(_Debuggee.R2RValue) $HELIX_CORRELATION_PAYLOAD/dotnet exec $HELIX_WORKITEM_PAYLOAD/debuggees/%(_Debuggee.Identity)/%(_Debuggee.Identity).dll || true" /> From aae8ed76db151bfc2d7de03410265aa846b34e02 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Mon, 20 Apr 2026 21:41:23 -0400 Subject: [PATCH 3/9] Address review feedback: add GetCodeForInterpreterOrJitted API and consolidate helpers - Add GetCodeForInterpreterOrJitted to IRuntimeTypeSystem to unify the pattern of getting native code then resolving interpreter precodes - Add ResolveInterpreterPrecode private helper in SOSDacImpl to replace 3 verbose call sites - Use GetCodeForInterpreterOrJitted in ClrDataMethodInstance and InterpreterStackDumpTests - Collapse duplicate GetMethodInfo pseudocode in ExecutionManager.md - Fix double-registration of BumpAllocator fragments in tests - Fix int->string contract version parameters after rebase Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/ExecutionManager.md | 37 ++++--------------- .../Contracts/IRuntimeTypeSystem.cs | 8 ++++ .../Contracts/RuntimeTypeSystem_1.cs | 6 +++ .../ClrDataMethodInstance.cs | 5 +-- .../SOSDacImpl.cs | 15 ++++---- .../DumpTests/InterpreterStackDumpTests.cs | 7 +--- .../ExecutionManager/ExecutionManagerTests.cs | 4 +- .../managed/cdac/tests/FrameIteratorTests.cs | 14 ------- .../managed/cdac/tests/PrecodeStubsTests.cs | 5 +-- 9 files changed, 35 insertions(+), 66 deletions(-) diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index ae41bf98b6b551..aee7a07a1add9f 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -263,9 +263,11 @@ The bulk of the work is done by the `GetCodeBlockHandle` API that maps a code po } ``` -There are two JIT managers: the "EE JitManager" for jitted code and "R2R JitManager" for ReadyToRun code. +There are three JIT managers: the "EE JitManager" for jitted code, the "Interpreter JitManager" for interpreter code, and the "R2R JitManager" for ReadyToRun code. -The EE JitManager `GetMethodInfo` implements the nibble map lookup, summarized below, followed by returning the `RealCodeHeader` data: +The EE JitManager and Interpreter JitManager both use the same nibble map lookup to find method code. +The only difference is which code header type is read: the EE JitManager reads a `RealCodeHeader` while the Interpreter JitManager reads an `InterpreterRealCodeHeader`. +Their shared `GetMethodInfo` is summarized below: ```csharp bool GetMethodInfo(TargetPointer rangeSection, TargetCodePointer jittedCodeAddress, [NotNullWhen(true)] out CodeBlock? info) @@ -284,33 +286,10 @@ bool GetMethodInfo(TargetPointer rangeSection, TargetCodePointer jittedCodeAddre return false; TargetPointer codeHeaderAddress = Target.ReadPointer(codeHeaderIndirect); - TargetPointer methodDesc = Target.ReadPointer(codeHeaderAddress + /* RealCodeHeader::MethodDesc offset */); - info = new CodeBlock(jittedCodeAddress, realCodeHeader.MethodDesc, relativeOffset); - return true; -} -``` - -The Interpreter JitManager `GetMethodInfo` uses the same nibble map lookup as the EE JitManager, but reads an `InterpreterRealCodeHeader` instead of a `RealCodeHeader`: - -```csharp -bool GetMethodInfo(TargetPointer rangeSection, TargetCodePointer jittedCodeAddress, [NotNullWhen(true)] out CodeBlock? info) -{ - info = default; - TargetPointer start = // look up jittedCodeAddress in nibble map for rangeSection - see NibbleMap below - if (start == TargetPointer.Null) - return false; - - TargetNUInt relativeOffset = jittedCodeAddress - start; - int codeHeaderOffset = Target.PointerSize; - TargetPointer codeHeaderIndirect = start - codeHeaderOffset; - - // Check if address is in a stub code block - if (codeHeaderIndirect < Target.ReadGlobal("StubCodeBlockLast")) - return false; - - TargetPointer codeHeaderAddress = Target.ReadPointer(codeHeaderIndirect); - Data.InterpreterRealCodeHeader realCodeHeader = // read InterpreterRealCodeHeader at codeHeaderAddress - info = new CodeBlock(jittedCodeAddress, realCodeHeader.MethodDesc, relativeOffset); + // EE JitManager: read RealCodeHeader at codeHeaderAddress + // Interpreter JitManager: read InterpreterRealCodeHeader at codeHeaderAddress + TargetPointer methodDesc = // read MethodDesc field from the appropriate code header + info = new CodeBlock(jittedCodeAddress, methodDesc, relativeOffset); return true; } ``` diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs index 8cc74fc5d1ee72..0b872eaf9284be 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs @@ -214,6 +214,14 @@ public interface IRuntimeTypeSystem : IContract TargetPointer GetMethodDescVersioningState(MethodDescHandle methodDesc) => throw new NotImplementedException(); TargetCodePointer GetNativeCode(MethodDescHandle methodDesc) => throw new NotImplementedException(); + + /// + /// Returns the jitted or interpreter code address for the given method. + /// If the method has an interpreter precode, resolves it to the actual interpreter code address. + /// Mirrors MethodDesc::GetCodeForInterpreterOrJitted() in the runtime. + /// + TargetCodePointer GetCodeForInterpreterOrJitted(MethodDescHandle methodDesc) => throw new NotImplementedException(); + TargetCodePointer GetMethodEntryPointIfExists(MethodDescHandle methodDesc) => throw new NotImplementedException(); ushort GetSlotNumber(MethodDescHandle methodDesc) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index 27e971fc0cd615..9f989919a51329 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -1681,6 +1681,12 @@ TargetCodePointer IRuntimeTypeSystem.GetNativeCode(MethodDescHandle methodDescHa return GetStableEntryPoint(md); } + TargetCodePointer IRuntimeTypeSystem.GetCodeForInterpreterOrJitted(MethodDescHandle methodDescHandle) + { + TargetCodePointer nativeCode = ((IRuntimeTypeSystem)this).GetNativeCode(methodDescHandle); + return _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode); + } + TargetCodePointer IRuntimeTypeSystem.GetMethodEntryPointIfExists(MethodDescHandle methodDescHandle) { MethodDesc md = _methodDescs[methodDescHandle.Address]; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodInstance.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodInstance.cs index 5c4e1f025c023c..26d3f8a3d4361e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodInstance.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodInstance.cs @@ -287,10 +287,7 @@ int IXCLRDataMethodInstance.GetILAddressMap(uint mapLen, uint* mapNeeded, [In, O try { - TargetCodePointer pCode = _target.Contracts.RuntimeTypeSystem.GetNativeCode(_methodDesc); - // Resolve interpreter precode to actual interpreter code address if present. - // Mirrors GetInterpreterCodeFromInterpreterPrecodeIfPresent in daccess.cpp:5631-5694. - pCode = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(pCode); + TargetCodePointer pCode = _target.Contracts.RuntimeTypeSystem.GetCodeForInterpreterOrJitted(_methodDesc); TargetPointer codeStart = pCode.ToAddress(_target); // No debug info exists at all (e.g. ILStubs). diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index b82b2271efb3f4..b94ee7510f0094 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -2205,6 +2205,10 @@ private TargetPointer DecodeJump64(TargetPointer pThunk) return _target.ReadPointer(pThunk + 2); } + + private TargetCodePointer ResolveInterpreterPrecode(TargetCodePointer nativeCode) + => _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode); + int ISOSDacInterface.GetJumpThunkTarget(void* ctx, ClrDataAddress* targetIP, ClrDataAddress* targetMD) { int hr = HResults.S_OK; @@ -2307,9 +2311,7 @@ int ISOSDacInterface.GetMethodDescData(ClrDataAddress addr, ClrDataAddress ip, D if (nativeCodeAddr != TargetCodePointer.Null) { data->bHasNativeCode = 1; - // Resolve interpreter precode to actual interpreter code address if present. - TargetCodePointer resolvedAddr = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCodeAddr); - data->NativeCodeAddr = resolvedAddr.ToAddress(_target).ToClrDataAddress(_target); + data->NativeCodeAddr = ResolveInterpreterPrecode(nativeCodeAddr).ToAddress(_target).ToClrDataAddress(_target); } else { @@ -2521,9 +2523,8 @@ private void CopyNativeCodeVersionToReJitData( ILCodeVersionHandle ilCodeVersion = cv.GetILCodeVersion(nativeCodeVersion); pReJitData->rejitID = rejit.GetRejitId(ilCodeVersion).Value; - // Resolve interpreter precode to actual interpreter code address if present. TargetCodePointer nativeCode = cv.GetNativeCode(nativeCodeVersion); - pReJitData->NativeCodeAddr = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode).Value; + pReJitData->NativeCodeAddr = ResolveInterpreterPrecode(nativeCode).Value; if (nativeCodeVersion.CodeVersionNodeAddress != activeNativeCodeVersion.CodeVersionNodeAddress || nativeCodeVersion.MethodDescAddress != activeNativeCodeVersion.MethodDescAddress) @@ -5222,9 +5223,7 @@ int ISOSDacInterface5.GetTieredVersions( int count = 0; foreach (NativeCodeVersionHandle nativeCodeVersionHandle in codeVersions.GetNativeCodeVersions(methodDescPtr, ilCodeVersionHandle)) { - // Resolve interpreter precode to actual interpreter code address if present. - TargetCodePointer nativeCode = codeVersions.GetNativeCode(nativeCodeVersionHandle); - nativeCode = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode); + TargetCodePointer nativeCode = ResolveInterpreterPrecode(codeVersions.GetNativeCode(nativeCodeVersionHandle)); TargetPointer nativeCodeAddr = nativeCode.ToAddress(_target); nativeCodeAddrs[count].nativeCodeAddr = nativeCodeAddr.ToClrDataAddress(_target); nativeCodeAddrs[count].nativeCodeVersionNodePtr = nativeCodeVersionHandle.CodeVersionNodeAddress.ToClrDataAddress(_target); diff --git a/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs index 18a7e358422b6d..0945a10e10e9c1 100644 --- a/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs @@ -44,12 +44,9 @@ private void AssertInterpreted(ResolvedFrame f) IExecutionManager executionManager = Target.Contracts.ExecutionManager; MethodDescHandle md = rts.GetMethodDescHandle(f.MethodDescPtr); - TargetCodePointer nativeCode = rts.GetNativeCode(md); - Assert.NotEqual(TargetCodePointer.Null, nativeCode); + TargetCodePointer resolvedCode = rts.GetCodeForInterpreterOrJitted(md); + Assert.NotEqual(TargetCodePointer.Null, resolvedCode); - // The native code address for interpreter methods is a precode address. - // Resolve it to the actual interpreter code address before looking up the code block. - TargetCodePointer resolvedCode = Target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode); CodeBlockHandle? codeBlock = executionManager.GetCodeBlockHandle(resolvedCode); Assert.NotNull(codeBlock); Assert.Equal(JitType.Interpreter, executionManager.GetJITType(codeBlock.Value)); diff --git a/src/native/managed/cdac/tests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdac/tests/ExecutionManager/ExecutionManagerTests.cs index 76de54317c93b5..548111e4896d9d 100644 --- a/src/native/managed/cdac/tests/ExecutionManager/ExecutionManagerTests.cs +++ b/src/native/managed/cdac/tests/ExecutionManager/ExecutionManagerTests.cs @@ -671,7 +671,7 @@ public static IEnumerable StdArchAllVersions() [Theory] [MemberData(nameof(StdArchAllVersions))] - public void GetMethodDesc_InterpreterOneMethod(int version, MockTarget.Architecture arch) + public void GetMethodDesc_InterpreterOneMethod(string version, MockTarget.Architecture arch) { const ulong codeRangeStart = 0x0a0a_0000u; const uint codeRangeSize = 0xc000u; @@ -713,7 +713,7 @@ public void GetMethodDesc_InterpreterOneMethod(int version, MockTarget.Architect [Theory] [MemberData(nameof(StdArchAllVersions))] - public void GetCodeBlockHandle_InterpreterPrecode_ReturnsNull(int version, MockTarget.Architecture arch) + public void GetCodeBlockHandle_InterpreterPrecode_ReturnsNull(string version, MockTarget.Architecture arch) { const ulong precodeRangeStart = 0x0b0b_0000u; const uint precodeRangeSize = 0x1000u; diff --git a/src/native/managed/cdac/tests/FrameIteratorTests.cs b/src/native/managed/cdac/tests/FrameIteratorTests.cs index 3834a2b6539b81..a8cbab277a28f0 100644 --- a/src/native/managed/cdac/tests/FrameIteratorTests.cs +++ b/src/native/managed/cdac/tests/FrameIteratorTests.cs @@ -165,11 +165,6 @@ public void GetMethodDescPtr_InterpreterFrame_FollowsFullChain(MockTarget.Archit helpers.WritePointer(frameFrag.Data.AsSpan(frameNextOffset, pointerSize), terminator); helpers.WritePointer(frameFrag.Data.AsSpan(topContextFrameOffset, pointerSize), contextFrameFrag.Address); - builder.MemoryBuilder.AddHeapFragment(interpMethodFrag); - builder.MemoryBuilder.AddHeapFragment(byteCodeStartFrag); - builder.MemoryBuilder.AddHeapFragment(contextFrameFrag); - builder.MemoryBuilder.AddHeapFragment(frameFrag); - builder.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); var target = builder.Build(); @@ -204,7 +199,6 @@ public void GetMethodDescPtr_InterpreterFrame_NullContextFrame_ReturnsNull(MockT helpers.WritePointer(frameFrag.Data.AsSpan(frameNextOffset, pointerSize), terminator); helpers.WritePointer(frameFrag.Data.AsSpan(topContextFrameOffset, pointerSize), 0); - builder.MemoryBuilder.AddHeapFragment(frameFrag); builder.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); var target = builder.Build(); @@ -247,8 +241,6 @@ public void GetMethodDescPtr_InterpreterFrame_NullStartIp_ReturnsNull(MockTarget helpers.WritePointer(frameFrag.Data.AsSpan(frameNextOffset, pointerSize), terminator); helpers.WritePointer(frameFrag.Data.AsSpan(topContextFrameOffset, pointerSize), contextFrameFrag.Address); - builder.MemoryBuilder.AddHeapFragment(contextFrameFrag); - builder.MemoryBuilder.AddHeapFragment(frameFrag); builder.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); var target = builder.Build(); @@ -296,9 +288,6 @@ public void GetMethodDescPtr_InterpreterFrame_NullMethod_ReturnsNull(MockTarget. helpers.WritePointer(frameFrag.Data.AsSpan(frameNextOffset, pointerSize), terminator); helpers.WritePointer(frameFrag.Data.AsSpan(topContextFrameOffset, pointerSize), contextFrameFrag.Address); - builder.MemoryBuilder.AddHeapFragment(byteCodeStartFrag); - builder.MemoryBuilder.AddHeapFragment(contextFrameFrag); - builder.MemoryBuilder.AddHeapFragment(frameFrag); builder.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); var target = builder.Build(); @@ -354,9 +343,6 @@ MockMemorySpace.HeapFragment CreateContextChainEntry(ulong methodDesc, ulong par var contextFrameB = CreateContextChainEntry(methodDescB, contextFrameA.Address, out var interpMethodB, out var byteCodeStartB); var contextFrameC = CreateContextChainEntry(methodDescC, contextFrameB.Address, out var interpMethodC, out var byteCodeStartC); - foreach (var frag in new[] { interpMethodA, byteCodeStartA, contextFrameA, interpMethodB, byteCodeStartB, contextFrameB, interpMethodC, byteCodeStartC, contextFrameC }) - builder.MemoryBuilder.AddHeapFragment(frag); - var target = builder.Build(); // Resolve each context frame individually — verifies the chain links resolve to distinct MethodDescs diff --git a/src/native/managed/cdac/tests/PrecodeStubsTests.cs b/src/native/managed/cdac/tests/PrecodeStubsTests.cs index 5802b2368cf1dc..b79c2b23e1388c 100644 --- a/src/native/managed/cdac/tests/PrecodeStubsTests.cs +++ b/src/native/managed/cdac/tests/PrecodeStubsTests.cs @@ -448,20 +448,17 @@ public TargetCodePointer AddInterpreterPrecodeEntry(string name, TargetPointer m var interpMethodTypeInfo = Types[DataType.InterpMethod]; MockMemorySpace.HeapFragment interpMethodFragment = StubDataPageAllocator.Allocate((ulong)interpMethodTypeInfo.Size, $"InterpMethod for {name}"); - Builder.AddHeapFragment(interpMethodFragment); Span interpMethodData = Builder.BorrowAddressRange(interpMethodFragment.Address, (int)interpMethodTypeInfo.Size); Builder.TargetTestHelpers.WritePointer(interpMethodData.Slice(interpMethodTypeInfo.Fields[nameof(Data.InterpMethod.MethodDesc)].Offset, Builder.TargetTestHelpers.PointerSize), methodDesc); MockMemorySpace.HeapFragment byteCodeStartFragment = StubDataPageAllocator.Allocate((ulong)interpByteCodeStartTypeInfo.Size, $"InterpByteCodeStart for {name}"); - Builder.AddHeapFragment(byteCodeStartFragment); Span byteCodeStartData = Builder.BorrowAddressRange(byteCodeStartFragment.Address, (int)interpByteCodeStartTypeInfo.Size); Builder.TargetTestHelpers.WritePointer(byteCodeStartData.Slice(interpByteCodeStartTypeInfo.Fields[nameof(Data.InterpByteCodeStart.Method)].Offset, Builder.TargetTestHelpers.PointerSize), interpMethodFragment.Address); ulong stubCodeSize = (ulong)Math.Max(_v3StubBytes!.Length, (int)interpPrecodeTypeInfo.Size); MockMemorySpace.HeapFragment stubDataFragment = StubDataPageAllocator.Allocate(Math.Max((ulong)interpPrecodeTypeInfo.Size, stubCodeSize), $"Interp precode data for {name}"); - Builder.AddHeapFragment(stubDataFragment); - ulong stubCodeStart = stubDataFragment.Address - stubCodePageSize; + ulong stubCodeStart= stubDataFragment.Address - stubCodePageSize; MockMemorySpace.HeapFragment stubCodeFragment = new MockMemorySpace.HeapFragment { Address = stubCodeStart, From c1cd2af7aabecc6875340ee904bfd5adab63ca5f Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:42:39 -0400 Subject: [PATCH 4/9] Fix CI pipeline: restore buildConfig to release for dump test stages During the rebase conflict resolution, buildConfig was accidentally changed from 'release' to 'checked' in the DumpCreation, DumpTest, and XPlatDumpTest pipeline stages. The -rc checked flag in buildArgs already ensures the CLR is built as Checked; buildConfig controls the overall build including libs which must remain Release. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/runtime-diagnostics.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/pipelines/runtime-diagnostics.yml b/eng/pipelines/runtime-diagnostics.yml index b2e15db0a61e6a..f6a3f9215fdd0b 100644 --- a/eng/pipelines/runtime-diagnostics.yml +++ b/eng/pipelines/runtime-diagnostics.yml @@ -262,7 +262,7 @@ extends: - template: /eng/pipelines/common/platform-matrix.yml parameters: jobTemplate: /eng/pipelines/common/global-build-job.yml - buildConfig: checked + buildConfig: release platforms: ${{ parameters.cdacDumpPlatforms }} shouldContinueOnError: true jobParameters: @@ -306,7 +306,7 @@ extends: - template: /eng/pipelines/common/platform-matrix.yml parameters: jobTemplate: /eng/pipelines/common/global-build-job.yml - buildConfig: checked + buildConfig: release platforms: ${{ parameters.cdacDumpPlatforms }} shouldContinueOnError: true jobParameters: @@ -353,7 +353,7 @@ extends: - template: /eng/pipelines/common/platform-matrix.yml parameters: jobTemplate: /eng/pipelines/common/global-build-job.yml - buildConfig: checked + buildConfig: release platforms: ${{ parameters.cdacDumpPlatforms }} shouldContinueOnError: true jobParameters: From e5a5c8c6d86dfefbeb08bbf889fa00282e0bb975 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:46:35 -0400 Subject: [PATCH 5/9] Fix comment and doc typos from review feedback - FrameIterator.cs: fix XML comment to use MethodDesc instead of methodHnd - PrecodeStubs.md: fix pseudocode to use TargetPointer.Null and TargetCodePointer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/PrecodeStubs.md | 4 ++-- .../Contracts/StackWalk/FrameHandling/FrameIterator.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/design/datacontracts/PrecodeStubs.md b/docs/design/datacontracts/PrecodeStubs.md index 226afc677621f4..47307b581e3ff7 100644 --- a/docs/design/datacontracts/PrecodeStubs.md +++ b/docs/design/datacontracts/PrecodeStubs.md @@ -339,10 +339,10 @@ After the initial precode type is determined, for stub precodes a refined precod TargetPointer dataAddr = instrPointer + MachineDescriptor.StubCodePageSize; Data.InterpreterPrecodeData precodeData = // read InterpreterPrecodeData at dataAddr - if (precodeData.ByteCodeAddr == null) + if (precodeData.ByteCodeAddr == TargetPointer.Null) return entryPoint; - return precodeData.ByteCodeAddr; + return new TargetCodePointer(precodeData.ByteCodeAddr); } catch { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs index 8ad0910c701bba..e40344fbf88cc5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs @@ -241,7 +241,7 @@ public static TargetPointer GetMethodDescPtr(Target target, TargetPointer frameP /// /// Resolves the MethodDesc from a specific InterpMethodContextFrame by following: - /// InterpMethodContextFrame.StartIp -> InterpByteCodeStart.Method -> InterpMethod.methodHnd + /// InterpMethodContextFrame.StartIp -> InterpByteCodeStart.Method -> InterpMethod.MethodDesc /// internal static TargetPointer ResolveMethodDescFromInterpFrame(Target target, TargetPointer interpMethodFramePtr) { From 63f4e8ec562842b1ffac1ad2011c9febfc8a5cc3 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Wed, 22 Apr 2026 10:44:14 -0400 Subject: [PATCH 6/9] Remove GetCodeForInterpreterOrJitted API and inline ResolveInterpreterPrecode Remove the GetCodeForInterpreterOrJitted contract API since it only had a single consumer. Instead, call GetNativeCode followed by GetInterpreterCodeFromInterpreterPrecodeIfPresent directly at each call site. Also inline the ResolveInterpreterPrecode private helper in SOSDacImpl since it was a single-line wrapper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/IRuntimeTypeSystem.cs | 7 ------- .../Contracts/RuntimeTypeSystem_1.cs | 6 ------ .../RuntimeTypeSystemHelpers/MethodValidation.cs | 2 +- .../ClrDataMethodInstance.cs | 3 ++- .../SOSDacImpl.cs | 9 +++------ .../cdac/tests/DumpTests/InterpreterStackDumpTests.cs | 3 ++- 6 files changed, 8 insertions(+), 22 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs index 0b872eaf9284be..e3873970e460c5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs @@ -215,13 +215,6 @@ public interface IRuntimeTypeSystem : IContract TargetCodePointer GetNativeCode(MethodDescHandle methodDesc) => throw new NotImplementedException(); - /// - /// Returns the jitted or interpreter code address for the given method. - /// If the method has an interpreter precode, resolves it to the actual interpreter code address. - /// Mirrors MethodDesc::GetCodeForInterpreterOrJitted() in the runtime. - /// - TargetCodePointer GetCodeForInterpreterOrJitted(MethodDescHandle methodDesc) => throw new NotImplementedException(); - TargetCodePointer GetMethodEntryPointIfExists(MethodDescHandle methodDesc) => throw new NotImplementedException(); ushort GetSlotNumber(MethodDescHandle methodDesc) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index 9f989919a51329..27e971fc0cd615 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -1681,12 +1681,6 @@ TargetCodePointer IRuntimeTypeSystem.GetNativeCode(MethodDescHandle methodDescHa return GetStableEntryPoint(md); } - TargetCodePointer IRuntimeTypeSystem.GetCodeForInterpreterOrJitted(MethodDescHandle methodDescHandle) - { - TargetCodePointer nativeCode = ((IRuntimeTypeSystem)this).GetNativeCode(methodDescHandle); - return _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode); - } - TargetCodePointer IRuntimeTypeSystem.GetMethodEntryPointIfExists(MethodDescHandle methodDescHandle) { MethodDesc md = _methodDescs[methodDescHandle.Address]; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodValidation.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodValidation.cs index 112dbd23202175..245fff0908cc5a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodValidation.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodValidation.cs @@ -221,7 +221,7 @@ internal bool ValidateMethodDescPointer(TargetPointer methodDescPointer, [NotNul // The NativeCodeSlot may point to a precode or portable entry point // (e.g., interpreter methods with FEATURE_PORTABLE_ENTRYPOINTS). // Try resolving via precode stubs as a fallback. - // See usage of GetCodeForInterpreterOrJitted in DacValidateMD for more details. + // See DacValidateMD for more details. Contracts.IPrecodeStubs precode = _target.Contracts.PrecodeStubs; TargetPointer methodDesc = precode.GetMethodDescFromStubAddress(jitCodeAddr); if (methodDesc != methodDescPointer) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodInstance.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodInstance.cs index 26d3f8a3d4361e..39cde3cda24c28 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodInstance.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodInstance.cs @@ -287,7 +287,8 @@ int IXCLRDataMethodInstance.GetILAddressMap(uint mapLen, uint* mapNeeded, [In, O try { - TargetCodePointer pCode = _target.Contracts.RuntimeTypeSystem.GetCodeForInterpreterOrJitted(_methodDesc); + TargetCodePointer nativeCode = _target.Contracts.RuntimeTypeSystem.GetNativeCode(_methodDesc); + TargetCodePointer pCode = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode); TargetPointer codeStart = pCode.ToAddress(_target); // No debug info exists at all (e.g. ILStubs). diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index b94ee7510f0094..8420c04df5a958 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -2206,9 +2206,6 @@ private TargetPointer DecodeJump64(TargetPointer pThunk) return _target.ReadPointer(pThunk + 2); } - private TargetCodePointer ResolveInterpreterPrecode(TargetCodePointer nativeCode) - => _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode); - int ISOSDacInterface.GetJumpThunkTarget(void* ctx, ClrDataAddress* targetIP, ClrDataAddress* targetMD) { int hr = HResults.S_OK; @@ -2311,7 +2308,7 @@ int ISOSDacInterface.GetMethodDescData(ClrDataAddress addr, ClrDataAddress ip, D if (nativeCodeAddr != TargetCodePointer.Null) { data->bHasNativeCode = 1; - data->NativeCodeAddr = ResolveInterpreterPrecode(nativeCodeAddr).ToAddress(_target).ToClrDataAddress(_target); + data->NativeCodeAddr = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCodeAddr).ToAddress(_target).ToClrDataAddress(_target); } else { @@ -2524,7 +2521,7 @@ private void CopyNativeCodeVersionToReJitData( pReJitData->rejitID = rejit.GetRejitId(ilCodeVersion).Value; TargetCodePointer nativeCode = cv.GetNativeCode(nativeCodeVersion); - pReJitData->NativeCodeAddr = ResolveInterpreterPrecode(nativeCode).Value; + pReJitData->NativeCodeAddr = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode).Value; if (nativeCodeVersion.CodeVersionNodeAddress != activeNativeCodeVersion.CodeVersionNodeAddress || nativeCodeVersion.MethodDescAddress != activeNativeCodeVersion.MethodDescAddress) @@ -5223,7 +5220,7 @@ int ISOSDacInterface5.GetTieredVersions( int count = 0; foreach (NativeCodeVersionHandle nativeCodeVersionHandle in codeVersions.GetNativeCodeVersions(methodDescPtr, ilCodeVersionHandle)) { - TargetCodePointer nativeCode = ResolveInterpreterPrecode(codeVersions.GetNativeCode(nativeCodeVersionHandle)); + TargetCodePointer nativeCode = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(codeVersions.GetNativeCode(nativeCodeVersionHandle)); TargetPointer nativeCodeAddr = nativeCode.ToAddress(_target); nativeCodeAddrs[count].nativeCodeAddr = nativeCodeAddr.ToClrDataAddress(_target); nativeCodeAddrs[count].nativeCodeVersionNodePtr = nativeCodeVersionHandle.CodeVersionNodeAddress.ToClrDataAddress(_target); diff --git a/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs index 0945a10e10e9c1..24167cd62e0c1c 100644 --- a/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs @@ -44,7 +44,8 @@ private void AssertInterpreted(ResolvedFrame f) IExecutionManager executionManager = Target.Contracts.ExecutionManager; MethodDescHandle md = rts.GetMethodDescHandle(f.MethodDescPtr); - TargetCodePointer resolvedCode = rts.GetCodeForInterpreterOrJitted(md); + TargetCodePointer nativeCode = rts.GetNativeCode(md); + TargetCodePointer resolvedCode = Target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode); Assert.NotEqual(TargetCodePointer.Null, resolvedCode); CodeBlockHandle? codeBlock = executionManager.GetCodeBlockHandle(resolvedCode); From 147360618cc4522e483755e518c7c59408c55289 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:01:55 -0400 Subject: [PATCH 7/9] Address janvorli feedback: resolve TopInterpMethodContextFrame hint and prevent double-walk - Add Ip and NextPtr fields to InterpMethodContextFrame data descriptor - Implement ResolveTopInterpMethodContextFrame() to scan chain for first active frame (non-null Ip), since the hint may point to an inactive frame - Add SkipNextInterpreterFrame flag to prevent double-walking interpreter frames when a debugger breakpoint fires in interpreted code - Add unit tests for hint resolution and double-walk prevention - Add dump test StackWalk_NoDoubledInterpreterFrames - Fix typo in ExecutionManager.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/ExecutionManager.md | 2 +- docs/design/datacontracts/StackWalk.md | 24 +- .../vm/datadescriptor/datadescriptor.inc | 2 + .../StackWalk/FrameHandling/FrameIterator.cs | 55 +++- .../Contracts/StackWalk/StackWalk_1.cs | 35 ++- .../Data/InterpMethodContextFrame.cs | 4 + .../DumpTests/InterpreterStackDumpTests.cs | 36 +++ .../managed/cdac/tests/FrameIteratorTests.cs | 282 ++++++++++++++++++ 8 files changed, 425 insertions(+), 15 deletions(-) diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index aee7a07a1add9f..f3d0722fc45db4 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -263,7 +263,7 @@ The bulk of the work is done by the `GetCodeBlockHandle` API that maps a code po } ``` -There are three JIT managers: the "EE JitManager" for jitted code, the "Interpreter JitManager" for interpreter code, and the "R2R JitManager" for ReadyToRun code. +There are three JIT managers: the "EE JitManager" for jitted code, the "Interpreter JitManager" for interpreted code, and the "R2R JitManager" for ReadyToRun code. The EE JitManager and Interpreter JitManager both use the same nibble map lookup to find method code. The only difference is which code header type is read: the EE JitManager reads a `RealCodeHeader` while the Interpreter JitManager reads an `InterpreterRealCodeHeader`. diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 88d3a540a249cc..3c7f10faa364ef 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -76,6 +76,8 @@ This contract depends on the following descriptors: | `InterpreterFrame` | `TopInterpMethodContextFrame` | Pointer to the InterpreterFrame's top `InterpMethodContextFrame` | | `InterpMethodContextFrame` | `StartIp` | Pointer to the `InterpByteCodeStart` for resolving the MethodDesc | | `InterpMethodContextFrame` | `ParentPtr` | Pointer to the parent `InterpMethodContextFrame` in the call chain (null for outermost frame) | +| `InterpMethodContextFrame` | `Ip` | The actual instruction pointer within the method (null if frame is inactive/reusable) | +| `InterpMethodContextFrame` | `NextPtr` | Pointer to the next `InterpMethodContextFrame` toward the top of the stack | | `ArgumentRegisters` (arm) | For each register `r` saved in ArgumentRegisters, `r` | Register names associated with stored register values | | `CalleeSavedRegisters` | For each callee saved register `r`, `r` | Register names associated with stored register values | | `TailCallFrame` (x86 Windows) | `CalleeSavedRegisters` | CalleeSavedRegisters data structure | @@ -124,21 +126,31 @@ In reality, the actual algorithm is a little more complex fow two reasons. It re #### Interpreter Frame Expansion -When the stack walker encounters an `InterpreterFrame`, it expands it into multiple logical frames by walking the `InterpMethodContextFrame.ParentPtr` chain. The runtime maintains a linked list of `InterpMethodContextFrame` nodes representing each interpreted method currently on the call stack within a single `InterpreterFrame`. The `TopInterpMethodContextFrame` field points to the most recently entered interpreted method, and each node's `ParentPtr` points to its caller. +When the stack walker encounters an `InterpreterFrame`, it expands it into multiple logical frames by walking the `InterpMethodContextFrame` chain. The runtime maintains a linked list of `InterpMethodContextFrame` nodes representing each interpreted method currently on the call stack within a single `InterpreterFrame`. -For each `InterpMethodContextFrame` in the chain, the stack walker yields a separate frame. The `MethodDesc` for each frame is resolved by following: +The `TopInterpMethodContextFrame` field is an approximate hint that may point to a stale frame during dump or native debugging. The actual top frame must be resolved using the `Ip` and `NextPtr`/`ParentPtr` fields, replicating `InterpreterFrame::GetTopInterpMethodContextFrame()`: + +- If the hinted frame's `Ip` is non-null (active): seek upward via `NextPtr` while the next frame's `Ip` is also non-null. +- If the hinted frame's `Ip` is null (inactive/reusable): seek downward via `ParentPtr` until finding a frame with non-null `Ip`. + +Only frames with non-null `Ip` (active frames) are yielded during the walk. Each node's `ParentPtr` points to its caller. + +For each active `InterpMethodContextFrame` in the chain, the stack walker yields a separate frame. The `MethodDesc` for each frame is resolved by following: `InterpMethodContextFrame.StartIp` -> `InterpByteCodeStart.Method` -> `InterpMethod.MethodDesc` ``` InterpreterFrame - └─ TopInterpMethodContextFrame -> InterpMethodContextFrame (method C) - └─ ParentPtr -> InterpMethodContextFrame (method B) - └─ ParentPtr -> InterpMethodContextFrame (method A) - └─ ParentPtr -> null + └-> TopInterpMethodContextFrame (hint, may be stale) + └-> ResolveTop() -> InterpMethodContextFrame (method C, Ip != null) + └-> ParentPtr -> InterpMethodContextFrame (method B, Ip != null) + └-> ParentPtr -> InterpMethodContextFrame (method A, Ip != null) + └-> ParentPtr -> null ``` This produces three frames in order: C, B, A (innermost to outermost). +When the stack walk starts with an explicit context in interpreted code (e.g., from a debugger breakpoint), the interpreted frames are already yielded from the initial context as frameless frames. When the walker subsequently encounters the corresponding `InterpreterFrame`, it skips expanding it to prevent the same frames from being walked twice. + #### Simple Example diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 5b2e784aa27f64..d70c5a7775950e 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -862,6 +862,8 @@ CDAC_TYPE_BEGIN(InterpMethodContextFrame) CDAC_TYPE_INDETERMINATE(InterpMethodContextFrame) CDAC_TYPE_FIELD(InterpMethodContextFrame, T_POINTER, StartIp, offsetof(InterpMethodContextFrame, startIp)) CDAC_TYPE_FIELD(InterpMethodContextFrame, T_POINTER, ParentPtr, offsetof(InterpMethodContextFrame, pParent)) +CDAC_TYPE_FIELD(InterpMethodContextFrame, T_POINTER, Ip, offsetof(InterpMethodContextFrame, ip)) +CDAC_TYPE_FIELD(InterpMethodContextFrame, T_POINTER, NextPtr, offsetof(InterpMethodContextFrame, pNext)) CDAC_TYPE_END(InterpMethodContextFrame) #endif // FEATURE_INTERPRETER diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs index e40344fbf88cc5..371f90fe61e3bb 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs @@ -208,7 +208,8 @@ public static TargetPointer GetMethodDescPtr(Target target, TargetPointer frameP case FrameType.InterpreterFrame: { Data.InterpreterFrame interpreterFrame = target.ProcessedData.GetOrAdd(frame.Address); - return ResolveMethodDescFromInterpFrame(target, interpreterFrame.TopInterpMethodContextFrame); + TargetPointer topContextFrame = ResolveTopInterpMethodContextFrame(target, interpreterFrame.TopInterpMethodContextFrame); + return ResolveMethodDescFromInterpFrame(target, topContextFrame); } case FrameType.PInvokeCalliFrame: return TargetPointer.Null; @@ -262,17 +263,61 @@ internal static TargetPointer ResolveMethodDescFromInterpFrame(Target target, Ta } /// - /// Walks the InterpMethodContextFrame.ParentPtr chain for an InterpreterFrame, - /// yielding one context frame pointer per interpreted method in the call chain. + /// Resolves the actual top InterpMethodContextFrame from the hint stored in InterpreterFrame, + /// replicating InterpreterFrame::GetTopInterpMethodContextFrame() from frames.cpp. + /// The stored TopInterpMethodContextFrame is only an approximate hint; during dump or native + /// debugging it may point to a stale frame. This method seeks to the correct top frame using + /// the Ip field (null = inactive, non-null = active) and the NextPtr/ParentPtr chains. + /// + internal static TargetPointer ResolveTopInterpMethodContextFrame(Target target, TargetPointer hintPtr) + { + if (hintPtr == TargetPointer.Null) + return TargetPointer.Null; + + Data.InterpMethodContextFrame frame = target.ProcessedData.GetOrAdd(hintPtr); + TargetPointer currentPtr = hintPtr; + + if (frame.Ip != TargetPointer.Null) + { + // Active frame — seek upward via NextPtr while next frame is also active + while (frame.NextPtr != TargetPointer.Null) + { + Data.InterpMethodContextFrame next = target.ProcessedData.GetOrAdd(frame.NextPtr); + if (next.Ip == TargetPointer.Null) + break; + currentPtr = frame.NextPtr; + frame = next; + } + } + else + { + // Inactive frame — seek downward via ParentPtr to find first active frame + while (frame.ParentPtr != TargetPointer.Null && frame.Ip == TargetPointer.Null) + { + currentPtr = frame.ParentPtr; + frame = target.ProcessedData.GetOrAdd(currentPtr); + } + } + + return currentPtr; + } + + /// + /// Walks the InterpMethodContextFrame chain for an InterpreterFrame, + /// yielding one context frame pointer per active interpreted method in the call chain. + /// The TopInterpMethodContextFrame hint is first resolved to the actual top frame + /// via ResolveTopInterpMethodContextFrame, then the ParentPtr chain is walked. + /// Only active frames (Ip != null) are yielded. /// internal static IEnumerable WalkInterpreterFrameChain(Target target, TargetPointer frameAddress) { Data.InterpreterFrame interpFrame = target.ProcessedData.GetOrAdd(frameAddress); - TargetPointer interpMethodFramePtr = interpFrame.TopInterpMethodContextFrame; + TargetPointer interpMethodFramePtr = ResolveTopInterpMethodContextFrame(target, interpFrame.TopInterpMethodContextFrame); while (interpMethodFramePtr != TargetPointer.Null) { - yield return interpMethodFramePtr; Data.InterpMethodContextFrame contextFrame = target.ProcessedData.GetOrAdd(interpMethodFramePtr); + if (contextFrame.Ip != TargetPointer.Null) + yield return interpMethodFramePtr; interpMethodFramePtr = contextFrame.ParentPtr; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs index ab8bdda895d217..ecefc56f41433d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs @@ -62,6 +62,13 @@ private class StackWalkData(IPlatformAgnosticContext context, StackWalkState sta // set back to true when encountering a ResumableFrame (FRAME_ATTR_RESUMABLE). public bool IsFirst { get; set; } = true; + // When the stack walk starts with an explicit context in interpreted code (e.g., from + // a debugger breakpoint), the first InterpreterFrame on the frame chain corresponds to + // the frames already yielded from the initial context. Setting this flag causes YieldFrames + // to skip expanding that InterpreterFrame, preventing double-walking. + // See PR #126953 for the equivalent native fix. + public bool SkipNextInterpreterFrame { get; set; } + public bool IsCurrentFrameResumable() { if (State is not (StackWalkState.SW_FRAME or StackWalkState.SW_SKIPPED_FRAME)) @@ -125,7 +132,17 @@ private IEnumerable CreateStackWalkCore(ThreadData thread { IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); FillContextFromThread(context, threadData); - StackWalkState state = IsManaged(context.InstructionPointer, out _) ? StackWalkState.SW_FRAMELESS : StackWalkState.SW_FRAME; + bool startedInInterpreterCode = false; + StackWalkState state; + if (IsManaged(context.InstructionPointer, out CodeBlockHandle? initialCbh)) + { + state = StackWalkState.SW_FRAMELESS; + startedInInterpreterCode = _eman.GetJITType(initialCbh.Value) == JitType.Interpreter; + } + else + { + state = StackWalkState.SW_FRAME; + } FrameIterator frameIterator = new(_target, threadData); if (skipInitialFrames) @@ -153,7 +170,10 @@ private IEnumerable CreateStackWalkCore(ThreadData thread yield break; } - StackWalkData stackWalkData = new(context, state, frameIterator, threadData); + StackWalkData stackWalkData = new(context, state, frameIterator, threadData) + { + SkipNextInterpreterFrame = startedInInterpreterCode, + }; // Mirror native Init() -> ProcessCurrentFrame() -> CheckForSkippedFrames(): // When the initial frame is managed (SW_FRAMELESS), check if there are explicit @@ -178,7 +198,7 @@ private IEnumerable CreateStackWalkCore(ThreadData thread /// /// Yields one or more data frames for the current stack walk position. - /// For InterpreterFrame, walks the InterpMethodContextFrame.pParent chain + /// For InterpreterFrame, walks the InterpMethodContextFrame chain /// to yield a separate frame for each interpreted method in the call chain. /// private IEnumerable YieldFrames(StackWalkData stackWalkData) @@ -189,6 +209,15 @@ private IEnumerable YieldFrames(StackWalkData stackWalkDat if (frameAddress != TargetPointer.Null && stackWalkData.FrameIter.GetCurrentFrameType() == FrameIterator.FrameType.InterpreterFrame) { + // When the stack walk started with a context in interpreted code (e.g., from + // a debugger breakpoint), the frames from the initial context were already yielded + // as frameless frames. Skip the first InterpreterFrame to avoid double-walking. + if (stackWalkData.SkipNextInterpreterFrame) + { + stackWalkData.SkipNextInterpreterFrame = false; + yield break; + } + foreach (TargetPointer contextFramePtr in FrameIterator.WalkInterpreterFrameChain(_target, frameAddress)) yield return stackWalkData.ToDataFrame(contextFramePtr); yield break; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpMethodContextFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpMethodContextFrame.cs index 5252c284a14628..edee3128b43f86 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpMethodContextFrame.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpMethodContextFrame.cs @@ -13,8 +13,12 @@ public InterpMethodContextFrame(Target target, TargetPointer address) Target.TypeInfo type = target.GetTypeInfo(DataType.InterpMethodContextFrame); StartIp = target.ReadPointerField(address, type, nameof(StartIp)); ParentPtr = target.ReadPointerField(address, type, nameof(ParentPtr)); + Ip = target.ReadPointerField(address, type, nameof(Ip)); + NextPtr = target.ReadPointerField(address, type, nameof(NextPtr)); } public TargetPointer StartIp { get; init; } public TargetPointer ParentPtr { get; init; } + public TargetPointer Ip { get; init; } + public TargetPointer NextPtr { get; init; } } diff --git a/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs index 24167cd62e0c1c..5e7adcc08c5946 100644 --- a/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs @@ -149,4 +149,40 @@ public void Thread_CanEnumerateWithInterpreterFrames(TestConfiguration config) Assert.True(threadCount >= 1, "Expected at least one thread when walking the list"); } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void StackWalk_NoDoubledInterpreterFrames(TestConfiguration config) + { + InitializeDumpTest(config); + SkipIfInterpreterNotAvailable(); + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + DumpTestStackWalker walker = DumpTestStackWalker.Walk(Target, crashingThread); + + // Verify that no interpreted method appears more than once in the stack walk. + // This guards against the double-walking bug where interpreter frames are yielded + // both from the initial context and again from the InterpreterFrame expansion. + var interpreterMethods = walker.Frames + .Where(f => f.FrameName is "InterpreterFrame") + .Select(f => f.Name) + .Where(n => n is not null) + .ToList(); + + var duplicates = interpreterMethods + .GroupBy(n => n) + .Where(g => g.Count() > 1) + .Select(g => g.Key) + .ToList(); + + Assert.True(duplicates.Count == 0, + $"Doubled interpreter frames detected: [{string.Join(", ", duplicates)}]. " + + $"Full stack: [{string.Join(", ", walker.Frames.Select(f => f.Name ?? ""))}]"); + + // Also verify all expected interpreter methods are present exactly once + Assert.Contains(interpreterMethods, n => n is "MethodA"); + Assert.Contains(interpreterMethods, n => n is "MethodB"); + Assert.Contains(interpreterMethods, n => n is "MethodC"); + Assert.Contains(interpreterMethods, n => n is "MethodD"); + } } diff --git a/src/native/managed/cdac/tests/FrameIteratorTests.cs b/src/native/managed/cdac/tests/FrameIteratorTests.cs index a8cbab277a28f0..256731668d7827 100644 --- a/src/native/managed/cdac/tests/FrameIteratorTests.cs +++ b/src/native/managed/cdac/tests/FrameIteratorTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; using Xunit; @@ -79,6 +80,8 @@ static TargetTestHelpers.LayoutResult GetLayout(TargetTestHelpers helpers, TypeF [ new(nameof(Data.InterpMethodContextFrame.StartIp), DataType.pointer), new(nameof(Data.InterpMethodContextFrame.ParentPtr), DataType.pointer), + new(nameof(Data.InterpMethodContextFrame.Ip), DataType.pointer), + new(nameof(Data.InterpMethodContextFrame.NextPtr), DataType.pointer), ] }; @@ -154,6 +157,12 @@ public void GetMethodDescPtr_InterpreterFrame_FollowsFullChain(MockTarget.Archit helpers.WritePointer( contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.ParentPtr)].Offset, pointerSize), 0); + helpers.WritePointer( + contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.Ip)].Offset, pointerSize), + 0xCAFE_0001); + helpers.WritePointer( + contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.NextPtr)].Offset, pointerSize), + 0); int frameNextOffset = types[DataType.Frame].Fields[nameof(Data.Frame.Next)].Offset; int topContextFrameOffset = types[DataType.InterpreterFrame].Fields[nameof(Data.InterpreterFrame.TopInterpMethodContextFrame)].Offset; @@ -230,6 +239,12 @@ public void GetMethodDescPtr_InterpreterFrame_NullStartIp_ReturnsNull(MockTarget helpers.WritePointer( contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.ParentPtr)].Offset, pointerSize), 0); + helpers.WritePointer( + contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.Ip)].Offset, pointerSize), + 0xCAFE_0002); + helpers.WritePointer( + contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.NextPtr)].Offset, pointerSize), + 0); int topContextFrameOffset = types[DataType.InterpreterFrame].Fields[nameof(Data.InterpreterFrame.TopInterpMethodContextFrame)].Offset; int frameNextOffset = types[DataType.Frame].Fields[nameof(Data.Frame.Next)].Offset; @@ -277,6 +292,12 @@ public void GetMethodDescPtr_InterpreterFrame_NullMethod_ReturnsNull(MockTarget. helpers.WritePointer( contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.ParentPtr)].Offset, pointerSize), 0); + helpers.WritePointer( + contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.Ip)].Offset, pointerSize), + 0xCAFE_0003); + helpers.WritePointer( + contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.NextPtr)].Offset, pointerSize), + 0); int topContextFrameOffset = types[DataType.InterpreterFrame].Fields[nameof(Data.InterpreterFrame.TopInterpMethodContextFrame)].Offset; int frameNextOffset = types[DataType.Frame].Fields[nameof(Data.Frame.Next)].Offset; @@ -334,6 +355,12 @@ MockMemorySpace.HeapFragment CreateContextChainEntry(ulong methodDesc, ulong par helpers.WritePointer( contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.ParentPtr)].Offset, pointerSize), parentPtr); + helpers.WritePointer( + contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.Ip)].Offset, pointerSize), + 0xCAFE_0000 + methodDesc); + helpers.WritePointer( + contextFrameFrag.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.NextPtr)].Offset, pointerSize), + 0); return contextFrameFrag; } @@ -388,4 +415,259 @@ public void GetFrameName_UnknownFrame_ReturnsEmpty(MockTarget.Architecture arch) Assert.Equal(string.Empty, name); } + + [Theory] + [MemberData(nameof(InterpreterFrameArchitectures))] + public void ResolveTopInterpMethodContextFrame_HintIsStale_SeeksViaParentPtr(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + Dictionary types = GetTypes(helpers); + + var builder = new TestPlaceholderTarget.Builder(arch) + .AddTypes(types); + + int pointerSize = helpers.PointerSize; + var alloc = builder.MemoryBuilder.CreateAllocator(0x1000_0000, 0x2000_0000); + + ulong expectedMethodDesc = 0xDEAD_BEEF; + + var interpMethodFrag = alloc.Allocate((ulong)types[DataType.InterpMethod].Size!, "InterpMethod"); + helpers.WritePointer( + interpMethodFrag.Data.AsSpan(types[DataType.InterpMethod].Fields[nameof(Data.InterpMethod.MethodDesc)].Offset, pointerSize), + expectedMethodDesc); + + var byteCodeStartFrag = alloc.Allocate((ulong)types[DataType.InterpByteCodeStart].Size!, "InterpByteCodeStart"); + helpers.WritePointer( + byteCodeStartFrag.Data.AsSpan(types[DataType.InterpByteCodeStart].Fields[nameof(Data.InterpByteCodeStart.Method)].Offset, pointerSize), + interpMethodFrag.Address); + + // Active frame (ip != null) — this is the real top + var activeFrame = alloc.Allocate((ulong)types[DataType.InterpMethodContextFrame].Size!, "ActiveFrame"); + helpers.WritePointer( + activeFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.StartIp)].Offset, pointerSize), + byteCodeStartFrag.Address); + helpers.WritePointer( + activeFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.ParentPtr)].Offset, pointerSize), + 0); + helpers.WritePointer( + activeFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.Ip)].Offset, pointerSize), + 0xCAFE_0010); + helpers.WritePointer( + activeFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.NextPtr)].Offset, pointerSize), + 0); + + // Stale frame (ip == null) — this is the hint that points to the active frame via ParentPtr + var staleFrame = alloc.Allocate((ulong)types[DataType.InterpMethodContextFrame].Size!, "StaleFrame"); + helpers.WritePointer( + staleFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.StartIp)].Offset, pointerSize), + 0); + helpers.WritePointer( + staleFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.ParentPtr)].Offset, pointerSize), + activeFrame.Address); + helpers.WritePointer( + staleFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.Ip)].Offset, pointerSize), + 0); + helpers.WritePointer( + staleFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.NextPtr)].Offset, pointerSize), + 0); + + int frameNextOffset = types[DataType.Frame].Fields[nameof(Data.Frame.Next)].Offset; + int topContextFrameOffset = types[DataType.InterpreterFrame].Fields[nameof(Data.InterpreterFrame.TopInterpMethodContextFrame)].Offset; + int totalFrameSize = Math.Max(topContextFrameOffset + pointerSize, frameNextOffset + pointerSize); + + // InterpreterFrame with hint pointing to the stale frame + var frameFrag = alloc.Allocate((ulong)totalFrameSize, "InterpreterFrame"); + ulong interpreterFrameIdentifierValue = 0xAAAA_6666; + helpers.WritePointer(frameFrag.Data.AsSpan(0, pointerSize), interpreterFrameIdentifierValue); + ulong terminator = arch.Is64Bit ? ulong.MaxValue : uint.MaxValue; + helpers.WritePointer(frameFrag.Data.AsSpan(frameNextOffset, pointerSize), terminator); + helpers.WritePointer(frameFrag.Data.AsSpan(topContextFrameOffset, pointerSize), staleFrame.Address); + + builder.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); + + var target = builder.Build(); + + // GetMethodDescPtr should resolve through the stale hint to the active frame + TargetPointer result = FrameIterator.GetMethodDescPtr(target, new TargetPointer(frameFrag.Address)); + Assert.Equal(new TargetPointer(expectedMethodDesc), result); + + // WalkInterpreterFrameChain should yield only the active frame + var chain = FrameIterator.WalkInterpreterFrameChain(target, new TargetPointer(frameFrag.Address)).ToList(); + Assert.Single(chain); + Assert.Equal(new TargetPointer(activeFrame.Address), chain[0]); + } + + [Theory] + [MemberData(nameof(InterpreterFrameArchitectures))] + public void ResolveTopInterpMethodContextFrame_SeeksViaNextPtr(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + Dictionary types = GetTypes(helpers); + + var builder = new TestPlaceholderTarget.Builder(arch) + .AddTypes(types); + + int pointerSize = helpers.PointerSize; + var alloc = builder.MemoryBuilder.CreateAllocator(0x1000_0000, 0x2000_0000); + + ulong methodDescLower = 0xAA00_0001; + ulong methodDescUpper = 0xBB00_0002; + + // Create two InterpMethod/InterpByteCodeStart chains + var interpMethodLower = alloc.Allocate((ulong)types[DataType.InterpMethod].Size!, "InterpMethodLower"); + helpers.WritePointer( + interpMethodLower.Data.AsSpan(types[DataType.InterpMethod].Fields[nameof(Data.InterpMethod.MethodDesc)].Offset, pointerSize), + methodDescLower); + var byteCodeStartLower = alloc.Allocate((ulong)types[DataType.InterpByteCodeStart].Size!, "ByteCodeStartLower"); + helpers.WritePointer( + byteCodeStartLower.Data.AsSpan(types[DataType.InterpByteCodeStart].Fields[nameof(Data.InterpByteCodeStart.Method)].Offset, pointerSize), + interpMethodLower.Address); + + var interpMethodUpper = alloc.Allocate((ulong)types[DataType.InterpMethod].Size!, "InterpMethodUpper"); + helpers.WritePointer( + interpMethodUpper.Data.AsSpan(types[DataType.InterpMethod].Fields[nameof(Data.InterpMethod.MethodDesc)].Offset, pointerSize), + methodDescUpper); + var byteCodeStartUpper = alloc.Allocate((ulong)types[DataType.InterpByteCodeStart].Size!, "ByteCodeStartUpper"); + helpers.WritePointer( + byteCodeStartUpper.Data.AsSpan(types[DataType.InterpByteCodeStart].Fields[nameof(Data.InterpByteCodeStart.Method)].Offset, pointerSize), + interpMethodUpper.Address); + + // Upper frame (real top) — active, no next + var upperFrame = alloc.Allocate((ulong)types[DataType.InterpMethodContextFrame].Size!, "UpperFrame"); + helpers.WritePointer( + upperFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.StartIp)].Offset, pointerSize), + byteCodeStartUpper.Address); + helpers.WritePointer( + upperFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.ParentPtr)].Offset, pointerSize), + 0); + helpers.WritePointer( + upperFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.Ip)].Offset, pointerSize), + 0xCAFE_0020); + helpers.WritePointer( + upperFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.NextPtr)].Offset, pointerSize), + 0); + + // Lower frame (hint) — active, NextPtr points to upper + var lowerFrame = alloc.Allocate((ulong)types[DataType.InterpMethodContextFrame].Size!, "LowerFrame"); + helpers.WritePointer( + lowerFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.StartIp)].Offset, pointerSize), + byteCodeStartLower.Address); + helpers.WritePointer( + lowerFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.ParentPtr)].Offset, pointerSize), + 0); + helpers.WritePointer( + lowerFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.Ip)].Offset, pointerSize), + 0xCAFE_0021); + helpers.WritePointer( + lowerFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.NextPtr)].Offset, pointerSize), + upperFrame.Address); + + // Set upper's ParentPtr to lower (upper is the caller of lower) + helpers.WritePointer( + upperFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.ParentPtr)].Offset, pointerSize), + lowerFrame.Address); + + int frameNextOffset = types[DataType.Frame].Fields[nameof(Data.Frame.Next)].Offset; + int topContextFrameOffset = types[DataType.InterpreterFrame].Fields[nameof(Data.InterpreterFrame.TopInterpMethodContextFrame)].Offset; + int totalFrameSize = Math.Max(topContextFrameOffset + pointerSize, frameNextOffset + pointerSize); + + // InterpreterFrame with hint pointing to the lower frame + var frameFrag = alloc.Allocate((ulong)totalFrameSize, "InterpreterFrame"); + ulong interpreterFrameIdentifierValue = 0xAAAA_7777; + helpers.WritePointer(frameFrag.Data.AsSpan(0, pointerSize), interpreterFrameIdentifierValue); + ulong terminator = arch.Is64Bit ? ulong.MaxValue : uint.MaxValue; + helpers.WritePointer(frameFrag.Data.AsSpan(frameNextOffset, pointerSize), terminator); + helpers.WritePointer(frameFrag.Data.AsSpan(topContextFrameOffset, pointerSize), lowerFrame.Address); + + builder.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); + + var target = builder.Build(); + + // GetMethodDescPtr should resolve through NextPtr to upper (the real top) + TargetPointer result = FrameIterator.GetMethodDescPtr(target, new TargetPointer(frameFrag.Address)); + Assert.Equal(new TargetPointer(methodDescUpper), result); + + // WalkInterpreterFrameChain should yield upper then lower (top to bottom via ParentPtr) + var chain = FrameIterator.WalkInterpreterFrameChain(target, new TargetPointer(frameFrag.Address)).ToList(); + Assert.Equal(2, chain.Count); + Assert.Equal(new TargetPointer(upperFrame.Address), chain[0]); + Assert.Equal(new TargetPointer(lowerFrame.Address), chain[1]); + } + + [Theory] + [MemberData(nameof(InterpreterFrameArchitectures))] + public void WalkInterpreterFrameChain_SkipsInactiveFrames(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + Dictionary types = GetTypes(helpers); + + var builder = new TestPlaceholderTarget.Builder(arch) + .AddTypes(types); + + int pointerSize = helpers.PointerSize; + var alloc = builder.MemoryBuilder.CreateAllocator(0x1000_0000, 0x2000_0000); + + ulong methodDescA = 0xAA00_0001; + + var interpMethodA = alloc.Allocate((ulong)types[DataType.InterpMethod].Size!, "InterpMethodA"); + helpers.WritePointer( + interpMethodA.Data.AsSpan(types[DataType.InterpMethod].Fields[nameof(Data.InterpMethod.MethodDesc)].Offset, pointerSize), + methodDescA); + var byteCodeStartA = alloc.Allocate((ulong)types[DataType.InterpByteCodeStart].Size!, "ByteCodeStartA"); + helpers.WritePointer( + byteCodeStartA.Data.AsSpan(types[DataType.InterpByteCodeStart].Fields[nameof(Data.InterpByteCodeStart.Method)].Offset, pointerSize), + interpMethodA.Address); + + // Active frame at bottom of chain + var activeFrame = alloc.Allocate((ulong)types[DataType.InterpMethodContextFrame].Size!, "ActiveFrame"); + helpers.WritePointer( + activeFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.StartIp)].Offset, pointerSize), + byteCodeStartA.Address); + helpers.WritePointer( + activeFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.ParentPtr)].Offset, pointerSize), + 0); + helpers.WritePointer( + activeFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.Ip)].Offset, pointerSize), + 0xCAFE_0030); + helpers.WritePointer( + activeFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.NextPtr)].Offset, pointerSize), + 0); + + // Inactive frame above active (ip == null, returned from this method) + var inactiveFrame = alloc.Allocate((ulong)types[DataType.InterpMethodContextFrame].Size!, "InactiveFrame"); + helpers.WritePointer( + inactiveFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.StartIp)].Offset, pointerSize), + 0); + helpers.WritePointer( + inactiveFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.ParentPtr)].Offset, pointerSize), + activeFrame.Address); + helpers.WritePointer( + inactiveFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.Ip)].Offset, pointerSize), + 0); + helpers.WritePointer( + inactiveFrame.Data.AsSpan(types[DataType.InterpMethodContextFrame].Fields[nameof(Data.InterpMethodContextFrame.NextPtr)].Offset, pointerSize), + 0); + + int frameNextOffset = types[DataType.Frame].Fields[nameof(Data.Frame.Next)].Offset; + int topContextFrameOffset = types[DataType.InterpreterFrame].Fields[nameof(Data.InterpreterFrame.TopInterpMethodContextFrame)].Offset; + int totalFrameSize = Math.Max(topContextFrameOffset + pointerSize, frameNextOffset + pointerSize); + + // InterpreterFrame with hint pointing to the inactive frame + var frameFrag = alloc.Allocate((ulong)totalFrameSize, "InterpreterFrame"); + ulong interpreterFrameIdentifierValue = 0xAAAA_8888; + helpers.WritePointer(frameFrag.Data.AsSpan(0, pointerSize), interpreterFrameIdentifierValue); + ulong terminator = arch.Is64Bit ? ulong.MaxValue : uint.MaxValue; + helpers.WritePointer(frameFrag.Data.AsSpan(frameNextOffset, pointerSize), terminator); + helpers.WritePointer(frameFrag.Data.AsSpan(topContextFrameOffset, pointerSize), inactiveFrame.Address); + + builder.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); + + var target = builder.Build(); + + // WalkInterpreterFrameChain should resolve the hint to the active frame + // and skip the inactive frame during enumeration + var chain = FrameIterator.WalkInterpreterFrameChain(target, new TargetPointer(frameFrag.Address)).ToList(); + Assert.Single(chain); + Assert.Equal(new TargetPointer(activeFrame.Address), chain[0]); + } } From a8246a49aa6bed0bb0c72be18fbf1412999bd0d8 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Fri, 24 Apr 2026 18:10:03 -0400 Subject: [PATCH 8/9] Add InterpreterStackDoubleWalk dump test and fix unwind infinite loop Add a multi-threaded debuggee (InterpreterStackDoubleWalk) that validates interpreter stack frame walking on a non-crashing thread. A worker thread builds a full interpreter call chain (MethodA->B->Bounce->C->D->spin loop) while the main thread triggers FailFast. Tests walk the worker thread to verify: - Interleaved JIT/interpreter frame layout is correct - No interpreter method appears more than once (no doubled frames) The worker spins in interpreted code (volatile field-read loop) so the full interpreter frame chain is on the stack at dump time. The CPU IP is inside the native interpreter engine, so the walk starts from SW_FRAME state. Fix a pre-existing infinite loop in StackWalk_1.Next() where Context.Unwind() fails to advance IP/SP on certain frames (e.g. FailFast PInvoke, SleepEx). When IP and SP are unchanged after Unwind, fall back to Frame chain if available, otherwise mark walk as complete. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/StackWalk/StackWalk_1.cs | 16 +++ .../InterpreterStackDoubleWalk.csproj | 19 +++ .../InterpreterStackDoubleWalk/Program.cs | 84 +++++++++++ .../InterpreterStackDoubleWalkDumpTests.cs | 132 ++++++++++++++++++ 4 files changed, 251 insertions(+) create mode 100644 src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStackDoubleWalk/InterpreterStackDoubleWalk.csproj create mode 100644 src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStackDoubleWalk/Program.cs create mode 100644 src/native/managed/cdac/tests/DumpTests/InterpreterStackDoubleWalkDumpTests.cs diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs index ecefc56f41433d..8b05e454a94c99 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs @@ -639,6 +639,8 @@ private bool Next(StackWalkData handle) switch (handle.State) { case StackWalkState.SW_FRAMELESS: + TargetPointer prevIP = handle.Context.InstructionPointer; + TargetPointer prevSP = handle.Context.StackPointer; try { handle.Context.Unwind(_target); @@ -648,6 +650,20 @@ private bool Next(StackWalkData handle) handle.State = StackWalkState.SW_ERROR; throw; } + // Guard against infinite loops when Unwind fails to advance. + // If both IP and SP are unchanged, the unwinder made no progress. + // Fall back to the Frame chain if possible, otherwise complete. + if (handle.Context.InstructionPointer == prevIP + && handle.Context.StackPointer == prevSP) + { + if (handle.FrameIter.IsValid()) + { + handle.State = StackWalkState.SW_FRAME; + return true; + } + handle.State = StackWalkState.SW_COMPLETE; + return false; + } break; case StackWalkState.SW_SKIPPED_FRAME: handle.FrameIter.Next(); diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStackDoubleWalk/InterpreterStackDoubleWalk.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStackDoubleWalk/InterpreterStackDoubleWalk.csproj new file mode 100644 index 00000000000000..0db41b7c90c728 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStackDoubleWalk/InterpreterStackDoubleWalk.csproj @@ -0,0 +1,19 @@ + + + Full + Jit + + DOTNET_Interpreter=Method* + false + + + + + + + + + diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStackDoubleWalk/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStackDoubleWalk/Program.cs new file mode 100644 index 00000000000000..e68346419797d3 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStackDoubleWalk/Program.cs @@ -0,0 +1,84 @@ +// 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.Runtime.CompilerServices; +using System.Threading; +using InterpreterStack.Trampoline; + +/// +/// Debuggee for cDAC dump tests — validates interpreter stack frame walking on a +/// thread that has a full interpreter call chain. A worker thread builds the chain +/// and spins in interpreted code, then the main thread triggers a FailFast dump. +/// +/// Under DOTNET_Interpreter=Method*, methods from this assembly that match +/// the filter are interpreted. The call chain routes through JitTrampoline.Bounce +/// (in a separate assembly, always JIT'd) to create two distinct InterpreterFrame +/// regions on the stack: +/// +/// Worker thread: +/// MethodA (interp) -> MethodB (interp) -> [InterpreterFrame 1] +/// -> JitTrampoline.Bounce (JIT) -> MethodC (interp) -> MethodD (interp) -> [InterpreterFrame 2] +/// -> spinning in interpreted code (volatile field-read loop) +/// +/// Main thread: +/// Main (JIT) -> waits for signal -> FailFast +/// +/// Note: Even though the worker is executing interpreted code, the CPU's instruction +/// pointer is inside the native interpreter engine at dump time. The stack walk +/// starts in SW_FRAME state and encounters InterpreterFrames via the Frame chain. +/// This tests that interpreter frame expansion produces correct, non-duplicated results. +/// +internal static class Program +{ + private static readonly ManualResetEventSlim s_workerReady = new(false); + + private static void Main() + { + Thread worker = new(MethodA) + { + IsBackground = true, + Name = "InterpreterWorker", + }; + worker.Start(); + + // Wait for the worker to reach MethodD (full call chain on stack). + s_workerReady.Wait(); + + Environment.FailFast("cDAC dump test: InterpreterStackDoubleWalk debuggee intentional crash"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void MethodA() + { + MethodB(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void MethodB() + { + JitTrampoline.Bounce(MethodC); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void MethodC() + { + MethodD(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void MethodD() + { + // Signal the main thread that the full call chain is on the stack. + s_workerReady.Set(); + + // Spin in interpreted code so that the worker thread's IP is inside + // interpreter-managed code at dump time. This ensures startedInInterpreterCode=true + // in the stack walker, exercising the SkipNextInterpreterFrame logic. + // Use a simple volatile field-read loop to stay in interpreted code without + // calling native methods like Thread.Sleep or Thread.SpinWait. + while (s_keepSpinning) { } + } + + private static volatile bool s_keepSpinning = true; +} diff --git a/src/native/managed/cdac/tests/DumpTests/InterpreterStackDoubleWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/InterpreterStackDoubleWalkDumpTests.cs new file mode 100644 index 00000000000000..882f11b8c6501a --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/InterpreterStackDoubleWalkDumpTests.cs @@ -0,0 +1,132 @@ +// 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.Linq; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.DotNet.XUnitExtensions; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Dump-based integration tests for the InterpreterStackDoubleWalk debuggee. +/// This debuggee uses two threads: +/// - Worker thread: MethodA -> MethodB -> Bounce -> MethodC -> MethodD -> spin loop (interpreted) +/// - Main thread: waits for worker, then calls FailFast +/// +/// The tests walk the worker thread (not the crashing thread) to verify +/// interpreter frame handling on a thread that has a fully populated InterpreterFrame +/// chain while spinning in interpreted code. Even though the worker executes interpreted +/// code, the CPU IP is inside the native interpreter engine at dump time, so the +/// walk starts from SW_FRAME state and encounters InterpreterFrames via the Frame chain. +/// +public class InterpreterStackDoubleWalkDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "InterpreterStackDoubleWalk"; + protected override string DumpType => "full"; + + private void SkipIfInterpreterNotAvailable() + { + try + { + Target.GetTypeInfo(DataType.InterpreterFrame); + } + catch (InvalidOperationException) + { + throw new SkipTestException("Interpreter support not available in this runtime build (FEATURE_INTERPRETER not enabled)."); + } + } + + private void AssertInterpreted(ResolvedFrame f) + { + // Interpreted methods may appear as frameless frames (via normal unwind + // after InterpreterFrame sets the context) or as InterpreterFrame entries. + // We verify only that the MethodDesc has interpreter code. + IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; + IExecutionManager executionManager = Target.Contracts.ExecutionManager; + + MethodDescHandle md = rts.GetMethodDescHandle(f.MethodDescPtr); + TargetCodePointer nativeCode = rts.GetNativeCode(md); + TargetCodePointer resolvedCode = Target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode); + Assert.NotEqual(TargetCodePointer.Null, resolvedCode); + + CodeBlockHandle? codeBlock = executionManager.GetCodeBlockHandle(resolvedCode); + Assert.NotNull(codeBlock); + Assert.Equal(JitType.Interpreter, executionManager.GetJITType(codeBlock.Value)); + } + + private void AssertJitted(ResolvedFrame f) + { + Assert.Null(f.FrameName); + + IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; + IExecutionManager executionManager = Target.Contracts.ExecutionManager; + + MethodDescHandle md = rts.GetMethodDescHandle(f.MethodDescPtr); + TargetCodePointer nativeCode = rts.GetNativeCode(md); + Assert.NotEqual(TargetCodePointer.Null, nativeCode); + CodeBlockHandle? codeBlock = executionManager.GetCodeBlockHandle(nativeCode); + Assert.NotNull(codeBlock); + Assert.Equal(JitType.Jit, executionManager.GetJITType(codeBlock.Value)); + } + + /// + /// Walks the worker thread and verifies the interleaved JIT/interpreter frame layout. + /// The worker is spinning in MethodD (interpreted code; CPU IP in native interpreter), + /// so the full call chain should be: + /// MethodD (interp) -> MethodC (interp) -> Bounce (JIT) -> MethodB (interp) -> MethodA (interp) + /// + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void StackWalk_VerifyInterleavedStackLayout(TestConfiguration config) + { + InitializeDumpTest(config); + SkipIfInterpreterNotAvailable(); + + ThreadData workerThread = DumpTestHelpers.FindThreadWithMethod(Target, "MethodD"); + + DumpTestStackWalker.Walk(Target, workerThread) + .ExpectFrame("MethodD", AssertInterpreted) + .ExpectAdjacentFrame("MethodC", AssertInterpreted) + .ExpectFrame("Bounce", AssertJitted) + .ExpectFrame("MethodB", AssertInterpreted) + .ExpectAdjacentFrame("MethodA", AssertInterpreted) + .Verify(); + } + + /// + /// Walks the worker thread and verifies no interpreted method appears more than once. + /// The worker thread is spinning in interpreted code but the CPU IP is inside the native + /// interpreter engine, so the walk starts from SW_FRAME and encounters InterpreterFrames + /// via the Frame chain. This verifies that interpreter frame expansion produces exactly + /// one entry per interpreted method — no doubled frames. + /// + /// Note: The SkipNextInterpreterFrame double-walk prevention logic (which fires when a + /// walk starts with IP in interpreter-managed code, e.g. a debugger breakpoint) cannot + /// be exercised from a crash dump, since the interpreter's CPU IP is always in native code. + /// That logic is covered by the FrameIterator unit tests. + /// + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void StackWalk_NoDoubledInterpreterFrames(TestConfiguration config) + { + InitializeDumpTest(config); + SkipIfInterpreterNotAvailable(); + + ThreadData workerThread = DumpTestHelpers.FindThreadWithMethod(Target, "MethodD"); + DumpTestStackWalker walker = DumpTestStackWalker.Walk(Target, workerThread); + + // MethodA-D should each appear exactly once on the stack. + // They may appear as frameless frames (via normal unwind after + // InterpreterFrame sets the context) rather than as InterpreterFrame entries. + string[] expectedMethods = ["MethodA", "MethodB", "MethodC", "MethodD"]; + foreach (string method in expectedMethods) + { + int count = walker.Frames.Count(f => string.Equals(f.Name, method, StringComparison.Ordinal)); + Assert.True(count == 1, + $"Expected '{method}' to appear exactly once but found {count} occurrence(s). " + + $"Full stack: [{string.Join(", ", walker.Frames.Select(f => $"{f.Name ?? ""}({f.FrameName ?? "frameless"})"))}]"); + } + } +} From 5410ee8a1a6c5b638eab385f005814993ffcff45 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:08:48 -0400 Subject: [PATCH 9/9] Address review feedback: revert IRuntimeTypeSystem and refactor ResolveTopInterpMethodContextFrame Revert the whitespace-only change to IRuntimeTypeSystem.cs. Change ResolveTopInterpMethodContextFrame to take Data.InterpreterFrame instead of the raw hint pointer, simplifying call sites. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/IRuntimeTypeSystem.cs | 1 - .../Contracts/StackWalk/FrameHandling/FrameIterator.cs | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs index e3873970e460c5..8cc74fc5d1ee72 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs @@ -214,7 +214,6 @@ public interface IRuntimeTypeSystem : IContract TargetPointer GetMethodDescVersioningState(MethodDescHandle methodDesc) => throw new NotImplementedException(); TargetCodePointer GetNativeCode(MethodDescHandle methodDesc) => throw new NotImplementedException(); - TargetCodePointer GetMethodEntryPointIfExists(MethodDescHandle methodDesc) => throw new NotImplementedException(); ushort GetSlotNumber(MethodDescHandle methodDesc) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs index 371f90fe61e3bb..04aea4de14f7ec 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs @@ -208,7 +208,7 @@ public static TargetPointer GetMethodDescPtr(Target target, TargetPointer frameP case FrameType.InterpreterFrame: { Data.InterpreterFrame interpreterFrame = target.ProcessedData.GetOrAdd(frame.Address); - TargetPointer topContextFrame = ResolveTopInterpMethodContextFrame(target, interpreterFrame.TopInterpMethodContextFrame); + TargetPointer topContextFrame = ResolveTopInterpMethodContextFrame(target, interpreterFrame); return ResolveMethodDescFromInterpFrame(target, topContextFrame); } case FrameType.PInvokeCalliFrame: @@ -269,8 +269,9 @@ internal static TargetPointer ResolveMethodDescFromInterpFrame(Target target, Ta /// debugging it may point to a stale frame. This method seeks to the correct top frame using /// the Ip field (null = inactive, non-null = active) and the NextPtr/ParentPtr chains. /// - internal static TargetPointer ResolveTopInterpMethodContextFrame(Target target, TargetPointer hintPtr) + internal static TargetPointer ResolveTopInterpMethodContextFrame(Target target, Data.InterpreterFrame interpreterFrame) { + TargetPointer hintPtr = interpreterFrame.TopInterpMethodContextFrame; if (hintPtr == TargetPointer.Null) return TargetPointer.Null; @@ -312,7 +313,7 @@ internal static TargetPointer ResolveTopInterpMethodContextFrame(Target target, internal static IEnumerable WalkInterpreterFrameChain(Target target, TargetPointer frameAddress) { Data.InterpreterFrame interpFrame = target.ProcessedData.GetOrAdd(frameAddress); - TargetPointer interpMethodFramePtr = ResolveTopInterpMethodContextFrame(target, interpFrame.TopInterpMethodContextFrame); + TargetPointer interpMethodFramePtr = ResolveTopInterpMethodContextFrame(target, interpFrame); while (interpMethodFramePtr != TargetPointer.Null) { Data.InterpMethodContextFrame contextFrame = target.ProcessedData.GetOrAdd(interpMethodFramePtr);