From c9772e2d2091cf86bc985e055328440bc6ecd446 Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Fri, 23 Oct 2026 07:27:44 -0400 Subject: [PATCH 1/2] [cDAC] Map entry point to code start when finding a native code version GetSpecificNativeCodeVersion compared the MethodDesc native code slot (an InterpreterPrecode for interpreted methods) with the code-block start (the bytecode), so it never matched. GetNativeCodeVersionForIP then returned an invalid handle, and GetNativeCodeSequencePointsAndVarInfo failed because DebugInfo's GetMethodVarInfo threw "No NativeCodeVersion found". Map the default and explicit versions' native code through GetDiagnosticCodeStartFromEntryPoint first, as the native DAC does with GetInterpreterCodeFromEntryPointIfPresent. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/CodeVersions.md | 8 +++- .../Contracts/CodeVersions_1.cs | 10 +++- .../cdac/tests/UnitTests/CodeVersionsTests.cs | 47 +++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/docs/design/datacontracts/CodeVersions.md b/docs/design/datacontracts/CodeVersions.md index d87d818f2473f6..d2512c6eb8cffe 100644 --- a/docs/design/datacontracts/CodeVersions.md +++ b/docs/design/datacontracts/CodeVersions.md @@ -294,7 +294,10 @@ NativeCodeVersionHandle ICodeVersions.GetNativeCodeVersionForIP(TargetCodePointe NativeCodeVersionHandle GetSpecificNativeCodeVersion(MethodDescHandle md, TargetCodePointer startAddress) { // "Initial" stage of NativeCodeVersionIterator::Next() with a null m_ilCodeFilter - TargetCodePointer firstNativeCode = rts.GetNativeCode(md); + // Interpreted methods store an InterpreterPrecode in the native code slot, while code-block start + // addresses are the bytecode start; compare against the diagnostic code start + // (native: GetInterpreterCodeFromEntryPointIfPresent(NativeCodeVersion::GetNativeCode())). + TargetCodePointer firstNativeCode = target.Contracts.ExecutionManager.GetDiagnosticCodeStartFromEntryPoint(rts.GetNativeCode(md)); if (firstNativeCode == startAddress) { NativeCodeVersionHandle first = NativeCodeVersionHandle.OfSynthetic(md.Address); @@ -303,7 +306,8 @@ NativeCodeVersionHandle GetSpecificNativeCodeVersion(MethodDescHandle md, Target return FindNativeCodeVersionNodes(rts, md, (codeVersion) => { - return codeVersion.MethodDesc == md.Address && codeVersion.NativeCode == startAddress; + return codeVersion.MethodDesc == md.Address + && target.Contracts.ExecutionManager.GetDiagnosticCodeStartFromEntryPoint(codeVersion.NativeCode) == startAddress; }).FirstOrDefault(NativeCodeVersionHandle.Invalid); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CodeVersions_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CodeVersions_1.cs index 811d6747a13f20..315d1be53e5c7e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CodeVersions_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CodeVersions_1.cs @@ -238,8 +238,14 @@ internal enum MethodDescVersioningStateFlags : byte private NativeCodeVersionHandle GetSpecificNativeCodeVersion(IRuntimeTypeSystem rts, MethodDescHandle md, TargetCodePointer startAddress) { + // Code-block start addresses are the code a method starts executing (for interpreted methods, the + // bytecode), while the MethodDesc native code slot / version node may hold an entry point (for + // interpreted methods, an InterpreterPrecode). Compare against the code start, as the native DAC does + // with GetInterpreterCodeFromEntryPointIfPresent(NativeCodeVersion::GetNativeCode()). + IExecutionManager eman = _target.Contracts.ExecutionManager; + // initial stage of NativeCodeVersionIterator::Next() with a null m_ilCodeFilter - TargetCodePointer firstNativeCode = rts.GetNativeCode(md); + TargetCodePointer firstNativeCode = eman.GetDiagnosticCodeStartFromEntryPoint(rts.GetNativeCode(md)); if (firstNativeCode == startAddress) { NativeCodeVersionHandle first = NativeCodeVersionHandle.CreateSynthetic(md.Address); @@ -249,7 +255,7 @@ private NativeCodeVersionHandle GetSpecificNativeCodeVersion(IRuntimeTypeSystem // CodeVersionManager::GetNativeCodeVersion(PTR_MethodDesc, PCODE startAddress) return FindNativeCodeVersionNodes(rts, md, (codeVersion) => { - return codeVersion.MethodDesc == md.Address && codeVersion.NativeCode == startAddress; + return codeVersion.MethodDesc == md.Address && eman.GetDiagnosticCodeStartFromEntryPoint(codeVersion.NativeCode) == startAddress; }).FirstOrDefault(NativeCodeVersionHandle.Invalid); } diff --git a/src/native/managed/cdac/tests/UnitTests/CodeVersionsTests.cs b/src/native/managed/cdac/tests/UnitTests/CodeVersionsTests.cs index f11b12370f8e53..7ef2b01e52de3b 100644 --- a/src/native/managed/cdac/tests/UnitTests/CodeVersionsTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/CodeVersionsTests.cs @@ -81,6 +81,9 @@ public static void AddCodeBlock(this Mock mock, MockCodeBlock .Returns(handle); mock.Setup(e => e.GetStartAddress(handle)).Returns(block.StartAddress.AsTargetPointer); mock.Setup(e => e.GetMethodDesc(handle)).Returns(block.MethodDesc.Address); + // Entry points that are not interpreter precodes / portable entry points map to themselves. + mock.Setup(e => e.GetDiagnosticCodeStartFromEntryPoint(It.IsAny())) + .Returns(entryPoint => entryPoint); } public static void AddModule(this Mock mock, CodeVersionsMockModule module) @@ -282,6 +285,50 @@ public void GetNativeCodeVersion_OneVersion_Versionable(MockTarget.Architecture } } + // Interpreted methods: the MethodDesc native code slot holds an InterpreterPrecode while the code block + // (and therefore the IP-derived start address) is the bytecode. The entry point must be mapped to its + // code start before comparing, as the native DAC does with GetInterpreterCodeFromEntryPointIfPresent. + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetNativeCodeVersion_Interpreted_MapsPrecodeToBytecode(MockTarget.Architecture arch) + { + var builder = new MockCodeVersions(arch); + MockMethodDescVersioningState methodDescVersioningState = builder.AddMethodDescVersioningState(); + methodDescVersioningState.NativeCodeVersionNode = 0; + methodDescVersioningState.Flags = MockMethodDescVersioningState.IsDefaultVersionActiveChildFlag; + TargetCodePointer bytecodeStart = new TargetCodePointer(0x0a0a_0000); + TargetCodePointer interpreterPrecode = new TargetCodePointer(0x0b0b_0000); + CodeVersionsMockMethodDesc oneMethod = CodeVersionsMockMethodDesc.CreateVersionable( + selfAddress: new TargetPointer(0x1a0a_0000), + methodDescVersioningState: new TargetPointer(methodDescVersioningState.Address), + nativeCode: interpreterPrecode); + MockCodeBlockStart oneBlock = new MockCodeBlockStart() + { + StartAddress = bytecodeStart, + Length = 0x40, + MethodDesc = oneMethod, + }; + + Mock mockExecutionManager = new Mock(); + Mock mockRuntimeTypeSystem = new Mock(); + mockExecutionManager.AddCodeBlock(oneBlock); + mockExecutionManager.Setup(e => e.GetDiagnosticCodeStartFromEntryPoint(It.IsAny())) + .Returns(entryPoint => entryPoint == interpreterPrecode ? bytecodeStart : entryPoint); + mockRuntimeTypeSystem.AddMethodDesc(oneMethod); + + var target = CreateTarget( + arch, + builder, + mockExecutionManager: mockExecutionManager, + mockRuntimeTypeSystem: mockRuntimeTypeSystem); + + var codeVersions = target.Contracts.CodeVersions; + NativeCodeVersionHandle handle = codeVersions.GetNativeCodeVersionForIP(bytecodeStart + 0x20); + Assert.True(handle.Valid); + Assert.Equal(oneMethod.Address, handle.MethodDescAddress); + Assert.Equal(TargetPointer.Null, handle.CodeVersionNodeAddress); + } + [Theory] [ClassData(typeof(MockTarget.StdArch))] public void GetActiveNativeCodeVersion_DefaultCase(MockTarget.Architecture arch) From f36e067bb7af4483c9d1ea65acf208badaf81d5f Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Fri, 23 Oct 2026 07:27:45 -0400 Subject: [PATCH 2/2] [cDAC] Compute caller SP for interpreted frames like the native code manager CallerStackPointer always ran the OS unwinder, which has no unwind info for bytecode, so on caller-SP architectures such as ARM64 fp/fpParentOrSelf came back as the frame's own SP. For interpreter code, use the parent InterpMethodContextFrame (including the zeroed root frame), or the owning InterpreterFrame when there is no parent, matching VirtualUnwindInterpreterCallFrame in eetwain.cpp. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/StackWalk.md | 2 +- .../StackWalk/FrameHandling/FrameHelpers.cs | 9 +++ .../Contracts/StackWalk/StackWalk_1.cs | 12 +++- .../cdac/tests/UnitTests/StackWalkTests.cs | 62 +++++++++++++++++++ 4 files changed, 82 insertions(+), 3 deletions(-) diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 19757b4d4227e2..3b57e59661f1a2 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -701,7 +701,7 @@ DebuggerEvalData GetDebuggerEvalData(TargetPointer funcEvalFrameAddress) TargetCodePointer GetInstructionPointer(IStackDataFrameHandle stackDataFrameHandle) ``` -`GetRuntimeFramePointer` returns the debugger frame pointer that uniquely identifies the current frame. On x64 it is the current stack pointer. On ARM, ARM64, RISCV64, and LoongArch64 it is the caller stack pointer. On x86, frameless managed methods use the unwound stack pointer minus the callee-popped argument size and one pointer, while runtime-unwindable native markers use the return-address slot from the recovered hijacked context. +`GetRuntimeFramePointer` returns the debugger frame pointer that uniquely identifies the current frame. On x64 it is the current stack pointer. On ARM, ARM64, RISCV64, and LoongArch64 it is the caller stack pointer; for interpreted frames the caller stack pointer is the parent `InterpMethodContextFrame` (including the zeroed root frame of an interpreter activation), or the owning `InterpreterFrame` when there is no parent, matching native `VirtualUnwindInterpreterCallFrame`. On x86, frameless managed methods use the unwound stack pointer minus the callee-popped argument size and one pointer, while runtime-unwindable native markers use the return-address slot from the recovered hijacked context. ```csharp TargetPointer GetRuntimeFramePointer(IStackDataFrameHandle stackDataFrameHandle) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs index 46cd63058e4aca..de30ac89ac9b42 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs @@ -644,6 +644,15 @@ public void InterpreterVirtualUnwind(IPlatformAgnosticContext context) } } + // The SP that native VirtualUnwindInterpreterCallFrame (eetwain.cpp) gives the caller context: the parent + // InterpMethodContextFrame whenever there is one -- including the zeroed root frame (ip == NULL) of an + // interpreter activation -- otherwise the owning InterpreterFrame (first-argument register). + public TargetPointer InterpreterCallerStackPointer(IPlatformAgnosticContext context) + { + Data.InterpMethodContextFrame currentFrame = _target.ProcessedData.GetOrAdd(context.StackPointer); + return currentFrame.ParentPtr != TargetPointer.Null ? currentFrame.ParentPtr : GetFirstArgRegister(context); + } + private bool VirtualUnwindInterpreterCallFrame(IPlatformAgnosticContext context) { TargetPointer currentFramePtr = context.StackPointer; 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 46f53022ce29a2..6b61f266d2bdd9 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 @@ -1134,9 +1134,17 @@ TargetPointer IStackWalk.GetFuncletRootId(IStackDataFrameHandle stackDataFrameHa return CallerStackPointer(handle); } - private TargetPointer CallerStackPointer(StackDataFrameHandle handle) + private TargetPointer CallerStackPointer(StackDataFrameHandle handle) => CallerStackPointer(handle.Context); + + // Mirrors the caller context the native code manager produces (GetRegdisplayStackMark on + // ARM64 et al.): interpreted frames are unwound by InterpreterCodeManager + // (VirtualUnwindInterpreterCallFrame), not by the OS unwinder, which has no unwind info for bytecode. + internal TargetPointer CallerStackPointer(IPlatformAgnosticContext context) { - IPlatformAgnosticContext callerContext = handle.Context.Clone(); + if (IsInterpreterCode(context.InstructionPointer)) + return _frameHelpers.InterpreterCallerStackPointer(context); + + IPlatformAgnosticContext callerContext = context.Clone(); callerContext.Unwind(_target); return callerContext.StackPointer; } diff --git a/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs b/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs index 27a431f1d7aa37..3b007b88e1973f 100644 --- a/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs @@ -2003,4 +2003,66 @@ public void InterpreterVirtualUnwind_WasmExhaustedChainNoOwningFrame_TerminatesG // Chain exhausted with a null owning frame: context SP is left unchanged, no throw. Assert.Equal(frame0, context.StackPointer.Value); } + + // GetRuntimeFramePointer on ARM64 is the caller SP. For an interpreted frame the caller must come from the + // interpreter virtual unwind (parent InterpMethodContextFrame), as native InterpreterCodeManager does; + // the OS unwinder has no unwind info for bytecode and would leave SP unchanged. + [Fact] + public void CallerStackPointer_InterpretedFrame_IsParentInterpMethodContextFrame() + { + MockTarget.Architecture arm64 = new() { IsLittleEndian = true, Is64Bit = true }; + const ulong bytecodeIp = 0x0005_1020, parentIp = 0x0005_2040, parentStack = 0x0006_2000; + + ulong frame = 0, parent = 0; + Mock executionManager = new(); + executionManager.Setup(e => e.GetCodeKind(new TargetCodePointer(bytecodeIp))).Returns(CodeKind.Interpreter); + TestPlaceholderTarget target = CreateTarget( + arm64, + threadBuilder => threadBuilder.AddThread(1, 1234), + frameBuilder => + { + parent = frameBuilder.AddInterpMethodContextFrame(parentPtr: 0, ip: parentIp, stack: parentStack).Address; + frame = frameBuilder.AddInterpMethodContextFrame(parentPtr: parent, ip: bytecodeIp, stack: 0x0006_1000).Address; + }, + executionManager: executionManager.Object); + + ContextHolder context = new(); + context.InstructionPointer = new TargetCodePointer(bytecodeIp); + context.StackPointer = new TargetPointer(frame); + + StackWalk_1 stackWalk = new(target); + Assert.Equal(parent, stackWalk.CallerStackPointer(context).Value); + Assert.Equal(frame, context.StackPointer.Value); + Assert.Equal(bytecodeIp, context.InstructionPointer.Value); + } + + // The first interpreted method of an interpreter activation has the zeroed root InterpMethodContextFrame + // (ip == NULL) as its parent. Native VirtualUnwindInterpreterCallFrame still sets the caller SP to that + // parent, so the debugger frame pointer is the root frame's address. + [Fact] + public void CallerStackPointer_InterpretedFrameWithRootParent_IsRootFrameAddress() + { + MockTarget.Architecture arm64 = new() { IsLittleEndian = true, Is64Bit = true }; + const ulong bytecodeIp = 0x0005_1020; + + ulong frame = 0, root = 0; + Mock executionManager = new(); + executionManager.Setup(e => e.GetCodeKind(new TargetCodePointer(bytecodeIp))).Returns(CodeKind.Interpreter); + TestPlaceholderTarget target = CreateTarget( + arm64, + threadBuilder => threadBuilder.AddThread(1, 1234), + frameBuilder => + { + root = frameBuilder.AddInterpMethodContextFrame(parentPtr: 0, ip: 0, stack: 0).Address; + frame = frameBuilder.AddInterpMethodContextFrame(parentPtr: root, ip: bytecodeIp, stack: 0x0006_1000).Address; + }, + executionManager: executionManager.Object); + + ContextHolder context = new(); + context.InstructionPointer = new TargetCodePointer(bytecodeIp); + context.StackPointer = new TargetPointer(frame); + + StackWalk_1 stackWalk = new(target); + Assert.Equal(root, stackWalk.CallerStackPointer(context).Value); + } }