Skip to content
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

Add documents on the Mono interpreter analysis #2857

Open
wants to merge 14 commits into
base: feature/CoreclrInterpreter
Choose a base branch
from
Open
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
46 changes: 46 additions & 0 deletions docs/design/interpreter/debugger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Debugger integration with the interpreter
## Debugger to interpreter calls
### Mono
The interpreter exposes a subset of functions implemented in the interp.c as an interface that Mono runtime calls into. The functions listed below are called by the debugger related code.
* interp_set_resume_state
* interp_get_resume_state
* interp_set_breakpoint
* interp_clear_breakpoint
* interp_frame_get_jit_info
* interp_frame_get_ip - dtto
* interp_frame_get_local
* interp_frame_get_this
* interp_frame_get_arg
* interp_start_single_stepping
* interp_stop_single_stepping

Mono supports debugger connection via the ICorDebug interface. The calls to that interface are translated to Mono debugging protocol that delivers messages to the debuggee side and calls some of the functions listed above to handle the specific operations.
The interp_set_resume_state and interp_get_resume_state don't seem to be used in the ICorDebug related code paths.
The interp_frame_get_jit_info and interp_frame_get_ip seem to be used during single step / breakpoint processing, but it is not clear how they precisely relate to the ICorDebug stuff.

Here is how relevant ICorDebug interface methods are wired to the interpreter functions:
* CordbJITILFrame::GetLocalVariable -> interp_frame_get_local
* CordbJITILFrame::GetArgument(0) -> interp_frame_get_this
* CordbJITILFrame::GetArgument(1..n) -> interp_frame_get_arg
* CordbFunctionBreakpoint::Activate(true) -> interp_set_breakpoint
* CordbFunctionBreakpoint::Activate(false) -> interp_clear_breakpoint
* CordbStepper::Step, StepRange, StepOut -> interp_start_single_stepping
* CordbStepper::Deactivate -> interp_stop_single_stepping

### CoreCLR
CoreCLR also uses the ICorDebug interface for debugger connection. Most of the calls to that interface are translated to IPC events and the debuggee side handles the events in Debugger::HandleIPCEvent. So we can handle the events stemming from some of the above mentioned ICorDebug interface methods there by calling the same interpreter functions that Mono calls.
The CordbJITILFrame methods don't send IPC events though and rather uses DAC and remote memory access to get the variable and argument data. It ends up getting the variable locations using the IJitManager::GetBoundariesAndVars method. We would have a JIT manager for the interpreted code and the implementation of this method could use the DAC-ified interp_frame_get_local, interp_frame_get_this and interp_frame_get_arg to get the actual details.

## Interpreter to debugger calls
### Mono
There are just two "events" that the interpreter notifies the debugger about:
* Single stepping: when MINT_SDB_INTR_LOC IR opcode is executed, a trampoline obtained from mini_get_single_step_trampoline() is called. This trampoline ends up calling mono_component_debugger()->single_step_from_context. That in turn end up calling CordbProcess()->GetCallback()->StepComplete
* Breakpoint hit: when MINT_SDB_BREAKPOINT IR opcode is executed a trampoline obtained from mini_get_breakpoint_trampoline() is called. This trampoline ends up calling mono_component_debugger()->breakpoint_from_context. That in turn end up calling CordbProcess()->GetCallback()->Breakpoint
* System.Diagnostics.Debugger.Break(): when MINT_BREAK IR opcode is executed, a trampoline obtained from mono_component_debugger()->user_break is called. That in turn end up calling CordbProcess()->GetCallback()->Break
### CoreCLR
* Single stepping: when MINT_SDB_INTR_LOC IR opcode is executed, Debugger::SendStep will be called. That sends DB_IPCE_STEP_COMPLETE event and the debugger processes it by ICorDebugManagedCallback::StepComplete)
* Breakpoint hit: when MINT_SDB_BREAKPOINT IR opcode is executed, Debugger::SendBreakpoint will be called (that sends DB_IPCE_BREAKPOINT and the debugger processes it by calling ICorDebugManagedCallback::Breakpoint)
* System.Diagnostics.Debugger.Break(): when MINT_BREAK IR opcode is executed, Debugger::SendRawUserBreakpoint will be called (that sends DB_IPCE_USER_BREAKPOINT and the debugger processes it by calling ICorDebugManagedCallback::Break)

