Delve — Architecture#
Architectural style#
Layered + Strategy pattern with dual protocol surface
Delve is a monolithic binary built as a strict 4-layer system. Each layer depends only on the layer below it; no upward imports exist. The bottom layer (process abstraction) uses a textbook Strategy pattern: four pluggable backends all implement ProcessInternal, making every upper layer completely backend-agnostic. The service layer presents two parallel protocol implementations (JSON-RPC 2.0 and DAP) both behind a single Server interface, enabling the same debugger engine to power both CLI clients and IDEs without code duplication.
The architecture is justified by Delve’s dual purpose: it must be embeddable by IDE extensions (requiring the DAP/RPC server mode) while remaining a standalone interactive debugger (requiring the terminal REPL).
Component diagram (textual)#
┌─────────────────────────────────────────────────────────┐
│ cmd/dlv (CLI - Cobra command tree) │
│ - Parses flags, selects mode (headless/interactive) │
│ - Calls execute() which wires up the service stack │
└──────────────┬──────────────────────────────────────────┘
│ net.Listener (TCP or in-process pipe)
▼
┌─────────────────────────────────────────────────────────┐
│ service layer (service/) │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ rpccommon/rpc2 │ │ dap/ │ │
│ │ JSON-RPC 2.0 │ │ Debug Adapter Protocol │ │
│ │ server+client │ │ server (VS Code, etc.) │ │
│ └────────┬─────────┘ └────────────┬─────────────┘ │
│ └──────────────┬───────────┘ │
│ service.Server interface │
└──────────────────────────┼──────────────────────────────┘
│
┌───────────▼────────────┐
│ service/debugger │
│ Debugger struct │
│ - launch/attach │
│ - breakpoints │
│ - stepping │
│ - variable eval │
│ wraps proc.TargetGroup│
└───────────┬────────────┘
│
┌───────────▼────────────┐
│ pkg/proc │
│ TargetGroup / Target │
│ Process interface │
│ ProcessInternal iface │
└───────┬───────────────┘
┌────────┬────────┼────────┬────────────┐
▼ ▼ ▼ ▼ ▼
native/ gdbserial/ core/ ebpf/ pkg/dwarf/
ptrace GDB remote dumps uprobes DWARF parser
Win APIs LLDB/rr (eBPF) (custom Go-aware)Core components#
CLI / Command layer#
- Package:
cmd/dlv/cmds - Responsibility: Parses subcommands and flags; selects operating mode (interactive vs headless, RPC2 vs DAP); calls
execute()to wire up the service stack; handles terminal mode by starting both server and terminal client in the same process using an in-process pipe listener. - Key types:
cobra.Commandtree; package-level flag variables;execute()function - Dependencies:
service,service/debugger,service/rpc2,service/dap,service/rpccommon,pkg/terminal,pkg/config,pkg/gobuild
Service Server interface#
- Package:
service - Responsibility: Defines the single
Serverinterface (Run() error,Stop() error) that all protocol servers implement. Also definesConfigandListenerPipe()for in-process communication. - Key types:
service.Server,service.Config,service.ListenerPipe() - Dependencies: None (interface definition only)
JSON-RPC 2.0 Server#
- Package:
service/rpccommon,service/rpc2 - Responsibility: Implements
service.Serverover TCP using Go’snet/rpcwith custom JSON codec. Exposes the fullRPCServerAPI to remote clients. Also provides anRPCClientfor the terminal to use locally. - Key types:
rpccommon.ServerImpl,rpc2.RPCServer,rpc2.RPCClient - Dependencies:
service/debugger,service/api,service/internal/sameuser
DAP Server#
- Package:
service/dap - Responsibility: Implements
service.Serverover TCP using the Debug Adapter Protocol. Handles DAP request/response cycles (launch, setBreakpoints, stackTrace, variables, etc.) for IDE clients such as VS Code via thego-delve/dlvVS Code extension. - Key types:
dap.Server,dap.Session - Dependencies:
service/debugger,service/api,github.com/google/go-dap
Debugger (high-level ops)#
- Package:
service/debugger - Responsibility: Translates high-level client operations (launch, attach, set breakpoints, continue, step, evaluate expression) into calls on
proc.TargetGroup. Handles type conversion betweenservice/apiwire types andpkg/procinternal types. Owns the backend selection logic (native, lldb/gdbserial, rr, core). - Key types:
debugger.Debugger,debugger.Config - Dependencies:
pkg/proc,pkg/proc/native,pkg/proc/gdbserial,pkg/proc/core,service/api,pkg/gobuild,pkg/locspec
Process Abstraction#
- Package:
pkg/proc - Responsibility: Defines the target abstraction:
Process(read-only public interface),ProcessInternal(state-modifying backend interface),Target(wraps ProcessInternal + adds debugging state: breakpoints, goroutines, call injection), andTargetGroup(manages a group of targets for exec-following). Implements variable evaluation, expression evaluation, goroutine enumeration, stepping logic — all backend-agnostic. - Key types:
Process,ProcessInternal,Target,TargetGroup,Thread,MemoryReadWriter,BinaryInfo,Breakpoint,LogicalBreakpoint - Dependencies:
pkg/dwarf/*,pkg/goversion,pkg/locspec
Backends (four implementations of ProcessInternal)#
pkg/proc/native— ptrace (Linux/FreeBSD), Mach exception ports (macOS), Windows debug APIs. The default backend.pkg/proc/gdbserial— GDB remote serial protocol client. Used with LLDB (macOS), rr (record-and-replay), and hardware/JTAG targets.pkg/proc/core— ELF and Mach-O core dump reader. Read-only, post-mortem analysis.pkg/proc/internal/ebpf— eBPF uprobe tracer. Non-stop: never halts the target; traces function entries via Linux uprobes + perf ring buffer.
DWARF Parser#
- Package:
pkg/dwarf/(9 sub-packages) - Responsibility: Custom DWARF parser with Go-specific extensions. Handles goroutine metadata, Go interface types, generics, and compiler-generated variables that
debug/dwarf(stdlib) mishandles. Sub-packages:godwarf(type reading),frame(.debug_frame),line(.debug_line),op(expression evaluator),leb128,loclist,reader,regnum(per-arch register numbers). - Key types:
godwarf.Typehierarchy,frame.FrameDescriptionEntries,line.DebugLinePkg,op.DwarfRegisters - Dependencies: stdlib
debug/dwarf; self-contained
Terminal / REPL#
- Package:
pkg/terminal - Responsibility: Interactive command-line REPL. Dispatches user commands to the
service.Clientinterface (same interface used over TCP, but here backed byrpc2.RPCClientconnected to the in-process server). Handles line editing (readline), output formatting, Starlark scripting (starbind/). - Key types:
terminal.Term,terminal.Commands - Dependencies:
service(Client interface),pkg/config,github.com/go-delve/readline
Data flow#
Interactive debugging session (dlv debug ./mypkg):
1. main() → cmds.New(false).Execute() → debugCmd()
2. debugCmd() → execute(0, args, conf, "", ExecutingGeneratedFile, ...)
3. execute():
a. service.ListenerPipe() → (listener, clientConn) [in-process pipe]
b. rpccommon.NewServer(Config{Listener, debugger.Config{...}})
c. server.Run() [goroutine: starts JSON-RPC server on in-process pipe]
d. rpc2.NewClient(clientConn) → terminal.New(client)
e. terminal.Run() [blocks on user input]
4. User types "break main.main":
terminal → RPCClient.CreateBreakpoint(api.Breakpoint{...})
→ JSON-RPC → RPCServer.CreateBreakpoint()
→ debugger.Debugger.CreateBreakpoint()
→ proc.TargetGroup → proc.Target.SetBreakpoint()
→ ProcessInternal.WriteBreakpoint() [native: writes INT3 to target memory]
5. User types "continue":
terminal → RPCClient.Command(api.DebuggerCommand{Name:"continue"})
→ debugger.Command() → proc.TargetGroup.Continue()
→ ProcessInternal.ContinueOnce() → ptrace CONT syscall
→ target process resumes; stops at breakpoint
→ thread state propagated back up through proc → debugger → RPCServer → terminalHeadless DAP session (VS Code):
1. dlv dap → dapCmd() → dap.NewServer(Config{Listener: TCP socket})
2. VS Code connects; sends DAP "launch" request
3. dap.Server → dap.Session.handleLaunch()
→ debugger.New(config) → backend selection → proc.TargetGroup created
4. VS Code sends "setBreakpoints" → dap.Session translates to debugger.CreateBreakpoint()
5. VS Code sends "continue" → dap.Session → debugger.Command()
→ same path as interactive: proc → native → ptrace
6. Process stops → dap.Session sends DAP "stopped" event to VS CodeInitialization / Bootstrap#
main()
├── telemetry.Start() # Go telemetry crash reporting
├── Set CGO_CFLAGS=-O0 -g # Disable optimizations in debugged CGO
└── cmds.New(false).Execute() # Build Cobra tree and execute matched cmd
└── execute() [shared by debug/exec/attach/test/trace cmds]
├── logflags.Setup()
├── net.Listener creation (TCP or in-process pipe)
├── service.Server creation (rpccommon.NewServer or dap.NewServer)
│ └── internally: debugger.New(Config) is deferred until server.Run()
└── server.Run()
├── debugger.New() → backend selection → proc.TargetGroup.New()
│ ├── gobuild.GoBuild() [if debug/test command]
│ └── native.Launch() / gdbserial.LLDBLaunch() / core.OpenCore()
└── [headless: block on listener]
[interactive: terminal.New(client).Run()]Dependency injection: Entirely manual via Config structs passed top-down. No DI framework. debugger.Config is the root configuration object; it propagates backend selection (Backend field), build flags, attach PID, etc. No global state beyond package-level logger setup.
Configuration#
- Source:
.delvercYAML config file (loaded bypkg/config) + CLI flags (persistent Cobra flags on root command) - Config struct:
config.Config(user preferences: source paths, substitute paths, max string length, etc.) merged with per-invocation flags - Backend selection:
--backendflag →debugger.Config.Backend→ resolved to one of"native","lldb","rr","default"indebugger.New() - No Viper: Config loading uses a custom YAML decoder (gopkg.in/yaml.v2) against a typed struct. No dynamic reconfiguration; config is read once at startup.
- RPC-level config:
api-version,accept-multiclient,only-same-user,listenaddress are all CLI flags only; not in.delverc.
Key design decisions#
Two-level interface split (
ProcessvsProcessInternal). TheProcessinterface is read-only and forms the public contract.ProcessInternalextends it with state-mutating methods only used insidepkg/procitself. This prevents external callers from accidentally calling backend internals while keeping the implementation cohesive. TheTargetwrapper re-exposes only what upper layers need.In-process JSON-RPC pipe for interactive mode. When not headless, Delve creates an in-process
net.Connpair (service.ListenerPipe()) and runs both the RPC server and the terminal client in the same binary. This means the interactive CLI is just a regular RPC client — identical to a remote client — eliminating an entire separate code path and ensuring that the CLI and IDE paths stay in sync.Custom DWARF parser instead of stdlib. Go’s
debug/dwarfdoesn’t understand goroutine stacks,interface{}values, generics type parameters, or closures with multiple variables sharing a name. Delve’spkg/dwarf/implements Go-specific extensions on top of the raw DWARF binary format. This is a “bet on correctness”: it costs maintenance but ensures Delve keeps pace with Go compiler DWARF innovations.eBPF backend as an orthogonal tracing mode. The eBPF backend never halts the target — it attaches Linux uprobes that fire asynchronously and store results in a perf ring buffer. This is architecturally different from all other backends (which stop-inspect-resume). It fits the
ProcessInternalinterface only partially (SupportsBPF()/SetUProbe()/GetBufferedTracepoints()), making it a specialized mode rather than a full backend.Filename-based platform isolation, no runtime switches. Zero
if runtime.GOOSchecks in generic files. All platform-specific implementations live in*_linux.go,*_darwin.go,*_windows.go,regs_amd64.go, etc. This makes portability explicit at compile time, eliminates dead code in any given binary, and makes it trivially clear which files to touch when porting.