GhostScope Architecture

June 7, 2026 · View on GitHub

A deep dive into the design and implementation of GhostScope's eBPF-based runtime tracing system.

System Overview

┌──────────────────────────────────────────────────────────┐
│                    Terminal UI (TUI)                     │
│         ┌──────────────────────────────────┐             │
│         │      TEA Architecture            │             │
│         │   (Model-Update-View Pattern)    │             │
│         └────────────┬─────────────────────┘             │
│                      │ Action Events                     │
└──────────────────────┼───────────────────────────────────┘

            ┌──────────▼──────────┐
            │  Event Registry     │  Channel-based Communication
            │  (mpsc channels)    │
            └──────────┬──────────┘

┌──────────────────────▼────────────────────────────────────┐
│              Runtime Coordinator                          │
│         (Tokio-based async orchestration)                 │
│                                                           │
│  ┌─────────────┐  ┌────────────┐  ┌─────────────┐         │
│  │ GhostSession│  │   DWARF    │  │    Trace    │         │
│  │  (State)    │  │  Analyzer  │  │   Manager   │         │
│  └─────────────┘  └────────────┘  └─────────────┘         │
│                                                           │
│  Event Loop: tokio::select! {                             │
│    - Wait for eBPF events (from all loaders)              │
│    - Handle runtime commands (from TUI)                   │
│    - Send status updates                                  │
│  }                                                        │
└───────────┬────────────────────────────┬──────────────────┘
            │                            │
   ┌────────▼─────────┐        ┌────────▼──────────┐
   │ Script Compiler  │        │  eBPF Loaders     │
   │  (Multi-stage)   │        │ (Per-Trace Pool)  │
   └──────────────────┘        └───────────────────┘
            │                            │
            └────────────┬───────────────┘

                  ┌──────▼──────┐
                  │   Target    │
                  │   Process   │
                  │  (uprobes)  │
                  └─────────────┘

Workspace Structure

GhostScope uses Cargo workspace for modular design:

CratePurpose
ghostscopeMain binary and runtime coordinator - orchestrates all components via async event loop
ghostscope-compilerScript compilation pipeline - transforms user scripts into verified eBPF bytecode via LLVM
ghostscope-dwarfPC-context DWARF semantic engine - resolves source locations, visible variables, type layouts, address mappings, and compiler read plans
ghostscope-loadereBPF program lifecycle manager - handles uprobe attachment and ring buffer management via Aya
ghostscope-uiTerminal user interface - implements interactive TUI with TEA (The Elm Architecture) pattern
ghostscope-protocolCommunication protocol - defines message format for eBPF-userspace data exchange
ghostscope-platformPlatform abstraction - encapsulates architecture-specific code (calling conventions, ABIs)
ghostscope-processRuntime process introspection and offsets — single source of truth for module cookies and ASLR section offsets in both -p and -t modes; provides PID/module enumeration and cached offsets for loaders/compilers

Core Architecture Components

1. Runtime Coordinator

Role: Async orchestrator that multiplexes eBPF events and UI commands.

Key responsibilities:

  • Polls eBPF ring buffers for trace events (non-blocking)
  • Receives commands from UI (script execution, trace enable/disable)
  • Forwards events to UI for display
  • Manages trace lifecycle

2. GhostSession

Role: Central state container for the entire tracing session.

Manages:

  • DWARF analyzer (debug info for all loaded modules)
  • Trace manager (pool of active traces)
  • Target process information (PID, binary path)
  • Configuration state

Key feature: Progressive loading with callbacks for UI progress updates.

3. DWARF Semantic Engine

Role: High-performance multi-module debug information system and PC-context semantic planner.