## WASM debugger event loop
The Mono debugger runs a debugger event loop that receives commands from the debugger in a separate thread. Since WASM is a single threaded environment, the debugger has to use a different mechanism. The Mono interpreter calls mono_component_debugger()->receive_and_process_command_from_debugger_agent() for each MINT_SDB_SEQ_POINT it interprets.
18 changes: 18 additions & 0 deletions docs/design/interpreter/exception-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# CoreCLR exception handling integration with the interpreter
There are several parts of the exception handling that need to take into consideration presence of interpreter frames on the call stack.
* Stack walking
* Exception clauses enumeration
* Exception handlers invocation
* Resuming execution after a catch handler exits

The stack walking changes are described in the [stack walking](stackwalk.md) document.
Exception clauses enumeration in CoreCLR is agnostic of the actual low level details of EH clauses information storage. It uses an interface to a JIT manager that does the real work. As described in the stack walking document, a new InterpreterJitManager will be implemented for the interpreted code.
To enumerate the EH clauses, it will use data provided by the InterpMethod::clauses/num_clauses.

Regarding the exception handlers invocation, CoreCLR uses native helpers CallCatchFunclet, CallFinallyFunclet and CallFilterFunclet. These will need to be modified to recognize interpreted frames and call the related interpreter functions. CallFinallyFunclet would call interp_run_finally and CallFilterFunclet would call the interp_run_filter. As for the CallCatchFunclet, Mono handles calling catch funclets by calling interp_set_resume_state to record the catch handler IR code address and then returning to the mono_interp_exec_method that checks for this stored state and redirects execution to the catch handler if it is set. In CoreCLR, we may want to handle it differently, in a manner close to how interp_run_finally works.

Resuming execution in the parent of a catch handler after the catch handler exits depends on whether the catch handler is in an interpreted code or in compiled managed code.
For the compiled code case, the existing CoreCLR EH code will handle it without changes. It would just restore the context at the resume location.
For the interpreted code though, CoreCLR will resume execution in the mono_interp_exec_method, then pop some InterpFrame instances from the local linked list (depending on how many interpreted managed frames need to be removed) and restore the interpreter SP from the last popped one and the interpreter IP will be set to the resume location IP.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not a big fan of context restoring into C code as we currently do on mono. I think it would be simpler to always have a C++ try/catch when we leave interpreter. Asking to resume into interpreter would be a simple throw. Catchers would check if the resume information is for the current block of linked interpreter frames, if it is not found then it would just rethrow. This is more or less the approach described below that we would need to take for wasm anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't say that we would necessarily restore context into the C code, I said we would do that for the compiled managed code case. However, from perf point of view, it might be better to restore context back into the interp_throw on other than WASM. I have found unwinding native frames to be very costy, see my change #108480 where just getting rid of two levels of native code when propagating an exception resulted in a significant perf win.


WASM doesn't support stack unwinding and context manipulation, so the resuming mechanism cannot use context restoring. Mono currently returns from the mono_handle_exception after the catch is executed back to the interp_throw. When the resume frame is in the interpreted frames belonging to the current mono_interp_exec_method, it uses the mechanism described in the previous paragraph to "restore" to the resume context. But when the resume frame is above all the frames belonging to the current mono_interp_exec_method, it exits from the mono_interp_exec_method and then throws a C++ exception (of int32 * type set to NULL) that will propagate through native frames until it is caught in a compiled managed code or in another interpreter function up the call chain (see usage of mono_llvm_catch_exception) where the propagation through the interpreted frames continues the same way as in the previous interpreted frames block.
Copy link
Member

@jkotas jkotas Dec 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is C++ exception handling implemented in Wasm? Does Wasm have any primitives for stack unwinding to support C++ exception handling?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it adds new instructions, new tags section in the wasm format and modifies few existing sections.

Note that the current wasm EH, which is implemented in most browser and engines, is deprecated. The new EH with the exnref type is being standardized and is becoming available or included as preview feature. Here you can see the feature extensions table https://webassembly.org/features/

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of our current Wasm runtimes also support targeting VMs with no exception extensions. This is one of those cases where it is useful to think of the browser and WASI separately because the all the major browsers currently support the deprecated Wasm exception instructions and none of the WASI runtimes do.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To answer the unwinding part of the question, the stack unwinding is done by the VM itself and not by the running code. So after exception is thrown, the stack is unwound by VM and control flow is transferred to the catch instruction.

On wasm, the clang/llvm handles C++ exceptions by inserting a call to a wrapper in the libunwind after the catch instruction. The wrapper then calls the libcxxabi's personality function. More details can be found in https://llvm.org/docs/ExceptionHandling.html#overview and in the comments on top of https://github.com/dotnet/llvm-project/blob/dotnet/main-19.x/llvm/lib/CodeGen/WasmEHPrepare.cpp

Loading