diff --git a/src/coreclr/debug/daccess/dacdbiimplstackwalk.cpp b/src/coreclr/debug/daccess/dacdbiimplstackwalk.cpp index 4450a7df8384ef..d66ea84025684d 100644 --- a/src/coreclr/debug/daccess/dacdbiimplstackwalk.cpp +++ b/src/coreclr/debug/daccess/dacdbiimplstackwalk.cpp @@ -307,6 +307,18 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::UnwindStackWalkFrame(StackWalkHan continue; } +#ifdef FEATURE_INTERPRETER + // The interpreter cannot inline CallEntryPoint, so it + // appears as a visible frame. Skip it to match JIT behavior. + { + MethodDesc* pCallEntryPoint = CoreLibBinder::GetMethod(METHOD__ENVIRONMENT__CALL_ENTRY_POINT); + if (pMD == pCallEntryPoint) + { + continue; + } + } +#endif // FEATURE_INTERPRETER + fIsAtEndOfStack = FALSE; } else diff --git a/src/coreclr/debug/ee/controller.cpp b/src/coreclr/debug/ee/controller.cpp index 72b0360c53c3d4..3e663028ec6926 100644 --- a/src/coreclr/debug/ee/controller.cpp +++ b/src/coreclr/debug/ee/controller.cpp @@ -7878,6 +7878,39 @@ TP_RESULT DebuggerStepper::TriggerPatch(DebuggerControllerPatch *patch, { LOG((LF_CORDB, LL_INFO10000, "Step patch hit at 0x%x\n", offset)); +#ifdef FEATURE_INTERPRETER + // INTOP_CALL_FINALLY is internal EH machinery (catch→finally transition). + // If the stepper lands here, continue stepping into the finally body. + { + PCODE currentPC = GetControlPC(&(info.m_activeFrame.registers)); + EECodeInfo codeInfo(currentPC); + if (codeInfo.IsInterpretedCode()) + { + InterpreterWalker walker; + walker.Init((const int32_t*)currentPC, NULL); + + bool isCallFinally = walker.IsCallFinally(); + + // A sequence point may precede CALL_FINALLY; check next instruction too. + if (!isCallFinally && walker.GetSkipIP() != NULL) + { + InterpreterWalker nextWalker; + nextWalker.Init(walker.GetSkipIP(), NULL); + isCallFinally = nextWalker.IsCallFinally(); + } + + if (isCallFinally) + { + LOG((LF_CORDB, LL_INFO10000, "DS::TP: Skipping interpreter CALL_FINALLY at %p, continuing step\n", currentPC)); + if (!TrapStep(&info, m_stepIn)) + TrapStepNext(&info); + EnableUnwind(m_fp); + return TPR_IGNORE; + } + } + } +#endif // FEATURE_INTERPRETER + // For a JMC stepper, we have an additional constraint: // skip non-user code. So if we're still in non-user code, then // we've got to keep going diff --git a/src/coreclr/debug/ee/debugger.cpp b/src/coreclr/debug/ee/debugger.cpp index ffd41afb34570a..c54472ec399bda 100644 --- a/src/coreclr/debug/ee/debugger.cpp +++ b/src/coreclr/debug/ee/debugger.cpp @@ -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 bpInfoSegmentWriterHolder(bpInfoSegmentRX, sizeof(DebuggerEvalBreakpointInfoSegment)); - DebuggerEvalBreakpointInfoSegment *bpInfoSegmentRW = bpInfoSegmentWriterHolder.GetRW(); + ExecutableWriterHolder 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; @@ -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(); @@ -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 @@ -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 + /****************************************************************************** * ******************************************************************************/ @@ -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 @@ -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 { @@ -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; } @@ -16114,7 +16206,7 @@ TADDR FuncEvalFrame::GetReturnAddressPtr_Impl() { LIMITED_METHOD_DAC_CONTRACT; - if (GetDebuggerEval()->m_evalDuringException) + if (!GetDebuggerEval()->m_evalUsesHijack) { return (TADDR)NULL; } @@ -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; } diff --git a/src/coreclr/debug/ee/debugger.h b/src/coreclr/debug/ee/debugger.h index f1ed7e1ea70960..8512a2329f458a 100644 --- a/src/coreclr/debug/ee/debugger.h +++ b/src/coreclr/debug/ee/debugger.h @@ -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 @@ -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; @@ -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; } diff --git a/src/coreclr/debug/ee/frameinfo.cpp b/src/coreclr/debug/ee/frameinfo.cpp index 874e61b097c31b..8c7f38f6c9e07b 100644 --- a/src/coreclr/debug/ee/frameinfo.cpp +++ b/src/coreclr/debug/ee/frameinfo.cpp @@ -1411,6 +1411,20 @@ StackWalkAction DebuggerWalkStackProc(CrawlFrame *pCF, void *data) } } // if (d->needParentInfo) +#ifdef FEATURE_INTERPRETER + // Skip CallEntryPoint — the interpreter cannot inline it, but it has + // [StackTraceHidden] so it should not appear in debugger stack walks. + { + MethodDesc *md = pCF->GetFunction(); + extern MethodDesc* g_pEnvironmentCallEntryPointMethodDesc; + if (md != NULL && md == g_pEnvironmentCallEntryPointMethodDesc) + { + LOG((LF_CORDB, LL_INFO1000, "DWSP: Skipping CallEntryPoint frame in interpreter mode\n")); + return SWA_CONTINUE; + } + } +#endif // FEATURE_INTERPRETER + // The tricky part here is that we want to skip all frames between a funclet method frame // and the parent method frame UNLESS the funclet is a filter. We only have to check for fpParent // here (instead of checking d->info.fIsFunclet and d->info.fIsFilter as well, as in the beginning of diff --git a/src/coreclr/debug/ee/funceval.cpp b/src/coreclr/debug/ee/funceval.cpp index f251de3016ce34..d40c71c6d4daff 100644 --- a/src/coreclr/debug/ee/funceval.cpp +++ b/src/coreclr/debug/ee/funceval.cpp @@ -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. @@ -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); } @@ -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()); } @@ -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 diff --git a/src/coreclr/debug/ee/interpreterwalker.cpp b/src/coreclr/debug/ee/interpreterwalker.cpp index c9c278aee60540..59fdfbf39ac390 100644 --- a/src/coreclr/debug/ee/interpreterwalker.cpp +++ b/src/coreclr/debug/ee/interpreterwalker.cpp @@ -203,4 +203,9 @@ const int32_t* InterpreterWalker::GetSwitchTarget(int32_t caseIndex) const return m_ip + offset; } +bool InterpreterWalker::IsCallFinally() const +{ + return m_opcode == INTOP_CALL_FINALLY; +} + #endif // FEATURE_INTERPRETER diff --git a/src/coreclr/debug/ee/interpreterwalker.h b/src/coreclr/debug/ee/interpreterwalker.h index eb78c34fd7f4ed..896c2de495ab0c 100644 --- a/src/coreclr/debug/ee/interpreterwalker.h +++ b/src/coreclr/debug/ee/interpreterwalker.h @@ -58,6 +58,9 @@ class InterpreterWalker // Decode the instruction at the current IP void Decode(); + // Check if the current instruction is INTOP_CALL_FINALLY + bool IsCallFinally() const; + private: // Resolve opcode at address, handling breakpoint patches int32_t ResolveOpcode(const int32_t* ip) const; diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 427b7c8341a775..0c1353e397b97d 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -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 diff --git a/src/coreclr/vm/dbginterface.h b/src/coreclr/vm/dbginterface.h index 75c3e387175c89..47e40138437dbd 100644 --- a/src/coreclr/vm/dbginterface.h +++ b/src/coreclr/vm/dbginterface.h @@ -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 }; diff --git a/src/coreclr/vm/interpexec.cpp b/src/coreclr/vm/interpexec.cpp index b9ca224add2365..711fa03d500ffa 100644 --- a/src/coreclr/vm/interpexec.cpp +++ b/src/coreclr/vm/interpexec.cpp @@ -670,7 +670,7 @@ static void InterpHalt() #endif // DEBUG #ifdef DEBUGGING_SUPPORTED -static void InterpBreakpoint(const int32_t *ip, const InterpMethodContextFrame *pFrame, const int8_t *stack, InterpreterFrame *pInterpreterFrame) +static const int32_t* InterpBreakpoint(const int32_t *ip, const InterpMethodContextFrame *pFrame, const int8_t *stack, InterpreterFrame *pInterpreterFrame) { Thread *pThread = GetThread(); if (pThread != NULL && g_pDebugInterface != NULL) @@ -702,7 +702,50 @@ static void InterpBreakpoint(const int32_t *ip, const InterpMethodContextFrame * &ctx, STATUS_BREAKPOINT, pThread); + + // Execute pending func evals set by the debugger's FuncEvalSetup, if any. + // DispatchNativeException clears the filter context before returning. + // Re-set it as filter context so FuncEvalHijackWorker can pass the managed-code / GC-safe-point checks. + InterpThreadContext *pThreadContext = pThread->GetInterpThreadContext(); + + // Save and restore bypass state around func eval execution. + const int32_t *savedBypassAddress = pThreadContext->m_bypassAddress; + int32_t savedBypassOpcode = pThreadContext->m_bypassOpcode; + if (savedBypassAddress != NULL) + pThreadContext->ClearBypass(); + + pThread->SetFilterContext(&ctx); + EX_TRY + { + g_pDebugInterface->ExecutePendingInterpreterFuncEval(pThread); + } + EX_CATCH + { + pThread->SetFilterContext(NULL); + + if (savedBypassAddress != NULL && pThreadContext->m_bypassAddress == NULL) + { + pThreadContext->m_bypassAddress = savedBypassAddress; + pThreadContext->m_bypassOpcode = savedBypassOpcode; + } + + EX_RETHROW; + } + EX_END_CATCH + pThread->SetFilterContext(NULL); + + if (savedBypassAddress != NULL && pThreadContext->m_bypassAddress == NULL) + { + pThreadContext->m_bypassAddress = savedBypassAddress; + pThreadContext->m_bypassOpcode = savedBypassOpcode; + } + + // The debugger may have modified the IP via SetIP (e.g. the setip command). + // Return the potentially updated IP so the interpreter can resume from the + // new position. + return (const int32_t*)(TADDR)GetIP(&ctx); } + return ip; } #endif // DEBUGGING_SUPPORTED @@ -1280,12 +1323,33 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr #ifdef DEBUGGING_SUPPORTED case INTOP_BREAKPOINT: { + int32_t bypassOpcode = 0; + + // Check bypass before notifying the debugger to avoid redundant callbacks. + if (pThreadContext->HasBypass(ip, &bypassOpcode)) + { + LOG((LF_CORDB, LL_INFO10000, "InterpExecMethod: Pre-callback bypass at IP %p with opcode 0x%x\n", ip, bypassOpcode)); + pThreadContext->ClearBypass(); + opcode = bypassOpcode; + goto SWITCH_OPCODE; + } + LOG((LF_CORDB, LL_INFO10000, "InterpExecMethod: Hit breakpoint at IP %p\n", ip)); - InterpBreakpoint(ip, pFrame, stack, pInterpreterFrame); + const int32_t *newIp = InterpBreakpoint(ip, pFrame, stack, pInterpreterFrame); + + // The debugger may have changed the IP via setip. If so, update + // the interpreter's bytecode IP and resume from the new location. + if (newIp != ip) + { + LOG((LF_CORDB, LL_INFO10000, "InterpExecMethod: SetIP changed IP from %p to %p\n", ip, newIp)); + ip = newIp; + pFrame->ip = ip; + opcode = *ip; + goto SWITCH_OPCODE; + } - int32_t bypassOpcode = 0; - // After debugger callback, check if bypass was set on the thread context + // Post-callback bypass check. if (pThreadContext->HasBypass(ip, &bypassOpcode)) { LOG((LF_CORDB, LL_INFO10000, "InterpExecMethod: Post-callback bypass at IP %p with opcode 0x%x\n", ip, bypassOpcode)); 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 f27cd11ecc36b9..3d56a63532613e 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 @@ -59,8 +59,8 @@ public virtual void HandleFuncEvalFrame(FuncEvalFrame funcEvalFrame) { Data.DebuggerEval debuggerEval = _target.ProcessedData.GetOrAdd(funcEvalFrame.DebuggerEvalPtr); - // No context to update if we're doing a func eval from within exception processing. - if (debuggerEval.EvalDuringException) + // No context to update if the eval doesn't use a hijack (exception or interpreter path). + if (!debuggerEval.EvalUsesHijack) { return; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/X86FrameHandler.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/X86FrameHandler.cs index af2d60cd31c156..8b06193322f99f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/X86FrameHandler.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/X86FrameHandler.cs @@ -50,8 +50,8 @@ public override void HandleFuncEvalFrame(FuncEvalFrame funcEvalFrame) { Data.DebuggerEval debuggerEval = _target.ProcessedData.GetOrAdd(funcEvalFrame.DebuggerEvalPtr); - // No context to update if we're doing a func eval from within exception processing. - if (debuggerEval.EvalDuringException) + // No context to update if the eval doesn't use a hijack (exception or interpreter path). + if (!debuggerEval.EvalUsesHijack) { return; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DebuggerEval.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DebuggerEval.cs index e162ce98ae3771..a68afc4f53d3a0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DebuggerEval.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DebuggerEval.cs @@ -12,11 +12,11 @@ public DebuggerEval(Target target, TargetPointer address) { Target.TypeInfo type = target.GetTypeInfo(DataType.DebuggerEval); TargetContext = address + (ulong)type.Fields[nameof(TargetContext)].Offset; - EvalDuringException = target.ReadField(address, type, nameof(EvalDuringException)) != 0; + EvalUsesHijack = target.ReadField(address, type, nameof(EvalUsesHijack)) != 0; Address = address; } public TargetPointer Address { get; } public TargetPointer TargetContext { get; } - public bool EvalDuringException { get; } + public bool EvalUsesHijack { get; } }