Delve — Interfaces#

Interface catalog#

Process#

  • Package: pkg/proc
  • File: pkg/proc/interface.go:26
  • Methods:
    BinInfo() *BinaryInfo
    EntryPoint() (uint64, error)
    FindThread(threadID int) (Thread, bool)
    ThreadList() []Thread
    Breakpoints() *BreakpointMap
    Memory() MemoryReadWriter
  • Purpose: Read-only public contract for a debugged process. Exposes inspection operations only — no state mutation. This is what upper layers (debugger, rpc server) hold references to.
  • Implementations: All four backends implement this indirectly via ProcessInternalpkg/proc/native, pkg/proc/gdbserial, pkg/proc/core, pkg/proc/internal/ebpf. The Target struct wraps any ProcessInternal and satisfies Process.
  • Design quality: Minimal and well-segregated. Six methods covering exactly what a caller needs to inspect a process without mutating it. Follows ISP — consumers that only inspect the target depend on this, not the full ProcessInternal.

ProcessInternal#

  • Package: pkg/proc
  • File: pkg/proc/interface.go:43
  • Methods:
    Process                                           // embedded
    Valid() (bool, error)
    RequestManualStop(cctx *ContinueOnceContext) error
    WriteBreakpoint(*Breakpoint) error
    EraseBreakpoint(*Breakpoint) error
    SupportsBPF() bool
    SetUProbe(string, int64, []ebpf.UProbeArgMap) error
    GetBufferedTracepoints() []ebpf.RawUProbeParams
    DumpProcessNotes(notes []elfwriter.Note, threadDone func()) (bool, []elfwriter.Note, error)
    MemoryMap() ([]MemoryMapEntry, error)
    StartCallInjection() (func(), error)
    FollowExec(bool) error
  • Purpose: Full backend implementation contract. Extends Process with all state-mutating operations (breakpoint write/erase, eBPF uprobe management, core dump, call injection, exec follow). Only used inside pkg/proc; upper layers never hold a ProcessInternal directly.
  • Implementations: native.nativeProcess (ptrace/Mach/Windows), gdbserial.Process (GDB remote/LLDB/rr), core.Process (ELF/Mach-O dumps), ebpf partial (uprobe subset only).
  • Design quality: Excellent two-level split. Separating Process (public) from ProcessInternal (backend-only) is the central design decision of the entire architecture. It prevents external callers from accidentally accessing backend internals while keeping all implementations cohesive within pkg/proc.

ProcessGroup#

  • Package: pkg/proc
  • File: pkg/proc/interface.go:12
  • Methods:
    ContinueOnce(*ContinueOnceContext) (Thread, StopReason, error)
    StepInstruction(int) error
    Detach(int, bool) error
    Close() error
  • Purpose: Abstracts coordinated resumption of a group of processes (used for --follow-exec mode, where child processes are also debugged). The TargetGroup struct implements this.
  • Implementations: proc.TargetGroup
  • Design quality: Clean 4-method interface for group lifecycle. The ContinueOnceContext parameter carries a channel to communicate halt signals asynchronously, which is the only concurrency-safe method per the documented contract.

RecordingManipulation#

  • Package: pkg/proc
  • File: pkg/proc/interface.go:74
  • Methods:
    Recorded() (recorded bool, tracedir string)
    ChangeDirection(Direction) error
    GetDirection() Direction
    When() (string, error)
    Checkpoint(where string) (id int, err error)
    Checkpoints() ([]Checkpoint, error)
    ClearCheckpoint(id int) error
  • Purpose: Optional interface for record-and-replay backends (rr via gdbserial). Exposes reverse execution controls and checkpoint management to service/debugger. Checked at runtime via type assertion.
  • Implementations: gdbserial.Process when operating in rr mode.
  • Design quality: Good optional-capability pattern. Non-recording backends simply don’t implement it; the debugger uses a type assertion to detect capability. The companion RecordingManipulationInternal adds Restart() for backend use only, mirroring the Process/ProcessInternal split.

RecordingManipulationInternal#

  • Package: pkg/proc
  • File: pkg/proc/interface.go:94
  • Methods:
    RecordingManipulation                             // embedded
    Restart(cctx *ContinueOnceContext, pos string) (Thread, error)
  • Purpose: Backend-facing extension of RecordingManipulation with Restart, which is internal (restarts from a position or checkpoint). The split mirrors Process/ProcessInternal.
  • Implementations: gdbserial.Process in rr mode.
  • Design quality: Consistent with the two-level split pattern. Restart is excluded from the public RecordingManipulation because callers should use the higher-level debugger.Restart() which handles thread re-selection.

Server#

  • Package: service
  • File: service/server.go:5
  • Methods:
    Run() error
    Stop() error
  • Purpose: Minimal lifecycle interface for protocol servers. Both JSON-RPC 2.0 and DAP servers implement this. The CLI wires the selected server and calls Run(), which blocks until the session ends.
  • Implementations: rpccommon.ServerImpl (JSON-RPC), dap.Server (DAP).
  • Design quality: Exemplary ISP compliance. Two methods — exactly the lifecycle control a caller needs. All protocol-specific concerns are hidden. The simplicity is intentional: the CLI never needs to know what protocol the server speaks.

