Native vs Managed Debugging
Native vs Managed Debugging
Two completely different mechanisms underlying the same DAP frontend. The choice depends on whether the debuggee is native code (raw machine instructions) or managed (running inside a runtime).
The frontend (Lazydap, VS Code) doesn't care which — DAP looks the same on top. But the DAP Adapter for each language uses radically different machinery underneath.
Native debugging
Languages: C, C++, Rust, Go (compiled, mostly), Zig, Swift native.
Mechanism: the OS-level process debugging API.
- Linux: ptrace
- macOS: Mach exception ports
- Windows: Windows Debug API (
DebugActiveProcess,WaitForDebugEvent, etc.) - BSD: ptrace (mostly Linux-compatible)
The debugger is a separate process that uses these APIs to inspect the debuggee's raw memory and CPU state. It needs DWARF Debug Symbols (or PDB on Windows) to map machine addresses back to source code, variable names, types.
Native adapters: codelldb, lldb-dap (both via LLDB library), GDB, dlv-dap (via delve, which uses ptrace).
Managed debugging
Languages: Python, JavaScript, Java, .NET, Ruby, etc.
Mechanism: runtime-internal debug hooks. The runtime cooperates with the debugger.
The runtime exposes an API or protocol the debugger can connect to. The runtime itself does the actual pausing, inspecting, and resuming — the OS-level ptrace is not involved.
Python — sys.settrace
Python's interpreter has a built-in tracing hook. The debugger registers a Python callback via sys.settrace. The interpreter calls the callback on every line / function entry / exception. The callback can pause execution, inspect frames, evaluate expressions, then return to let the interpreter continue.
debugpy is the standard Python DAP adapter; uses sys.settrace underneath.
JavaScript — V8 Inspector Protocol
V8 (Chrome's JS engine, also Node.js) exposes a WebSocket interface called the V8 Inspector Protocol. Same interface Chrome DevTools uses. The debugger connects via WebSocket, sends commands like "set breakpoint," receives events.
js-debug is the standard Node DAP adapter; bridges DAP to V8 Inspector Protocol.
JVM — JDWP
Java Debug Wire Protocol. The JVM exposes a TCP port; debugger connects, sends commands, gets responses + events. Defined as part of the Java platform.
.NET — ICorDebug
The CLR exposes a C++ COM interface (ICorDebug). Modern .NET also has alternative debug interfaces.
Key differences
| Native | Managed | |
|---|---|---|
| Debugger needs special privileges | Yes (ptrace, root, capabilities) | No (runtime cooperates) |
| Source of truth for state | Raw memory, registers | Runtime's own model |
| Variable inspection | Walk DWARF, read memory | Ask the runtime |
| Breakpoints | INT3 patching (Software Breakpoints) | Runtime-managed |
| Multi-thread | Manage at OS level | Runtime usually handles |
| Performance overhead | Low | Can be significant (Python's settrace slows execution materially) |
| Optimisation can hide variables | Yes (<optimised out>) |
Less so |
Why this distinction matters
For Lazydap: every adapter we wrap is one of these two types. Adding support for a new language usually means picking up an existing adapter someone else built (rather than writing one from scratch), because the adapter encapsulates this radical mechanism difference. The adapter trait in lazydap (DebugAdapter) is the seam — above it, lazydap doesn't care which mechanism; below it, the adapter does whatever its language's debug story requires.
Hybrid cases
A few languages don't fit cleanly:
- Go — compiled to native, but the runtime has its own scheduler (goroutines). delve handles both: ptrace for the underlying process, plus understanding of goroutine state on top.
- Rust — pure native; codelldb handles it like C++.
- Mojo, Crystal, Nim — native; whatever DWARF-emitting compiler they use, an LLDB-based adapter usually works.
- Zig — native; LLDB handles it.
See also
- DAP Adapter — the wrapper around each mechanism
- ptrace — native debugging primitive
- How Debuggers Actually Work — full layer cake