Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/design/datacontracts/CodeVersions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}

Expand Down
2 changes: 1 addition & 1 deletion docs/design/datacontracts/StackWalk.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Data.InterpMethodContextFrame>(context.StackPointer);
return currentFrame.ParentPtr != TargetPointer.Null ? currentFrame.ParentPtr : GetFirstArgRegister(context);
}

private bool VirtualUnwindInterpreterCallFrame(IPlatformAgnosticContext context)
{
TargetPointer currentFramePtr = context.StackPointer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
47 changes: 47 additions & 0 deletions src/native/managed/cdac/tests/UnitTests/CodeVersionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ public static void AddCodeBlock(this Mock<IExecutionManager> 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<TargetCodePointer>()))
.Returns<TargetCodePointer>(entryPoint => entryPoint);
}

public static void AddModule(this Mock<ILoader> mock, CodeVersionsMockModule module)
Expand Down Expand Up @@ -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<IExecutionManager> mockExecutionManager = new Mock<IExecutionManager>();
Mock<IRuntimeTypeSystem> mockRuntimeTypeSystem = new Mock<IRuntimeTypeSystem>();
mockExecutionManager.AddCodeBlock(oneBlock);
mockExecutionManager.Setup(e => e.GetDiagnosticCodeStartFromEntryPoint(It.IsAny<TargetCodePointer>()))
.Returns<TargetCodePointer>(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)
Expand Down
62 changes: 62 additions & 0 deletions src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IExecutionManager> 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<ARM64Context> 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<IExecutionManager> 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<ARM64Context> context = new();
context.InstructionPointer = new TargetCodePointer(bytecodeIp);
context.StackPointer = new TargetPointer(frame);

StackWalk_1 stackWalk = new(target);
Assert.Equal(root, stackWalk.CallerStackPointer(context).Value);
}
}
Loading