Client#

  • Package: service
  • File: service/client.go:11
  • Methods: ~50 methods covering:
    • Lifecycle: ProcessPid, BuildID, Detach, Restart, Disconnect
    • Execution control: Continue, Next, Step, StepOut, Halt, Call, StepInstruction (and Reverse* variants)
    • Thread/goroutine: SwitchThread, SwitchGoroutine, ListThreads, ListGoroutines
    • Breakpoints: CreateBreakpoint, CreateWatchpoint, ListBreakpoints, ClearBreakpoint, AmendBreakpoint, ToggleBreakpoint
    • Variables: EvalVariable, SetVariable, ListLocalVariables, ListFunctionArgs, ListPackageVariables, ListScopeRegisters
    • Symbols: ListSources, ListFunctions, ListTypes, FindLocation
    • Recording: Checkpoint, Rewind, TraceDirectory, ClearCheckpoint
    • Core dump: CoreDumpStart, CoreDumpWait, CoreDumpCancel
    • Misc: DisassembleRange, ExamineMemory, FollowExec, CallAPI
  • Purpose: The full debugging protocol contract consumed by pkg/terminal (the REPL) and any programmatic client. Abstracting this allows the terminal to be fully decoupled from the transport — it works identically over an in-process pipe or a TCP connection.
  • Implementations: rpc2.RPCClient (the only production implementation).
  • Design quality: This is a deliberately broad interface — the antithesis of ISP, but justified. The terminal and Starlark scripting need the full API surface. The design accepts the breadth in exchange for a single protocol-agnostic client type. A role-based split (ExecutionClient, BreakpointClient, InspectionClient) would be more ISP-correct but would complicate the terminal’s dependency management.

MemoryReader#

  • Package: pkg/proc
  • File: pkg/proc/mem.go:17
  • Methods:
    ReadMemory(buf []byte, addr uint64) (n int, err error)
  • Purpose: Single-method read interface for process memory. Analogous to io.ReaderAt but with uint64 address to cover the full 64-bit address space. Used throughout DWARF evaluation and variable reading.
  • Implementations: memCache, compositeMemory, all ProcessInternal implementations.
  • Design quality: Minimal and correctly modeled. The uint64 offset choice over int64 (as in io.ReaderAt) is a necessary deviation from stdlib given 64-bit address spaces where negative offsets don’t make sense.

MemoryReadWriter#

  • Package: pkg/proc
  • File: pkg/proc/mem.go:25
  • Methods:
    MemoryReader                                       // embedded
    WriteMemory(addr uint64, data []byte) (written int, err error)
  • Purpose: Read+write memory interface returned by Process.Memory(). Used for breakpoint insertion (writing INT3 bytes) and call injection (writing arguments). The compositeMemory implementation handles register-spilled variables.
  • Implementations: memCache (read-through cache), compositeMemory (register + memory pieces), backend process types.
  • Design quality: Correct embedding of MemoryReader. The memCache wrapper demonstrates the Decorator pattern — it adds caching to any MemoryReadWriter transparently.

Thread#

  • Package: pkg/proc
  • File: pkg/proc/threads.go:10
  • Methods:
    Breakpoint() *BreakpointState
    ThreadID() int
    Registers() (Registers, error)
    RestoreRegisters(Registers) error
    BinInfo() *BinaryInfo
    ProcessMemory() MemoryReadWriter
    SetCurrentBreakpoint(adjustPC bool) error
    SoftExc() bool
    Common() *CommonThread
    SetReg(uint64, *op.DwarfRegister) error
  • Purpose: OS thread abstraction. Carries the CPU state (registers), current breakpoint status, and access to process memory. The common stepping machinery in pkg/proc operates on Thread values returned by ProcessInternal.ThreadList().
  • Implementations: native.nativeThread (Linux/macOS/Windows), gdbserial.Thread, core.Thread.
  • Design quality: Well-balanced. Covers exactly what the stepping/variable-eval code needs from a thread. CommonThread (returned by Common()) carries shared state (return values from call injection) that all implementations reuse via embedding, avoiding code duplication.

Registers#

  • Package: pkg/proc
  • File: pkg/proc/registers.go:16
  • Methods:
    PC() uint64
    SP() uint64
    BP() uint64
    LR() uint64
    TLS() uint64
    GAddr() (uint64, bool)
    Slice(floatingPoint bool) ([]Register, error)
    Copy() (Registers, error)
  • Purpose: Architecture-neutral CPU register snapshot. The named accessors (PC, SP, BP, LR, TLS) cover what the stepping logic needs across all architectures. GAddr() is Go-specific: returns the address of the current goroutine descriptor. Slice enumerates all registers for display or Starlark inspection.
  • Implementations: native.Regs (per arch: regs_amd64.go, regs_arm64.go, etc.), gdbserial.Regs.
  • Design quality: Good. The Go-specific GAddr() is an intentional extension beyond generic register conventions — Delve must track goroutine state, which requires knowing where g lives. The Copy() method pattern (documented as necessary because live register state may change) is a useful idiom for snapshot semantics.