Core Optimizations:

  1. Parallel Module Loading

    • Asynchronous parallel loading of all process modules (main executable + dynamic libraries)
    • Progress callbacks for real-time UI feedback during initialization
    • Efficient discovery via /proc/PID/maps parsing
  2. Cross-Module Symbol Resolution

    • Unified namespace across all loaded modules
    • Function lookup spanning main binary and shared libraries
    • Source line to address mapping with inline function support
    • Type resolution across module boundaries
  3. Memory-Efficient Caching

    • Multi-level cache for frequently accessed symbols
    • Lazy evaluation of debug information (parsed on-demand)
    • Minimizes memory footprint for large binaries with extensive debug info
  4. Address Translation

    • Automatic ASLR/PIE address handling
    • Virtual address to file offset conversion
    • Runtime address mapping for process-specific traces
  5. PC-Context Read Planning

    • Resolves locals, parameters, globals, and inline scopes at a specific probe PC
    • Produces typed read plans for the compiler instead of exposing raw DWARF locations
    • Preserves semantic distinctions such as optimized-out values, rebased absolute addresses, and value-backed aggregates
    • Reports compile-time diagnostics when a variable is visible but cannot be safely lowered

TODO: Still slow, need to research how GDB optimizes DWARF parsing performance.

4. Compilation Pipeline

Multi-stage pipeline with type safety at each level:

┌──────────────────────────────────────────────────────────┐
│ Stage 1: Script Parsing                                  │
│                                                          │
│   User Script (*.gs)                                     │
│         ↓                                                │
│   Pest Parser (PEG grammar)                              │
│         ↓                                                │
│   Abstract Syntax Tree (AST)                             │
└──────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────┐
│ Stage 2: LLVM IR Generation                              │
│                                                          │
│   AST + PC Context + DWARF Read Plans                    │
│         ↓                                                │
│   Plan Lowering (variables, types, availability)         │
│         ↓                                                │
│   LLVM IR (type-safe intermediate representation)        │
└──────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────┐
│ Stage 3: eBPF Backend                                    │
│                                                          │
│   LLVM IR                                                │
│         ↓                                                │
│   LLVM BPF Backend (optimizations + codegen)             │
│         ↓                                                │
│   eBPF Bytecode (verifier-friendly)                      │
└──────────────────────────────────────────────────────────┘

The diagram below is from Crafting Interpreters, with the red path highlighting GhostScope's compilation flow. Of course, Pest and LLVM do the heavy lifting for us.

