From 5a5a7e647f6fee220dd548aa7e24a0547c771698 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Fri, 1 May 2026 10:50:11 -0400 Subject: [PATCH 01/17] [cDAC] Add interpreter support for stack walking and diagnostics Add cDAC contracts and implementations to walk interpreter frames during stack walks and identify interpreter-managed methods. The cDAC stack walker now mirrors the native DAC behavior: * Yields `InterpreterFrame` as a runtime frame marker (pMD = NULL), matching the native DAC. * Implements interpreter virtual unwind by following the `InterpMethodContextFrame.pParent` chain so each interpreted method is yielded as a frameless frame. * Resolves the top `InterpMethodContextFrame` from the `InterpreterFrame` in the same way as the native runtime (`GetTopInterpMethodContextFrame`). * Adds `InterpreterJitManager` for interpreter `CodeBlockHandle` resolution and a precode-stubs fallthrough that recognizes `InterpreterPrecode`. Includes data descriptors for `InterpreterFrame`, `InterpMethodContextFrame`, `InterpMethod`, `InterpByteCodeStart`, `InterpreterPrecodeData`, and `InterpreterRealCodeHeader`, plus documentation updates and unit tests covering the new contract behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/ExecutionManager.md | 29 +- docs/design/datacontracts/PrecodeStubs.md | 54 ++ docs/design/datacontracts/StackWalk.md | 32 + .../vm/datadescriptor/datadescriptor.h | 4 + .../vm/datadescriptor/datadescriptor.inc | 43 ++ src/coreclr/vm/frames.h | 8 + .../Contracts/IExecutionManager.cs | 3 +- .../Contracts/IPrecodeStubs.cs | 8 + .../DataType.cs | 6 + ...cutionManagerCore.InterpreterJitManager.cs | 157 ++++ .../ExecutionManager/ExecutionManagerCore.cs | 12 +- .../Contracts/PrecodeStubs_1.cs | 5 + .../Contracts/PrecodeStubs_2.cs | 5 + .../Contracts/PrecodeStubs_3.cs | 10 + .../Contracts/PrecodeStubs_Common.cs | 37 + .../StackWalk/FrameHandling/FrameHelpers.cs | 407 +++++++++++ .../StackWalk/FrameHandling/FrameIterator.cs | 293 +------- .../Contracts/StackWalk/GC/GcScanner.cs | 18 +- ...ng.cs => StackWalk_1.ExceptionHandling.cs} | 0 .../Contracts/StackWalk/StackWalk_1.cs | 198 ++++- .../Data/InterpByteCodeStart.cs | 18 + .../Data/InterpMethod.cs | 18 + .../Data/InterpMethodContextFrame.cs | 24 + .../Data/InterpreterFrame.cs | 18 + .../Data/InterpreterPrecodeData.cs | 20 + .../Data/InterpreterRealCodeHeader.cs | 25 + .../MethodValidation.cs | 26 +- .../ClrDataMethodInstance.cs | 3 +- .../SOSDacImpl.cs | 16 +- .../ExecutionManager/ExecutionManagerTests.cs | 68 ++ .../managed/cdac/tests/FrameIteratorTests.cs | 675 ++++++++++++++++++ .../managed/cdac/tests/MethodDescTests.cs | 137 +++- ...iagnostics.DataContractReader.Tests.csproj | 1 + .../MockDescriptors.ExecutionManager.cs | 77 ++ .../managed/cdac/tests/PrecodeStubsTests.cs | 188 ++++- .../cdac/tests/SOSDacInterface5Tests.cs | 12 + 36 files changed, 2297 insertions(+), 358 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/Contracts/StackWalk/FrameHandling/FrameHelpers.cs rename src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/{ExceptionHandling.cs => StackWalk_1.ExceptionHandling.cs} (100%) 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/FrameIteratorTests.cs diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index dd178a9f572dff..79e5d8ee121780 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -138,7 +138,8 @@ public enum CodeKind : uint CallCountingStub = 9, MethodCallThunk = 10, Jitted = 11, - ReadyToRun = 12 + ReadyToRun = 12, + Interpreter = 13 } ``` @@ -184,6 +185,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 | @@ -282,9 +287,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 interpreted 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) @@ -303,8 +310,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); + // 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; } ``` @@ -407,7 +416,7 @@ public override void GetMethodRegionInfo(RangeSection rangeSection, TargetCodePo ``` -`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. +`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. ```csharp TargetPointer IExecutionManager.NonVirtualEntry2MethodDesc(TargetCodePointer entrypoint) { @@ -480,6 +489,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`. @@ -490,6 +501,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`. @@ -498,6 +511,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. @@ -515,7 +530,7 @@ After obtaining the clause array bounds, the common iteration logic classifies e `IsFilterFunclet` first checks `IsFunclet`. If the code block is a funclet, it retrieves the EH clauses for the method and checks whether any filter clause's handler offset matches the funclet's relative offset. If a match is found, the funclet is a filter funclet. -`GetCodeKind` classifies a code address by finding its owning range section and determining the code kind. It distinguishes between jitted code, stub code blocks (jump stubs, precode stubs, VSD stubs, etc.), and ReadyToRun code. Returns `Unknown` if the address cannot be classified. We depend on the values of the StubCodeBlockKind enum defined in codeman.h; for non-R2R code, we compare either the RangeList type or the code header against the values of this enum. +`GetCodeKind` classifies a code address by finding its owning range section and determining the code kind. It distinguishes between jitted code, stub code blocks (jump stubs, precode stubs, VSD stubs, etc.), ReadyToRun code, and interpreter code. Returns `Unknown` if the address cannot be classified. We depend on the values of the StubCodeBlockKind enum defined in codeman.h; for non-R2R code, we compare either the RangeList type or the code header against the values of this enum. ### FindReadyToRunModule `FindReadyToRunModule` locates the ReadyToRun module whose PE image contains the given address. Unlike `GetCodeBlockHandle` (which only matches code regions), this API matches against the full PE image range - including data sections such as import tables. This is used in GCRefMap resolution as it requires finding the module that owns an import section indirection address, which is in the data section rather than the code section. diff --git a/docs/design/datacontracts/PrecodeStubs.md b/docs/design/datacontracts/PrecodeStubs.md index b268531576fd26..217c275b6ef4b8 100644 --- a/docs/design/datacontracts/PrecodeStubs.md +++ b/docs/design/datacontracts/PrecodeStubs.md @@ -11,6 +11,11 @@ This contract provides support for examining [precode](../coreclr/botr/method-de // Given an interior address within a precode stub and the kind of stub (StubPrecode or FixupPrecode), // computes the entry point of the precode. TargetPointer GetPrecodeEntryPointFromInteriorAddress(TargetCodePointer interiorAddress, bool isFixupPrecode); + + // 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 @@ -44,6 +49,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. @@ -263,6 +272,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 @@ -286,6 +311,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; } @@ -299,6 +326,33 @@ 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 == TargetPointer.Null) + return entryPoint; + + return new TargetCodePointer(precodeData.ByteCodeAddr); + } + catch + { + return entryPoint; + } + } ``` ### `GetPrecodeEntryPointFromInteriorAddress` diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index e8d88ed9957bca..8aa7373b2e3bc5 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -84,6 +84,11 @@ 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) | +| `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 | @@ -133,6 +138,33 @@ 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` 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 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 (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.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 307b2eef5bb9f7..99c560ed9fad3a 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -852,6 +852,42 @@ 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_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 + CDAC_TYPE_BEGIN(EEExceptionClause) CDAC_TYPE_SIZE(sizeof(EE_ILEXCEPTION_CLAUSE)) CDAC_TYPE_FIELD(EEExceptionClause, T_UINT32, Flags, offsetof(EE_ILEXCEPTION_CLAUSE, Flags)) @@ -985,6 +1021,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 00f4d86f2578c3..c4081fd2612544 100644 --- a/src/coreclr/vm/frames.h +++ b/src/coreclr/vm/frames.h @@ -2332,6 +2332,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 288d202ac0ecbf..9bf944b96f1556 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 @@ -56,7 +56,8 @@ public enum CodeKind : uint CallCountingStub = 9, MethodCallThunk = 10, Jitted = 11, - ReadyToRun = 12 + ReadyToRun = 12, + Interpreter = 13 } public interface ICodeHeapInfo 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 9e2aab6bef3711..a31b215ba93c09 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 @@ -13,6 +13,14 @@ public interface IPrecodeStubs : IContract // Given an interior address within a precode stub and the kind of stub (StubPrecode or FixupPrecode), // computes the entry point of the precode. TargetPointer GetPrecodeEntryPointFromInteriorAddress(TargetCodePointer interiorAddress, bool isFixupPrecode) => 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 27a8cdbc90d89a..b3802a8bcc03bc 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -92,6 +92,10 @@ public enum DataType StubPrecodeData, FixupPrecodeData, ThisPtrRetBufPrecodeData, + InterpreterPrecodeData, + InterpByteCodeStart, + InterpMethod, + InterpMethodContextFrame, Array, SyncBlock, SyncTableEntry, @@ -110,6 +114,7 @@ public enum DataType RangeSectionFragment, RangeSection, RealCodeHeader, + InterpreterRealCodeHeader, CodeHeapListNode, CodeHeap, LoaderCodeHeap, @@ -164,6 +169,7 @@ public enum DataType StubDispatchFrame, ExternalMethodFrame, DynamicHelperFrame, + InterpreterFrame, ComCallWrapper, SimpleComCallWrapper, 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..54cf0b7fc83860 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.InterpreterJitManager.cs @@ -0,0 +1,157 @@ +// 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 CodeKind GetCodeKind(RangeSection rangeSection, TargetCodePointer codeAddress) + { + return CodeKind.Interpreter; + } + + 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 7d14a60fc4d371..89bda5e2ff51b8 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. @@ -133,6 +136,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) { @@ -170,7 +176,11 @@ internal static RangeSection Find(Target target, Data.RangeSectionMap topRangeSe private JitManager? GetJitManager(RangeSection rangeSection) { - if (rangeSection.Data!.R2RModule != TargetPointer.Null) + if (rangeSection.IsInterpreter) + { + return _interpreterJitManager; + } + else if (rangeSection.Data!.R2RModule != TargetPointer.Null) { return _r2rJitManager; } 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 a55b283c0c4601..b558fc99e84537 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; } @@ -172,4 +185,28 @@ TargetPointer IPrecodeStubs.GetPrecodeEntryPointFromInteriorAddress(TargetCodePo return new TargetPointer(entryPointAddress); } + + 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 (System.Exception ex) when (ex is VirtualReadException or NotImplementedException) + { + return entryPoint; + } + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs new file mode 100644 index 00000000000000..65264683b452fd --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs @@ -0,0 +1,407 @@ +// 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; +using Microsoft.Diagnostics.DataContractReader.Data; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +internal enum FrameType +{ + Unknown, + + InlinedCallFrame, + SoftwareExceptionFrame, + + /* TransitionFrame Types */ + FramedMethodFrame, + PInvokeCalliFrame, + PrestubMethodFrame, + StubDispatchFrame, + CallCountingHelperFrame, + ExternalMethodFrame, + DynamicHelperFrame, + InterpreterFrame, + + FuncEvalFrame, + + /* ResumableFrame Types */ + ResumableFrame, + RedirectedThreadFrame, + + FaultingExceptionFrame, + + HijackFrame, + + TailCallFrame, + + /* Other Frame Types not handled by the iterator */ + ProtectValueClassFrame, + DebuggerClassInitMarkFrame, + DebuggerExitFrame, + DebuggerU2MCatchHandlerFrame, + ExceptionFilterFrame, +} + +/// +/// Helpers for inspecting and operating on a single capital-F . +/// These do not depend on iterator state — given a frame (or frame pointer/identifier) they +/// return information about that one frame or use it to update a context. +/// drives the chain traversal and uses these helpers to +/// classify and decode each frame as it walks. +/// +internal sealed class FrameHelpers +{ + private readonly Target _target; + + public FrameHelpers(Target target) + { + _target = target; + } + + public string GetFrameName(TargetPointer frameIdentifier) + { + FrameType frameType = GetFrameType(frameIdentifier); + if (frameType == FrameType.Unknown) + { + return string.Empty; + } + return frameType.ToString(); + } + + internal FrameType GetFrameType(TargetPointer frameIdentifier) + { + foreach (FrameType frameType in Enum.GetValues()) + { + if (_target.TryReadGlobalPointer(frameType.ToString() + "Identifier", out TargetPointer? id)) + { + if (frameIdentifier == id) + { + return frameType; + } + } + } + + return FrameType.Unknown; + } + + public TargetPointer GetMethodDescPtr(TargetPointer framePtr) + { + Data.Frame frame = _target.ProcessedData.GetOrAdd(framePtr); + FrameType frameType = GetFrameType(frame.Identifier); + switch (frameType) + { + case FrameType.FramedMethodFrame: + case FrameType.DynamicHelperFrame: + case FrameType.ExternalMethodFrame: + case FrameType.PrestubMethodFrame: + case FrameType.CallCountingHelperFrame: + Data.FramedMethodFrame framedMethodFrame = _target.ProcessedData.GetOrAdd(frame.Address); + return framedMethodFrame.MethodDescPtr; + case FrameType.InterpreterFrame: + // InterpreterFrame is constructed with pMD=NULL in native. + // The interpreted methods are yielded as frameless frames via + // interpreter virtual unwind, not through the Frame's MethodDesc. + return TargetPointer.Null; + case FrameType.PInvokeCalliFrame: + return TargetPointer.Null; + case FrameType.StubDispatchFrame: + Data.StubDispatchFrame stubDispatchFrame = _target.ProcessedData.GetOrAdd(frame.Address); + if (stubDispatchFrame.MethodDescPtr != TargetPointer.Null) + { + return stubDispatchFrame.MethodDescPtr; + } + else if (stubDispatchFrame.RepresentativeMTPtr != TargetPointer.Null) + { + IRuntimeTypeSystem rtsContract = _target.Contracts.RuntimeTypeSystem; + TypeHandle mtHandle = rtsContract.GetTypeHandle(stubDispatchFrame.RepresentativeMTPtr); + return rtsContract.GetMethodDescForSlot(mtHandle, (ushort)stubDispatchFrame.RepresentativeSlot); + } + else + { + return TargetPointer.Null; + } + case FrameType.InlinedCallFrame: + Data.InlinedCallFrame inlinedCallFrame = _target.ProcessedData.GetOrAdd(frame.Address); + if (InlinedCallFrameHasActiveCall(inlinedCallFrame) && InlinedCallFrameHasFunction(inlinedCallFrame)) + return inlinedCallFrame.Datum & ~(ulong)(_target.PointerSize - 1); + else + return TargetPointer.Null; + default: + return TargetPointer.Null; + } + } + + /// + /// Updates based on 's type, replicating + /// the per-frame context update performed by the native stack walker before it yields a + /// SW_FRAME (or transitions to SW_FRAMELESS for InterpreterFrame). + /// + public void UpdateContextFromFrame(Data.Frame frame, IPlatformAgnosticContext context) + { + switch (GetFrameType(frame.Identifier)) + { + case FrameType.InlinedCallFrame: + Data.InlinedCallFrame inlinedCallFrame = _target.ProcessedData.GetOrAdd(frame.Address); + GetFrameHandler(context).HandleInlinedCallFrame(inlinedCallFrame); + return; + + case FrameType.SoftwareExceptionFrame: + Data.SoftwareExceptionFrame softwareExceptionFrame = _target.ProcessedData.GetOrAdd(frame.Address); + GetFrameHandler(context).HandleSoftwareExceptionFrame(softwareExceptionFrame); + return; + + // TransitionFrame type frames + case FrameType.FramedMethodFrame: + case FrameType.PInvokeCalliFrame: + case FrameType.PrestubMethodFrame: + case FrameType.StubDispatchFrame: + case FrameType.CallCountingHelperFrame: + case FrameType.ExternalMethodFrame: + case FrameType.DynamicHelperFrame: + // FrameMethodFrame is the base type for all transition Frames + Data.FramedMethodFrame framedMethodFrame = _target.ProcessedData.GetOrAdd(frame.Address); + GetFrameHandler(context).HandleTransitionFrame(framedMethodFrame); + return; + + case FrameType.InterpreterFrame: + { + // Match native stackwalk.cpp:2248-2261: Set context to the top + // InterpMethodContextFrame so the walker transitions to SW_FRAMELESS + // and yields each interpreted method individually via virtual unwind. + Data.InterpreterFrame interpreterFrame = _target.ProcessedData.GetOrAdd(frame.Address); + TargetPointer topContextFramePtr = ResolveTopInterpMethodContextFrame(interpreterFrame); + if (topContextFramePtr != TargetPointer.Null) + { + Data.InterpMethodContextFrame topContextFrame = _target.ProcessedData.GetOrAdd(topContextFramePtr); + context.InstructionPointer = new TargetPointer((ulong)topContextFrame.Ip); + context.StackPointer = topContextFramePtr; + } + return; + } + + case FrameType.FuncEvalFrame: + Data.FuncEvalFrame funcEvalFrame = _target.ProcessedData.GetOrAdd(frame.Address); + GetFrameHandler(context).HandleFuncEvalFrame(funcEvalFrame); + return; + + // ResumableFrame type frames + case FrameType.ResumableFrame: + case FrameType.RedirectedThreadFrame: + Data.ResumableFrame resumableFrame = _target.ProcessedData.GetOrAdd(frame.Address); + GetFrameHandler(context).HandleResumableFrame(resumableFrame); + return; + + case FrameType.FaultingExceptionFrame: + Data.FaultingExceptionFrame faultingExceptionFrame = _target.ProcessedData.GetOrAdd(frame.Address); + GetFrameHandler(context).HandleFaultingExceptionFrame(faultingExceptionFrame); + return; + + case FrameType.HijackFrame: + Data.HijackFrame hijackFrame = _target.ProcessedData.GetOrAdd(frame.Address); + GetFrameHandler(context).HandleHijackFrame(hijackFrame); + return; + case FrameType.TailCallFrame: + Data.TailCallFrame tailCallFrame = _target.ProcessedData.GetOrAdd(frame.Address); + GetFrameHandler(context).HandleTailCallFrame(tailCallFrame); + return; + default: + // Unknown Frame type. This could either be a Frame that we don't know how to handle, + // or a Frame that does not update the context. + return; + } + } + + /// + /// Returns the return address for , matching native Frame::GetReturnAddress(). + /// Returns TargetPointer.Null if the Frame has no return address (e.g., non-active ICF, + /// base Frame types, FuncEvalFrame during exception eval). + /// + public TargetPointer GetReturnAddress(Data.Frame frame) + { + FrameType frameType = GetFrameType(frame.Identifier); + switch (frameType) + { + // InlinedCallFrame: returns 0 if inactive, else m_pCallerReturnAddress + case FrameType.InlinedCallFrame: + Data.InlinedCallFrame icf = _target.ProcessedData.GetOrAdd(frame.Address); + return InlinedCallFrameHasActiveCall(icf) ? icf.CallerReturnAddress : TargetPointer.Null; + + // TransitionFrame types: read return address from the transition block + case FrameType.FramedMethodFrame: + case FrameType.PInvokeCalliFrame: + case FrameType.PrestubMethodFrame: + case FrameType.StubDispatchFrame: + case FrameType.CallCountingHelperFrame: + case FrameType.ExternalMethodFrame: + case FrameType.DynamicHelperFrame: + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frame.Address); + Data.TransitionBlock tb = _target.ProcessedData.GetOrAdd(fmf.TransitionBlockPtr); + return tb.ReturnAddress; + + // SoftwareExceptionFrame: stored m_ReturnAddress + case FrameType.SoftwareExceptionFrame: + Data.SoftwareExceptionFrame sef = _target.ProcessedData.GetOrAdd(frame.Address); + return sef.ReturnAddress; + + // ResumableFrame / RedirectedThreadFrame: RIP from captured context + case FrameType.ResumableFrame: + case FrameType.RedirectedThreadFrame: + { + Data.ResumableFrame rf = _target.ProcessedData.GetOrAdd(frame.Address); + IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(_target); + ctx.ReadFromAddress(_target, rf.TargetContextPtr); + return ctx.InstructionPointer; + } + + // FaultingExceptionFrame: RIP from embedded context + case FrameType.FaultingExceptionFrame: + { + Data.FaultingExceptionFrame fef = _target.ProcessedData.GetOrAdd(frame.Address); + IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(_target); + ctx.ReadFromAddress(_target, fef.TargetContext); + return ctx.InstructionPointer; + } + + // HijackFrame: stored m_ReturnAddress + case FrameType.HijackFrame: + Data.HijackFrame hf = _target.ProcessedData.GetOrAdd(frame.Address); + return hf.ReturnAddress; + + // TailCallFrame: stored m_ReturnAddress + case FrameType.TailCallFrame: + Data.TailCallFrame tcf = _target.ProcessedData.GetOrAdd(frame.Address); + return tcf.ReturnAddress; + + // FuncEvalFrame: returns 0 during exception eval, else from transition block + case FrameType.FuncEvalFrame: + Data.FuncEvalFrame funcEval = _target.ProcessedData.GetOrAdd(frame.Address); + Data.DebuggerEval dbgEval = _target.ProcessedData.GetOrAdd(funcEval.DebuggerEvalPtr); + if (dbgEval.EvalUsesHijack) + return TargetPointer.Null; + Data.FramedMethodFrame funcEvalFmf = _target.ProcessedData.GetOrAdd(frame.Address); + Data.TransitionBlock funcEvalTb = _target.ProcessedData.GetOrAdd(funcEvalFmf.TransitionBlockPtr); + return funcEvalTb.ReturnAddress; + + // Base Frame and unknown types: return 0 (matches native Frame::GetReturnAddressPtr_Impl) + default: + return TargetPointer.Null; + } + } + + internal IPlatformFrameHandler GetFrameHandler(IPlatformAgnosticContext context) + { + return context switch + { + ContextHolder contextHolder => new X86FrameHandler(_target, contextHolder), + ContextHolder contextHolder => new AMD64FrameHandler(_target, contextHolder), + ContextHolder contextHolder => new ARMFrameHandler(_target, contextHolder), + ContextHolder contextHolder => new ARM64FrameHandler(_target, contextHolder), + ContextHolder contextHolder => new RISCV64FrameHandler(_target, contextHolder), + ContextHolder contextHolder => new LoongArch64FrameHandler(_target, contextHolder), + _ => throw new InvalidOperationException("Unsupported context type"), + }; + } + + internal static bool InlinedCallFrameHasActiveCall(Data.InlinedCallFrame frame) + { + return frame.CallerReturnAddress != TargetPointer.Null; + } + + private bool InlinedCallFrameHasFunction(Data.InlinedCallFrame frame) + { + if (_target.PointerSize == sizeof(ulong)) + { + return frame.Datum != TargetPointer.Null && (frame.Datum.Value & 0x1) == 0; + } + else + { + return ((long)frame.Datum.Value & ~0xffff) != 0; + } + } + + /// + /// Resolves the MethodDesc from a specific InterpMethodContextFrame by following: + /// InterpMethodContextFrame.StartIp -> InterpByteCodeStart.Method -> InterpMethod.MethodDesc + /// + internal TargetPointer ResolveMethodDescFromInterpFrame(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; + } + + /// + /// 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 TargetPointer ResolveTopInterpMethodContextFrame(Data.InterpreterFrame interpreterFrame) + { + TargetPointer hintPtr = interpreterFrame.TopInterpMethodContextFrame; + 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 , then the ParentPtr chain is walked. + /// Only active frames (Ip != null) are yielded. + /// + internal IEnumerable WalkInterpreterFrameChain(TargetPointer frameAddress) + { + Data.InterpreterFrame interpFrame = _target.ProcessedData.GetOrAdd(frameAddress); + TargetPointer interpMethodFramePtr = ResolveTopInterpMethodContextFrame(interpFrame); + while (interpMethodFramePtr != TargetPointer.Null) + { + 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/FrameHandling/FrameIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs index 915e8504082d69..cb108746bed366 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 @@ -1,52 +1,22 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using Microsoft.Diagnostics.DataContractReader.Data; namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; +/// +/// Walks the linked list of capital-F structures pushed on a +/// managed thread (Thread::m_pFrame chain), maintaining a single current-frame cursor. +/// This class only owns iteration state; per-frame inspection and operations live in +/// . Convenience methods on this class forward to +/// for the current frame. +/// internal sealed class FrameIterator { - internal enum FrameType - { - Unknown, - - InlinedCallFrame, - SoftwareExceptionFrame, - - /* TransitionFrame Types */ - FramedMethodFrame, - PInvokeCalliFrame, - PrestubMethodFrame, - StubDispatchFrame, - CallCountingHelperFrame, - ExternalMethodFrame, - DynamicHelperFrame, - - FuncEvalFrame, - - /* ResumableFrame Types */ - ResumableFrame, - RedirectedThreadFrame, - - FaultingExceptionFrame, - - HijackFrame, - - TailCallFrame, - - /* Other Frame Types not handled by the iterator */ - ProtectValueClassFrame, - DebuggerClassInitMarkFrame, - DebuggerExitFrame, - DebuggerU2MCatchHandlerFrame, - ExceptionFilterFrame, - InterpreterFrame, - } - private readonly Target target; private readonly TargetPointer terminator; + private readonly FrameHelpers frameHelpers; private TargetPointer currentFramePointer; internal Data.Frame CurrentFrame => target.ProcessedData.GetOrAdd(currentFramePointer); @@ -57,6 +27,7 @@ public FrameIterator(Target target, ThreadData threadData) { this.target = target; terminator = new TargetPointer(target.PointerSize == 8 ? ulong.MaxValue : uint.MaxValue); + frameHelpers = new FrameHelpers(target); currentFramePointer = threadData.Frame; } @@ -74,241 +45,21 @@ public bool Next() return currentFramePointer != terminator; } - public void UpdateContextFromFrame(IPlatformAgnosticContext context) - { - switch (GetFrameType(target, CurrentFrame.Identifier)) - { - case FrameType.InlinedCallFrame: - Data.InlinedCallFrame inlinedCallFrame = target.ProcessedData.GetOrAdd(CurrentFrame.Address); - GetFrameHandler(context).HandleInlinedCallFrame(inlinedCallFrame); - return; - - case FrameType.SoftwareExceptionFrame: - Data.SoftwareExceptionFrame softwareExceptionFrame = target.ProcessedData.GetOrAdd(CurrentFrame.Address); - GetFrameHandler(context).HandleSoftwareExceptionFrame(softwareExceptionFrame); - return; - - // TransitionFrame type frames - case FrameType.FramedMethodFrame: - case FrameType.PInvokeCalliFrame: - case FrameType.PrestubMethodFrame: - case FrameType.StubDispatchFrame: - case FrameType.CallCountingHelperFrame: - case FrameType.ExternalMethodFrame: - case FrameType.DynamicHelperFrame: - // FrameMethodFrame is the base type for all transition Frames - Data.FramedMethodFrame framedMethodFrame = target.ProcessedData.GetOrAdd(CurrentFrame.Address); - GetFrameHandler(context).HandleTransitionFrame(framedMethodFrame); - return; - - case FrameType.FuncEvalFrame: - Data.FuncEvalFrame funcEvalFrame = target.ProcessedData.GetOrAdd(CurrentFrame.Address); - GetFrameHandler(context).HandleFuncEvalFrame(funcEvalFrame); - return; - - // ResumableFrame type frames - case FrameType.ResumableFrame: - case FrameType.RedirectedThreadFrame: - Data.ResumableFrame resumableFrame = target.ProcessedData.GetOrAdd(CurrentFrame.Address); - GetFrameHandler(context).HandleResumableFrame(resumableFrame); - return; - - case FrameType.FaultingExceptionFrame: - Data.FaultingExceptionFrame faultingExceptionFrame = target.ProcessedData.GetOrAdd(CurrentFrame.Address); - GetFrameHandler(context).HandleFaultingExceptionFrame(faultingExceptionFrame); - return; - - case FrameType.HijackFrame: - Data.HijackFrame hijackFrame = target.ProcessedData.GetOrAdd(CurrentFrame.Address); - GetFrameHandler(context).HandleHijackFrame(hijackFrame); - return; - case FrameType.TailCallFrame: - Data.TailCallFrame tailCallFrame = target.ProcessedData.GetOrAdd(CurrentFrame.Address); - GetFrameHandler(context).HandleTailCallFrame(tailCallFrame); - return; - default: - // Unknown Frame type. This could either be a Frame that we don't know how to handle, - // or a Frame that does not update the context. - return; - } - } - /// - /// Returns the return address for the current Frame, matching native Frame::GetReturnAddress(). - /// Returns TargetPointer.Null if the Frame has no return address (e.g., non-active ICF, - /// base Frame types, FuncEvalFrame during exception eval). + /// Returns the of the current frame. /// - public TargetPointer GetReturnAddress() - { - FrameType frameType = GetCurrentFrameType(); - switch (frameType) - { - // InlinedCallFrame: returns 0 if inactive, else m_pCallerReturnAddress - case FrameType.InlinedCallFrame: - Data.InlinedCallFrame icf = target.ProcessedData.GetOrAdd(currentFramePointer); - return InlinedCallFrameHasActiveCall(icf) ? icf.CallerReturnAddress : TargetPointer.Null; - - // TransitionFrame types: read return address from the transition block - case FrameType.FramedMethodFrame: - case FrameType.PInvokeCalliFrame: - case FrameType.PrestubMethodFrame: - case FrameType.StubDispatchFrame: - case FrameType.CallCountingHelperFrame: - case FrameType.ExternalMethodFrame: - case FrameType.DynamicHelperFrame: - Data.FramedMethodFrame fmf = target.ProcessedData.GetOrAdd(currentFramePointer); - Data.TransitionBlock tb = target.ProcessedData.GetOrAdd(fmf.TransitionBlockPtr); - return tb.ReturnAddress; - - // SoftwareExceptionFrame: stored m_ReturnAddress - case FrameType.SoftwareExceptionFrame: - Data.SoftwareExceptionFrame sef = target.ProcessedData.GetOrAdd(currentFramePointer); - return sef.ReturnAddress; - - // ResumableFrame / RedirectedThreadFrame: RIP from captured context - case FrameType.ResumableFrame: - case FrameType.RedirectedThreadFrame: - { - Data.ResumableFrame rf = target.ProcessedData.GetOrAdd(currentFramePointer); - IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(target); - ctx.ReadFromAddress(target, rf.TargetContextPtr); - return ctx.InstructionPointer; - } - - // FaultingExceptionFrame: RIP from embedded context - case FrameType.FaultingExceptionFrame: - { - Data.FaultingExceptionFrame fef = target.ProcessedData.GetOrAdd(currentFramePointer); - IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(target); - ctx.ReadFromAddress(target, fef.TargetContext); - return ctx.InstructionPointer; - } - - // HijackFrame: stored m_ReturnAddress - case FrameType.HijackFrame: - Data.HijackFrame hf = target.ProcessedData.GetOrAdd(currentFramePointer); - return hf.ReturnAddress; + public FrameType GetCurrentFrameType() + => frameHelpers.GetFrameType(CurrentFrame.Identifier); - // TailCallFrame: stored m_ReturnAddress - case FrameType.TailCallFrame: - Data.TailCallFrame tcf = target.ProcessedData.GetOrAdd(currentFramePointer); - return tcf.ReturnAddress; - - // FuncEvalFrame: returns 0 during exception eval, else from transition block - case FrameType.FuncEvalFrame: - Data.FuncEvalFrame funcEval = target.ProcessedData.GetOrAdd(currentFramePointer); - Data.DebuggerEval dbgEval = target.ProcessedData.GetOrAdd(funcEval.DebuggerEvalPtr); - if (dbgEval.EvalUsesHijack) - return TargetPointer.Null; - Data.FramedMethodFrame funcEvalFmf = target.ProcessedData.GetOrAdd(currentFramePointer); - Data.TransitionBlock funcEvalTb = target.ProcessedData.GetOrAdd(funcEvalFmf.TransitionBlockPtr); - return funcEvalTb.ReturnAddress; - - // Base Frame and unknown types: return 0 (matches native Frame::GetReturnAddressPtr_Impl) - default: - return TargetPointer.Null; - } - } - - public static string GetFrameName(Target target, TargetPointer frameIdentifier) - { - FrameType frameType = GetFrameType(target, frameIdentifier); - if (frameType == FrameType.Unknown) - { - return string.Empty; - } - return frameType.ToString(); - } - - public FrameType GetCurrentFrameType() => GetFrameType(target, CurrentFrame.Identifier); - - internal static FrameType GetFrameType(Target target, TargetPointer frameIdentifier) - { - foreach (FrameType frameType in Enum.GetValues()) - { - if (target.TryReadGlobalPointer(frameType.ToString() + "Identifier", out TargetPointer? id)) - { - if (frameIdentifier == id) - { - return frameType; - } - } - } - - return FrameType.Unknown; - } - - private IPlatformFrameHandler GetFrameHandler(IPlatformAgnosticContext context) - { - return context switch - { - ContextHolder contextHolder => new X86FrameHandler(target, contextHolder), - ContextHolder contextHolder => new AMD64FrameHandler(target, contextHolder), - ContextHolder contextHolder => new ARMFrameHandler(target, contextHolder), - ContextHolder contextHolder => new ARM64FrameHandler(target, contextHolder), - ContextHolder contextHolder => new RISCV64FrameHandler(target, contextHolder), - ContextHolder contextHolder => new LoongArch64FrameHandler(target, contextHolder), - _ => throw new InvalidOperationException("Unsupported context type"), - }; - } - - public static TargetPointer GetMethodDescPtr(Target target, TargetPointer framePtr) - { - Data.Frame frame = target.ProcessedData.GetOrAdd(framePtr); - FrameType frameType = GetFrameType(target, frame.Identifier); - switch (frameType) - { - case FrameType.FramedMethodFrame: - case FrameType.DynamicHelperFrame: - 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.PInvokeCalliFrame: - return TargetPointer.Null; - case FrameType.StubDispatchFrame: - Data.StubDispatchFrame stubDispatchFrame = target.ProcessedData.GetOrAdd(frame.Address); - if (stubDispatchFrame.MethodDescPtr != TargetPointer.Null) - { - return stubDispatchFrame.MethodDescPtr; - } - else if (stubDispatchFrame.RepresentativeMTPtr != TargetPointer.Null) - { - IRuntimeTypeSystem rtsContract = target.Contracts.RuntimeTypeSystem; - TypeHandle mtHandle = rtsContract.GetTypeHandle(stubDispatchFrame.RepresentativeMTPtr); - return rtsContract.GetMethodDescForSlot(mtHandle, (ushort)stubDispatchFrame.RepresentativeSlot); - } - else - { - return TargetPointer.Null; - } - case FrameType.InlinedCallFrame: - Data.InlinedCallFrame inlinedCallFrame = target.ProcessedData.GetOrAdd(frame.Address); - if (InlinedCallFrameHasActiveCall(inlinedCallFrame) && InlinedCallFrameHasFunction(inlinedCallFrame, target)) - return inlinedCallFrame.Datum & ~(ulong)(target.PointerSize - 1); - else - return TargetPointer.Null; - default: - return TargetPointer.Null; - } - } - - private static bool InlinedCallFrameHasFunction(Data.InlinedCallFrame frame, Target target) - { - if (target.PointerSize == sizeof(ulong)) - { - return frame.Datum != TargetPointer.Null && (frame.Datum.Value & 0x1) == 0; - } - else - { - return ((long)frame.Datum.Value & ~0xffff) != 0; - } - } + /// + /// Returns the return address of the current frame, matching native Frame::GetReturnAddress(). + /// + public TargetPointer GetCurrentReturnAddress() + => frameHelpers.GetReturnAddress(CurrentFrame); - private static bool InlinedCallFrameHasActiveCall(Data.InlinedCallFrame frame) - { - return frame.CallerReturnAddress != TargetPointer.Null; - } + /// + /// Updates based on the current frame's type. + /// + public void UpdateContextFromCurrentFrame(IPlatformAgnosticContext context) + => frameHelpers.UpdateContextFromFrame(CurrentFrame, context); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs index 807c66ae8bacaf..7001ba2ec993fc 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -19,12 +19,14 @@ internal class GcScanner private readonly Target _target; private readonly IExecutionManager _eman; private readonly IGCInfo _gcInfo; + private readonly FrameHelpers _frameHelpers; internal GcScanner(Target target) { _target = target; _eman = target.Contracts.ExecutionManager; _gcInfo = target.Contracts.GCInfo; + _frameHelpers = new FrameHelpers(target); } /// @@ -100,11 +102,11 @@ public void GcScanRoots(TargetPointer frameAddress, GcScanContext scanContext) return; Data.Frame frameData = _target.ProcessedData.GetOrAdd(frameAddress); - FrameIterator.FrameType frameType = FrameIterator.GetFrameType(_target, frameData.Identifier); + FrameType frameType = _frameHelpers.GetFrameType(frameData.Identifier); switch (frameType) { - case FrameIterator.FrameType.StubDispatchFrame: + case FrameType.StubDispatchFrame: { Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); Data.StubDispatchFrame sdf = _target.ProcessedData.GetOrAdd(frameAddress); @@ -120,7 +122,7 @@ public void GcScanRoots(TargetPointer frameAddress, GcScanContext scanContext) break; } - case FrameIterator.FrameType.ExternalMethodFrame: + case FrameType.ExternalMethodFrame: { Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); Data.ExternalMethodFrame emf = _target.ProcessedData.GetOrAdd(frameAddress); @@ -136,7 +138,7 @@ public void GcScanRoots(TargetPointer frameAddress, GcScanContext scanContext) break; } - case FrameIterator.FrameType.DynamicHelperFrame: + case FrameType.DynamicHelperFrame: { Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); Data.DynamicHelperFrame dhf = _target.ProcessedData.GetOrAdd(frameAddress); @@ -144,19 +146,19 @@ public void GcScanRoots(TargetPointer frameAddress, GcScanContext scanContext) break; } - case FrameIterator.FrameType.CallCountingHelperFrame: - case FrameIterator.FrameType.PrestubMethodFrame: + case FrameType.CallCountingHelperFrame: + case FrameType.PrestubMethodFrame: { Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); PromoteCallerStack(frameAddress, fmf.TransitionBlockPtr, scanContext); break; } - case FrameIterator.FrameType.HijackFrame: + case FrameType.HijackFrame: // TODO(stackref): Implement HijackFrame scanning (X86 only with FEATURE_HIJACK) break; - case FrameIterator.FrameType.ProtectValueClassFrame: + case FrameType.ProtectValueClassFrame: // TODO(stackref): Implement ProtectValueClassFrame scanning break; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/ExceptionHandling.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.ExceptionHandling.cs similarity index 100% rename from src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/ExceptionHandling.cs rename to src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.ExceptionHandling.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 c11974119d131b..f721a5bbd3bd4c 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 @@ -18,12 +18,14 @@ internal partial class StackWalk_1 : IStackWalk private readonly Target _target; private readonly IExecutionManager _eman; private readonly GcScanner _gcScanner; + private readonly FrameHelpers _frameHelpers; internal StackWalk_1(Target target) { _target = target; _eman = target.Contracts.ExecutionManager; _gcScanner = new GcScanner(target); + _frameHelpers = new FrameHelpers(target); } public enum StackWalkState @@ -47,15 +49,10 @@ private record StackDataFrameHandle( TargetPointer FrameAddress, ThreadData ThreadData, bool IsResumableFrame = false, - bool IsActiveFrame = false) : IStackDataFrameHandle + bool IsActiveFrame = false, + TargetPointer InterpContextFramePtr = default) : IStackDataFrameHandle { } - private enum ContextFlags - { - Full = 0x1, - All = 0x2, - } - private class StackWalkData(IPlatformAgnosticContext context, StackWalkState state, FrameIterator frameIter, ThreadData threadData) { public IPlatformAgnosticContext Context { get; set; } = context; @@ -63,7 +60,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). @@ -80,7 +76,13 @@ private class StackWalkData(IPlatformAgnosticContext context, StackWalkState sta // The frame type of the last SW_FRAME processed by Next(). // Used by UpdateState to detect exception frames (FRAME_ATTR_EXCEPTION) and // set IsInterrupted when transitioning to a managed frame. - public FrameIterator.FrameType? LastProcessedFrameType { get; set; } + public FrameType? LastProcessedFrameType { get; set; } + + // The address of the InterpreterFrame that we're currently walking via virtual + // unwind. Saved when UpdateContextFromFrame transitions from SW_FRAME to + // SW_FRAMELESS, used by InterpreterVirtualUnwind for FMF transition when the + // interpreter chain is exhausted. In native, this is stored in FirstArgReg. + public TargetPointer CurrentInterpreterFrameAddress { get; set; } public bool IsCurrentFrameResumable() { @@ -95,9 +97,9 @@ public bool IsCurrentFrameResumable() // (see frames.h). On x86 it uses GcScanRoots_Impl instead of the // resumable frame pattern. When x86 cDAC stack walking is supported, // HijackFrame should be conditioned on the target architecture. - return ft is FrameIterator.FrameType.ResumableFrame - or FrameIterator.FrameType.RedirectedThreadFrame - or FrameIterator.FrameType.HijackFrame; + return ft is FrameType.ResumableFrame + or FrameType.RedirectedThreadFrame + or FrameType.HijackFrame; } /// @@ -128,20 +130,34 @@ 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); } } + private enum ContextFlags + { + Full = 0x1, + All = 0x2, + } + IEnumerable IStackWalk.CreateStackWalk(ThreadData threadData) { IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); uint contextFlags = context.AllContextFlags; FillContextFromThread(context, threadData, contextFlags); - StackWalkState state = IsManaged(context.InstructionPointer, out _) ? StackWalkState.SW_FRAMELESS : StackWalkState.SW_FRAME; + StackWalkState state; + if (IsManaged(context.InstructionPointer, out _)) + { + state = StackWalkState.SW_FRAMELESS; + } + else + { + state = StackWalkState.SW_FRAME; + } FrameIterator frameIterator = new(_target, threadData); // if the next Frame is not valid and we are not in managed code, there is nothing to return @@ -655,21 +671,46 @@ private bool Next(StackWalkData handle) Debug.Assert( !handle.FrameIter.IsValid() || handle.Context.StackPointer.Value < handle.FrameIter.CurrentFrameAddress.Value || - handle.FrameIter.GetCurrentFrameType() == FrameIterator.FrameType.FaultingExceptionFrame, + handle.FrameIter.GetCurrentFrameType() == FrameType.FaultingExceptionFrame, $"SP (0x{handle.Context.StackPointer:X}) should be below next Frame (0x{handle.FrameIter.CurrentFrameAddress:X})"); // Reset interrupted state after processing a managed frame. // Native stackwalk.cpp:2203-2205: isInterrupted = false; hasFaulted = false; handle.IsInterrupted = false; - try + TargetPointer prevIP = handle.Context.InstructionPointer; + TargetPointer prevSP = handle.Context.StackPointer; + + // Check if the current frame is interpreter code — if so, use + // interpreter virtual unwind instead of OS-level unwind. + // This mirrors VirtualUnwindInterpreterCallFrame in eetwain.cpp:2101. + if (IsInterpreterCode(prevIP)) { - handle.Context.Unwind(_target); + InterpreterVirtualUnwind(handle); } - catch + else { - handle.State = StackWalkState.SW_ERROR; - throw; + try + { + handle.Context.Unwind(_target); + } + catch + { + handle.State = StackWalkState.SW_ERROR; + throw; + } + // Guard against infinite loops when Unwind fails to advance. + 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: @@ -684,22 +725,53 @@ private bool Next(StackWalkData handle) { var frameType = handle.FrameIter.GetCurrentFrameType(); - TargetPointer returnAddress = handle.FrameIter.GetReturnAddress(); - bool isActiveICF = frameType == FrameIterator.FrameType.InlinedCallFrame + // Save the InterpreterFrame address before advancing the frame iterator. + // InterpreterVirtualUnwind needs this for the FMF transition when the + // interpreter chain is exhausted. + TargetPointer savedInterpreterFrameAddress = TargetPointer.Null; + if (frameType == FrameType.InterpreterFrame) + { + savedInterpreterFrameAddress = handle.FrameIter.CurrentFrameAddress; + handle.CurrentInterpreterFrameAddress = savedInterpreterFrameAddress; + } + + TargetPointer returnAddress = handle.FrameIter.GetCurrentReturnAddress(); + bool isActiveICF = frameType == FrameType.InlinedCallFrame && returnAddress != TargetPointer.Null; // Record the frame type so UpdateState can detect exception frames // and set IsInterrupted when transitioning to the managed frame. handle.LastProcessedFrameType = frameType; - if (returnAddress != TargetPointer.Null) + // For InterpreterFrame the FrameIterator has no GetReturnAddress + // (interpreter virtual unwind manages the IP), but we still need + // UpdateContextFromFrame to transition to SW_FRAMELESS in the + // interpreted method. + if (returnAddress != TargetPointer.Null + || frameType == FrameType.InterpreterFrame) { - handle.FrameIter.UpdateContextFromFrame(handle.Context); + handle.FrameIter.UpdateContextFromCurrentFrame(handle.Context); } if (!isActiveICF) { handle.FrameIter.Next(); } + + // After advancing past an InterpreterFrame, the next frame must NOT be + // the same InterpreterFrame. The native walker (PR #126953) prevents this + // via ResetRegDisp dedup; our UpdateContextFromFrame + Next() achieves + // the same by advancing the frame iterator past the InterpreterFrame + // before the interpreter virtual unwind exhausts the chain and triggers + // the FMF transition back to SW_FRAME. + if (savedInterpreterFrameAddress != TargetPointer.Null + && handle.FrameIter.IsValid() + && handle.FrameIter.GetCurrentFrameType() == FrameType.InterpreterFrame + && handle.FrameIter.CurrentFrameAddress == savedInterpreterFrameAddress) + { + Debug.Fail( + $"InterpreterFrame at {savedInterpreterFrameAddress} was not advanced past — " + + "this would cause doubled interpreter frames in the stack walk."); + } } break; case StackWalkState.SW_ERROR: @@ -730,8 +802,8 @@ private void UpdateState(StackWalkData handle) // Both FaultingExceptionFrame (hardware) and SoftwareExceptionFrame (managed throw) // have FRAME_ATTR_EXCEPTION set. The resulting managed frame gets ExecutionAborted, // causing GcInfoDecoder to skip live slot reporting at non-interruptible offsets. - if (handle.LastProcessedFrameType is FrameIterator.FrameType.FaultingExceptionFrame - or FrameIterator.FrameType.SoftwareExceptionFrame) + if (handle.LastProcessedFrameType is FrameType.FaultingExceptionFrame + or FrameType.SoftwareExceptionFrame) { handle.IsInterrupted = true; } @@ -795,15 +867,21 @@ TargetPointer IStackWalk.GetInstructionPointer(IStackDataFrameHandle stackDataFr } string IStackWalk.GetFrameName(TargetPointer frameIdentifier) - => FrameIterator.GetFrameName(_target, frameIdentifier); + => _frameHelpers.GetFrameName(frameIdentifier); TargetPointer IStackWalk.GetMethodDescPtr(TargetPointer framePtr) - => FrameIterator.GetMethodDescPtr(_target, framePtr); + => _frameHelpers.GetMethodDescPtr(framePtr); TargetPointer IStackWalk.GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHandle) { 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 _frameHelpers.ResolveMethodDescFromInterpFrame(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) @@ -816,9 +894,9 @@ TargetPointer IStackWalk.GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHa bool reportInteropMD = false; Data.Frame frameData = _target.ProcessedData.GetOrAdd(framePtr); - FrameIterator.FrameType frameType = FrameIterator.GetFrameType(_target, frameData.Identifier); + FrameType frameType = _frameHelpers.GetFrameType(frameData.Identifier); - if (frameType == FrameIterator.FrameType.InlinedCallFrame && + if (frameType == FrameType.InlinedCallFrame && handle.State == StackWalkState.SW_SKIPPED_FRAME) { IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; @@ -886,4 +964,62 @@ private static StackDataFrameHandle AssertCorrectHandle(IStackDataFrameHandle st return handle; } + + #region Interpreter + + // Interpreter-specific stack walk logic. Interpreted methods do not have OS-level + // unwind info; the helpers below implement the cDAC equivalent of native + // VirtualUnwindInterpreterCallFrame so the walker can step through interpreted + // call chains and transition cleanly back to the native caller of InterpExecMethod + // when the chain is exhausted. + + /// + /// Checks if the given IP is in interpreter-managed code (CodeKind.Interpreter). + /// + private bool IsInterpreterCode(TargetPointer ip) + { + return _eman.GetCodeKind(new TargetCodePointer(ip)) == CodeKind.Interpreter; + } + + /// + /// Performs interpreter virtual unwind, matching the native + /// VirtualUnwindInterpreterCallFrame in eetwain.cpp:2101-2124. + /// + /// When unwinding from a frameless interpreter frame, the SP points to the + /// current InterpMethodContextFrame. We follow pParent to get to the next + /// interpreted method in the call chain. If pParent is null, the interpreter + /// chain under the current InterpreterFrame is exhausted — we advance the + /// frame iterator past the InterpreterFrame and transition to SW_FRAME. + /// + private void InterpreterVirtualUnwind(StackWalkData handle) + { + TargetPointer currentFramePtr = handle.Context.StackPointer; + Data.InterpMethodContextFrame currentFrame = _target.ProcessedData.GetOrAdd(currentFramePtr); + + if (currentFrame.ParentPtr != TargetPointer.Null) + { + Data.InterpMethodContextFrame parentFrame = _target.ProcessedData.GetOrAdd(currentFrame.ParentPtr); + if (parentFrame.Ip != TargetPointer.Null) + { + // Parent is active — set context to the parent interpreted method. + handle.Context.InstructionPointer = new TargetPointer((ulong)parentFrame.Ip); + handle.Context.StackPointer = currentFrame.ParentPtr; + return; + } + } + + // No active parent — interpreter chain under this InterpreterFrame is exhausted. + // Use the saved InterpreterFrame's transition block to restore the context to + // the native caller of InterpExecMethod. This is the cDAC equivalent of the + // native DummyCallerIP → UpdateRegDisplay path (stackwalk.cpp:2159-2164). + if (handle.CurrentInterpreterFrameAddress != TargetPointer.Null) + { + Data.FramedMethodFrame framedMethodFrame = _target.ProcessedData.GetOrAdd(handle.CurrentInterpreterFrameAddress); + _frameHelpers.GetFrameHandler(handle.Context).HandleTransitionFrame(framedMethodFrame); + handle.CurrentInterpreterFrameAddress = TargetPointer.Null; + } + // UpdateState (called by Next) will see the IP and determine next state. + } + + #endregion Interpreter } 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..edee3128b43f86 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpMethodContextFrame.cs @@ -0,0 +1,24 @@ +// 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)); + 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/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..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 @@ -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 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..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.GetNativeCode(_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 7a75716d938597..0014b69bbdab8c 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.CodeKind.Jitted => JitTypes.TYPE_JIT, Contracts.CodeKind.ReadyToRun => JitTypes.TYPE_PJIT, + Contracts.CodeKind.Interpreter => JitTypes.TYPE_INTERPRETER, _ => JitTypes.TYPE_UNKNOWN, }; @@ -2204,6 +2205,7 @@ private TargetPointer DecodeJump64(TargetPointer pThunk) return _target.ReadPointer(pThunk + 2); } + int ISOSDacInterface.GetJumpThunkTarget(void* ctx, ClrDataAddress* targetIP, ClrDataAddress* targetMD) { int hr = HResults.S_OK; @@ -2306,7 +2308,7 @@ int ISOSDacInterface.GetMethodDescData(ClrDataAddress addr, ClrDataAddress ip, D if (nativeCodeAddr != TargetCodePointer.Null) { data->bHasNativeCode = 1; - data->NativeCodeAddr = nativeCodeAddr.ToAddress(_target).ToClrDataAddress(_target); + data->NativeCodeAddr = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCodeAddr).ToAddress(_target).ToClrDataAddress(_target); } else { @@ -2518,7 +2520,8 @@ private void CopyNativeCodeVersionToReJitData( ILCodeVersionHandle ilCodeVersion = cv.GetILCodeVersion(nativeCodeVersion); pReJitData->rejitID = rejit.GetRejitId(ilCodeVersion).Value; - pReJitData->NativeCodeAddr = cv.GetNativeCode(nativeCodeVersion).Value; + TargetCodePointer nativeCode = cv.GetNativeCode(nativeCodeVersion); + pReJitData->NativeCodeAddr = _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode).Value; if (nativeCodeVersion.CodeVersionNodeAddress != activeNativeCodeVersion.CodeVersionNodeAddress || nativeCodeVersion.MethodDescAddress != activeNativeCodeVersion.MethodDescAddress) @@ -5315,19 +5318,18 @@ 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; + 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); - if (r2rImageBaseAddr <= nativeCodeAddr && nativeCodeAddr < r2rImageEndAddr) + if (r2rImageBase <= nativeCodeAddr && nativeCodeAddr < r2rImageEnd) { nativeCodeAddrs[count].optimizationTier = DacpTieredVersionData.OptimizationTier.ReadyToRun; } diff --git a/src/native/managed/cdac/tests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdac/tests/ExecutionManager/ExecutionManagerTests.cs index 482437a0f83789..c270ff719cc122 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), @@ -823,4 +824,71 @@ public static IEnumerable StdArchAllVersions() } } } + + [Theory] + [MemberData(nameof(StdArchAllVersions))] + public void GetMethodDesc_InterpreterOneMethod(string 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(CodeKind.Interpreter, em.GetCodeKind(new TargetCodePointer(methodStart))); + + 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(string 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..1b3e4ea067baf7 --- /dev/null +++ b/src/native/managed/cdac/tests/FrameIteratorTests.cs @@ -0,0 +1,675 @@ +// 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 System.Linq; +using Microsoft.Diagnostics.DataContractReader.Contracts; +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), + new(nameof(Data.InterpMethodContextFrame.Ip), DataType.pointer), + new(nameof(Data.InterpMethodContextFrame.NextPtr), 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); + 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; + 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.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); + + var target = builder.Build(); + + // InterpreterFrame has pMD=NULL in native, so GetMethodDescPtr returns Null + TargetPointer result = new FrameHelpers(target).GetMethodDescPtr(new TargetPointer(frameFrag.Address)); + + Assert.Equal(TargetPointer.Null, 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.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); + + var target = builder.Build(); + + TargetPointer result = new FrameHelpers(target).GetMethodDescPtr(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); + 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; + 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.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); + + var target = builder.Build(); + + TargetPointer result = new FrameHelpers(target).GetMethodDescPtr(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); + 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; + 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.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); + + var target = builder.Build(); + + TargetPointer result = new FrameHelpers(target).GetMethodDescPtr(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); + 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; + } + + // 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); + + var target = builder.Build(); + + // Resolve each context frame individually — verifies the chain links resolve to distinct MethodDescs + Assert.Equal(new TargetPointer(methodDescC), new FrameHelpers(target).ResolveMethodDescFromInterpFrame(new TargetPointer(contextFrameC.Address))); + Assert.Equal(new TargetPointer(methodDescB), new FrameHelpers(target).ResolveMethodDescFromInterpFrame(new TargetPointer(contextFrameB.Address))); + Assert.Equal(new TargetPointer(methodDescA), new FrameHelpers(target).ResolveMethodDescFromInterpFrame(new TargetPointer(contextFrameA.Address))); + + // Verify null terminates correctly + Assert.Equal(TargetPointer.Null, new FrameHelpers(target).ResolveMethodDescFromInterpFrame(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 = new FrameHelpers(target).GetFrameName(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 = new FrameHelpers(target).GetFrameName(new TargetPointer(0x9999_9999)); + + 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(); + + // InterpreterFrame has pMD=NULL in native, so GetMethodDescPtr returns Null + TargetPointer result = new FrameHelpers(target).GetMethodDescPtr(new TargetPointer(frameFrag.Address)); + Assert.Equal(TargetPointer.Null, result); + + // WalkInterpreterFrameChain should yield only the active frame + var chain = new FrameHelpers(target).WalkInterpreterFrameChain(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(); + + // InterpreterFrame has pMD=NULL in native, so GetMethodDescPtr returns Null + TargetPointer result = new FrameHelpers(target).GetMethodDescPtr(new TargetPointer(frameFrag.Address)); + Assert.Equal(TargetPointer.Null, result); + + // WalkInterpreterFrameChain should yield upper then lower (top to bottom via ParentPtr) + var chain = new FrameHelpers(target).WalkInterpreterFrameChain(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 = new FrameHelpers(target).WalkInterpreterFrameChain(new TargetPointer(frameFrag.Address)).ToList(); + Assert.Single(chain); + Assert.Equal(new TargetPointer(activeFrame.Address), chain[0]); + } +} 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 a3951ba48e1a21..868582002347e0 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 07d57ec083ed54..d5140be493fe4f 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs @@ -340,6 +340,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"; @@ -502,6 +542,7 @@ 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; @@ -558,6 +599,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; } @@ -614,6 +656,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); @@ -692,6 +735,17 @@ public MockRangeSection AddRangeListRangeSection(JittedCodeRange jittedCodeRange 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 MockJittedMethod AddStubCodeBlock(JittedCodeRange jittedCodeRange, uint codeSize, int stubCodeBlockKind) { MockJittedMethod stub = AllocateJittedMethod(jittedCodeRange, codeSize, "Stub Code Block"); @@ -699,6 +753,15 @@ public MockJittedMethod AddStubCodeBlock(JittedCodeRange jittedCodeRange, uint c return stub; } + 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); @@ -771,6 +834,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 8d220b8e4538cd..8c876c5d69468d 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,40 @@ 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}"); + 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}"); + 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}"); + + 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 +525,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 c1d3da6263c172c5063615adc8514f1dee4d712f Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Fri, 1 May 2026 10:50:29 -0400 Subject: [PATCH 02/17] [cDAC] Add interpreter dump tests for stack walking Add dump-test infrastructure for the interpreter scenarios and tests that exercise the cDAC interpreter stack walker against real coredumps: * `InterpreterStack` debuggee: single-threaded debuggee with a JIT->interpreter->JIT->interpreter call chain that triggers a `FailFast` from inside an interpreted method. * `InterpreterStackDoubleWalk` debuggee: multi-threaded debuggee where a worker thread is parked deep in an interpreted call chain while the main thread captures the dump. This exercises walking a thread other than the crashing thread and asserts the cDAC walker does not produce duplicated `InterpreterFrame` markers. The new dump tests verify the interleaved JIT/interpreter frame layout, the absence of doubled `InterpreterFrame` markers, and that `DumpTestStackWalker` adjacency assertions hold across the full interpreter call chain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Debuggees/Directory.Build.targets | 1 + .../InterpreterStack/InterpreterStack.csproj | 19 ++ .../Debuggees/InterpreterStack/Program.cs | 51 +++++ .../InterpreterStack/Trampoline/Trampoline.cs | 21 ++ .../Trampoline/Trampoline.csproj | 5 + .../InterpreterStackDoubleWalk.csproj | 19 ++ .../InterpreterStackDoubleWalk/Program.cs | 87 ++++++++ .../cdac/tests/DumpTests/DumpTestBase.cs | 25 +++ .../tests/DumpTests/DumpTestStackWalker.cs | 77 ++++++- .../cdac/tests/DumpTests/DumpTests.targets | 26 ++- .../InterpreterStackDoubleWalkDumpTests.cs | 199 ++++++++++++++++++ .../DumpTests/InterpreterStackDumpTests.cs | 185 ++++++++++++++++ ...ostics.DataContractReader.DumpTests.csproj | 2 +- .../cdac/tests/DumpTests/cdac-dump-helix.proj | 8 +- 14 files changed, 706 insertions(+), 19 deletions(-) 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/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 create mode 100644 src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs 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 4341b74a20714c..1cb3678a6988fc 100644 --- a/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.targets +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.targets @@ -24,6 +24,7 @@ <_DumpTypeOutput Include="$(MSBuildProjectName)" DumpTypes="$(DumpTypes)" R2RModes="$(R2RModes)" + EnvironmentVariables="$(EnvironmentVariables)" WindowsOnly="$(WindowsOnly)" MacOnly="$(MacOnly)" 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/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..8ba58191ab76f9 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStackDoubleWalk/Program.cs @@ -0,0 +1,87 @@ +// 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 via SpinStep() calls (each call through interpreter precode) +/// +/// Main thread: +/// Main (JIT) -> waits for signal -> FailFast +/// +/// Note: Even though the worker is executing interpreted code, in a FailFast dump +/// the CPU IP is inside the native interpreter engine. However, when a debugger +/// breaks the thread at SpinStep's interpreter precode (via cdb breakpoint), the +/// OS thread context has IP = precode address, which IS managed code registered as +/// JitType.Interpreter. This enables the SkipNextInterpreterFrame double-walk +/// prevention to be exercised from a debugger-collected dump. +/// +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 by repeatedly calling SpinStep(). Each call goes through SpinStep's + // interpreter precode, allowing a debugger to break at the precode and + // capture a dump where the thread's IP is in interpreter-managed code. + while (s_keepSpinning) { SpinStep(); } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void SpinStep() { } + + private static volatile bool s_keepSpinning = true; +} diff --git a/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs b/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs index f6507c8b7615c3..4c0aaa34cb508c 100644 --- a/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs +++ b/src/native/managed/cdac/tests/DumpTests/DumpTestBase.cs @@ -120,6 +120,31 @@ protected void InitializeDumpTest(TestConfiguration config, string debuggeeName, Assert.True(created, $"Failed to create ContractDescriptorTarget from dump: {dumpPath}"); } + /// + /// Loads the dump from the given directly. + /// Use this overload for ad-hoc dumps (e.g., collected via cdb) that don't follow + /// the standard naming conventions. + /// + protected void InitializeDumpTestFromPath(string dumpPath) + { + if (!File.Exists(dumpPath)) + throw new SkipTestException($"Dump not found: {dumpPath}"); + + _host = ClrMdDumpHost.Open(dumpPath, []); + ulong contractDescriptor = _host.FindContractDescriptorAddress(); + + bool created = ContractDescriptorTarget.TryCreate( + contractDescriptor, + _host.ReadFromTarget, + writeToTarget: static (_, _) => -1, + _host.GetThreadContext, + allocVirtual: static (ulong _, out ulong _) => throw new NotImplementedException("Dump tests do not provide AllocVirtual"), + [Contracts.CoreCLRContracts.Register], + out _target); + + Assert.True(created, $"Failed to create ContractDescriptorTarget from dump: {dumpPath}"); + } + public void Dispose() { _host?.Dispose(); 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 433dcdf50b9939..712d1358bd9701 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) @@ -146,7 +150,7 @@ + Properties="DebuggeeName=%(_DebuggeeWithDumpTypes.Identity);_DebuggeeDumpTypes=%(_DebuggeeWithDumpTypes.DumpTypes);_DebuggeeR2RModes=%(_DebuggeeWithDumpTypes.R2RModes);_DebuggeeEnvVars=%(_DebuggeeWithDumpTypes.EnvironmentVariables)" /> @@ -212,7 +216,7 @@ + Properties="DebuggeeName=$(DebuggeeName);_DumpTypeName=%(_DumpTypeItem.Identity);_DebuggeeR2RModes=$(_DebuggeeR2RModes);_DebuggeeEnvVars=$(_DebuggeeEnvVars)" /> @@ -232,7 +236,7 @@ + Properties="DebuggeeName=$(DebuggeeName);_MiniDumpType=$(_MiniDumpType);_DumpTypeDirName=$(_DumpTypeDirName);_DumpTypeName=$(_DumpTypeName);_R2RModeName=%(_R2RModeItem.Identity);_DebuggeeEnvVars=$(_DebuggeeEnvVars)" /> @@ -248,7 +252,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)" /> @@ -267,12 +271,12 @@ @@ -295,7 +299,7 @@ + EnvironmentVariables="DOTNET_DbgEnableMiniDump=1;DOTNET_DbgMiniDumpType=$(_MiniDumpType);DOTNET_DbgMiniDumpName=$(_DumpFile);DOTNET_ReadyToRun=$(_R2RValue);$(_DebuggeeEnvVars)" /> @@ -316,7 +320,7 @@ + EnvironmentVariables="DOTNET_DbgEnableMiniDump=1;DOTNET_DbgMiniDumpType=$(_MiniDumpType);DOTNET_DbgMiniDumpName=$(_DumpFile);DOTNET_ReadyToRun=$(_R2RValue);$(_DebuggeeEnvVars)" /> 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..f6bdb2b1398b8e --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/InterpreterStackDoubleWalkDumpTests.cs @@ -0,0 +1,199 @@ +// 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.IO; +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) + { + 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); + 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 + /// matching the native DAC stack walk output. + /// + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void StackWalk_VerifyInterleavedStackLayout(TestConfiguration config) + { + InitializeDumpTest(config); + SkipIfInterpreterNotAvailable(); + + ThreadData workerThread = DumpTestHelpers.FindThreadWithMethod(Target, "MethodA"); + + // Use adjacent assertions to verify no extra InterpreterFrame appears + // after the interpreted region — this catches the "doubled frame" bug + // where InterpreterFrame would appear both before AND after its methods. + DumpTestStackWalker.Walk(Target, workerThread) + .ExpectRuntimeFrame("InterpreterFrame") + .ExpectAdjacentFrame("MethodD", AssertInterpreted) + .ExpectAdjacentFrame("MethodC", AssertInterpreted) + .ExpectAdjacentFrame("Bounce", AssertJitted) + .ExpectAdjacentRuntimeFrame("InterpreterFrame") + .ExpectAdjacentFrame("MethodB", AssertInterpreted) + .ExpectAdjacentFrame("MethodA", AssertInterpreted) + .Verify(); + } + + /// + /// Walks the worker thread and verifies each interpreted method appears exactly once + /// and that InterpreterFrame never appears consecutively (the "doubled frame" bug + /// that native PR #126953 fixed via ResetRegDisp dedup). + /// + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void StackWalk_NoDoubledInterpreterFrames(TestConfiguration config) + { + InitializeDumpTest(config); + SkipIfInterpreterNotAvailable(); + + ThreadData workerThread = DumpTestHelpers.FindThreadWithMethod(Target, "MethodA"); + DumpTestStackWalker walker = DumpTestStackWalker.Walk(Target, workerThread); + + 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"})"))}]"); + } + + // Verify no InterpreterFrame appears after the interpreted methods it + // introduces. The original "doubled frame" bug (native PR #126953) was that + // InterpreterFrame appeared both before AND after its interpreted region. + // After the interpreter virtual unwind exhausts the chain, the frame + // iterator must have advanced past the owning InterpreterFrame. + for (int i = 0; i < walker.Frames.Count - 1; i++) + { + if (walker.Frames[i].FrameName == "InterpreterFrame" + && walker.Frames[i + 1].FrameName == "InterpreterFrame") + { + Assert.Fail( + $"Consecutive InterpreterFrame entries at indices {i} and {i + 1} — " + + $"this indicates the doubled InterpreterFrame bug. " + + $"Full stack: [{string.Join(", ", walker.Frames.Select(f => $"{f.Name ?? ""}({f.FrameName ?? "frameless"})"))}]"); + } + } + } + + /// + /// Loads a cdb-collected dump where the worker thread's DebuggerFilterContext was + /// patched to have RIP pointing to an interpreter precode. This simulates the + /// scenario where a managed debugger breaks inside interpreted code. + /// + /// NOTE: Per native PR #126953, Init() asserts that the initial IP is never in + /// interpreter code. This test exercises a scenario that doesn't occur in practice + /// but is kept for comparison and future reference. The cDAC doesn't currently + /// handle this path correctly (the CONTEXT SP doesn't point to an + /// InterpMethodContextFrame, so interpreter virtual unwind fails). + /// + /// To regenerate this dump: + /// 1. Run the debuggee under cdb with DOTNET_Interpreter=Method* + /// 2. Break at coreclr!EEPolicy::HandleFatalError + /// 3. Find SpinStep's interpreter precode via !name2ee + /// 4. Allocate a CONTEXT, set RIP = precode, write it to Thread::m_debuggerFilterContext + /// 5. .dump /o /ma + /// + [ConditionalFact] + public void StackWalk_NoDoubledInterpreterFrames_WithDebuggerFilterContext() + { + string dumpPath = GetCdbDumpPath(); + InitializeDumpTestFromPath(dumpPath); + SkipIfInterpreterNotAvailable(); + + // This test is kept for reference but the scenario is invalid per native + // PR #126953: Init() asserts IP is never in interpreter code. + // The cDAC's InterpreterVirtualUnwind reads InterpMethodContextFrame from SP, + // but our patched CONTEXT has SP = native stack pointer, not a frame address. + // Skip until the cDAC implements ResetRegDisp-style dedup (if ever needed). + throw new SkipTestException( + "DebuggerFilterContext with interpreter IP is not a valid scenario per native PR #126953. " + + "Kept for future reference."); + } + + private static string GetCdbDumpPath() + { + string? repoRoot = FindRepoRoot(); + if (repoRoot is null) + throw new InvalidOperationException("Could not locate the repository root."); + + return Path.Combine(repoRoot, "artifacts", "dumps", "cdac", "local", "full", "jit", + "InterpreterStackDoubleWalk", "InterpreterStackDoubleWalk_cdb.dmp"); + } + + private static string? FindRepoRoot() + { + string? dir = AppContext.BaseDirectory; + while (dir is not null) + { + if (File.Exists(Path.Combine(dir, "global.json"))) + return dir; + dir = Path.GetDirectoryName(dir); + } + return null; + } +} 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..e1e5fe44bbb9ee --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/InterpreterStackDumpTests.cs @@ -0,0 +1,185 @@ +// 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 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 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) + { + // In the DAC stack walk, interpreted methods appear as frameless frames + // (via interpreter virtual unwind). Verify frameless and interpreter code. + 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); + 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)); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void StackWalk_VerifyInterleavedStackLayout(TestConfiguration config) + { + InitializeDumpTest(config); + SkipIfInterpreterNotAvailable(); + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + + // Expected stack layout matching native DAC `!clrstack` output. + // Use adjacent assertions to verify no extra InterpreterFrame appears + // between the interpreter region and the JIT trampoline. + DumpTestStackWalker.Walk(Target, crashingThread) + .ExpectRuntimeFrame("InterpreterFrame") + .ExpectAdjacentFrame("MethodD", AssertInterpreted) + .ExpectAdjacentFrame("MethodC", AssertInterpreted) + .ExpectAdjacentFrame("Bounce", AssertJitted) + .ExpectAdjacentRuntimeFrame("InterpreterFrame") + .ExpectAdjacentFrame("MethodB", AssertInterpreted) + .ExpectAdjacentFrame("MethodA", AssertInterpreted) + .ExpectAdjacentFrame("Main", AssertJitted) + .Verify(); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void StackWalk_InterpreterMethodNativeCodeIsPrecode(TestConfiguration config) + { + InitializeDumpTest(config); + SkipIfInterpreterNotAvailable(); + 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); + SkipIfInterpreterNotAvailable(); + 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"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void StackWalk_NoDoubledInterpreterFrames(TestConfiguration config) + { + InitializeDumpTest(config); + SkipIfInterpreterNotAvailable(); + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + DumpTestStackWalker walker = DumpTestStackWalker.Walk(Target, crashingThread); + + // Matching DAC behavior: each interpreted method appears exactly once as a + // frameless frame. The InterpreterFrame entries have no method name (pMD=NULL). + 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"})"))}]"); + } + + // Verify no InterpreterFrame appears consecutively — this is the "doubled + // frame" bug that native PR #126953 fixed. After the interpreter virtual + // unwind exhausts the chain, the frame iterator must have advanced past + // the owning InterpreterFrame so it doesn't get yielded again. + for (int i = 0; i < walker.Frames.Count - 1; i++) + { + if (walker.Frames[i].FrameName == "InterpreterFrame" + && walker.Frames[i + 1].FrameName == "InterpreterFrame") + { + Assert.Fail( + $"Consecutive InterpreterFrame entries at indices {i} and {i + 1} — " + + $"this indicates the doubled InterpreterFrame bug. " + + $"Full stack: [{string.Join(", ", walker.Frames.Select(f => $"{f.Name ?? ""}({f.FrameName ?? "frameless"})"))}]"); + } + } + } +} 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 f458c4f674d55a..22568ab7090587 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 @@ -104,7 +104,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 a1e9d7206db94d..9aa5e2f52cacce 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 7aa3900b2ae647f3f118cf138aa5f68accc0909d Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Tue, 5 May 2026 14:56:11 -0400 Subject: [PATCH 03/17] Fix ARM/ARM64 unwinder crash on interpreter IPs When CheckForSkippedFrames clones the context for an interpreter IP and calls Unwind, the interpreter's GetUnwindInfo returns TargetPointer.Null. AMD64Unwinder already guards against this by checking for null and returning false; ARM and ARM64 unwinders did not, and crashed reading RuntimeFunction at address 0 (VirtualReadException at 0x00000000). Add the same null guard to ARM and ARM64 to make all three platforms behaviorally consistent. Also remove StackWalk_NoDoubledInterpreterFrames_WithDebuggerFilterContext since it cannot run in CI (depends on a manually-collected cdb dump in the local repo) and the scenario is invalid per native PR #126953. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../StackWalk/Context/ARM/ARMUnwinder.cs | 9 ++- .../StackWalk/Context/ARM64/ARM64Unwinder.cs | 7 ++- .../InterpreterStackDoubleWalkDumpTests.cs | 58 ------------------- 3 files changed, 13 insertions(+), 61 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM/ARMUnwinder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM/ARMUnwinder.cs index 5616079dcef437..96c8c6236e39ba 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM/ARMUnwinder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM/ARMUnwinder.cs @@ -21,14 +21,19 @@ public bool Unwind(ref ARMContext context) { if (_eman.GetCodeBlockHandle(context.InstructionPointer.Value) is not CodeBlockHandle cbh) { - throw new InvalidOperationException("Unwind failed, unable to find code block for the instruction pointer."); + return false; } uint startingPc = context.Pc; uint startingSp = context.Sp; TargetPointer imageBase = _eman.GetUnwindInfoBaseAddress(cbh); - Data.RuntimeFunction functionEntry = _target.ProcessedData.GetOrAdd(_eman.GetUnwindInfo(cbh)); + TargetPointer unwindInfoAddr = _eman.GetUnwindInfo(cbh); + + if (unwindInfoAddr == TargetPointer.Null) + return false; + + Data.RuntimeFunction functionEntry = _target.ProcessedData.GetOrAdd(unwindInfoAddr); if ((functionEntry.UnwindData & 0x3) != 0) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64/ARM64Unwinder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64/ARM64Unwinder.cs index 0279995da7ab16..625f972958ca23 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64/ARM64Unwinder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64/ARM64Unwinder.cs @@ -55,7 +55,12 @@ public bool Unwind(ref ARM64Context context) return false; TargetPointer imageBase = _eman.GetUnwindInfoBaseAddress(cbh); - Data.RuntimeFunction functionEntry = _target.ProcessedData.GetOrAdd(_eman.GetUnwindInfo(cbh)); + TargetPointer unwindInfoAddr = _eman.GetUnwindInfo(cbh); + + if (unwindInfoAddr == TargetPointer.Null) + return false; + + Data.RuntimeFunction functionEntry = _target.ProcessedData.GetOrAdd(unwindInfoAddr); ulong startingPc = context.Pc; ulong startingSp = context.Sp; diff --git a/src/native/managed/cdac/tests/DumpTests/InterpreterStackDoubleWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/InterpreterStackDoubleWalkDumpTests.cs index f6bdb2b1398b8e..07048d6dc38692 100644 --- a/src/native/managed/cdac/tests/DumpTests/InterpreterStackDoubleWalkDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/InterpreterStackDoubleWalkDumpTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.IO; using System.Linq; using Microsoft.Diagnostics.DataContractReader.Contracts; using Microsoft.DotNet.XUnitExtensions; @@ -139,61 +138,4 @@ public void StackWalk_NoDoubledInterpreterFrames(TestConfiguration config) } } } - - /// - /// Loads a cdb-collected dump where the worker thread's DebuggerFilterContext was - /// patched to have RIP pointing to an interpreter precode. This simulates the - /// scenario where a managed debugger breaks inside interpreted code. - /// - /// NOTE: Per native PR #126953, Init() asserts that the initial IP is never in - /// interpreter code. This test exercises a scenario that doesn't occur in practice - /// but is kept for comparison and future reference. The cDAC doesn't currently - /// handle this path correctly (the CONTEXT SP doesn't point to an - /// InterpMethodContextFrame, so interpreter virtual unwind fails). - /// - /// To regenerate this dump: - /// 1. Run the debuggee under cdb with DOTNET_Interpreter=Method* - /// 2. Break at coreclr!EEPolicy::HandleFatalError - /// 3. Find SpinStep's interpreter precode via !name2ee - /// 4. Allocate a CONTEXT, set RIP = precode, write it to Thread::m_debuggerFilterContext - /// 5. .dump /o /ma - /// - [ConditionalFact] - public void StackWalk_NoDoubledInterpreterFrames_WithDebuggerFilterContext() - { - string dumpPath = GetCdbDumpPath(); - InitializeDumpTestFromPath(dumpPath); - SkipIfInterpreterNotAvailable(); - - // This test is kept for reference but the scenario is invalid per native - // PR #126953: Init() asserts IP is never in interpreter code. - // The cDAC's InterpreterVirtualUnwind reads InterpMethodContextFrame from SP, - // but our patched CONTEXT has SP = native stack pointer, not a frame address. - // Skip until the cDAC implements ResetRegDisp-style dedup (if ever needed). - throw new SkipTestException( - "DebuggerFilterContext with interpreter IP is not a valid scenario per native PR #126953. " + - "Kept for future reference."); - } - - private static string GetCdbDumpPath() - { - string? repoRoot = FindRepoRoot(); - if (repoRoot is null) - throw new InvalidOperationException("Could not locate the repository root."); - - return Path.Combine(repoRoot, "artifacts", "dumps", "cdac", "local", "full", "jit", - "InterpreterStackDoubleWalk", "InterpreterStackDoubleWalk_cdb.dmp"); - } - - private static string? FindRepoRoot() - { - string? dir = AppContext.BaseDirectory; - while (dir is not null) - { - if (File.Exists(Path.Combine(dir, "global.json"))) - return dir; - dir = Path.GetDirectoryName(dir); - } - return null; - } } From a3cca287d997c7d4beff2d392b730f8170faac35 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Tue, 5 May 2026 15:31:03 -0400 Subject: [PATCH 04/17] Address PR self-review feedback - Restore single ternary at StackWalk_1.cs (was needlessly verbose) - Remove unrelated infinite-loop guard around Unwind - Replace unicode arrows/em-dashes with -> and -- in code/comments/docs - Drop direct line-number references in comments - Make FrameHelpers access modifiers consistent: public for externally callable methods, private for helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/ExecutionManager.md | 2 +- docs/design/datacontracts/PrecodeStubs.md | 2 +- .../StackWalk/FrameHandling/FrameHelpers.cs | 14 +++--- .../Contracts/StackWalk/StackWalk_1.cs | 47 +++++-------------- .../managed/cdac/tests/FrameIteratorTests.cs | 4 +- 5 files changed, 23 insertions(+), 46 deletions(-) diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index 79e5d8ee121780..00866ab98042ac 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -526,7 +526,7 @@ There are two distinct clause data types. JIT-compiled code uses `EEExceptionCla * For R2R code (`ReadyToRunJitManager`), exception clause data is found via the `ExceptionInfo` section (section type 104) of the R2R image. The section is located by traversing `ReadyToRunInfo::Composite` to reach the `ReadyToRunCoreInfo`, then reading its `Header` pointer to the `ReadyToRunCoreHeader`, and iterating through the inline `ReadyToRunSection` array that immediately follows the header. The `ExceptionInfo` section contains an `ExceptionLookupTableEntry` array, where each entry maps a `MethodStartRVA` to an `ExceptionInfoRVA`. A binary search (falling back to linear scan for small ranges) finds the entry matching the method's RVA. The exception clauses span from that entry's `ExceptionInfoRVA` to the next entry's `ExceptionInfoRVA`, both offset from the image base. The clause array is strided using the size of `R2RExceptionClause`. -After obtaining the clause array bounds, the common iteration logic classifies each clause by its flags. The native `COR_ILEXCEPTION_CLAUSE` flags are bit flags: `Filter` (0x1), `Finally` (0x2), `Fault` (0x4). If none are set, the clause is `Typed`. For typed clauses, if the `CachedClass` flag (0x10000000) is set (JIT-only, used for dynamic methods), the union field contains a resolved `TypeHandle` pointer; the clause is a catch-all if this pointer equals the `ObjectMethodTable` global. Otherwise, the union field is a metadata `ClassToken`. To determine whether a typed clause is a catch-all handler, the `ClassToken` (which may be a `TypeDef` or `TypeRef`) is resolved to a `MethodTable` via the `Loader` contract's module lookup maps (`TypeDefToMethodTable` or `TypeRefToMethodTable`) and compared against the `ObjectMethodTable` global. For typed clauses without a cached type handle, the module address is resolved by walking `CodeBlockHandle` → `MethodDesc` → `MethodTable` → `TypeHandle` → `Module` via the `RuntimeTypeSystem` contract. +After obtaining the clause array bounds, the common iteration logic classifies each clause by its flags. The native `COR_ILEXCEPTION_CLAUSE` flags are bit flags: `Filter` (0x1), `Finally` (0x2), `Fault` (0x4). If none are set, the clause is `Typed`. For typed clauses, if the `CachedClass` flag (0x10000000) is set (JIT-only, used for dynamic methods), the union field contains a resolved `TypeHandle` pointer; the clause is a catch-all if this pointer equals the `ObjectMethodTable` global. Otherwise, the union field is a metadata `ClassToken`. To determine whether a typed clause is a catch-all handler, the `ClassToken` (which may be a `TypeDef` or `TypeRef`) is resolved to a `MethodTable` via the `Loader` contract's module lookup maps (`TypeDefToMethodTable` or `TypeRefToMethodTable`) and compared against the `ObjectMethodTable` global. For typed clauses without a cached type handle, the module address is resolved by walking `CodeBlockHandle` -> `MethodDesc` -> `MethodTable` -> `TypeHandle` -> `Module` via the `RuntimeTypeSystem` contract. `IsFilterFunclet` first checks `IsFunclet`. If the code block is a funclet, it retrieves the EH clauses for the method and checks whether any filter clause's handler offset matches the funclet's relative offset. If a match is found, the funclet is a filter funclet. diff --git a/docs/design/datacontracts/PrecodeStubs.md b/docs/design/datacontracts/PrecodeStubs.md index 217c275b6ef4b8..817e09f1e17362 100644 --- a/docs/design/datacontracts/PrecodeStubs.md +++ b/docs/design/datacontracts/PrecodeStubs.md @@ -273,7 +273,7 @@ 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. + // the InterpreterPrecodeData -> InterpByteCodeStart -> InterpMethod -> MethodDesc chain. internal sealed class InterpreterPrecode : ValidPrecode { internal InterpreterPrecode(TargetPointer instrPointer) : base(instrPointer, KnownPrecodeType.Interpreter) { } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs index 65264683b452fd..7f0ba8cea7052e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs @@ -71,7 +71,7 @@ public string GetFrameName(TargetPointer frameIdentifier) return frameType.ToString(); } - internal FrameType GetFrameType(TargetPointer frameIdentifier) + public FrameType GetFrameType(TargetPointer frameIdentifier) { foreach (FrameType frameType in Enum.GetValues()) { @@ -168,7 +168,7 @@ public void UpdateContextFromFrame(Data.Frame frame, IPlatformAgnosticContext co case FrameType.InterpreterFrame: { - // Match native stackwalk.cpp:2248-2261: Set context to the top + // Match native stackwalk.cpp: Set context to the top // InterpMethodContextFrame so the walker transitions to SW_FRAMELESS // and yields each interpreted method individually via virtual unwind. Data.InterpreterFrame interpreterFrame = _target.ProcessedData.GetOrAdd(frame.Address); @@ -291,7 +291,7 @@ public TargetPointer GetReturnAddress(Data.Frame frame) } } - internal IPlatformFrameHandler GetFrameHandler(IPlatformAgnosticContext context) + public IPlatformFrameHandler GetFrameHandler(IPlatformAgnosticContext context) { return context switch { @@ -305,7 +305,7 @@ internal IPlatformFrameHandler GetFrameHandler(IPlatformAgnosticContext context) }; } - internal static bool InlinedCallFrameHasActiveCall(Data.InlinedCallFrame frame) + private static bool InlinedCallFrameHasActiveCall(Data.InlinedCallFrame frame) { return frame.CallerReturnAddress != TargetPointer.Null; } @@ -326,7 +326,7 @@ private bool InlinedCallFrameHasFunction(Data.InlinedCallFrame frame) /// Resolves the MethodDesc from a specific InterpMethodContextFrame by following: /// InterpMethodContextFrame.StartIp -> InterpByteCodeStart.Method -> InterpMethod.MethodDesc /// - internal TargetPointer ResolveMethodDescFromInterpFrame(TargetPointer interpMethodFramePtr) + public TargetPointer ResolveMethodDescFromInterpFrame(TargetPointer interpMethodFramePtr) { if (interpMethodFramePtr == TargetPointer.Null) return TargetPointer.Null; @@ -351,7 +351,7 @@ internal TargetPointer ResolveMethodDescFromInterpFrame(TargetPointer interpMeth /// 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 TargetPointer ResolveTopInterpMethodContextFrame(Data.InterpreterFrame interpreterFrame) + public TargetPointer ResolveTopInterpMethodContextFrame(Data.InterpreterFrame interpreterFrame) { TargetPointer hintPtr = interpreterFrame.TopInterpMethodContextFrame; if (hintPtr == TargetPointer.Null) @@ -392,7 +392,7 @@ internal TargetPointer ResolveTopInterpMethodContextFrame(Data.InterpreterFrame /// via , then the ParentPtr chain is walked. /// Only active frames (Ip != null) are yielded. /// - internal IEnumerable WalkInterpreterFrameChain(TargetPointer frameAddress) + public IEnumerable WalkInterpreterFrameChain(TargetPointer frameAddress) { Data.InterpreterFrame interpFrame = _target.ProcessedData.GetOrAdd(frameAddress); TargetPointer interpMethodFramePtr = ResolveTopInterpMethodContextFrame(interpFrame); 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 f721a5bbd3bd4c..610adb79e5441a 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 @@ -108,7 +108,7 @@ or FrameType.RedirectedThreadFrame /// - After a ResumableFrame: isFirst = true /// - After other Frames: isFirst = false /// - After a skipped frame: isFirst unchanged (native never modifies isFirst - /// in the SFITER_SKIPPED_FRAME_FUNCTION path — it keeps the value from Init) + /// in the SFITER_SKIPPED_FRAME_FUNCTION path -- it keeps the value from Init) /// public void AdvanceIsFirst() { @@ -118,7 +118,7 @@ public void AdvanceIsFirst() } else if (State == StackWalkState.SW_SKIPPED_FRAME) { - // Native SFITER_SKIPPED_FRAME_FUNCTION (stackwalk.cpp:2086-2128) does NOT + // Native SFITER_SKIPPED_FRAME_FUNCTION in stackwalk.cpp does NOT // modify isFirst. It stays true from Init() so the subsequent managed frame // gets IsActiveFunc()=true. This is important because skipped frames are // explicit Frames embedded within the active managed frame (e.g. InlinedCallFrame @@ -149,15 +149,7 @@ IEnumerable IStackWalk.CreateStackWalk(ThreadData threadD IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); uint contextFlags = context.AllContextFlags; FillContextFromThread(context, threadData, contextFlags); - StackWalkState state; - if (IsManaged(context.InstructionPointer, out _)) - { - state = StackWalkState.SW_FRAMELESS; - } - else - { - state = StackWalkState.SW_FRAME; - } + StackWalkState state = IsManaged(context.InstructionPointer, out _) ? StackWalkState.SW_FRAMELESS : StackWalkState.SW_FRAME; FrameIterator frameIterator = new(_target, threadData); // if the next Frame is not valid and we are not in managed code, there is nothing to return @@ -675,16 +667,13 @@ private bool Next(StackWalkData handle) $"SP (0x{handle.Context.StackPointer:X}) should be below next Frame (0x{handle.FrameIter.CurrentFrameAddress:X})"); // Reset interrupted state after processing a managed frame. - // Native stackwalk.cpp:2203-2205: isInterrupted = false; hasFaulted = false; + // Native stackwalk.cpp: isInterrupted = false; hasFaulted = false; handle.IsInterrupted = false; - TargetPointer prevIP = handle.Context.InstructionPointer; - TargetPointer prevSP = handle.Context.StackPointer; - - // Check if the current frame is interpreter code — if so, use + // Check if the current frame is interpreter code -- if so, use // interpreter virtual unwind instead of OS-level unwind. - // This mirrors VirtualUnwindInterpreterCallFrame in eetwain.cpp:2101. - if (IsInterpreterCode(prevIP)) + // This mirrors VirtualUnwindInterpreterCallFrame in eetwain.cpp. + if (IsInterpreterCode(handle.Context.InstructionPointer)) { InterpreterVirtualUnwind(handle); } @@ -699,18 +688,6 @@ private bool Next(StackWalkData handle) handle.State = StackWalkState.SW_ERROR; throw; } - // Guard against infinite loops when Unwind fails to advance. - 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: @@ -983,12 +960,12 @@ private bool IsInterpreterCode(TargetPointer ip) /// /// Performs interpreter virtual unwind, matching the native - /// VirtualUnwindInterpreterCallFrame in eetwain.cpp:2101-2124. + /// VirtualUnwindInterpreterCallFrame in eetwain.cpp. /// /// When unwinding from a frameless interpreter frame, the SP points to the /// current InterpMethodContextFrame. We follow pParent to get to the next /// interpreted method in the call chain. If pParent is null, the interpreter - /// chain under the current InterpreterFrame is exhausted — we advance the + /// chain under the current InterpreterFrame is exhausted -- we advance the /// frame iterator past the InterpreterFrame and transition to SW_FRAME. /// private void InterpreterVirtualUnwind(StackWalkData handle) @@ -1001,17 +978,17 @@ private void InterpreterVirtualUnwind(StackWalkData handle) Data.InterpMethodContextFrame parentFrame = _target.ProcessedData.GetOrAdd(currentFrame.ParentPtr); if (parentFrame.Ip != TargetPointer.Null) { - // Parent is active — set context to the parent interpreted method. + // Parent is active -- set context to the parent interpreted method. handle.Context.InstructionPointer = new TargetPointer((ulong)parentFrame.Ip); handle.Context.StackPointer = currentFrame.ParentPtr; return; } } - // No active parent — interpreter chain under this InterpreterFrame is exhausted. + // No active parent -- interpreter chain under this InterpreterFrame is exhausted. // Use the saved InterpreterFrame's transition block to restore the context to // the native caller of InterpExecMethod. This is the cDAC equivalent of the - // native DummyCallerIP → UpdateRegDisplay path (stackwalk.cpp:2159-2164). + // native DummyCallerIP -> UpdateRegDisplay path in stackwalk.cpp. if (handle.CurrentInterpreterFrameAddress != TargetPointer.Null) { Data.FramedMethodFrame framedMethodFrame = _target.ProcessedData.GetOrAdd(handle.CurrentInterpreterFrameAddress); diff --git a/src/native/managed/cdac/tests/FrameIteratorTests.cs b/src/native/managed/cdac/tests/FrameIteratorTests.cs index 1b3e4ea067baf7..2112398791aced 100644 --- a/src/native/managed/cdac/tests/FrameIteratorTests.cs +++ b/src/native/managed/cdac/tests/FrameIteratorTests.cs @@ -337,7 +337,7 @@ public void ResolveMethodDescFromContextFrame_MultipleContextFrames_ResolvesEach ulong methodDescB = 0xBB00_0002; ulong methodDescC = 0xCC00_0003; - // Build three independent InterpMethod → InterpByteCodeStart chains + // 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"); @@ -367,7 +367,7 @@ MockMemorySpace.HeapFragment CreateContextChainEntry(ulong methodDesc, ulong par return contextFrameFrag; } - // Build chain: C (leaf) → B → A (root, ParentPtr=0) + // 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); From 788563bdb375830fd1b57d7bea2a382261b5717d Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Tue, 5 May 2026 18:13:25 -0400 Subject: [PATCH 05/17] Mirror native interpreter context updates in cDAC; address PR feedback - Add SetContextToInterpMethodContextFrame and VirtualUnwindInterpreterCallFrame helpers in FrameHelpers, named to mirror native (frames.cpp / eetwain.cpp) - Add IsFaulting field on InterpreterFrame and Stack field on InterpMethodContextFrame; thread Data.InterpreterFrame.Address through - Add RawContextFlags abstraction across all platform contexts so the helper can OR in CONTEXT_EXCEPTION_ACTIVE for faulting top frames - Gate IExecutionManager.IsFunclet on JitType.Interpreter so interpreter code (no native unwind info) reports false without throwing in GetFuncletStartAddress - Remove flaky InterpreterStackDoubleWalk debuggee + tests (timing-sensitive SpinStep loop) - Address Copilot bot feedback: MSBuild escaping in DumpTests.targets, funclet null-safety for interpreter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/StackWalk.md | 2 + .../vm/datadescriptor/datadescriptor.inc | 2 + src/coreclr/vm/frames.h | 1 + .../ExecutionManager/ExecutionManagerCore.cs | 4 + .../StackWalk/Context/AMD64Context.cs | 2 + .../StackWalk/Context/ARM64Context.cs | 2 + .../Contracts/StackWalk/Context/ARMContext.cs | 2 + .../StackWalk/Context/ContextHolder.cs | 2 + .../Context/IPlatformAgnosticContext.cs | 2 + .../StackWalk/Context/IPlatformContext.cs | 2 + .../StackWalk/Context/LoongArch64Context.cs | 2 + .../StackWalk/Context/RISCV64Context.cs | 2 + .../Contracts/StackWalk/Context/X86Context.cs | 2 + .../StackWalk/FrameHandling/FrameHelpers.cs | 93 ++++++++++-- .../Contracts/StackWalk/StackWalk_1.cs | 18 +-- .../Data/InterpMethodContextFrame.cs | 2 + .../Data/InterpreterFrame.cs | 4 + .../InterpreterStackDoubleWalk.csproj | 19 --- .../InterpreterStackDoubleWalk/Program.cs | 87 ----------- .../cdac/tests/DumpTests/DumpTests.targets | 12 +- .../InterpreterStackDoubleWalkDumpTests.cs | 141 ------------------ .../managed/cdac/tests/FrameIteratorTests.cs | 16 +- .../managed/cdac/tests/PrecodeStubsTests.cs | 1 - 23 files changed, 134 insertions(+), 286 deletions(-) delete mode 100644 src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStackDoubleWalk/InterpreterStackDoubleWalk.csproj delete mode 100644 src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStackDoubleWalk/Program.cs delete mode 100644 src/native/managed/cdac/tests/DumpTests/InterpreterStackDoubleWalkDumpTests.cs diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 8aa7373b2e3bc5..19ab9a856c5cab 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -85,10 +85,12 @@ This contract depends on the following descriptors: | `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` | +| `InterpreterFrame` | `IsFaulting` | Boolean indicating whether the topmost interpreted frame has thrown an exception. When set, the context for the top interpreted frame must include `CONTEXT_EXCEPTION_ACTIVE` so exception unwinders treat the IP as a faulting instruction rather than a return-from-call | | `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 | +| `InterpMethodContextFrame` | `Stack` | Pointer to the stack base for this interpreted method, used as the frame pointer when interpreter GC info uses a stack-base register | | `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 | diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 99c560ed9fad3a..d6dc44e735ac72 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -885,6 +885,7 @@ CDAC_TYPE_FIELD(InterpMethodContextFrame, T_POINTER, StartIp, offsetof(InterpMet 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_FIELD(InterpMethodContextFrame, T_POINTER, Stack, offsetof(InterpMethodContextFrame, pStack)) CDAC_TYPE_END(InterpMethodContextFrame) #endif // FEATURE_INTERPRETER @@ -1025,6 +1026,7 @@ CDAC_TYPE_END(FramedMethodFrame) CDAC_TYPE_BEGIN(InterpreterFrame) CDAC_TYPE_INDETERMINATE(InterpreterFrame) CDAC_TYPE_FIELD(InterpreterFrame, T_POINTER, TopInterpMethodContextFrame, cdac_data::TopInterpMethodContextFrame) +CDAC_TYPE_FIELD(InterpreterFrame, T_BOOL, IsFaulting, cdac_data::IsFaulting) CDAC_TYPE_END(InterpreterFrame) #endif // FEATURE_INTERPRETER diff --git a/src/coreclr/vm/frames.h b/src/coreclr/vm/frames.h index c4081fd2612544..d2da804694077c 100644 --- a/src/coreclr/vm/frames.h +++ b/src/coreclr/vm/frames.h @@ -2340,6 +2340,7 @@ template<> struct cdac_data { static constexpr size_t TopInterpMethodContextFrame = offsetof(InterpreterFrame, m_pTopInterpMethodContextFrame); + static constexpr size_t IsFaulting = offsetof(InterpreterFrame, m_isFaulting); }; #endif // FEATURE_INTERPRETER 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 89bda5e2ff51b8..d4940e8a313dd9 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 @@ -318,6 +318,10 @@ TargetPointer IExecutionManager.NonVirtualEntry2MethodDesc(TargetCodePointer ent bool IExecutionManager.IsFunclet(CodeBlockHandle codeInfoHandle) { + // Interpreter code has no native unwind info and therefore no funclets. + if (((IExecutionManager)this).GetJITType(codeInfoHandle) == JitType.Interpreter) + return false; + return ((IExecutionManager)this).GetStartAddress(codeInfoHandle) != ((IExecutionManager)this).GetFuncletStartAddress(codeInfoHandle); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64Context.cs index 2ef6e567420edc..85b1c98db7d171 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64Context.cs @@ -53,6 +53,8 @@ public TargetPointer FramePointer set => Rbp = value.Value; } + public uint RawContextFlags { readonly get => ContextFlags; set => ContextFlags = value; } + public void Unwind(Target target) { AMD64Unwinder unwinder = new(target); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64Context.cs index 1f9508e517e1cb..c74e7ac90042c1 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64Context.cs @@ -60,6 +60,8 @@ public TargetPointer FramePointer set => Fp = value.Value; } + public uint RawContextFlags { readonly get => ContextFlags; set => ContextFlags = value; } + public void Unwind(Target target) { ARM64Unwinder unwinder = new(target); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARMContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARMContext.cs index 8fedf1cdd5a83e..1adcd7ab1c860d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARMContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARMContext.cs @@ -53,6 +53,8 @@ public TargetPointer FramePointer set => R11 = (uint)value.Value; } + public uint RawContextFlags { readonly get => ContextFlags; set => ContextFlags = value; } + public void Unwind(Target target) { ARMUnwinder unwinder = new(target); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ContextHolder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ContextHolder.cs index c952ea17a50829..246a4bfd3c733e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ContextHolder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ContextHolder.cs @@ -21,6 +21,8 @@ public sealed class ContextHolder : IPlatformAgnosticContext, IEquatable Context.InstructionPointer; set => Context.InstructionPointer = value; } public TargetPointer FramePointer { get => Context.FramePointer; set => Context.FramePointer = value; } + public uint RawContextFlags { get => Context.RawContextFlags; set => Context.RawContextFlags = value; } + public unsafe void ReadFromAddress(Target target, TargetPointer address) { Span buffer = new byte[Size]; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs index 6d42bba8fff30d..44dbb33a60b510 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs @@ -17,6 +17,8 @@ public interface IPlatformAgnosticContext public TargetPointer InstructionPointer { get; set; } public TargetPointer FramePointer { get; set; } + public uint RawContextFlags { get; set; } + public abstract void Clear(); public abstract void ReadFromAddress(Target target, TargetPointer address); public abstract void FillFromBuffer(Span buffer); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformContext.cs index 01ef3f60aed42a..81a42dc45f6dd7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformContext.cs @@ -15,6 +15,8 @@ public interface IPlatformContext TargetPointer InstructionPointer { get; set; } TargetPointer FramePointer { get; set; } + uint RawContextFlags { get; set; } + void Unwind(Target target); bool TrySetRegister(string name, TargetNUInt value); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/LoongArch64Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/LoongArch64Context.cs index 5fbe851e1b7105..48dedde40ef55c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/LoongArch64Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/LoongArch64Context.cs @@ -58,6 +58,8 @@ public TargetPointer FramePointer set => Fp = value.Value; } + public uint RawContextFlags { readonly get => ContextFlags; set => ContextFlags = value; } + public void Unwind(Target target) { LoongArch64Unwinder unwinder = new(target); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RISCV64Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RISCV64Context.cs index e4afafa0ecfece..0e85faf1459e4b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RISCV64Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RISCV64Context.cs @@ -58,6 +58,8 @@ public TargetPointer FramePointer set => Fp = value.Value; } + public uint RawContextFlags { readonly get => ContextFlags; set => ContextFlags = value; } + public void Unwind(Target target) { RISCV64Unwinder unwinder = new(target); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86Context.cs index 3a32e77bb76df8..0a43d535be4616 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86Context.cs @@ -60,6 +60,8 @@ public TargetPointer FramePointer set => Ebp = (uint)value.Value; } + public uint RawContextFlags { readonly get => ContextFlags; set => ContextFlags = value; } + public void Unwind(Target target) { X86Unwinder unwinder = new(target); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs index 7f0ba8cea7052e..acf077c1d5b4a1 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs @@ -168,17 +168,12 @@ public void UpdateContextFromFrame(Data.Frame frame, IPlatformAgnosticContext co case FrameType.InterpreterFrame: { - // Match native stackwalk.cpp: Set context to the top - // InterpMethodContextFrame so the walker transitions to SW_FRAMELESS - // and yields each interpreted method individually via virtual unwind. + // Mirrors native InterpreterFrame::SetContextToInterpMethodContextFrame + // (frames.cpp). Sets context to the top InterpMethodContextFrame so the + // walker transitions to SW_FRAMELESS and yields each interpreted method + // individually via virtual unwind. Data.InterpreterFrame interpreterFrame = _target.ProcessedData.GetOrAdd(frame.Address); - TargetPointer topContextFramePtr = ResolveTopInterpMethodContextFrame(interpreterFrame); - if (topContextFramePtr != TargetPointer.Null) - { - Data.InterpMethodContextFrame topContextFrame = _target.ProcessedData.GetOrAdd(topContextFramePtr); - context.InstructionPointer = new TargetPointer((ulong)topContextFrame.Ip); - context.StackPointer = topContextFramePtr; - } + SetContextToInterpMethodContextFrame(context, interpreterFrame); return; } @@ -404,4 +399,82 @@ public IEnumerable WalkInterpreterFrameChain(TargetPointer frameA interpMethodFramePtr = contextFrame.ParentPtr; } } + + // Matches the Windows CONTEXT_EXCEPTION_ACTIVE flag value. The PAL CONTEXT structures + // on Linux/macOS use the same bit so a single constant is sufficient across platforms. + private const uint CONTEXT_EXCEPTION_ACTIVE = 0x8000000; + + /// + /// Mirrors native InterpreterFrame::SetContextToInterpMethodContextFrame (frames.cpp). + /// + public void SetContextToInterpMethodContextFrame( + IPlatformAgnosticContext context, + Data.InterpreterFrame interpreterFrame) + { + TargetPointer topContextFramePtr = ResolveTopInterpMethodContextFrame(interpreterFrame); + if (topContextFramePtr == TargetPointer.Null) + return; + + Data.InterpMethodContextFrame topContextFrame = _target.ProcessedData.GetOrAdd(topContextFramePtr); + context.InstructionPointer = new TargetPointer((ulong)topContextFrame.Ip); + context.StackPointer = topContextFramePtr; + context.FramePointer = topContextFrame.Stack; + SetFirstArgRegister(context, interpreterFrame.Address); + + uint flags = context.FullContextFlags; + if (interpreterFrame.IsFaulting) + flags |= CONTEXT_EXCEPTION_ACTIVE; + context.RawContextFlags = flags; + } + + /// + /// Mirrors native VirtualUnwindInterpreterCallFrame (eetwain.cpp). + /// Returns false when the interpreter chain is exhausted (either no parent, or the + /// parent has no active IP). + /// + public bool VirtualUnwindInterpreterCallFrame(IPlatformAgnosticContext context) + { + TargetPointer currentFramePtr = context.StackPointer; + Data.InterpMethodContextFrame currentFrame = _target.ProcessedData.GetOrAdd(currentFramePtr); + + if (currentFrame.ParentPtr == TargetPointer.Null) + return false; + + Data.InterpMethodContextFrame parentFrame = _target.ProcessedData.GetOrAdd(currentFrame.ParentPtr); + if (parentFrame.Ip == TargetPointer.Null) + return false; + + context.InstructionPointer = new TargetPointer((ulong)parentFrame.Ip); + context.StackPointer = currentFrame.ParentPtr; + context.FramePointer = parentFrame.Stack; + context.RawContextFlags = context.FullContextFlags; + return true; + } + + private void SetFirstArgRegister(IPlatformAgnosticContext context, TargetPointer value) + { + string registerName = GetFirstArgRegisterName(); + if (!context.TrySetRegister(registerName, new TargetNUInt(value.Value))) + { + throw new InvalidOperationException( + $"Failed to set first argument register '{registerName}' on the context."); + } + } + + private string GetFirstArgRegisterName() + { + IRuntimeInfo runtimeInfo = _target.Contracts.RuntimeInfo; + return runtimeInfo.GetTargetArchitecture() switch + { + RuntimeInfoArchitecture.X64 => + runtimeInfo.GetTargetOperatingSystem() == RuntimeInfoOperatingSystem.Windows ? "rcx" : "rdi", + RuntimeInfoArchitecture.Arm64 => "x0", + RuntimeInfoArchitecture.Arm => "r0", + RuntimeInfoArchitecture.X86 => "ecx", + RuntimeInfoArchitecture.LoongArch64 => "a0", + RuntimeInfoArchitecture.RiscV64 => "a0", + var arch => throw new NotSupportedException( + $"Unsupported architecture for first argument register: {arch}"), + }; + } } 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 610adb79e5441a..c6354516c47b75 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 @@ -970,22 +970,10 @@ private bool IsInterpreterCode(TargetPointer ip) /// private void InterpreterVirtualUnwind(StackWalkData handle) { - TargetPointer currentFramePtr = handle.Context.StackPointer; - Data.InterpMethodContextFrame currentFrame = _target.ProcessedData.GetOrAdd(currentFramePtr); - - if (currentFrame.ParentPtr != TargetPointer.Null) - { - Data.InterpMethodContextFrame parentFrame = _target.ProcessedData.GetOrAdd(currentFrame.ParentPtr); - if (parentFrame.Ip != TargetPointer.Null) - { - // Parent is active -- set context to the parent interpreted method. - handle.Context.InstructionPointer = new TargetPointer((ulong)parentFrame.Ip); - handle.Context.StackPointer = currentFrame.ParentPtr; - return; - } - } + if (_frameHelpers.VirtualUnwindInterpreterCallFrame(handle.Context)) + return; - // No active parent -- interpreter chain under this InterpreterFrame is exhausted. + // No active parent: interpreter chain under this InterpreterFrame is exhausted. // Use the saved InterpreterFrame's transition block to restore the context to // the native caller of InterpExecMethod. This is the cDAC equivalent of the // native DummyCallerIP -> UpdateRegDisplay path in stackwalk.cpp. 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 edee3128b43f86..38d96cc79263a1 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 @@ -15,10 +15,12 @@ public InterpMethodContextFrame(Target target, TargetPointer address) ParentPtr = target.ReadPointerField(address, type, nameof(ParentPtr)); Ip = target.ReadPointerField(address, type, nameof(Ip)); NextPtr = target.ReadPointerField(address, type, nameof(NextPtr)); + Stack = target.ReadPointerField(address, type, nameof(Stack)); } public TargetPointer StartIp { get; init; } public TargetPointer ParentPtr { get; init; } public TargetPointer Ip { get; init; } public TargetPointer NextPtr { get; init; } + public TargetPointer Stack { 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 index b00a6b1ae1351d..fa06f8cead33a5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpreterFrame.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpreterFrame.cs @@ -10,9 +10,13 @@ static InterpreterFrame IData.Create(Target target, TargetPoin public InterpreterFrame(Target target, TargetPointer address) { + Address = address; Target.TypeInfo type = target.GetTypeInfo(DataType.InterpreterFrame); TopInterpMethodContextFrame = target.ReadPointerField(address, type, nameof(TopInterpMethodContextFrame)); + IsFaulting = target.ReadField(address, type, nameof(IsFaulting)) != 0; } + public TargetPointer Address { get; init; } public TargetPointer TopInterpMethodContextFrame { get; init; } + public bool IsFaulting { get; init; } } diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStackDoubleWalk/InterpreterStackDoubleWalk.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStackDoubleWalk/InterpreterStackDoubleWalk.csproj deleted file mode 100644 index 0db41b7c90c728..00000000000000 --- a/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStackDoubleWalk/InterpreterStackDoubleWalk.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - 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 deleted file mode 100644 index 8ba58191ab76f9..00000000000000 --- a/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStackDoubleWalk/Program.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.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 via SpinStep() calls (each call through interpreter precode) -/// -/// Main thread: -/// Main (JIT) -> waits for signal -> FailFast -/// -/// Note: Even though the worker is executing interpreted code, in a FailFast dump -/// the CPU IP is inside the native interpreter engine. However, when a debugger -/// breaks the thread at SpinStep's interpreter precode (via cdb breakpoint), the -/// OS thread context has IP = precode address, which IS managed code registered as -/// JitType.Interpreter. This enables the SkipNextInterpreterFrame double-walk -/// prevention to be exercised from a debugger-collected dump. -/// -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 by repeatedly calling SpinStep(). Each call goes through SpinStep's - // interpreter precode, allowing a debugger to break at the precode and - // capture a dump where the thread's IP is in interpreter-managed code. - while (s_keepSpinning) { SpinStep(); } - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void SpinStep() { } - - private static volatile bool s_keepSpinning = true; -} diff --git a/src/native/managed/cdac/tests/DumpTests/DumpTests.targets b/src/native/managed/cdac/tests/DumpTests/DumpTests.targets index 712d1358bd9701..3c9f813b261aff 100644 --- a/src/native/managed/cdac/tests/DumpTests/DumpTests.targets +++ b/src/native/managed/cdac/tests/DumpTests/DumpTests.targets @@ -150,7 +150,7 @@ + Properties="DebuggeeName=%(_DebuggeeWithDumpTypes.Identity);_DebuggeeDumpTypes=%(_DebuggeeWithDumpTypes.DumpTypes);_DebuggeeR2RModes=%(_DebuggeeWithDumpTypes.R2RModes);_DebuggeeEnvVars=$([MSBuild]::Escape('%(_DebuggeeWithDumpTypes.EnvironmentVariables)'))" /> @@ -216,7 +216,7 @@ + Properties="DebuggeeName=$(DebuggeeName);_DumpTypeName=%(_DumpTypeItem.Identity);_DebuggeeR2RModes=$(_DebuggeeR2RModes);_DebuggeeEnvVars=$([MSBuild]::Escape('$(_DebuggeeEnvVars)'))" /> @@ -236,7 +236,7 @@ + Properties="DebuggeeName=$(DebuggeeName);_MiniDumpType=$(_MiniDumpType);_DumpTypeDirName=$(_DumpTypeDirName);_DumpTypeName=$(_DumpTypeName);_R2RModeName=%(_R2RModeItem.Identity);_DebuggeeEnvVars=$([MSBuild]::Escape('$(_DebuggeeEnvVars)'))" /> @@ -252,7 +252,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=$([MSBuild]::Escape('$(_DebuggeeEnvVars)'));DumpRuntimeVersion=%(DumpRuntimeVersion.Identity)" /> @@ -271,12 +271,12 @@ diff --git a/src/native/managed/cdac/tests/DumpTests/InterpreterStackDoubleWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/InterpreterStackDoubleWalkDumpTests.cs deleted file mode 100644 index 07048d6dc38692..00000000000000 --- a/src/native/managed/cdac/tests/DumpTests/InterpreterStackDoubleWalkDumpTests.cs +++ /dev/null @@ -1,141 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.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) - { - 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); - 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 - /// matching the native DAC stack walk output. - /// - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - public void StackWalk_VerifyInterleavedStackLayout(TestConfiguration config) - { - InitializeDumpTest(config); - SkipIfInterpreterNotAvailable(); - - ThreadData workerThread = DumpTestHelpers.FindThreadWithMethod(Target, "MethodA"); - - // Use adjacent assertions to verify no extra InterpreterFrame appears - // after the interpreted region — this catches the "doubled frame" bug - // where InterpreterFrame would appear both before AND after its methods. - DumpTestStackWalker.Walk(Target, workerThread) - .ExpectRuntimeFrame("InterpreterFrame") - .ExpectAdjacentFrame("MethodD", AssertInterpreted) - .ExpectAdjacentFrame("MethodC", AssertInterpreted) - .ExpectAdjacentFrame("Bounce", AssertJitted) - .ExpectAdjacentRuntimeFrame("InterpreterFrame") - .ExpectAdjacentFrame("MethodB", AssertInterpreted) - .ExpectAdjacentFrame("MethodA", AssertInterpreted) - .Verify(); - } - - /// - /// Walks the worker thread and verifies each interpreted method appears exactly once - /// and that InterpreterFrame never appears consecutively (the "doubled frame" bug - /// that native PR #126953 fixed via ResetRegDisp dedup). - /// - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - public void StackWalk_NoDoubledInterpreterFrames(TestConfiguration config) - { - InitializeDumpTest(config); - SkipIfInterpreterNotAvailable(); - - ThreadData workerThread = DumpTestHelpers.FindThreadWithMethod(Target, "MethodA"); - DumpTestStackWalker walker = DumpTestStackWalker.Walk(Target, workerThread); - - 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"})"))}]"); - } - - // Verify no InterpreterFrame appears after the interpreted methods it - // introduces. The original "doubled frame" bug (native PR #126953) was that - // InterpreterFrame appeared both before AND after its interpreted region. - // After the interpreter virtual unwind exhausts the chain, the frame - // iterator must have advanced past the owning InterpreterFrame. - for (int i = 0; i < walker.Frames.Count - 1; i++) - { - if (walker.Frames[i].FrameName == "InterpreterFrame" - && walker.Frames[i + 1].FrameName == "InterpreterFrame") - { - Assert.Fail( - $"Consecutive InterpreterFrame entries at indices {i} and {i + 1} — " + - $"this indicates the doubled InterpreterFrame bug. " + - $"Full stack: [{string.Join(", ", walker.Frames.Select(f => $"{f.Name ?? ""}({f.FrameName ?? "frameless"})"))}]"); - } - } - } -} diff --git a/src/native/managed/cdac/tests/FrameIteratorTests.cs b/src/native/managed/cdac/tests/FrameIteratorTests.cs index 2112398791aced..28ac5aab77a080 100644 --- a/src/native/managed/cdac/tests/FrameIteratorTests.cs +++ b/src/native/managed/cdac/tests/FrameIteratorTests.cs @@ -70,6 +70,7 @@ static TargetTestHelpers.LayoutResult GetLayout(TargetTestHelpers helpers, TypeF Fields = [ new(nameof(Data.InterpreterFrame.TopInterpMethodContextFrame), DataType.pointer), + new(nameof(Data.InterpreterFrame.IsFaulting), DataType.uint8), ], BaseTypeFields = FrameFields }; @@ -83,6 +84,7 @@ static TargetTestHelpers.LayoutResult GetLayout(TargetTestHelpers helpers, TypeF new(nameof(Data.InterpMethodContextFrame.ParentPtr), DataType.pointer), new(nameof(Data.InterpMethodContextFrame.Ip), DataType.pointer), new(nameof(Data.InterpMethodContextFrame.NextPtr), DataType.pointer), + new(nameof(Data.InterpMethodContextFrame.Stack), DataType.pointer), ] }; @@ -167,7 +169,7 @@ public void GetMethodDescPtr_InterpreterFrame_FollowsFullChain(MockTarget.Archit 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); + int totalFrameSize = (int)types[DataType.InterpreterFrame].Size!.Value; var frameFrag = alloc.Allocate((ulong)totalFrameSize, "InterpreterFrame"); helpers.WritePointer(frameFrag.Data.AsSpan(0, pointerSize), interpreterFrameIdentifierValue); @@ -202,7 +204,7 @@ public void GetMethodDescPtr_InterpreterFrame_NullContextFrame_ReturnsNull(MockT 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); + int totalFrameSize = (int)types[DataType.InterpreterFrame].Size!.Value; var frameFrag = alloc.Allocate((ulong)totalFrameSize, "InterpreterFrame"); helpers.WritePointer(frameFrag.Data.AsSpan(0, pointerSize), interpreterFrameIdentifierValue); @@ -250,7 +252,7 @@ public void GetMethodDescPtr_InterpreterFrame_NullStartIp_ReturnsNull(MockTarget 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); + int totalFrameSize = (int)types[DataType.InterpreterFrame].Size!.Value; var frameFrag = alloc.Allocate((ulong)totalFrameSize, "InterpreterFrame"); helpers.WritePointer(frameFrag.Data.AsSpan(0, pointerSize), interpreterFrameIdentifierValue); @@ -303,7 +305,7 @@ public void GetMethodDescPtr_InterpreterFrame_NullMethod_ReturnsNull(MockTarget. 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); + int totalFrameSize = (int)types[DataType.InterpreterFrame].Size!.Value; var frameFrag = alloc.Allocate((ulong)totalFrameSize, "InterpreterFrame"); helpers.WritePointer(frameFrag.Data.AsSpan(0, pointerSize), interpreterFrameIdentifierValue); @@ -475,7 +477,7 @@ public void ResolveTopInterpMethodContextFrame_HintIsStale_SeeksViaParentPtr(Moc 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); + int totalFrameSize = (int)types[DataType.InterpreterFrame].Size!.Value; // InterpreterFrame with hint pointing to the stale frame var frameFrag = alloc.Allocate((ulong)totalFrameSize, "InterpreterFrame"); @@ -571,7 +573,7 @@ public void ResolveTopInterpMethodContextFrame_SeeksViaNextPtr(MockTarget.Archit 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); + int totalFrameSize = (int)types[DataType.InterpreterFrame].Size!.Value; // InterpreterFrame with hint pointing to the lower frame var frameFrag = alloc.Allocate((ulong)totalFrameSize, "InterpreterFrame"); @@ -652,7 +654,7 @@ public void WalkInterpreterFrameChain_SkipsInactiveFrames(MockTarget.Architectur 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); + int totalFrameSize = (int)types[DataType.InterpreterFrame].Size!.Value; // InterpreterFrame with hint pointing to the inactive frame var frameFrag = alloc.Allocate((ulong)totalFrameSize, "InterpreterFrame"); diff --git a/src/native/managed/cdac/tests/PrecodeStubsTests.cs b/src/native/managed/cdac/tests/PrecodeStubsTests.cs index 8c876c5d69468d..4f22833ec8394b 100644 --- a/src/native/managed/cdac/tests/PrecodeStubsTests.cs +++ b/src/native/managed/cdac/tests/PrecodeStubsTests.cs @@ -300,7 +300,6 @@ public PrecodeBuilder(AllocationRange allocationRange, MockMemorySpace.Builder b 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() { From 2bcca076b6ae3b2485c11dabd103fa1b3a20e527 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Tue, 5 May 2026 18:38:42 -0400 Subject: [PATCH 06/17] Address PR feedback: route GetCodeHeaderData GC info decode by JIT type; hide GetFrameHandler behind helper - SOSDacImpl.GetCodeHeaderData: when JIT type is Interpreter, decode the GC info using DecodeInterpreterGCInfo instead of DecodePlatformSpecificGCInfo. This mirrors native ClrDataAccess::GetCodeHeaderData routing through EECodeInfo::GetCodeManager()->GetFunctionSize, where interpreter code goes through InterpreterCodeManager::GetFunctionSize / InterpreterGcInfoDecoder. - FrameHelpers: make GetFrameHandler private and add a semantic helper ApplyInterpreterFrameTransition(context, interpreterFrameAddress) that encapsulates reading the InterpreterFrame as a FramedMethodFrame and invoking HandleTransitionFrame on it. Update StackWalk_1.InterpreterVirtualUnwind to use the new helper instead of reaching into the frame-handler dispatch directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ExecutionManager/ExecutionManagerCore.cs | 5 +++-- .../Contracts/StackWalk/FrameHandling/FrameHelpers.cs | 8 +++++++- .../Contracts/StackWalk/StackWalk_1.cs | 5 ++--- .../SOSDacImpl.cs | 11 +++++++++-- 4 files changed, 21 insertions(+), 8 deletions(-) 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 d4940e8a313dd9..a1d0065700d922 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 @@ -319,10 +319,11 @@ TargetPointer IExecutionManager.NonVirtualEntry2MethodDesc(TargetCodePointer ent bool IExecutionManager.IsFunclet(CodeBlockHandle codeInfoHandle) { // Interpreter code has no native unwind info and therefore no funclets. - if (((IExecutionManager)this).GetJITType(codeInfoHandle) == JitType.Interpreter) + TargetCodePointer startAddress = ((IExecutionManager)this).GetStartAddress(codeInfoHandle); + if (((IExecutionManager)this).GetCodeKind(startAddress) == CodeKind.Interpreter) return false; - return ((IExecutionManager)this).GetStartAddress(codeInfoHandle) != + return startAddress != ((IExecutionManager)this).GetFuncletStartAddress(codeInfoHandle); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs index acf077c1d5b4a1..062938b91f3e0c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs @@ -286,7 +286,7 @@ public TargetPointer GetReturnAddress(Data.Frame frame) } } - public IPlatformFrameHandler GetFrameHandler(IPlatformAgnosticContext context) + private IPlatformFrameHandler GetFrameHandler(IPlatformAgnosticContext context) { return context switch { @@ -404,6 +404,12 @@ public IEnumerable WalkInterpreterFrameChain(TargetPointer frameA // on Linux/macOS use the same bit so a single constant is sufficient across platforms. private const uint CONTEXT_EXCEPTION_ACTIVE = 0x8000000; + public void ApplyInterpreterFrameTransition(IPlatformAgnosticContext context, TargetPointer interpreterFrameAddress) + { + Data.FramedMethodFrame framedMethodFrame = _target.ProcessedData.GetOrAdd(interpreterFrameAddress); + GetFrameHandler(context).HandleTransitionFrame(framedMethodFrame); + } + /// /// Mirrors native InterpreterFrame::SetContextToInterpMethodContextFrame (frames.cpp). /// 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 c6354516c47b75..0f72fbb1f1002f 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 @@ -974,13 +974,12 @@ private void InterpreterVirtualUnwind(StackWalkData handle) return; // No active parent: interpreter chain under this InterpreterFrame is exhausted. - // Use the saved InterpreterFrame's transition block to restore the context to + // Apply the InterpreterFrame's transition-block state to restore the context to // the native caller of InterpExecMethod. This is the cDAC equivalent of the // native DummyCallerIP -> UpdateRegDisplay path in stackwalk.cpp. if (handle.CurrentInterpreterFrameAddress != TargetPointer.Null) { - Data.FramedMethodFrame framedMethodFrame = _target.ProcessedData.GetOrAdd(handle.CurrentInterpreterFrameAddress); - _frameHelpers.GetFrameHandler(handle.Context).HandleTransitionFrame(framedMethodFrame); + _frameHelpers.ApplyInterpreterFrameTransition(handle.Context, handle.CurrentInterpreterFrameAddress); handle.CurrentInterpreterFrameAddress = TargetPointer.Null; } // UpdateState (called by Next) will see the IP and determine next state. 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 0014b69bbdab8c..a077d9b49c71e0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -813,7 +813,8 @@ int ISOSDacInterface.GetCodeHeaderData(ClrDataAddress ip, DacpCodeHeaderData* da { data->MethodDescPtr = eman.GetMethodDesc(cbh).ToClrDataAddress(_target); - data->JITType = eman.GetCodeKind(targetCodePointer) switch + Contracts.CodeKind codeKind = eman.GetCodeKind(targetCodePointer); + data->JITType = codeKind switch { Contracts.CodeKind.Jitted => JitTypes.TYPE_JIT, Contracts.CodeKind.ReadyToRun => JitTypes.TYPE_PJIT, @@ -826,7 +827,13 @@ int ISOSDacInterface.GetCodeHeaderData(ClrDataAddress ip, DacpCodeHeaderData* da data->MethodStart = eman.GetStartAddress(cbh).Value; - IGCInfoHandle gcInfoHandle = gcInfo.DecodePlatformSpecificGCInfo(pGcInfo, gcVersion); + // Mirrors native ClrDataAccess::GetCodeHeaderData which routes through + // EECodeInfo::GetCodeManager()->GetFunctionSize: interpreter code uses the + // interpreter-specific GC info encoding, all other code uses the platform + // GC info encoding. + IGCInfoHandle gcInfoHandle = codeKind == Contracts.CodeKind.Interpreter + ? gcInfo.DecodeInterpreterGCInfo(pGcInfo, gcVersion) + : gcInfo.DecodePlatformSpecificGCInfo(pGcInfo, gcVersion); data->MethodSize = gcInfo.GetCodeLength(gcInfoHandle); eman.GetMethodRegionInfo(cbh, out uint hotRegionSize, out TargetPointer coldRegionStart, out uint coldRegionSize); From b8937a930787d5fdd606d7a6b675c5fa4006155e Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 5 May 2026 18:15:29 -0400 Subject: [PATCH 07/17] Add interpreter SOS leg to runtime-diagnostics pipeline Adds an opt-in Interpreter SOS test leg to runtime-diagnostics.yml that exercises the diagnostics-side SOSInterpreterTests against a Checked CoreCLR drop. FEATURE_INTERPRETER is only compiled into Debug/Checked CoreCLR, so the existing Release build leg cannot run this test. Changes: * eng/pipelines/diagnostics/runtime-diag-job.yml: adds a testInterpreter parameter (default false). When true, _TestInterpreterArgs resolves to -testInterpreter and is forwarded to the diagnostics build script alongside the existing -useCdac / -noFallback flags. Default-false invocation is identical to today. * eng/pipelines/runtime-diagnostics.yml: - New build leg that produces a Checked CoreCLR (libs/SDK stay Release) under a distinct artifact name (..._coreclr_Checked). - New Interpreter test job that depends on the Checked build leg, downloads its artifact, and is the only job that sets testInterpreter: true. - The cDAC / cDAC_no_fallback / DAC test jobs and the existing Release build leg are unchanged. Tested with a manual ADO queue using diagnosticsBranch pointed at the companion diagnostics PR (dotnet/diagnostics#5829). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/diagnostics/runtime-diag-job.yml | 6 ++++++ eng/pipelines/runtime-diagnostics.yml | 10 +++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/eng/pipelines/diagnostics/runtime-diag-job.yml b/eng/pipelines/diagnostics/runtime-diag-job.yml index 8f93002ff2f972..8989222f9f3cea 100644 --- a/eng/pipelines/diagnostics/runtime-diag-job.yml +++ b/eng/pipelines/diagnostics/runtime-diag-job.yml @@ -41,6 +41,7 @@ parameters: noFallback: false classFilter: '' methodFilter: '' + testInterpreter: false jobs: - template: /eng/common/${{ parameters.templatePath }}/job/job.yml @@ -94,6 +95,7 @@ jobs: - _NoFallbackArgs: '' - _ClassFilterArgs: '' - _MethodFilterArgs: '' + - _TestInterpreterArgs: '' - _buildScript: $(Build.SourcesDirectory)$(dir)build$(scriptExt) @@ -118,6 +120,9 @@ jobs: - ${{ if ne(parameters.methodFilter, '') }}: - _MethodFilterArgs: '-methodfilter ${{ parameters.methodFilter }}' + - ${{ if eq(parameters.testInterpreter, 'true') }}: + - _TestInterpreterArgs: '-testInterpreter' + # For testing msrc's and service releases. The RuntimeSourceVersion is either "default" or the service release version to test - _InternalInstallArgs: '' - ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest'), eq(parameters.isCodeQLRun, 'false')) }}: @@ -212,6 +217,7 @@ jobs: -privatebuild $(_CdacArgs) $(_NoFallbackArgs) + $(_TestInterpreterArgs) -liveRuntimeDir ${{ parameters.liveRuntimeDir }} $(_TestArgs) $(_Cross) diff --git a/eng/pipelines/runtime-diagnostics.yml b/eng/pipelines/runtime-diagnostics.yml index d394167a81efd0..ca38617222003e 100644 --- a/eng/pipelines/runtime-diagnostics.yml +++ b/eng/pipelines/runtime-diagnostics.yml @@ -81,7 +81,12 @@ extends: platforms: - windows_x64 jobParameters: - buildArgs: -s clr+libs+tools.cdac+host+packs -c Debug -rc $(_BuildConfig) -lc $(_BuildConfig) + # Pass -clrinterpreter so FEATURE_INTERPRETER is compiled into the Release + # CoreCLR. This lets the Interpreter SOS leg below share a single build with + # the cDAC/cDAC_no_fallback/DAC legs instead of producing a separate Checked + # drop. (Note from review on dotnet/runtime#127840: -clrinterpreter enables + # the interpreter in Release as well.) + buildArgs: -s clr+libs+tools.cdac+host+packs -c Debug -rc $(_BuildConfig) -lc $(_BuildConfig) -clrinterpreter nameSuffix: AllSubsets_CoreCLR isOfficialBuild: ${{ variables.isOfficialBuild }} timeoutInMinutes: 360 @@ -114,6 +119,7 @@ extends: jobParameters: name: cDAC useCdac: true + testInterpreter: true methodFilter: SOS* isOfficialBuild: ${{ variables.isOfficialBuild }} liveRuntimeDir: $(Build.SourcesDirectory)/artifacts/runtime @@ -168,6 +174,7 @@ extends: name: cDAC_no_fallback useCdac: true noFallback: true + testInterpreter: true methodFilter: SOS* isOfficialBuild: ${{ variables.isOfficialBuild }} liveRuntimeDir: $(Build.SourcesDirectory)/artifacts/runtime @@ -221,6 +228,7 @@ extends: jobParameters: name: DAC useCdac: false + testInterpreter: true methodFilter: SOS* isOfficialBuild: ${{ variables.isOfficialBuild }} liveRuntimeDir: $(Build.SourcesDirectory)/artifacts/runtime From 9ea1221b836e7d0dbc452cee45868bee94793bb2 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Wed, 6 May 2026 21:58:23 -0400 Subject: [PATCH 08/17] [cDAC] Set first-arg register to InterpreterFrame in per-arch InlinedCallFrame handlers Mirror the per-architecture native InlinedCallFrame::UpdateRegDisplay_Impl logic in the corresponding cDAC frame handlers: - AMD64FrameHandler.HandleInlinedCallFrame: sets Rcx (Windows) or Rdi (Unix SysV) to the InterpreterFrame address when the next Frame is an InterpreterFrame, matching src/coreclr/vm/amd64/cgenamd64.cpp:212-218. - ARM64FrameHandler.HandleInlinedCallFrame: sets X0 to the InterpreterFrame address when the next Frame is an InterpreterFrame, matching src/coreclr/vm/arm64/stubs.cpp:408-414. ARM, x86, LoongArch64, and RISCV64 native InlinedCallFrame::UpdateRegDisplay_Impl do NOT perform this update, so the corresponding cDAC handlers (or BaseFrameHandler inheritance) do not either. A protected GetNextFrame helper is added to BaseFrameHandler so each handler can inspect the chain itself without changing the IPlatformFrameHandler interface (handles the FRAME_TOP all-ones terminator). BaseFrameHandler also constructs its own FrameHelpers from the target so derived handlers can inline calls to GetFrameType. Without this update, cDAC reports the thread's literal saved Rcx for frames between an InlinedCallFrame and its successor InterpreterFrame, while the legacy DAC reports the InterpreterFrame address. This trips Debug.Assert(contextStruct.Equals(localContextStruct)) in ClrDataStackWalk.GetContext during !ClrStack on a thread captured during a P/Invoke from interpreted code. Verified locally against the SOS.InterpreterStackTest.Heap.dmp captured in CI: !ClrStack and !PrintException both succeed with the cDAC parity check enabled, walking through [InlinedCallFrame], JIT IL stub frames, [InterpreterFrame: ...], and the interpreted method frames in correct order. All 2081 cDAC unit tests continue to pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Directory.Build.props | 6 +++--- .../FrameHandling/AMD64FrameHandler.cs | 18 ++++++++++++++++++ .../FrameHandling/ARM64FrameHandler.cs | 11 +++++++++++ .../FrameHandling/BaseFrameHandler.cs | 12 ++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props b/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props index 52fcfcf860bbf9..12547327020701 100644 --- a/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props +++ b/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props @@ -121,9 +121,9 @@ - - - + + + diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/AMD64FrameHandler.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/AMD64FrameHandler.cs index 86d36f963343b4..6b83d9b6108386 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/AMD64FrameHandler.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/AMD64FrameHandler.cs @@ -11,6 +11,24 @@ internal class AMD64FrameHandler(Target target, ContextHolder cont { private readonly ContextHolder _holder = contextHolder; + public override void HandleInlinedCallFrame(InlinedCallFrame inlinedCallFrame) + { + base.HandleInlinedCallFrame(inlinedCallFrame); + + Data.Frame? next = GetNextFrame(inlinedCallFrame.Address); + if (next is not null && _frameHelpers.GetFrameType(next.Identifier) == FrameType.InterpreterFrame) + { + if (_target.Contracts.RuntimeInfo.GetTargetOperatingSystem() == RuntimeInfoOperatingSystem.Windows) + { + _holder.Context.Rcx = next.Address.Value; + } + else + { + _holder.Context.Rdi = next.Address.Value; + } + } + } + public void HandleHijackFrame(HijackFrame frame) { HijackArgsAMD64 args = _target.ProcessedData.GetOrAdd(frame.HijackArgsPtr); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/ARM64FrameHandler.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/ARM64FrameHandler.cs index 5efa802f7bd725..616ef6454e9149 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/ARM64FrameHandler.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/ARM64FrameHandler.cs @@ -13,6 +13,17 @@ internal class ARM64FrameHandler(Target target, ContextHolder cont { private readonly ContextHolder _holder = contextHolder; + public override void HandleInlinedCallFrame(InlinedCallFrame inlinedCallFrame) + { + base.HandleInlinedCallFrame(inlinedCallFrame); + + Data.Frame? next = GetNextFrame(inlinedCallFrame.Address); + if (next is not null && _frameHelpers.GetFrameType(next.Identifier) == FrameType.InterpreterFrame) + { + _holder.Context.X0 = next.Address.Value; + } + } + public void HandleHijackFrame(HijackFrame frame) { HijackArgs args = _target.ProcessedData.GetOrAdd(frame.HijackArgsPtr); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/BaseFrameHandler.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/BaseFrameHandler.cs index 3d56a63532613e..19819546d959a3 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/BaseFrameHandler.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/BaseFrameHandler.cs @@ -13,6 +13,7 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; internal abstract class BaseFrameHandler(Target target, IPlatformAgnosticContext context) { protected readonly Target _target = target; + protected readonly FrameHelpers _frameHelpers = new FrameHelpers(target); private readonly IPlatformAgnosticContext _context = context; public virtual void HandleInlinedCallFrame(InlinedCallFrame inlinedCallFrame) @@ -107,4 +108,15 @@ protected void UpdateCalleeSavedRegistersFromOtherContext(IPlatformAgnosticConte } } } + + protected Data.Frame? GetNextFrame(TargetPointer currentFrameAddress) + { + Data.Frame current = _target.ProcessedData.GetOrAdd(currentFrameAddress); + if (current.Next == TargetPointer.Null) + return null; + ulong terminator = _target.PointerSize == 8 ? ulong.MaxValue : uint.MaxValue; + if (current.Next.Value == terminator) + return null; + return _target.ProcessedData.GetOrAdd(current.Next); + } } From 3e32f6b868b6a343363ed0a35bcf4a787ceee13a Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Thu, 7 May 2026 17:30:53 -0400 Subject: [PATCH 09/17] [cDAC] Allow GetILOffsetsByAddress to succeed for interpreter IPs The cDAC's InterpreterJitManager (added in this PR) registers interpreter code as code blocks with an interpreter-format DebugInfo, and DebugInfo_2.GetMethodNativeMap successfully decodes that DebugInfo into an OffsetMapping list. This means cDAC produces real, correct IL offsets for interpreter pseudo-IPs (e.g. for !ClrStack -lines source-line resolution), where the legacy DAC bails out early with E_INVALIDARG because ExecutionManager::GetNativeCodeVersion(address) returns null for interpreter IPs (daccess.cpp:5660-5666). Switch the parity validation in GetILOffsetsByAddress from the default AllowDivergentFailures (which rejects cDAC-success vs DAC-failure) to AllowCdacSuccess so cDAC's better behavior is permitted. Gate the offset-comparison block on hrLocal == S_OK as well, so we don't spuriously compare against a zero-initialized localOffsetsNeeded / localIlOffsets when only cDAC succeeded. Verified locally against the SOS.InterpreterStackTest.Heap.dmp from CI: !ClrStack -lines now resolves source lines for the JIT-compiled EH frames AND for the interpreter frames (InterpTestMethodThrow @ 19, InterpTestMethodRunNested @ 13, InterpreterStackTestApp.Main @ 12), with the cDAC parity check enabled. All 2081 cDAC unit tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ClrDataMethodInstance.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 39cde3cda24c28..96bb383b43659c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodInstance.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataMethodInstance.cs @@ -255,9 +255,10 @@ int IXCLRDataMethodInstance.GetILOffsetsByAddress(ClrDataAddress address, uint o validateIlOffsets ? localIlOffsetsPtr : null); } - Debug.ValidateHResult(hr, hrLocal); + // AllowCdacSuccess: the DAC fails on interpreted code. + Debug.ValidateHResult(hr, hrLocal, HResultValidationMode.AllowCdacSuccess); - if (hr == HResults.S_OK) + if (hr == HResults.S_OK && hrLocal == HResults.S_OK) { if (validateOffsetsNeeded) { From 4ef488e5ccb371fa2261b573ead1c9596629978a Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Thu, 7 May 2026 21:44:54 -0400 Subject: [PATCH 10/17] address comments --- .../StackWalk/FrameHandling/FrameHelpers.cs | 74 +- .../Contracts/StackWalk/StackWalk_1.cs | 75 +- .../MethodValidation.cs | 4 +- .../cdac/tests/DumpTests/DumpTests.targets | 4 +- .../managed/cdac/tests/FrameIteratorTests.cs | 677 ------------------ .../managed/cdac/tests/MethodDescTests.cs | 4 +- 6 files changed, 51 insertions(+), 787 deletions(-) delete mode 100644 src/native/managed/cdac/tests/FrameIteratorTests.cs diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs index 062938b91f3e0c..17002b97595b69 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs @@ -317,28 +317,6 @@ private bool InlinedCallFrameHasFunction(Data.InlinedCallFrame frame) } } - /// - /// Resolves the MethodDesc from a specific InterpMethodContextFrame by following: - /// InterpMethodContextFrame.StartIp -> InterpByteCodeStart.Method -> InterpMethod.MethodDesc - /// - public TargetPointer ResolveMethodDescFromInterpFrame(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; - } - /// /// Resolves the actual top InterpMethodContextFrame from the hint stored in InterpreterFrame, /// replicating InterpreterFrame::GetTopInterpMethodContextFrame() from frames.cpp. @@ -404,12 +382,6 @@ public IEnumerable WalkInterpreterFrameChain(TargetPointer frameA // on Linux/macOS use the same bit so a single constant is sufficient across platforms. private const uint CONTEXT_EXCEPTION_ACTIVE = 0x8000000; - public void ApplyInterpreterFrameTransition(IPlatformAgnosticContext context, TargetPointer interpreterFrameAddress) - { - Data.FramedMethodFrame framedMethodFrame = _target.ProcessedData.GetOrAdd(interpreterFrameAddress); - GetFrameHandler(context).HandleTransitionFrame(framedMethodFrame); - } - /// /// Mirrors native InterpreterFrame::SetContextToInterpMethodContextFrame (frames.cpp). /// @@ -434,11 +406,32 @@ public void SetContextToInterpMethodContextFrame( } /// - /// Mirrors native VirtualUnwindInterpreterCallFrame (eetwain.cpp). - /// Returns false when the interpreter chain is exhausted (either no parent, or the - /// parent has no active IP). + /// Performs interpreter virtual unwind, matching the native + /// VirtualUnwindInterpreterCallFrame in eetwain.cpp. + /// + /// When unwinding from a frameless interpreter frame, the SP points to the + /// current InterpMethodContextFrame. We follow pParent to get to the next + /// interpreted method in the call chain. If pParent is null, the interpreter + /// chain under the current InterpreterFrame is exhausted, we apply the + /// InterpreterFrame's transition-block state to restore the context to the + /// native caller of InterpExecMethod. /// - public bool VirtualUnwindInterpreterCallFrame(IPlatformAgnosticContext context) + public void InterpreterVirtualUnwind(IPlatformAgnosticContext context) + { + if (VirtualUnwindInterpreterCallFrame(context)) + return; + + // No active parent: interpreter chain under this InterpreterFrame is exhausted. + // The owning InterpreterFrame address lives in the first-argument register -- + // populated by SetContextToInterpMethodContextFrame + TargetPointer interpreterFrame = GetFirstArgRegister(context); + if (interpreterFrame != TargetPointer.Null) + { + ApplyInterpreterFrameTransition(context, interpreterFrame); + } + } + + private bool VirtualUnwindInterpreterCallFrame(IPlatformAgnosticContext context) { TargetPointer currentFramePtr = context.StackPointer; Data.InterpMethodContextFrame currentFrame = _target.ProcessedData.GetOrAdd(currentFramePtr); @@ -457,6 +450,23 @@ public bool VirtualUnwindInterpreterCallFrame(IPlatformAgnosticContext context) return true; } + private void ApplyInterpreterFrameTransition(IPlatformAgnosticContext context, TargetPointer interpreterFrameAddress) + { + Data.FramedMethodFrame framedMethodFrame = _target.ProcessedData.GetOrAdd(interpreterFrameAddress); + GetFrameHandler(context).HandleTransitionFrame(framedMethodFrame); + } + + private TargetPointer GetFirstArgRegister(IPlatformAgnosticContext context) + { + string registerName = GetFirstArgRegisterName(); + if (!context.TryReadRegister(registerName, out TargetNUInt value)) + { + throw new InvalidOperationException( + $"Failed to read first argument register '{registerName}' from the context."); + } + return new TargetPointer(value.Value); + } + private void SetFirstArgRegister(IPlatformAgnosticContext context, TargetPointer value) { string registerName = GetFirstArgRegisterName(); 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 0f72fbb1f1002f..8006109a545b27 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 @@ -49,8 +49,7 @@ private record StackDataFrameHandle( TargetPointer FrameAddress, ThreadData ThreadData, bool IsResumableFrame = false, - bool IsActiveFrame = false, - TargetPointer InterpContextFramePtr = default) : IStackDataFrameHandle + bool IsActiveFrame = false) : IStackDataFrameHandle { } private class StackWalkData(IPlatformAgnosticContext context, StackWalkState state, FrameIterator frameIter, ThreadData threadData) @@ -78,12 +77,6 @@ private class StackWalkData(IPlatformAgnosticContext context, StackWalkState sta // set IsInterrupted when transitioning to a managed frame. public FrameType? LastProcessedFrameType { get; set; } - // The address of the InterpreterFrame that we're currently walking via virtual - // unwind. Saved when UpdateContextFromFrame transitions from SW_FRAME to - // SW_FRAMELESS, used by InterpreterVirtualUnwind for FMF transition when the - // interpreter chain is exhausted. In native, this is stored in FirstArgReg. - public TargetPointer CurrentInterpreterFrameAddress { get; set; } - public bool IsCurrentFrameResumable() { if (State is not (StackWalkState.SW_FRAME or StackWalkState.SW_SKIPPED_FRAME)) @@ -130,11 +123,11 @@ public void AdvanceIsFirst() } } - public StackDataFrameHandle ToDataFrame(TargetPointer interpContextFramePtr = default) + public StackDataFrameHandle ToDataFrame() { bool isResumable = IsCurrentFrameResumable(); bool isActiveFrame = IsFirst && State == StackWalkState.SW_FRAMELESS; - return new(Context.Clone(), State, FrameIter.CurrentFrameAddress, ThreadData, isResumable, isActiveFrame, interpContextFramePtr); + return new(Context.Clone(), State, FrameIter.CurrentFrameAddress, ThreadData, isResumable, isActiveFrame); } } @@ -675,7 +668,7 @@ private bool Next(StackWalkData handle) // This mirrors VirtualUnwindInterpreterCallFrame in eetwain.cpp. if (IsInterpreterCode(handle.Context.InstructionPointer)) { - InterpreterVirtualUnwind(handle); + _frameHelpers.InterpreterVirtualUnwind(handle.Context); } else { @@ -701,17 +694,6 @@ private bool Next(StackWalkData handle) // pInlinedFrame is set only for active InlinedCallFrames. { var frameType = handle.FrameIter.GetCurrentFrameType(); - - // Save the InterpreterFrame address before advancing the frame iterator. - // InterpreterVirtualUnwind needs this for the FMF transition when the - // interpreter chain is exhausted. - TargetPointer savedInterpreterFrameAddress = TargetPointer.Null; - if (frameType == FrameType.InterpreterFrame) - { - savedInterpreterFrameAddress = handle.FrameIter.CurrentFrameAddress; - handle.CurrentInterpreterFrameAddress = savedInterpreterFrameAddress; - } - TargetPointer returnAddress = handle.FrameIter.GetCurrentReturnAddress(); bool isActiveICF = frameType == FrameType.InlinedCallFrame && returnAddress != TargetPointer.Null; @@ -733,22 +715,6 @@ private bool Next(StackWalkData handle) { handle.FrameIter.Next(); } - - // After advancing past an InterpreterFrame, the next frame must NOT be - // the same InterpreterFrame. The native walker (PR #126953) prevents this - // via ResetRegDisp dedup; our UpdateContextFromFrame + Next() achieves - // the same by advancing the frame iterator past the InterpreterFrame - // before the interpreter virtual unwind exhausts the chain and triggers - // the FMF transition back to SW_FRAME. - if (savedInterpreterFrameAddress != TargetPointer.Null - && handle.FrameIter.IsValid() - && handle.FrameIter.GetCurrentFrameType() == FrameType.InterpreterFrame - && handle.FrameIter.CurrentFrameAddress == savedInterpreterFrameAddress) - { - Debug.Fail( - $"InterpreterFrame at {savedInterpreterFrameAddress} was not advanced past — " + - "this would cause doubled interpreter frames in the stack walk."); - } } break; case StackWalkState.SW_ERROR: @@ -853,12 +819,6 @@ 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 _frameHelpers.ResolveMethodDescFromInterpFrame(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) @@ -958,32 +918,5 @@ private bool IsInterpreterCode(TargetPointer ip) return _eman.GetCodeKind(new TargetCodePointer(ip)) == CodeKind.Interpreter; } - /// - /// Performs interpreter virtual unwind, matching the native - /// VirtualUnwindInterpreterCallFrame in eetwain.cpp. - /// - /// When unwinding from a frameless interpreter frame, the SP points to the - /// current InterpMethodContextFrame. We follow pParent to get to the next - /// interpreted method in the call chain. If pParent is null, the interpreter - /// chain under the current InterpreterFrame is exhausted -- we advance the - /// frame iterator past the InterpreterFrame and transition to SW_FRAME. - /// - private void InterpreterVirtualUnwind(StackWalkData handle) - { - if (_frameHelpers.VirtualUnwindInterpreterCallFrame(handle.Context)) - return; - - // No active parent: interpreter chain under this InterpreterFrame is exhausted. - // Apply the InterpreterFrame's transition-block state to restore the context to - // the native caller of InterpExecMethod. This is the cDAC equivalent of the - // native DummyCallerIP -> UpdateRegDisplay path in stackwalk.cpp. - if (handle.CurrentInterpreterFrameAddress != TargetPointer.Null) - { - _frameHelpers.ApplyInterpreterFrameTransition(handle.Context, handle.CurrentInterpreterFrameAddress); - handle.CurrentInterpreterFrameAddress = TargetPointer.Null; - } - // UpdateState (called by Next) will see the IP and determine next state. - } - #endregion Interpreter } 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 245fff0908cc5a..1337d7d2d83c25 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 @@ -220,10 +220,8 @@ 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 DacValidateMD for more details. - Contracts.IPrecodeStubs precode = _target.Contracts.PrecodeStubs; - TargetPointer methodDesc = precode.GetMethodDescFromStubAddress(jitCodeAddr); + TargetPointer methodDesc = executionManager.NonVirtualEntry2MethodDesc(jitCodeAddr); if (methodDesc != methodDescPointer) { return false; diff --git a/src/native/managed/cdac/tests/DumpTests/DumpTests.targets b/src/native/managed/cdac/tests/DumpTests/DumpTests.targets index 3c9f813b261aff..cac9cc85219315 100644 --- a/src/native/managed/cdac/tests/DumpTests/DumpTests.targets +++ b/src/native/managed/cdac/tests/DumpTests/DumpTests.targets @@ -299,7 +299,7 @@ + EnvironmentVariables="DOTNET_DbgEnableMiniDump=1;DOTNET_DbgMiniDumpType=$(_MiniDumpType);DOTNET_DbgMiniDumpName=$(_DumpFile);DOTNET_ReadyToRun=$(_R2RValue);$([MSBuild]::Unescape('$(_DebuggeeEnvVars)'))" /> @@ -320,7 +320,7 @@ + EnvironmentVariables="DOTNET_DbgEnableMiniDump=1;DOTNET_DbgMiniDumpType=$(_MiniDumpType);DOTNET_DbgMiniDumpName=$(_DumpFile);DOTNET_ReadyToRun=$(_R2RValue);$([MSBuild]::Unescape('$(_DebuggeeEnvVars)'))" /> diff --git a/src/native/managed/cdac/tests/FrameIteratorTests.cs b/src/native/managed/cdac/tests/FrameIteratorTests.cs deleted file mode 100644 index 28ac5aab77a080..00000000000000 --- a/src/native/managed/cdac/tests/FrameIteratorTests.cs +++ /dev/null @@ -1,677 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Diagnostics.DataContractReader.Contracts; -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), - new(nameof(Data.InterpreterFrame.IsFaulting), DataType.uint8), - ], - 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), - new(nameof(Data.InterpMethodContextFrame.Ip), DataType.pointer), - new(nameof(Data.InterpMethodContextFrame.NextPtr), DataType.pointer), - new(nameof(Data.InterpMethodContextFrame.Stack), 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); - 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; - int totalFrameSize = (int)types[DataType.InterpreterFrame].Size!.Value; - 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.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); - - var target = builder.Build(); - - // InterpreterFrame has pMD=NULL in native, so GetMethodDescPtr returns Null - TargetPointer result = new FrameHelpers(target).GetMethodDescPtr(new TargetPointer(frameFrag.Address)); - - Assert.Equal(TargetPointer.Null, 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 = (int)types[DataType.InterpreterFrame].Size!.Value; - 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.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); - - var target = builder.Build(); - - TargetPointer result = new FrameHelpers(target).GetMethodDescPtr(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); - 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; - int totalFrameSize = (int)types[DataType.InterpreterFrame].Size!.Value; - 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.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); - - var target = builder.Build(); - - TargetPointer result = new FrameHelpers(target).GetMethodDescPtr(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); - 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; - int totalFrameSize = (int)types[DataType.InterpreterFrame].Size!.Value; - 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.AddGlobals(("InterpreterFrameIdentifier", interpreterFrameIdentifierValue)); - - var target = builder.Build(); - - TargetPointer result = new FrameHelpers(target).GetMethodDescPtr(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); - 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; - } - - // 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); - - var target = builder.Build(); - - // Resolve each context frame individually — verifies the chain links resolve to distinct MethodDescs - Assert.Equal(new TargetPointer(methodDescC), new FrameHelpers(target).ResolveMethodDescFromInterpFrame(new TargetPointer(contextFrameC.Address))); - Assert.Equal(new TargetPointer(methodDescB), new FrameHelpers(target).ResolveMethodDescFromInterpFrame(new TargetPointer(contextFrameB.Address))); - Assert.Equal(new TargetPointer(methodDescA), new FrameHelpers(target).ResolveMethodDescFromInterpFrame(new TargetPointer(contextFrameA.Address))); - - // Verify null terminates correctly - Assert.Equal(TargetPointer.Null, new FrameHelpers(target).ResolveMethodDescFromInterpFrame(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 = new FrameHelpers(target).GetFrameName(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 = new FrameHelpers(target).GetFrameName(new TargetPointer(0x9999_9999)); - - 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 = (int)types[DataType.InterpreterFrame].Size!.Value; - - // 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(); - - // InterpreterFrame has pMD=NULL in native, so GetMethodDescPtr returns Null - TargetPointer result = new FrameHelpers(target).GetMethodDescPtr(new TargetPointer(frameFrag.Address)); - Assert.Equal(TargetPointer.Null, result); - - // WalkInterpreterFrameChain should yield only the active frame - var chain = new FrameHelpers(target).WalkInterpreterFrameChain(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 = (int)types[DataType.InterpreterFrame].Size!.Value; - - // 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(); - - // InterpreterFrame has pMD=NULL in native, so GetMethodDescPtr returns Null - TargetPointer result = new FrameHelpers(target).GetMethodDescPtr(new TargetPointer(frameFrag.Address)); - Assert.Equal(TargetPointer.Null, result); - - // WalkInterpreterFrameChain should yield upper then lower (top to bottom via ParentPtr) - var chain = new FrameHelpers(target).WalkInterpreterFrameChain(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 = (int)types[DataType.InterpreterFrame].Size!.Value; - - // 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 = new FrameHelpers(target).WalkInterpreterFrameChain(new TargetPointer(frameFrag.Address)).ToList(); - Assert.Single(chain); - Assert.Equal(new TargetPointer(activeFrame.Address), chain[0]); - } -} diff --git a/src/native/managed/cdac/tests/MethodDescTests.cs b/src/native/managed/cdac/tests/MethodDescTests.cs index a6f19e702081f9..3963750e6e26e6 100644 --- a/src/native/managed/cdac/tests/MethodDescTests.cs +++ b/src/native/managed/cdac/tests/MethodDescTests.cs @@ -707,7 +707,7 @@ public void Validation_NativeCodeSlot_PrecodeFallback(MockTarget.Architecture ar }, mockExecutionManager, mockPrecodeStubs); mockExecutionManager.Setup(em => em.GetCodeBlockHandle(nativeCode)).Returns((CodeBlockHandle?)null); - mockPrecodeStubs.Setup(ps => ps.GetMethodDescFromStubAddress(nativeCode)).Returns(methodDescAddress); + mockExecutionManager.Setup(em => em.NonVirtualEntry2MethodDesc(nativeCode)).Returns(methodDescAddress); MethodDescHandle handle = rts.GetMethodDescHandle(methodDescAddress); Assert.NotEqual(TargetPointer.Null, handle.Address); @@ -768,7 +768,7 @@ public void Validation_NativeCodeSlot_PrecodeFallback_WrongMethodDesc_Fails(Mock }, mockExecutionManager, mockPrecodeStubs); mockExecutionManager.Setup(em => em.GetCodeBlockHandle(nativeCode)).Returns((CodeBlockHandle?)null); - mockPrecodeStubs.Setup(ps => ps.GetMethodDescFromStubAddress(nativeCode)).Returns(wrongMethodDescAddress); + mockExecutionManager.Setup(em => em.NonVirtualEntry2MethodDesc(nativeCode)).Returns(wrongMethodDescAddress); Assert.Throws(() => rts.GetMethodDescHandle(methodDescAddress)); } From b307e0c9f8f7b182e16abf291eb2b305100e8736 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Thu, 7 May 2026 22:40:30 -0400 Subject: [PATCH 11/17] fix doubled frame issue --- .../StackWalk/FrameHandling/FrameHelpers.cs | 2 +- .../Contracts/StackWalk/StackWalk_1.cs | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs index 17002b97595b69..a336e1dc6b0151 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs @@ -414,7 +414,7 @@ public void SetContextToInterpMethodContextFrame( /// interpreted method in the call chain. If pParent is null, the interpreter /// chain under the current InterpreterFrame is exhausted, we apply the /// InterpreterFrame's transition-block state to restore the context to the - /// native caller of InterpExecMethod. + /// native caller of InterpExecMethod. /// public void InterpreterVirtualUnwind(IPlatformAgnosticContext context) { 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 8006109a545b27..8ea9fc5048bbfa 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 @@ -145,6 +145,19 @@ IEnumerable IStackWalk.CreateStackWalk(ThreadData threadD StackWalkState state = IsManaged(context.InstructionPointer, out _) ? StackWalkState.SW_FRAMELESS : StackWalkState.SW_FRAME; FrameIterator frameIterator = new(_target, threadData); + // Skip the head InterpreterFrame when entering with a context already + // inside an interpreter execution (e.g. a managed-debugger breakpoint + // synthesized callback context). Without this, SW_FRAME would later + // re-process it and re-walk the same InterpMethodContextFrame chain. + // Mirrors the native walker fix in dotnet/runtime#126953. + if (state == StackWalkState.SW_FRAMELESS + && IsInterpreterCode(context.InstructionPointer) + && frameIterator.IsValid() + && frameIterator.GetCurrentFrameType() == FrameType.InterpreterFrame) + { + frameIterator.Next(); + } + // if the next Frame is not valid and we are not in managed code, there is nothing to return if (state == StackWalkState.SW_FRAME && !frameIterator.IsValid()) { @@ -180,6 +193,16 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre StackWalkState state = IsManaged(context.InstructionPointer, out _) ? StackWalkState.SW_FRAMELESS : StackWalkState.SW_FRAME; FrameIterator frameIterator = new(_target, threadData); + // See CreateStackWalk: skip the head InterpreterFrame when entering + // already inside an interpreter execution to avoid double-walking. + if (state == StackWalkState.SW_FRAMELESS + && IsInterpreterCode(context.InstructionPointer) + && frameIterator.IsValid() + && frameIterator.GetCurrentFrameType() == FrameType.InterpreterFrame) + { + frameIterator.Next(); + } + if (state == StackWalkState.SW_FRAME && !frameIterator.IsValid()) return []; From 99175ae229a5cab18e1607188eac0f26e0b129d9 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Fri, 8 May 2026 18:29:39 -0400 Subject: [PATCH 12/17] Add ISOSDacInterface GetCodeHeaderData test for interpreter methods Adds ISOSDacInterfaceTests dump test class mirroring ISOSDacInterface13Tests, with a GetCodeHeaderData test that validates TYPE_INTERPRETER routing through the interpreter precode unwrap path. Also switches the InterpreterStack debuggee from a full dump to a heap dump (107 MB -> 16 MB) since the heap dump captures all memory needed for interpreter stack walking and code-header lookup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../InterpreterStack/InterpreterStack.csproj | 2 +- .../tests/DumpTests/ISOSDacInterfaceTests.cs | 62 +++++++++++++++++++ .../DumpTests/InterpreterStackDumpTests.cs | 2 +- 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 src/native/managed/cdac/tests/DumpTests/ISOSDacInterfaceTests.cs diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/InterpreterStack.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/InterpreterStack.csproj index e7dc497211fa3d..0520e83a759bd2 100644 --- a/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/InterpreterStack.csproj +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/InterpreterStack.csproj @@ -1,6 +1,6 @@ - Full + Heap Jit + Full Jit