LocationSpec#

  • Package: pkg/locspec
  • File: pkg/locspec/locations.go:22
  • Methods:
    Find(t *proc.Target, processArgs []string, scope *proc.EvalScope,
         locStr string, includeNonExecutableLines bool,
         substitutePathRules [][2]string) ([]api.Location, string, error)
  • Purpose: Abstracts over the many forms of location strings: file:line, func:line, /regex/, +offset, *address. Each syntax is parsed into a concrete LocationSpec implementor; the single Find method resolves it to concrete addresses using the target’s debug info.
  • Implementations: NormalLocationSpec, RegexLocationSpec, AddrLocationSpec, OffsetLocationSpec, LineLocationSpec (all in the same package).
  • Design quality: Classic Strategy pattern. The parser in locspec returns one of five implementations; debugger.FindLocation just calls spec.Find(...) without caring which syntax was used. Single-method, purpose-focused.

Interface patterns#

  • Size distribution: Heavily bimodal. service.Server and MemoryReader have 1–2 methods; service.Client has ~50. The core proc interfaces cluster around 6–10 methods. Single-method interfaces (MemoryReader, LocationSpec) are used where a capability needs to be swappable.

  • Embedding: Used deliberately to express capability extension:

    • ProcessInternal embeds Process (mutable extends read-only)
    • RecordingManipulationInternal embeds RecordingManipulation (backend extends public)
    • MemoryReadWriter embeds MemoryReader (write extends read) This mirrors the Go stdlib io.ReadWriter embeds io.Reader + io.Writer convention.
  • Implicit satisfaction: All interfaces are defined at abstraction boundaries (layer interfaces or capability interfaces), not alongside their implementations. Process/ProcessInternal are defined in pkg/proc but implemented in sub-packages (native, gdbserial, core). service.Server is defined in service but implemented in service/rpccommon and service/dap. No explicit registration or factory pattern required.

  • stdlib interfaces used: MemoryReader is a deliberate io.ReaderAt analogue with uint64 offsets. No direct io.Reader / io.Writer satisfaction, as process memory needs non-standard addressing. pkg/logflags.Logger (not analyzed here) mirrors logrus.FieldLogger. No fmt.Stringer or sort.Interface usage in the core abstractions.


Key abstractions#

  1. proc.Process / proc.ProcessInternal — The central design. The two-level read-only/mutable split is the single decision that makes all four backends interchangeable. Every upper layer stays backend-agnostic because it only sees Process; the stepping machinery inside pkg/proc talks to ProcessInternal.

  2. service.Client — The full debugging protocol contract. Its breadth (~50 methods) is justified: it decouples the terminal and Starlark scripting from transport, making the interactive CLI and remote IDE clients identical code paths. The in-process pipe trick only works because both sides speak service.Client.

  3. service.Server — Tiny but critical. Two methods (Run, Stop) are all the CLI layer needs to be protocol-agnostic. This is what lets dlv debug and dlv dap share identical startup code while serving radically different protocols.

  4. proc.Thread — The OS thread contract. All stepping logic, register reads, and breakpoint detection operate via this interface, making the entire debugging engine portable across the three OS backends without any #ifdef equivalent.

  5. proc.MemoryReadWriter — The memory access contract. The compositeMemory implementation (reading variables that live partly in registers, partly in stack) and the memCache decorator both satisfy this interface, making variable evaluation completely transparent to whether data lives in memory or registers.


Interface-driven extensibility#

Backend extensibility is Delve’s primary extension point. Adding a new debugging backend (e.g., a JTAG interface for embedded Go) requires implementing ProcessInternal (and optionally RecordingManipulationInternal). All upper-layer code — the debugger, both protocol servers, the terminal — immediately works with no changes. The four existing backends demonstrate this: they range from in-process ptrace to network GDB packets to static core file reading, all behind the same interface.

Protocol extensibility via service.Server: adding a third protocol (e.g., the Chrome DevTools Protocol) requires only implementing the 2-method Server interface and wiring a new --listen-cdp flag in the CLI. The debugger engine would be untouched.

Optional capabilities are handled via type assertions rather than interface bloat. RecordingManipulation, RecordingManipulationInternal, and the eBPF-specific methods (SupportsBPF, SetUProbe, GetBufferedTracepoints) are checked at runtime. This keeps ProcessInternal from becoming the union of all possible backend features while still exposing them cleanly when available.

Location syntax extensibility via LocationSpec: adding a new location syntax (e.g., a DWARF expression syntax) requires implementing one Find method. The parser, the debugger, and all clients are unaffected.