Skip to content
Draft
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
189 changes: 141 additions & 48 deletions src/coreclr/debug/ee/debugger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1322,22 +1322,37 @@ DebuggerEval::DebuggerEval(CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEval
{
WRAPPER_NO_CONTRACT;

// bpInfoSegmentRX is NULL only for interpreter func evals — the interpreter signals completion
// directly via FuncEvalComplete, not the native breakpoint trap mechanism.
#ifdef FEATURE_INTERPRETER
_ASSERTE(bpInfoSegmentRX != NULL || (pContext != NULL && EECodeInfo((PCODE)GetIP(pContext)).IsInterpretedCode()));
#else
_ASSERTE(bpInfoSegmentRX != NULL);
#endif
if (bpInfoSegmentRX != NULL)
{
#if !defined(DBI_COMPILE) && !defined(DACCESS_COMPILE) && defined(HOST_OSX) && defined(HOST_ARM64)
ExecutableWriterHolder<DebuggerEvalBreakpointInfoSegment> bpInfoSegmentWriterHolder(bpInfoSegmentRX, sizeof(DebuggerEvalBreakpointInfoSegment));
DebuggerEvalBreakpointInfoSegment *bpInfoSegmentRW = bpInfoSegmentWriterHolder.GetRW();
ExecutableWriterHolder<DebuggerEvalBreakpointInfoSegment> bpInfoSegmentWriterHolder(bpInfoSegmentRX, sizeof(DebuggerEvalBreakpointInfoSegment));
DebuggerEvalBreakpointInfoSegment *bpInfoSegmentRW = bpInfoSegmentWriterHolder.GetRW();
#else // !DBI_COMPILE && !DACCESS_COMPILE && HOST_OSX && HOST_ARM64
DebuggerEvalBreakpointInfoSegment *bpInfoSegmentRW = bpInfoSegmentRX;
DebuggerEvalBreakpointInfoSegment *bpInfoSegmentRW = bpInfoSegmentRX;
#endif // !DBI_COMPILE && !DACCESS_COMPILE && HOST_OSX && HOST_ARM64
new (bpInfoSegmentRW) DebuggerEvalBreakpointInfoSegment(this);
m_bpInfoSegment = bpInfoSegmentRX;
new (bpInfoSegmentRW) DebuggerEvalBreakpointInfoSegment(this);
m_bpInfoSegment = bpInfoSegmentRX;

// This must be non-zero so that the saved opcode is non-zero, and on IA64 we want it to be 0x16
// so that we can have a breakpoint instruction in any slot in the bundle.
bpInfoSegmentRW->m_breakpointInstruction[0] = 0x16;
// This must be non-zero so that the saved opcode is non-zero, and on IA64 we want it to be 0x16
// so that we can have a breakpoint instruction in any slot in the bundle.
bpInfoSegmentRW->m_breakpointInstruction[0] = 0x16;
#if defined(TARGET_ARM)
USHORT *bp = (USHORT*)&m_bpInfoSegment->m_breakpointInstruction;
*bp = CORDbg_BREAK_INSTRUCTION;
USHORT *bp = (USHORT*)&m_bpInfoSegment->m_breakpointInstruction;
*bp = CORDbg_BREAK_INSTRUCTION;
#endif // TARGET_ARM
}
else
{
m_bpInfoSegment = NULL;
}

m_thread = pEvalInfo->vmThreadToken.GetRawPtr();
m_evalType = pEvalInfo->funcEvalType;
m_methodToken = pEvalInfo->funcMetadataToken;
Expand All @@ -1363,7 +1378,7 @@ DebuggerEval::DebuggerEval(CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEval
m_aborting = FE_ABORT_NONE;
m_aborted = false;
m_completed = false;
m_evalDuringException = fInException;
m_evalUsesHijack = !fInException;
m_retValueBoxing = Debugger::NoValueTypeBoxing;
m_vmObjectHandle = VMPTR_OBJECTHANDLE::NullPtr();

Expand Down Expand Up @@ -7690,7 +7705,7 @@ void Debugger::ProcessAnyPendingEvals(Thread *pThread)
{
DebuggerEval *pDE = pfe->pDE;

_ASSERTE(pDE->m_evalDuringException);
_ASSERTE(!pDE->m_evalUsesHijack);
_ASSERTE(pDE->m_thread == GetThreadNULLOk());

// Remove the pending eval from the hash. This ensures that if we take a first chance exception during the eval
Expand Down Expand Up @@ -9849,6 +9864,27 @@ void Debugger::UnloadClass(mdTypeDef classMetadataToken,

}

#ifdef FEATURE_INTERPRETER
/******************************************************************************
* Execute pending func evals on the interpreter thread. Called from the
* interpreter's INTOP_BREAKPOINT handler after the debugger callback returns.
* Routes through ProcessAnyPendingEvals to share the dispatch logic with the
* exception-time func-eval path.
******************************************************************************/
void Debugger::ExecutePendingInterpreterFuncEval(Thread* pThread)
{
CONTRACTL
{
THROWS;
GC_TRIGGERS;
MODE_COOPERATIVE;
}
CONTRACTL_END;

ProcessAnyPendingEvals(pThread);
}
#endif // FEATURE_INTERPRETER

/******************************************************************************
*
******************************************************************************/
Expand Down Expand Up @@ -14335,24 +14371,54 @@ HRESULT Debugger::FuncEvalSetup(DebuggerIPCE_FuncEvalInfo *pEvalInfo,
return CORDBG_E_ILLEGAL_AT_GC_UNSAFE_POINT;
}

if (filterContext != NULL && ::GetSP(filterContext) != ALIGN_DOWN(::GetSP(filterContext), STACK_ALIGN_SIZE))
#ifdef FEATURE_INTERPRETER
// For interpreter threads, the filter context contains synthetic values (IP = bytecode address,
// SP = InterpMethodContextFrame*, FP = stack pointer) — not real native register values.
// Skip the SP alignment check since it only applies to native stack pointers.
bool fIsInterpreterThread = false;
if (filterContext != NULL)
{
// SP is not aligned, we cannot do a FuncEval here
LOG((LF_CORDB, LL_INFO1000, "D::FES SP is unaligned"));
EECodeInfo codeInfo((PCODE)GetIP(filterContext));
fIsInterpreterThread = codeInfo.IsInterpretedCode();
}
else if (!fInException && pThread->GetInterpThreadContext() != NULL)
{
// The thread is an interpreter thread but not at a breakpoint (no filter context).
// Non-exception evals on interpreter threads require a breakpoint stop.
LOG((LF_CORDB, LL_INFO1000, "D::FES: Func eval requested on non-breakpoint interpreter thread\n"));
return CORDBG_E_FUNC_EVAL_BAD_START_POINT;
}

// Allocate the breakpoint instruction info for the debugger info in executable memory.
DebuggerHeap *pHeap = g_pDebugger->GetInteropSafeExecutableHeap_NoThrow();
if (pHeap == NULL)
if (!fIsInterpreterThread)
#endif // FEATURE_INTERPRETER
{
return E_OUTOFMEMORY;
if (filterContext != NULL && ::GetSP(filterContext) != ALIGN_DOWN(::GetSP(filterContext), STACK_ALIGN_SIZE))
{
// SP is not aligned, we cannot do a FuncEval here
LOG((LF_CORDB, LL_INFO1000, "D::FES SP is unaligned"));
return CORDBG_E_FUNC_EVAL_BAD_START_POINT;
}
}

DebuggerEvalBreakpointInfoSegment *bpInfoSegmentRX = (DebuggerEvalBreakpointInfoSegment*)pHeap->Alloc(sizeof(DebuggerEvalBreakpointInfoSegment));
if (bpInfoSegmentRX == NULL)
{
return E_OUTOFMEMORY;
// Allocate the breakpoint instruction info for the debugger info in executable memory.
// Interpreter func evals don't need this — completion is signaled directly via
// FuncEvalComplete, not a native breakpoint trap. Skip the allocation to avoid
// requiring executable memory on platforms where it's unavailable (e.g. iOS).
DebuggerEvalBreakpointInfoSegment *bpInfoSegmentRX = NULL;
#ifdef FEATURE_INTERPRETER
if (!fIsInterpreterThread)
#endif // FEATURE_INTERPRETER
{
DebuggerHeap *pHeap = g_pDebugger->GetInteropSafeExecutableHeap_NoThrow();
if (pHeap == NULL)
{
return E_OUTOFMEMORY;
}

bpInfoSegmentRX = (DebuggerEvalBreakpointInfoSegment*)pHeap->Alloc(sizeof(DebuggerEvalBreakpointInfoSegment));
if (bpInfoSegmentRX == NULL)
{
return E_OUTOFMEMORY;
}
}

// Create a DebuggerEval to hold info about this eval while its in progress. Constructor copies the thread's
Expand Down Expand Up @@ -14402,40 +14468,66 @@ HRESULT Debugger::FuncEvalSetup(DebuggerIPCE_FuncEvalInfo *pEvalInfo,
{
_ASSERTE(filterContext != NULL);

::SetIP(filterContext, (UINT_PTR)GetEEFuncEntryPoint(::FuncEvalHijack));
#ifdef FEATURE_INTERPRETER
// For interpreter threads, we cannot hijack the native CPU context because the interpreter
// manages execution through its own bytecode dispatch loop. Instead, we queue the DebuggerEval
// in the pending evals table. The INTOP_BREAKPOINT handler will call ProcessAnyPendingEvals
// after the debugger callback returns.
if (fIsInterpreterThread)
{
pDE->m_evalUsesHijack = false;

// Don't be fooled into thinking you can push things onto the thread's stack now. If the thread is stopped at a
// breakpoint or from a single step, then its really suspended in the SEH filter. ESP in the thread's CONTEXT,
// therefore, points into the middle of the thread's current stack. So we pass things we need in the hijack in
// the thread's registers.
HRESULT hr = CheckInitPendingFuncEvalTable();
if (FAILED(hr))
{
DeleteInteropSafeExecutable(pDE);
return hr;
}
GetPendingEvals()->AddPendingEval(pDE->m_thread, pDE);

LOG((LF_CORDB, LL_INFO1000, "D::FES: Interpreter func eval setup for pDE:%p on thread %p\n", pDE, pThread));

// No context modification needed — interpreter checks pending evals on resume.
// No IncThreadsAtUnsafePlaces — stack remains walkable (no context change).
}
else
#endif // FEATURE_INTERPRETER
{
::SetIP(filterContext, (UINT_PTR)GetEEFuncEntryPoint(::FuncEvalHijack));

// Don't be fooled into thinking you can push things onto the thread's stack now. If the thread is stopped at a
// breakpoint or from a single step, then its really suspended in the SEH filter. ESP in the thread's CONTEXT,
// therefore, points into the middle of the thread's current stack. So we pass things we need in the hijack in
// the thread's registers.

// Set the first argument to point to the DebuggerEval.
// Set the first argument to point to the DebuggerEval.
#if defined(TARGET_X86)
filterContext->Eax = (DWORD)pDE;
filterContext->Eax = (DWORD)pDE;
#elif defined(TARGET_AMD64)
#ifdef UNIX_AMD64_ABI
filterContext->Rdi = (SIZE_T)pDE;
filterContext->Rdi = (SIZE_T)pDE;
#else // UNIX_AMD64_ABI
filterContext->Rcx = (SIZE_T)pDE;
filterContext->Rcx = (SIZE_T)pDE;
#endif // !UNIX_AMD64_ABI
#elif defined(TARGET_ARM)
filterContext->R0 = (DWORD)pDE;
filterContext->R0 = (DWORD)pDE;
#elif defined(TARGET_ARM64)
filterContext->X0 = (SIZE_T)pDE;
filterContext->X0 = (SIZE_T)pDE;
#elif defined(TARGET_RISCV64)
filterContext->A0 = (SIZE_T)pDE;
filterContext->A0 = (SIZE_T)pDE;
#elif defined(TARGET_LOONGARCH64)
filterContext->A0 = (SIZE_T)pDE;
filterContext->A0 = (SIZE_T)pDE;
#else
PORTABILITY_ASSERT("Debugger::FuncEvalSetup is not implemented on this platform.");
PORTABILITY_ASSERT("Debugger::FuncEvalSetup is not implemented on this platform.");
#endif

//
// To prevent GCs until the func-eval gets a chance to run, we increment the counter here.
// We only need to do this if we have changed the filter CONTEXT, since the stack will be unwalkable
// in this case.
//
g_pDebugger->IncThreadsAtUnsafePlaces();
//
// To prevent GCs until the func-eval gets a chance to run, we increment the counter here.
// We only need to do this if we have changed the filter CONTEXT, since the stack will be unwalkable
// in this case.
//
g_pDebugger->IncThreadsAtUnsafePlaces();
}
}
else
{
Expand Down Expand Up @@ -16100,7 +16192,7 @@ unsigned FuncEvalFrame::GetFrameAttribs_Impl(void)
{
LIMITED_METHOD_DAC_CONTRACT;

if (GetDebuggerEval()->m_evalDuringException)
if (!GetDebuggerEval()->m_evalUsesHijack)
{
return FRAME_ATTR_NONE;
}
Expand All @@ -16114,7 +16206,7 @@ TADDR FuncEvalFrame::GetReturnAddressPtr_Impl()
{
LIMITED_METHOD_DAC_CONTRACT;

if (GetDebuggerEval()->m_evalDuringException)
if (!GetDebuggerEval()->m_evalUsesHijack)
{
return (TADDR)NULL;
}
Expand All @@ -16132,8 +16224,9 @@ void FuncEvalFrame::UpdateRegDisplay_Impl(const PREGDISPLAY pRD, bool updateFloa
SUPPORTS_DAC;
DebuggerEval * pDE = GetDebuggerEval();

// No context to update if we're doing a func eval from within exception processing.
if (pDE->m_evalDuringException)
// No context to update if we're doing a func eval from within exception processing
// or from interpreter code (both skip the hijack path).
if (!pDE->m_evalUsesHijack)
{
return;
}
Expand Down
11 changes: 10 additions & 1 deletion src/coreclr/debug/ee/debugger.h
Original file line number Diff line number Diff line change
Expand Up @@ -2575,6 +2575,11 @@ class Debugger : public DebugInterface
#ifndef DACCESS_COMPILE
void MulticastTraceNextStep(DELEGATEREF pbDel, INT32 count);
void ExternalMethodFixupNextStep(PCODE address);

#ifdef FEATURE_INTERPRETER
void ExecutePendingInterpreterFuncEval(Thread* pThread);
#endif // FEATURE_INTERPRETER

#endif

#ifdef DACCESS_COMPILE
Expand Down Expand Up @@ -3482,7 +3487,7 @@ class DebuggerEval
FUNC_EVAL_ABORT_TYPE m_aborting; // Has an abort been requested, and what type.
bool m_aborted; // Was this eval aborted
bool m_completed; // Is the eval complete - successfully or by aborting
bool m_evalDuringException;
bool m_evalUsesHijack;
VMPTR_OBJECTHANDLE m_vmObjectHandle;
TypeHandle m_ownerTypeHandle;
DebuggerEvalBreakpointInfoSegment* m_bpInfoSegment;
Expand All @@ -3491,6 +3496,10 @@ class DebuggerEval

bool Init()
{
// Interpreter func evals and exception-time evals don't use the breakpoint instruction segment, so skip the executability check.
if (!m_evalUsesHijack)
return true;

_ASSERTE(DbgIsExecutable(&m_bpInfoSegment->m_breakpointInstruction, sizeof(m_bpInfoSegment->m_breakpointInstruction)));
return true;
}
Expand Down
8 changes: 4 additions & 4 deletions src/coreclr/debug/ee/funceval.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3822,7 +3822,7 @@ void * STDCALL FuncEvalHijackWorker(DebuggerEval *pDE)
#endif
#endif

if (!pDE->m_evalDuringException)
if (pDE->m_evalUsesHijack)
{
//
// From this point forward we use FORBID regions to guard against GCs.
Expand All @@ -3842,7 +3842,7 @@ void * STDCALL FuncEvalHijackWorker(DebuggerEval *pDE)

if (filterContext)
{
_ASSERTE(pDE->m_evalDuringException);
_ASSERTE(!pDE->m_evalUsesHijack);
g_pEEInterface->SetThreadFilterContext(pDE->m_thread, NULL);
}

Expand Down Expand Up @@ -3901,7 +3901,7 @@ void * STDCALL FuncEvalHijackWorker(DebuggerEval *pDE)
// Codepitching can hijack our frame's return address. That means that we'll need to update PC in our saved context
// so that when its restored, its like we've returned to the codepitching hijack. At this point, the old value of
// EIP is worthless anyway.
if (!pDE->m_evalDuringException)
if (pDE->m_evalUsesHijack)
{
SetIP(&pDE->m_context, (SIZE_T)FEFrame.GetReturnAddress());
}
Expand All @@ -3913,7 +3913,7 @@ void * STDCALL FuncEvalHijackWorker(DebuggerEval *pDE)

void *dest = NULL;

if (!pDE->m_evalDuringException)
if (pDE->m_evalUsesHijack)
{
// Signal to the helper thread that we're done with our func eval. Start by creating a DebuggerFuncEvalComplete
// object. Give it an address at which to create the patch, which is a chunk of memory specified by our
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/vm/datadescriptor/datadescriptor.inc
Original file line number Diff line number Diff line change
Expand Up @@ -961,7 +961,7 @@ CDAC_TYPE_END(FuncEvalFrame)
CDAC_TYPE_BEGIN(DebuggerEval)
CDAC_TYPE_SIZE(sizeof(DebuggerEval))
CDAC_TYPE_FIELD(DebuggerEval, EXTERN_TYPE(Context), TargetContext, offsetof(DebuggerEval, m_context))
CDAC_TYPE_FIELD(DebuggerEval, T_BOOL, EvalDuringException, offsetof(DebuggerEval, m_evalDuringException))
CDAC_TYPE_FIELD(DebuggerEval, T_BOOL, EvalUsesHijack, offsetof(DebuggerEval, m_evalUsesHijack))
CDAC_TYPE_END(DebuggerEval)
#endif // DEBUGGING_SUPPORTED

Expand Down
7 changes: 7 additions & 0 deletions src/coreclr/vm/dbginterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,13 @@ class DebugInterface
virtual HRESULT IsMethodDeoptimized(Module *pModule, mdMethodDef methodDef, BOOL *pResult) = 0;
virtual void MulticastTraceNextStep(DELEGATEREF pbDel, INT32 count) = 0;
virtual void ExternalMethodFixupNextStep(PCODE address) = 0;

#ifdef FEATURE_INTERPRETER
// Execute the pending func eval on the interpreter thread context, if any.
// Called from the interpreter's INTOP_BREAKPOINT handler after the debugger callback returns.
virtual void ExecutePendingInterpreterFuncEval(Thread* pThread) = 0;
#endif // FEATURE_INTERPRETER

#endif //DACCESS_COMPILE
};

Expand Down
Loading
Loading