Engine overview
DebuggerCore is the part that would still be a debugger if both frontends were
deleted: the Windows debug-event loop, the symbol readers, and the Pascal expression
evaluator.
DebuggerCore
Three responsibilities, none of which know anything about DAP or MCP:
- The debug-event loop — the Win32 debug API, process and thread events, module load and unload, exceptions.
-
Symbol readers for TD32,
.map,.rsm,.dcpand JCL debug information. See Symbol formats. -
The Pascal expression evaluator, which is what Watch, hover, the Debug
Console, conditional breakpoints, logpoint substitutions and MCP
evaluate_expressionall go through — one implementation, not five.
IDebugTarget and the two engines
IDebugTarget is the interface the rest of the system talks to. Two classes
implement it:
| Class | Target |
|---|---|
TWinDebugger | 64-bit debuggee, debugged natively |
TWin32Debugger | 32-bit debuggee, debugged across the WOW64 boundary |
Which one is constructed is decided by reading the machine type from the target executable’s PE header, before the process is created. There is no setting, no probing of the running process, and no second adapter to install.
Once the process exists, its actual architecture is cross-checked against that decision.
A disagreement produces an explicit [FATAL]-prefixed diagnostic rather than a
session that misbehaves in ways that are hard to attribute.
Why the adapter is always a 64-bit process
The debug adapter itself is a 64-bit executable no matter what it debugs. Debugging a 32-bit target across WOW64 is more work than running a 32-bit adapter would be, and it is done deliberately, for one reason: symbol data is the memory hog, and it lives in the debugger’s address space, not the debuggee’s.
The scale is not theoretical. Indexing a 523 MB .rsm from a real single-EXE
build peaks at 692 MB of working set. A 32-bit debugger has a 2 GB user address space to
fit that into, alongside everything else it needs, and on a large Delphi project that is a
wall you hit rather than approach. A 64-bit debugger does not compete with its debuggee
for address space at all.
The visible consequence for a user is that there is one binary and nothing to choose at install time — see Preparing your project.
Constraints that follow from the design
-
One process per session. The debuggee is created with
DEBUG_ONLY_THIS_PROCESS, so processes it spawns are not tracked. -
Locals on 32-bit targets need
-$O-. Not an engine choice — an optimized 32-bit build routinely has no frame pointer, and there is then nothing to resolve a local against. Run control is unaffected. - The whole process stops as a unit at a breakpoint or exception. Stepping is the one place threads are treated individually — see the threading model.
TDebugSession
Above IDebugTarget sits TDebugSession, the facade both frontends
use. It owns the engine, the symbol providers, source-file resolution and the session
state machine — so state handling exists once rather than twice, and a behaviour fixed for
VS Code is fixed for an agent at the same time.
What each frontend adds on top is the next page.