-
Notifications
You must be signed in to change notification settings - Fork 5.3k
[WIP][clr-interp] Support for managed debugger breakpoints #123251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag |
ed5ce4c to
dce1adc
Compare
- New InterpreterWalker class decodes bytecode control flow for stepping - Update TrapStep to use InterpreterWalker for interpreted code - Add per-thread TSNC_InterpreterSingleStep flag for step tracking - ApplyPatch now uses INTOP_SINGLESTEP vs INTOP_BREAKPOINT based on flag - Handle INTOP_SINGLESTEP in interpreter execution loop
- needed for step-in support in virtual calls
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This work-in-progress pull request adds support for managed debugger breakpoints in the CoreCLR interpreter. The changes extend the existing user breakpoint support (e.g., Debugger.Break()) to support IDE breakpoints and enable setting breakpoints when the program is stopped.
Changes:
- Adds interpreter single-step thread state flag and supporting methods
- Introduces new
INTOP_SINGLESTEPopcode for step-over operations - Implements
InterpreterWalkerto analyze interpreter bytecode for debugger stepping - Modifies breakpoint execution logic to distinguish between IDE breakpoints and step-out breakpoints
- Enables JIT completion notifications for interpreter code
- Pre-inserts IL offset 0 entry in the IL-to-native map to support method entry breakpoints
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/coreclr/vm/threads.h | Adds TSNC_InterpreterSingleStep thread state flag and related methods |
| src/coreclr/vm/jitinterface.cpp | Removes interpreter code exclusion from JITComplete notifications |
| src/coreclr/vm/interpexec.cpp | Implements breakpoint and single-step handling with opcode replacement |
| src/coreclr/vm/codeman.h | Adds IsInterpretedCode() helper method |
| src/coreclr/interpreter/intops.h | Adds helper functions to classify interpreter opcodes |
| src/coreclr/interpreter/inc/intops.def | Defines INTOP_SINGLESTEP opcode |
| src/coreclr/interpreter/compiler.cpp | Pre-inserts IL offset 0 mapping for method entry breakpoints |
| src/coreclr/debug/ee/interpreterwalker.h | Declares InterpreterWalker class for bytecode analysis |
| src/coreclr/debug/ee/interpreterwalker.cpp | Implements bytecode walker for debugger stepping operations |
| src/coreclr/debug/ee/functioninfo.cpp | Uses GetInterpreterCodeFromInterpreterPrecodeIfPresent for code address |
| src/coreclr/debug/ee/executioncontrol.h | Defines BreakpointInfo structure and GetBreakpointInfo method |
| src/coreclr/debug/ee/executioncontrol.cpp | Implements INTOP_SINGLESTEP patch support and breakpoint info retrieval |
| src/coreclr/debug/ee/controller.h | Includes interpreterwalker.h header |
| src/coreclr/debug/ee/controller.cpp | Implements TrapStep for interpreter using InterpreterWalker |
| src/coreclr/debug/ee/CMakeLists.txt | Adds interpreterwalker source files to build |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 19 out of 19 changed files in this pull request and generated 9 comments.
src/coreclr/vm/interpexec.cpp
Outdated
| printf("Single-step at IP %p\n", ip); | ||
| fflush(stdout); | ||
| #endif // DEBUG | ||
| if (pThread != NULL && pThread->IsInterpreterSingleStepEnabled()) | ||
| { | ||
| #ifdef DEBUG | ||
| // This thread is single-stepping - trigger the event | ||
| printf("Single-step triggered for thread %d\n", pThread->GetThreadId()); | ||
| fflush(stdout); |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the INTOP_BREAKPOINT case, debug printf statements at lines 1063-1064 and 1069-1071 should be converted to use the LOG macro for consistency with the rest of the codebase's logging infrastructure.
| printf("Single-step at IP %p\n", ip); | |
| fflush(stdout); | |
| #endif // DEBUG | |
| if (pThread != NULL && pThread->IsInterpreterSingleStepEnabled()) | |
| { | |
| #ifdef DEBUG | |
| // This thread is single-stepping - trigger the event | |
| printf("Single-step triggered for thread %d\n", pThread->GetThreadId()); | |
| fflush(stdout); | |
| LOG((LF_INTERP, LL_INFO100, "Single-step at IP %p\n", ip)); | |
| #endif // DEBUG | |
| if (pThread != NULL && pThread->IsInterpreterSingleStepEnabled()) | |
| { | |
| #ifdef DEBUG | |
| // This thread is single-stepping - trigger the event | |
| LOG((LF_INTERP, LL_INFO100, "Single-step triggered for thread %d\n", pThread->GetThreadId())); |
| #ifdef FEATURE_INTERPRETER | ||
|
|
||
| // Result of GetBreakpointInfo - combines opcode and step-out flag | ||
| struct BreakpointInfo | ||
| { | ||
| InterpOpcode originalOpcode; | ||
| bool isStepOut; | ||
| }; |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The header file uses InterpOpcode type but doesn't include the header that defines it. While this works when included from files that already have the interpreter headers included (like interpexec.cpp), it violates header self-containment principles and could cause compilation errors if this header is included elsewhere. Add #include "../../interpreter/inc/intopsshared.h" after line 15 or before the BreakpointInfo struct definition to ensure the header is self-contained.
| // Check if there is already a breakpoint patch at this address | ||
| uint32_t currentOpcode = *(uint32_t*)patch->address; | ||
| if (currentOpcode == INTOP_BREAKPOINT || currentOpcode == INTOP_SINGLESTEP) | ||
| { | ||
| LOG((LF_CORDB, LL_INFO1000, "InterpreterEC::ApplyPatch Patch already applied at %p\n", | ||
| patch->address)); | ||
| return false; | ||
| } | ||
|
|
||
| patch->opcode = currentOpcode; // Save original opcode | ||
|
|
||
| // Check if this is a single-step patch by looking at the controller's thread's interpreter SS flag. | ||
| Thread* pThread = patch->controller->GetThread(); | ||
| if (pThread != NULL && pThread->IsInterpreterSingleStepEnabled()) | ||
| { | ||
| *(uint32_t*)patch->address = INTOP_SINGLESTEP; | ||
| LOG((LF_CORDB, LL_INFO10000, "InterpreterEC::ApplyPatch SingleStep inserted at %p, saved opcode 0x%x (%s)\n", | ||
| patch->address, patch->opcode, GetInterpOpName(patch->opcode))); | ||
| } | ||
| else | ||
| { | ||
| *(uint32_t*)patch->address = INTOP_BREAKPOINT; | ||
| LOG((LF_CORDB, LL_INFO10000, "InterpreterEC::ApplyPatch Breakpoint inserted at %p, saved opcode 0x%x (%s)\n", | ||
| patch->address, patch->opcode, GetInterpOpName(patch->opcode))); | ||
| } |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The bytecode patch operation at lines 76-99 has a potential race condition. The sequence of reading the current opcode (line 76), checking it (line 77), saving it (line 84), and writing the patch (line 90 or 96) is not atomic. If two threads try to apply patches at the same address simultaneously, or if one thread is executing the code while another is patching it, the results could be unpredictable. Consider using InterlockedCompareExchange or a similar atomic operation to ensure the patch is applied atomically. Additionally, ensure proper memory barriers are in place so that the patch is visible to all threads before execution continues.
| // Check if there is already a breakpoint patch at this address | |
| uint32_t currentOpcode = *(uint32_t*)patch->address; | |
| if (currentOpcode == INTOP_BREAKPOINT || currentOpcode == INTOP_SINGLESTEP) | |
| { | |
| LOG((LF_CORDB, LL_INFO1000, "InterpreterEC::ApplyPatch Patch already applied at %p\n", | |
| patch->address)); | |
| return false; | |
| } | |
| patch->opcode = currentOpcode; // Save original opcode | |
| // Check if this is a single-step patch by looking at the controller's thread's interpreter SS flag. | |
| Thread* pThread = patch->controller->GetThread(); | |
| if (pThread != NULL && pThread->IsInterpreterSingleStepEnabled()) | |
| { | |
| *(uint32_t*)patch->address = INTOP_SINGLESTEP; | |
| LOG((LF_CORDB, LL_INFO10000, "InterpreterEC::ApplyPatch SingleStep inserted at %p, saved opcode 0x%x (%s)\n", | |
| patch->address, patch->opcode, GetInterpOpName(patch->opcode))); | |
| } | |
| else | |
| { | |
| *(uint32_t*)patch->address = INTOP_BREAKPOINT; | |
| LOG((LF_CORDB, LL_INFO10000, "InterpreterEC::ApplyPatch Breakpoint inserted at %p, saved opcode 0x%x (%s)\n", | |
| patch->address, patch->opcode, GetInterpOpName(patch->opcode))); | |
| } | |
| // Use an atomic compare-exchange loop to apply the patch safely. | |
| volatile LONG* pOpcode = reinterpret_cast<volatile LONG*>(patch->address); | |
| // Check if this is a single-step patch by looking at the controller's thread's interpreter SS flag. | |
| Thread* pThread = patch->controller->GetThread(); | |
| const bool isSingleStep = (pThread != NULL) && pThread->IsInterpreterSingleStepEnabled(); | |
| while (true) | |
| { | |
| // Atomically read the current opcode with a full memory barrier. | |
| uint32_t currentOpcode = static_cast<uint32_t>(InterlockedCompareExchange(pOpcode, 0, 0)); | |
| // If there is already a breakpoint or single-step at this address, do not apply another patch. | |
| if (currentOpcode == INTOP_BREAKPOINT || currentOpcode == INTOP_SINGLESTEP) | |
| { | |
| LOG((LF_CORDB, LL_INFO1000, "InterpreterEC::ApplyPatch Patch already applied at %p\n", | |
| patch->address)); | |
| return false; | |
| } | |
| LONG newOpcode = isSingleStep | |
| ? static_cast<LONG>(INTOP_SINGLESTEP) | |
| : static_cast<LONG>(INTOP_BREAKPOINT); | |
| // Attempt to atomically replace the current opcode with the new one. | |
| uint32_t originalOpcode = static_cast<uint32_t>(InterlockedCompareExchange( | |
| pOpcode, | |
| newOpcode, | |
| static_cast<LONG>(currentOpcode))); | |
| // If the value we observed is still current, we successfully applied the patch. | |
| if (originalOpcode == currentOpcode) | |
| { | |
| patch->opcode = currentOpcode; // Save original opcode | |
| if (isSingleStep) | |
| { | |
| LOG((LF_CORDB, LL_INFO10000, "InterpreterEC::ApplyPatch SingleStep inserted at %p, saved opcode 0x%x (%s)\n", | |
| patch->address, patch->opcode, GetInterpOpName(patch->opcode))); | |
| } | |
| else | |
| { | |
| LOG((LF_CORDB, LL_INFO10000, "InterpreterEC::ApplyPatch Breakpoint inserted at %p, saved opcode 0x%x (%s)\n", | |
| patch->address, patch->opcode, GetInterpOpName(patch->opcode))); | |
| } | |
| break; | |
| } | |
| // Another thread modified the opcode concurrently; retry with the new value. | |
| } |
| { | ||
| // Method slot is at ip[3] for all direct call opcodes | ||
| LOG((LF_CORDB, LL_INFO10000, "InterpreterWalker::GetCallTarget: method slot=%d\n", m_ip[3])); | ||
| int32_t methodSlot = m_ip[3]; |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The GetCallTarget method accesses m_ip[3] and m_pInterpMethod->pDataItems[methodSlot] without bounds checking. If the instruction format is invalid or if methodSlot is out of bounds for the pDataItems array, this could cause an out-of-bounds access. Consider adding validation to ensure methodSlot is within valid bounds before accessing pDataItems. You can check against the data items count if it's available in InterpMethod, or add assertions to catch invalid values during debugging.
| { | |
| // Method slot is at ip[3] for all direct call opcodes | |
| LOG((LF_CORDB, LL_INFO10000, "InterpreterWalker::GetCallTarget: method slot=%d\n", m_ip[3])); | |
| int32_t methodSlot = m_ip[3]; | |
| { | |
| // Validate that the instruction format is large enough to contain ip[3] | |
| int opcode = m_opcode; | |
| if (opcode < 0 || opcode >= (int)(sizeof(s_interpOpLenWalker) / sizeof(s_interpOpLenWalker[0]))) | |
| { | |
| LOG((LF_CORDB, LL_INFO10000, "InterpreterWalker::GetCallTarget: invalid opcode=%d for direct call\n", opcode)); | |
| return NULL; | |
| } | |
| if (s_interpOpLenWalker[opcode] <= 3) | |
| { | |
| LOG((LF_CORDB, LL_INFO10000, "InterpreterWalker::GetCallTarget: opcode=%d has insufficient length for method slot\n", opcode)); | |
| return NULL; | |
| } | |
| // Method slot is at ip[3] for all direct call opcodes | |
| LOG((LF_CORDB, LL_INFO10000, "InterpreterWalker::GetCallTarget: method slot=%d\n", m_ip[3])); | |
| int32_t methodSlot = m_ip[3]; | |
| if (methodSlot < 0) | |
| { | |
| LOG((LF_CORDB, LL_INFO10000, "InterpreterWalker::GetCallTarget: negative method slot=%d\n", methodSlot)); | |
| return NULL; | |
| } | |
| #ifdef _DEBUG | |
| _ASSERTE((size_t)methodSlot < m_pInterpMethod->cDataItems); | |
| #endif | |
| if ((size_t)methodSlot >= m_pInterpMethod->cDataItems) | |
| { | |
| LOG((LF_CORDB, LL_INFO10000, "InterpreterWalker::GetCallTarget: method slot=%d out of range (count=%zu)\n", | |
| methodSlot, m_pInterpMethod->cDataItems)); | |
| return NULL; | |
| } |
src/coreclr/vm/interpexec.cpp
Outdated
| #ifdef DEBUG | ||
| printf("Breakpoint at IP %p, original opcode %u, isStepOut=%d\n", ip, bpInfo.originalOpcode, bpInfo.isStepOut); | ||
| fflush(stdout); | ||
| #endif // DEBUG | ||
|
|
||
| if (pThread != NULL && pThread->IsInterpreterSingleStepEnabled()) | ||
| { | ||
| #ifdef DEBUG | ||
| // This thread is single-stepping - trigger the event | ||
| printf("We hit a breakpoint while single-stepping for thread %d\n", pThread->GetThreadId()); | ||
| fflush(stdout); | ||
| #endif // DEBUG | ||
| bpInfo.isStepOut = false; | ||
| } | ||
|
|
||
| InterpBreakpoint(ip, pFrame, stack, pInterpreterFrame, STATUS_BREAKPOINT, bpInfo.isStepOut); | ||
| if (!bpInfo.isStepOut) | ||
| { | ||
| #ifdef DEBUG | ||
| printf("Executing original opcode %u at IP %p\n", bpInfo.originalOpcode, ip); | ||
| fflush(stdout); |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Debug printf statements are left in production code paths (lines 1029-1030, 1037-1038, 1047-1048). While they are guarded by #ifdef DEBUG, they use printf/fflush which can have performance implications and are not consistent with the runtime's logging infrastructure. Consider removing these debug prints or converting them to use the LOG macro like the rest of the debugger code (e.g., LOG((LF_CORDB, LL_INFO10000, "..."))) for consistency and better control over logging levels.
src/coreclr/vm/interpexec.cpp
Outdated
| } else { | ||
| break; | ||
| } |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the INTOP_BREAKPOINT case, when bpInfo.isStepOut is true, the code breaks out of the switch without incrementing ip. This means the next iteration will execute the same INTOP_BREAKPOINT instruction again, potentially causing an infinite loop. The step-out breakpoints should advance ip after the breakpoint handling, similar to how the non-step-out case uses goto to re-execute the original opcode. Consider adding ip++ before the break statement in the else branch at line 1052-1054.
| } else { | |
| break; | |
| } | |
| } | |
| ip++; | |
| break; |
| // For FEATURE_INTERPRETER, the SP can also point to interpreter stack memory which is outside native stack bounds. | ||
| // Skip validation in these cases as the interpreter uses heap-allocated frame structures. | ||
| // TODO: Figure this out without disabling the check entirely. | ||
| #if !defined(TARGET_WASM) && !defined(FEATURE_INTERPRETER) | ||
| if (pRD->SP && pRD->_pThread) | ||
| { | ||
| #ifndef NO_FIXED_STACK_LIMIT | ||
| _ASSERTE(pRD->_pThread->IsExecutingOnAltStack() || PTR_VOID(pRD->SP) >= pRD->_pThread->GetCachedStackLimit()); | ||
| #endif // NO_FIXED_STACK_LIMIT | ||
| _ASSERTE(pRD->_pThread->IsExecutingOnAltStack() || PTR_VOID(pRD->SP) < pRD->_pThread->GetCachedStackBase()); | ||
| } | ||
| #endif // !TARGET_WASM | ||
| #endif // !TARGET_WASM && !FEATURE_INTERPRETER |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disabling the SP validation check for all FEATURE_INTERPRETER builds is overly broad and reduces debug coverage. The check should only be skipped when actually in interpreter frames, not for all code when the interpreter feature is enabled. Consider checking if the current frame is an interpreter frame (e.g., using EECodeInfo::IsInterpretedCode on the current IP) before skipping the validation, similar to how the TARGET_WASM case is handled. This would preserve the debug check for JIT code while still allowing interpreter frames to work correctly.
| int InterpreterWalker::GetOpcodeLength(int32_t opcode) const | ||
| { | ||
| int len = s_interpOpLenWalker[opcode]; |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The GetOpcodeLength function at line 75 accesses s_interpOpLenWalker[opcode] without bounds checking. If the opcode value is corrupted or out of range (e.g., due to memory corruption or a bug in the interpreter), this could lead to an out-of-bounds array access. Consider adding a bounds check before the array access, similar to how InterpOpNameLocal checks bounds at lines 36-37. For example: if (opcode < 0 || opcode >= sizeof(s_interpOpLenWalker)/sizeof(s_interpOpLenWalker[0])) return 1; or similar error handling.
| } else { | ||
| break; | ||
| } | ||
| } |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The INTOP_SINGLESTEP handler is missing a case label separator, causing it to fall through from the INTOP_BREAKPOINT case. While both cases have similar handling logic, the fallthrough is unintentional based on the code structure. Add a break; statement at line 1055 after the closing brace of INTOP_BREAKPOINT to prevent unintentional fallthrough, or add a comment explicitly documenting why the fallthrough is intentional if that's the design.
| } | |
| } | |
| break; |
| { | ||
| // Indirect call (CALLVIRT, CALLI, CALLDELEGATE) - cannot determine target statically | ||
| // Use JMC backstop to catch method entry | ||
| // TODO: Could we do better? Why we can't use StubManagers to trace indirect calls? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can StubManagers if there is one that recognizes the code pattern being used to make the indirect call. If the indirect call doesn't need too many instructions to reach the destination you could also enable single-stepping and get there that way.
| const int32_t* skipIP = interpWalker.GetSkipIP(); | ||
| if (skipIP != NULL) | ||
| { | ||
| EnableSingleStep(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why we'd enable single step here? The next line appears to set a breakpoint on the target already.
| const int32_t* nextIP = interpWalker.GetNextIP(); | ||
| if (nextIP != NULL) | ||
| { | ||
| EnableSingleStep(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
both breakpoint and single-stepping is unnecessary. If the breakpoint is where we expect execution to be after one instruction single step might be simpler.
| int32_t caseCount = interpWalker.GetSwitchCaseCount(); | ||
| LOG((LF_CORDB,LL_INFO10000,"DS::TS: INTOP_SWITCH with %d cases\n", caseCount)); | ||
|
|
||
| EnableSingleStep(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
its unnecessary to have both single-stepping and breakpoints on all the cases. If all the breakpoints are 1 instruction in the future perhaps its easier to rely on the single step rather than set all the breakpoints?
| const int32_t* nextIP = interpWalker.GetNextIP(); // branch target | ||
| const int32_t* skipIP = interpWalker.GetSkipIP(); // fallthrough | ||
|
|
||
| EnableSingleStep(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as above
| const int32_t* skipIP = interpWalker.GetSkipIP(); | ||
| if (skipIP != NULL) | ||
| { | ||
| EnableSingleStep(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same thing
| (const BYTE*)(GetControlPC(&info->m_activeFrame.registers)))); | ||
| } | ||
|
|
||
| #ifdef FEATURE_INTERPRETER |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as a code factoring nit this function is already very long. I think it would be a little easier to follow if we factored some of this out into separate functions. Something like:
if(isInterpretter)
{
return TrapInterpretterCodeStep(...)
}
else
{
return TrapNativeCodeStep(...);
}
| *(uint32_t*)patch->address = INTOP_BREAKPOINT; | ||
| // Check if there is already a breakpoint patch at this address | ||
| uint32_t currentOpcode = *(uint32_t*)patch->address; | ||
| if (currentOpcode == INTOP_BREAKPOINT || currentOpcode == INTOP_SINGLESTEP) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems suspicious. the debugger could do something like this:
EnableSingleStepping()
ApplyPatch(nextAddress)
DisableSingleStepping()
The debugger would expect the breakpoint is still set at the end of that sequence but this code appears to avoid setting a breakpoint when single stepping is also enabled.
My guess is you either want one special opcode with state tracking on the side to describe what happens when you hit it, or you want three opcodes representing BREAKPOINT, SINGLESTEP, BREAKPOINT_AND_SINGLESTEP.
Also there is a comment lower down with broader questions about the single stepping abstraction. If we decide to go with the debugger having an interpretter single step emulator then I'd expect INTOP_SINGLESTEP goes away.
| int32_t opcode = *ip; | ||
|
|
||
| // If this is a breakpoint or single-step patch, get the original opcode from the patch table | ||
| if (opcode == INTOP_BREAKPOINT || opcode == INTOP_SINGLESTEP) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't expect to see entries in the debugger patch table when doing single stepping. That suggests the interpretter isn't really providing the normal single-stepping abstraction because the debugger doesn't track any patches when doing hardware single stepping.
I think we'd want one of:
- a complete single step abstraction implemented at the interpreter layer with no dependencies on the debugger patch table
- no single step at the interpretter layer and an InterpretterSingleStepEmulator at the debugger layer that implements single steps using interpretter breakpoints and predicting the next instruction as primitives.
Right now the implementation seems like a mixture of both, but closer to (2) than to (1).
| return true; | ||
| } | ||
|
|
||
| BreakpointInfo InterpreterExecutionControl::GetBreakpointInfo(const void* address) const |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll come back to this since I need to run for the moment, but I'm suspicious about the abstraction here. I think of these ExecutionControl APIs as being a layer below the debugger, something the debugger depends on. I did not expect them to be using the debugger patch table.
TODO: