diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index dd178a9f572dff..da753bb8b08a43 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; } ``` @@ -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. @@ -511,11 +526,11 @@ 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. -`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..817e09f1e17362 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..19ab9a856c5cab 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -84,6 +84,13 @@ 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` | +| `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 | @@ -133,6 +140,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/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 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..d6dc44e735ac72 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -852,6 +852,43 @@ 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_FIELD(InterpMethodContextFrame, T_POINTER, Stack, offsetof(InterpMethodContextFrame, pStack)) +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 +1022,14 @@ 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_FIELD(InterpreterFrame, T_BOOL, IsFaulting, cdac_data::IsFaulting) +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..d2da804694077c 100644 --- a/src/coreclr/vm/frames.h +++ b/src/coreclr/vm/frames.h @@ -2332,6 +2332,15 @@ 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); + static constexpr size_t IsFaulting = offsetof(InterpreterFrame, m_isFaulting); }; #endif // FEATURE_INTERPRETER 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.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..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 @@ -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; } @@ -308,7 +318,12 @@ TargetPointer IExecutionManager.NonVirtualEntry2MethodDesc(TargetCodePointer ent bool IExecutionManager.IsFunclet(CodeBlockHandle codeInfoHandle) { - return ((IExecutionManager)this).GetStartAddress(codeInfoHandle) != + // Interpreter code has no native unwind info and therefore no funclets. + TargetCodePointer startAddress = ((IExecutionManager)this).GetStartAddress(codeInfoHandle); + if (((IExecutionManager)this).GetCodeKind(startAddress) == CodeKind.Interpreter) + return false; + + return startAddress != ((IExecutionManager)this).GetFuncletStartAddress(codeInfoHandle); } 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..3176e352d61065 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 (VirtualReadException) + { + return entryPoint; + } + } } 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/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/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/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); + } } 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..a336e1dc6b0151 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs @@ -0,0 +1,496 @@ +// 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(); + } + + public 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: + { + // 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); + SetContextToInterpMethodContextFrame(context, interpreterFrame); + 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; + } + } + + 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"), + }; + } + + private 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 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. + /// + public 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. + /// + public 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; + } + } + + // 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; + } + + /// + /// 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 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); + + 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 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(); + 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/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..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 @@ -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 @@ -50,12 +52,6 @@ private record StackDataFrameHandle( bool IsActiveFrame = false) : 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 +59,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 +75,7 @@ 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; } public bool IsCurrentFrameResumable() { @@ -95,9 +90,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; } /// @@ -106,7 +101,7 @@ or FrameIterator.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() { @@ -116,7 +111,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 @@ -136,6 +131,12 @@ public StackDataFrameHandle ToDataFrame() } } + private enum ContextFlags + { + Full = 0x1, + All = 0x2, + } + IEnumerable IStackWalk.CreateStackWalk(ThreadData threadData) { IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); @@ -144,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()) { @@ -179,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 []; @@ -655,21 +679,31 @@ 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; + // Native stackwalk.cpp: isInterrupted = false; hasFaulted = false; handle.IsInterrupted = false; - try + // 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. + if (IsInterpreterCode(handle.Context.InstructionPointer)) { - handle.Context.Unwind(_target); + _frameHelpers.InterpreterVirtualUnwind(handle.Context); } - catch + else { - handle.State = StackWalkState.SW_ERROR; - throw; + try + { + handle.Context.Unwind(_target); + } + catch + { + handle.State = StackWalkState.SW_ERROR; + throw; + } } break; case StackWalkState.SW_SKIPPED_FRAME: @@ -683,18 +717,22 @@ private bool Next(StackWalkData handle) // pInlinedFrame is set only for active InlinedCallFrames. { var frameType = handle.FrameIter.GetCurrentFrameType(); - - TargetPointer returnAddress = handle.FrameIter.GetReturnAddress(); - bool isActiveICF = frameType == FrameIterator.FrameType.InlinedCallFrame + 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) { @@ -730,8 +768,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,10 +833,10 @@ 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) { @@ -816,9 +854,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 +924,22 @@ 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; + } + + #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..38d96cc79263a1 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpMethodContextFrame.cs @@ -0,0 +1,26 @@ +// 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)); + 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 new file mode 100644 index 00000000000000..fa06f8cead33a5 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterpreterFrame.cs @@ -0,0 +1,22 @@ +// 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) + { + 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/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..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 @@ -208,18 +208,24 @@ 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). + // See DacValidateMD for more details. + TargetPointer methodDesc = executionManager.NonVirtualEntry2MethodDesc(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..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) { @@ -287,7 +288,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..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,10 +813,12 @@ 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, + Contracts.CodeKind.Interpreter => JitTypes.TYPE_INTERPRETER, _ => JitTypes.TYPE_UNKNOWN, }; @@ -825,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); @@ -2204,6 +2212,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 +2315,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 +2527,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 +5325,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/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..0e2c6354cd50d0 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/InterpreterStack/InterpreterStack.csproj @@ -0,0 +1,22 @@ + + + + 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/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..cac9cc85219315 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=$([MSBuild]::Escape('%(_DebuggeeWithDumpTypes.EnvironmentVariables)'))" /> @@ -212,7 +216,7 @@ + Properties="DebuggeeName=$(DebuggeeName);_DumpTypeName=%(_DumpTypeItem.Identity);_DebuggeeR2RModes=$(_DebuggeeR2RModes);_DebuggeeEnvVars=$([MSBuild]::Escape('$(_DebuggeeEnvVars)'))" /> @@ -232,7 +236,7 @@ + Properties="DebuggeeName=$(DebuggeeName);_MiniDumpType=$(_MiniDumpType);_DumpTypeDirName=$(_DumpTypeDirName);_DumpTypeName=$(_DumpTypeName);_R2RModeName=%(_R2RModeItem.Identity);_DebuggeeEnvVars=$([MSBuild]::Escape('$(_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=$([MSBuild]::Escape('$(_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);$([MSBuild]::Unescape('$(_DebuggeeEnvVars)'))" /> @@ -316,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/DumpTests/ISOSDacInterfaceTests.cs b/src/native/managed/cdac/tests/DumpTests/ISOSDacInterfaceTests.cs new file mode 100644 index 00000000000000..06d09ee6f991e5 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/ISOSDacInterfaceTests.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using Xunit; +using static Microsoft.Diagnostics.DataContractReader.Tests.TestHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Dump-based integration tests for ISOSDacInterface APIs. +/// Uses the InterpreterStack debuggee dump to validate interpreter-specific behavior. +/// +public class ISOSDacInterfaceTests : DumpTestBase +{ + protected override string DebuggeeName => "InterpreterStack"; + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public unsafe void GetCodeHeaderData_InterpreterMethod_ReturnsTypeInterpreter(TestConfiguration config) + { + InitializeDumpTest(config, "InterpreterStack", "full"); + + try + { + Target.GetTypeInfo(DataType.InterpreterFrame); + } + catch (System.InvalidOperationException) + { + throw new Microsoft.DotNet.XUnitExtensions.SkipTestException("Interpreter support not available in this runtime build (FEATURE_INTERPRETER not enabled)."); + } + + IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; + IPrecodeStubs precodeStubs = Target.Contracts.PrecodeStubs; + ISOSDacInterface sosDac = new SOSDacImpl(Target, legacyObj: null); + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + DumpTestStackWalker walker = DumpTestStackWalker.Walk(Target, crashingThread); + + ResolvedFrame interpFrame = walker.Frames + .First(f => f.Name is "MethodA" or "MethodB" or "MethodC" or "MethodD"); + + MethodDescHandle mdHandle = rts.GetMethodDescHandle(interpFrame.MethodDescPtr); + TargetCodePointer precodeAddr = rts.GetNativeCode(mdHandle); + Assert.NotEqual(TargetCodePointer.Null, precodeAddr); + + TargetCodePointer interpreterCodeAddr = precodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(precodeAddr); + Assert.NotEqual(precodeAddr, interpreterCodeAddr); + Assert.NotEqual(TargetCodePointer.Null, interpreterCodeAddr); + + DacpCodeHeaderData codeHeaderData; + int hr = sosDac.GetCodeHeaderData(interpreterCodeAddr.ToClrDataAddress(Target), &codeHeaderData); + AssertHResult(System.HResults.S_OK, hr); + + Assert.Equal(JitTypes.TYPE_INTERPRETER, codeHeaderData.JITType); + Assert.Equal(interpFrame.MethodDescPtr.ToClrDataAddress(Target), codeHeaderData.MethodDescPtr); + Assert.True(codeHeaderData.MethodSize > 0, + $"Expected non-zero MethodSize for interpreter code, got {codeHeaderData.MethodSize}"); + } +} 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..46fafb36ef5701 --- /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(CodeKind.Interpreter, executionManager.GetCodeKind(resolvedCode)); + } + + 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(CodeKind.Jitted, executionManager.GetCodeKind(nativeCode)); + } + + [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" /> diff --git a/src/native/managed/cdac/tests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdac/tests/ExecutionManager/ExecutionManagerTests.cs index 482437a0f83789..ed2796c34f66cb 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,73 @@ 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; + const ulong jitManagerAddress = 0x000b_ff00; + const int stubCodeBlockKindPrecode = 4; // STUB_CODE_BLOCK_STUBPRECODE + + IExecutionManager em = CreateExecutionManagerContract( + version, + arch, + emBuilder => + { + var precodeRange = emBuilder.AllocateJittedCodeRange(precodeRangeStart, precodeRangeSize); + MockRangeSection precodeRangeSection = emBuilder.AddRangeListRangeSection(precodeRange, jitManagerAddress, stubCodeBlockKindPrecode); + _ = 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/MethodDescTests.cs b/src/native/managed/cdac/tests/MethodDescTests.cs index 3de65ac9b148e6..3963750e6e26e6 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); + mockExecutionManager.Setup(em => em.NonVirtualEntry2MethodDesc(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); + mockExecutionManager.Setup(em => em.NonVirtualEntry2MethodDesc(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..e27fce343d92f6 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"); @@ -771,6 +825,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..4f22833ec8394b 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,38 @@ 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), + ]); + 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 +343,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 +382,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 +418,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 +439,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 +524,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);