Compile Pipeline Compilation pipeline diagram (red path shows GhostScope's flow)

5. Trace Manager

Role: Manages lifecycle of multiple independent trace points.

Architecture:

  • Each trace has its own eBPF program and ring buffer
  • Traces can be independently enabled/disabled
  • Resource isolation: one trace's failure doesn't affect others
  • Concurrent execution: all uprobes run in parallel in kernel space

6. UI Architecture (TEA Pattern)

Pattern: The Elm Architecture (Model-Update-View)

┌──────────────┐
│    Model     │  AppState (immutable UI state snapshots)
└──────┬───────┘

┌──────▼───────┐
│   Update     │  Event handlers (keypress → Action → State mutation)
└──────┬───────┘

┌──────▼───────┐
│    View      │  Rendering (State → Terminal output)
└──────────────┘

Benefits:

  • Testable: Pure functions for state updates
  • Predictable: Same input always produces same output
  • Debuggable: Can replay event sequences
  • Maintainable: Clear data flow

Communication: Channels to runtime (send commands, receive trace events).

7. eBPF to Userspace Communication

Core mechanism: Ring buffer (per-CPU circular buffer in kernel memory).

Ring Buffer Architecture

┌───────────────────────────────────────────────────────┐
│              Kernel Space                             │
│                                                       │
│  ┌────────────┐                                       │
│  │  eBPF      │  Trace event occurs                   │
│  │  Program   │         ↓                             │
│  │  (uprobe)  │  Collect data (registers, memory)     │
│  └─────┬──────┘         ↓                             │
│        │         Serialize to protocol format         │
│        │                ↓                             │
│        │         bpf_ringbuf_output()                 │
│        │                ↓                             │
│        └────────►┌─────────────────────┐              │
│                  │  Ring Buffer        │              │
│                  │  (per-CPU, 256KB)   │              │
│                  │                     │              │
│                  │  [Event1][Event2]...│              │
│                  └──────────┬──────────┘              │
└─────────────────────────────┼─────────────────────────┘
                              │ Memory-mapped

┌─────────────────────────────┼─────────────────────────┐
│              User Space     │                         │
│                             │                         │
│  ┌──────────────────────────▼──────────┐              │
│  │  Trace Manager                       │             │
│  │  (polls ring buffer)                 │             │
│  └──────────────────────┬───────────────┘             │
│                         │                             │
│              Read events (non-blocking)               │
│                         ↓                             │
│  ┌──────────────────────────────────────┐             │
│  │  Streaming Parser                    │             │
│  │  (handles variable-length messages)  │             │
│  └──────────────────────┬───────────────┘             │
│                         │                             │
│              Parsed trace events                      │
│                         ↓                             │
│  ┌──────────────────────────────────────┐             │
│  │  Runtime Coordinator                 │             │
│  │  (forwards to UI)                    │             │
│  └──────────────────────────────────────┘             │
└───────────────────────────────────────────────────────┘

Communication Flow

  1. Event Generation (Kernel):

    • Uprobe fires when target instruction executes
    • eBPF program collects data (registers, stack, memory via DWARF locations)
    • Serializes data according to protocol format
    • Calls bpf_ringbuf_output() to write to ring buffer
  2. Event Polling (User Space):

    • Trace manager polls ring buffer (via Aya framework)
    • Non-blocking: Returns immediately if no events
    • Memory-mapped: Zero-copy access to kernel buffer
  3. Event Parsing:

    • Streaming parser handles variable-length messages
    • State machine tracks partial reads across chunks
    • Reconstructs complete events
  4. Event Delivery:

    • Parsed events sent to runtime coordinator
    • Coordinator forwards to UI via channel
    • UI updates display in real-time

Protocol Format

GhostScope uses an instruction-based protocol for flexible trace event representation:

┌─────────────────────────────────────────────────────┐
│ TraceEventHeader (4 bytes)                          │
│   - magic: u32 (0x43484C53 "CHLS")                  │
├─────────────────────────────────────────────────────┤
│ TraceEventMessage (24 bytes)                        │
│   - trace_id: u64                                   │
│   - timestamp: u64                                  │
│   - pid: u32                                        │
│   - tid: u32                                        │
├─────────────────────────────────────────────────────┤
│ Instruction Sequence (variable length)              │
│                                                     │
│   ┌──────────────────────────────────────┐          │
│   │ InstructionHeader (4 bytes)          │          │
│   │   - inst_type: u8                    │          │
│   │   - data_length: u16                 │          │
│   │   - reserved: u8                     │          │
│   ├──────────────────────────────────────┤          │
│   │ InstructionData (variable length)    │          │
│   │   - Depends on instruction type      │          │
│   └──────────────────────────────────────┘          │
│                                                     │
│   ... (more instructions) ...                       │
│                                                     │
│   ┌──────────────────────────────────────┐          │
│   │ EndInstruction (final marker)        │          │
│   │   - total_instructions: u16          │          │
│   │   - execution_status: u8             │          │
│   └──────────────────────────────────────┘          │
└─────────────────────────────────────────────────────┘

Instruction Types:

TypeCodePurpose
PrintStringIndex0x01Print static string (indexed)
PrintVariableIndex0x02Print simple variable with type info
PrintComplexVariable0x03Print struct/array with access path
PrintComplexFormat0x05Formatted print with complex variables
Backtrace0x10Stack backtrace with frame addresses
EndInstruction0xFFMarks end of instruction sequence

Backtrace is a compact frame stream, not pre-rendered text. The compiler asks ghostscope-dwarf for compact DWARF CFI rows, loads those rows into a BPF array map, and the uprobe program records module cookies plus module-normalized PCs. Userspace then resolves raw IPs through the process module map and asks ghostscope-dwarf for function, source line, and inline-chain information. bt always means DWARF unwinding; the script language intentionally does not expose helper/fp/backend selection.

Variable Status Tracking:

Each variable instruction includes a status field (u8) indicating data acquisition result:

StatusValueMeaning
Ok0Variable read successfully
NullDeref1Attempted to dereference null pointer
ReadError2Memory read failed (invalid address)
AccessError3Memory access denied (permissions)
Truncated4Data truncated (exceeded size limit)

This per-variable error reporting allows:

  • Partial success: Print successfully read variables even if some fail
  • Precise diagnostics: Identify exact failure point in complex expressions
  • Safe operation: eBPF program continues execution despite individual read failures