diff --git a/src/coreclr/debug/ee/debugger.cpp b/src/coreclr/debug/ee/debugger.cpp index d48fc9e68ee83b..b0641ac5daa5bd 100644 --- a/src/coreclr/debug/ee/debugger.cpp +++ b/src/coreclr/debug/ee/debugger.cpp @@ -1213,31 +1213,39 @@ ULONG DebuggerMethodInfoTable::CheckDmiTable(void) // Arguments: // pContext - The context to return to when done with this eval. // pEvalInfo - Contains all the important information, such as parameters, type args, method. -// fInException - TRUE if the thread for the eval is currently in an exception notification. -// bpInfoSegmentRX - bpInfoSegmentRX is an InteropSafe allocation allocated by the caller. -// (Caller allocated as there is no way to fail the allocation without -// throwing, and this function is called in a NOTHROW region) +// bpInfoSegmentRX - Non-NULL only when the eval hijacks the native CPU context through +// FuncEvalHijack. NULL for non-hijack evals (exception-time or interpreter), +// which complete via the pending-eval queue instead of a native breakpoint +// trap. Caller-allocated because this function is NOTHROW. // -DebuggerEval::DebuggerEval(CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEvalInfo, bool fInException, DebuggerEvalBreakpointInfoSegment* bpInfoSegmentRX) +DebuggerEval::DebuggerEval(CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEvalInfo, DebuggerEvalBreakpointInfoSegment* bpInfoSegmentRX) { WRAPPER_NO_CONTRACT; + 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; @@ -1263,7 +1271,10 @@ DebuggerEval::DebuggerEval(CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEval m_aborting = FE_ABORT_NONE; m_aborted = false; m_completed = false; - m_evalDuringException = fInException; + // Hijacked evals redirect the native CPU context through FuncEvalHijack; non-hijack + // evals (exception-time and interpreter) complete via the pending-eval queue. The + // presence of the breakpoint info segment is the single source of truth. + m_evalUsesHijack = (bpInfoSegmentRX != NULL); m_retValueBoxing = Debugger::NoValueTypeBoxing; m_vmObjectHandle = VMPTR_OBJECTHANDLE::NullPtr(); @@ -7556,7 +7567,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 @@ -14198,29 +14209,57 @@ 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)) + // A func eval uses a CONTEXT hijack (redirects the native CPU context through FuncEvalHijack) + // only when the thread is stopped at a breakpoint or single-step in JIT-compiled code. For + // exception-time evals and interpreter evals we cannot hijack the native context — those paths + // queue the DebuggerEval into the pending-eval table and let the suspend-resume logic dispatch + // it: for exceptions via Debugger::ProcessAnyPendingEvals on continue, for the interpreter via + // INTOP_BREAKPOINT after the debugger callback returns. + bool funcEvalUsesHijack = !fInException; +#ifdef FEATURE_INTERPRETER + if (funcEvalUsesHijack && filterContext != NULL) { - // 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; + EECodeInfo codeInfo((PCODE)GetIP(filterContext)); + if (codeInfo.IsInterpretedCode()) + funcEvalUsesHijack = false; } +#endif // FEATURE_INTERPRETER - // Allocate the breakpoint instruction info for the debugger info in executable memory. - DebuggerHeap *pHeap = g_pDebugger->GetInteropSafeExecutableHeap_NoThrow(); - if (pHeap == NULL) + if (funcEvalUsesHijack) { - return E_OUTOFMEMORY; + _ASSERTE(filterContext != NULL); + if (::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) + // Allocate the breakpoint instruction info only for hijacked evals. Non-hijack paths + // (exception-time and interpreter) signal completion via FuncEvalComplete on the pending-eval + // queue, not via a native breakpoint trap, so the segment would never be used. Avoiding the + // allocation also means we don't require executable memory on platforms where it's unavailable + // (e.g. iOS). + DebuggerEvalBreakpointInfoSegment *bpInfoSegmentRX = NULL; + if (funcEvalUsesHijack) { - return E_OUTOFMEMORY; + 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 // CONTEXT. - DebuggerEval *pDE = new (interopsafe, nothrow) DebuggerEval(filterContext, pEvalInfo, fInException, bpInfoSegmentRX); + DebuggerEval *pDE = new (interopsafe, nothrow) DebuggerEval(filterContext, pEvalInfo, bpInfoSegmentRX); if (pDE == NULL) { @@ -14259,9 +14298,9 @@ HRESULT Debugger::FuncEvalSetup(DebuggerIPCE_FuncEvalInfo *pEvalInfo, *argDataArea = pDE->m_argData; } - // Set the thread's IP (in the filter context) to our hijack function if we're stopped due to a breakpoint or single - // step. - if (!fInException) + // Hijacked evals rewrite the thread's native context to enter FuncEvalHijack when execution resumes. + // Non-hijack evals are queued in the pending-eval table and dispatched from the resume path. + if (funcEvalUsesHijack) { _ASSERTE(filterContext != NULL); @@ -14309,9 +14348,15 @@ HRESULT Debugger::FuncEvalSetup(DebuggerIPCE_FuncEvalInfo *pEvalInfo, DeleteInteropSafeExecutable(pDE); // Note this runs the destructor for DebuggerEval, which releases its internal buffers return (hr); } - // If we're in an exception, then add a pending eval for this thread. This will cause us to perform the func - // eval when the user continues the process after the current exception event. + + // Queue the eval. Exception-time evals run from Debugger::ProcessAnyPendingEvals when + // the process continues. Interpreter evals run from the INTOP_BREAKPOINT handler after + // the debugger callback returns — no context modification and no IncThreadsAtUnsafePlaces + // needed because the stack remains walkable. GetPendingEvals()->AddPendingEval(pDE->m_thread, pDE); + + LOG((LF_CORDB, LL_INFO1000, "D::FES: Non-hijack func eval setup for pDE:%p on thread %p (fInException=%d)\n", + pDE, pThread, fInException)); } @@ -15963,7 +16008,7 @@ unsigned FuncEvalFrame::GetFrameAttribs_Impl(void) { LIMITED_METHOD_DAC_CONTRACT; - if (GetDebuggerEval()->m_evalDuringException) + if (!GetDebuggerEval()->m_evalUsesHijack) { return FRAME_ATTR_NONE; } @@ -15977,7 +16022,7 @@ TADDR FuncEvalFrame::GetReturnAddressPtr_Impl() { LIMITED_METHOD_DAC_CONTRACT; - if (GetDebuggerEval()->m_evalDuringException) + if (!GetDebuggerEval()->m_evalUsesHijack) { return (TADDR)NULL; } @@ -15995,8 +16040,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 6a3fbe9bd2b65c..20139885d95316 100644 --- a/src/coreclr/debug/ee/debugger.h +++ b/src/coreclr/debug/ee/debugger.h @@ -3478,15 +3478,18 @@ 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; - DebuggerEval(T_CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEvalInfo, bool fInException, DebuggerEvalBreakpointInfoSegment* bpInfoSegmentRX); + DebuggerEval(T_CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEvalInfo, DebuggerEvalBreakpointInfoSegment* bpInfoSegmentRX); bool Init() { + if (m_bpInfoSegment == NULL) + return true; + _ASSERTE(DbgIsExecutable(&m_bpInfoSegment->m_breakpointInstruction, sizeof(m_bpInfoSegment->m_breakpointInstruction))); return true; } 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/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 5fd9ac2e2a117a..f22dbae52e9060 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -982,7 +982,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 3c58a5cc2d1977..90bf54abfee821 100644 --- a/src/coreclr/vm/dbginterface.h +++ b/src/coreclr/vm/dbginterface.h @@ -388,6 +388,8 @@ 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; + virtual void ProcessAnyPendingEvals(Thread* pThread) = 0; + #endif //DACCESS_COMPILE }; diff --git a/src/coreclr/vm/interpexec.cpp b/src/coreclr/vm/interpexec.cpp index d89ae68d123d6f..7fe0a4b3f00955 100644 --- a/src/coreclr/vm/interpexec.cpp +++ b/src/coreclr/vm/interpexec.cpp @@ -766,7 +766,6 @@ static void InterpBreakpoint(const int32_t *ip, const InterpMethodContextFrame * exceptionRecord.ExceptionCode = STATUS_BREAKPOINT; exceptionRecord.ExceptionAddress = (PVOID)ip; - // Construct a CONTEXT for the debugger CONTEXT ctx; memset(&ctx, 0, sizeof(CONTEXT)); @@ -789,10 +788,41 @@ static void InterpBreakpoint(const int32_t *ip, const InterpMethodContextFrame * STATUS_BREAKPOINT, pThread)) { + InterpThreadContext *pThreadContext = pThread->GetInterpThreadContext(); + + const int32_t *savedBypassAddress = pThreadContext->m_bypassAddress; + int32_t savedBypassOpcode = pThreadContext->m_bypassOpcode; + + // Clear the bypass before dispatching pending evals + pThreadContext->m_bypassAddress = NULL; + pThreadContext->m_bypassOpcode = 0; + + pThread->SetFilterContext(&ctx); + EX_TRY + { + g_pDebugInterface->ProcessAnyPendingEvals(pThread); + } + EX_CATCH + { + pThread->SetFilterContext(NULL); + pThreadContext->m_bypassAddress = savedBypassAddress; + pThreadContext->m_bypassOpcode = savedBypassOpcode; + EX_RETHROW; + } + EX_END_CATCH + pThread->SetFilterContext(NULL); + + // The debugger may have moved execution via SetIP. If so, drop the bypass + // (it was set up for the original IP) and resume at the new context via + // ResumeAfterCatchException. if ((GetIP(&ctx) != (PCODE)ip) || (GetSP(&ctx) != (DWORD64)pFrame)) { ThrowResumeAfterCatchException(GetSP(&ctx), GetIP(&ctx)); } + + // No SetIP change — restore the bypass so the original opcode runs once. + pThreadContext->m_bypassAddress = savedBypassAddress; + pThreadContext->m_bypassOpcode = savedBypassOpcode; } } } 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; } }