GhostScope 架构

July 12, 2026 · View on GitHub

深入探讨基于 eBPF 的运行时追踪系统 GhostScope 的设计与实现。

规范性的保证与失败语义定义在设计保证与可信性模型中。本文档说明当前实现通过哪些机制执行这份契约。

可信观测链

GhostScope 的源码语义并不来自某一个独立组件。最终展示的值位于一条完整证据链的末端:

请求的目标范围
      |
      v
运行时 PID / namespace / 模块映射
      |
      v
模块内 probe PC + 匹配的 DWARF
      |
      v
基于 PC 的作用域、类型和位置计划
      |
      v
有界 eBPF 读取 + 带归因信息的事件
      |
      v
通过验证的协议记录或显式失败状态
阶段不变量保证机制与失败行为
目标选择SCOPE-1IDENT-1Linux x86_64 构建守卫和目标 ELF 验证建立平台边界;-p/-t 模式语义、PID 过滤和感知 namespace 的进程发现共同定义允许运行的范围。
模块与调试信息IDENT-1SEM-1运行时映射、模块 Cookie、加载偏移和可用的调试文件身份检查把语义绑定到模块;无法验证或 loose match 的情况会被标记为证据减弱。
语义规划SEM-1FAIL-1DWARF 引擎在 probe PC 上解析作用域、类型、位置和重定位;不支持的计划产生诊断,而不是猜测值。
eBPF 执行SAFE-1COST-1编译器生成有界的观测程序,内核 verifier 拒绝不安全程序。
事件传输LOSS-1RingBuf 或 PerfEventArray 传输事件;输出 helper 失败会增加按 trace 维护的丢失计数。
协议与展示IDENT-1FAIL-1Trace/PID/TID 元数据以及结构化的不可用、表达式错误和 backtrace 状态会保留给消费方。

系统概览

┌──────────────────────────────────────────────────────────┐
│                    终端 UI (TUI)                         │
│         ┌──────────────────────────────────┐             │
│         │      TEA 架构                    │             │
│         │   (Model-Update-View 模式)       │             │
│         └────────────┬─────────────────────┘             │
│                      │ 动作事件                          │
└──────────────────────┼───────────────────────────────────┘

            ┌──────────▼──────────┐
            │  事件注册器          │  基于通道的通信
            │  (mpsc channels)    │
            └──────────┬──────────┘

┌──────────────────────▼────────────────────────────────────┐
│              运行时协调器                                 │
│         (基于 Tokio 的异步编排)                           │
│                                                           │
│  ┌─────────────┐  ┌────────────┐  ┌─────────────┐         │
│  │ GhostSession│  │   DWARF    │  │    Trace    │         │
│  │  (状态)     │  │  Analyzer  │  │   Manager   │         │
│  └─────────────┘  └────────────┘  └─────────────┘         │
│                                                           │
│  事件循环:tokio::select! {                               │
│    - 等待 eBPF 事件 (来自所有 loaders)                    │
│    - 处理运行时命令 (来自 TUI)                            │
│    - 发送状态更新                                         │
│  }                                                        │
└───────────┬────────────────────────────┬──────────────────┘
            │                            │
   ┌────────▼─────────┐        ┌────────▼──────────┐
   │ 脚本编译器        │        │  eBPF Loaders     │
   │  (多阶段)        │        │ (每个trace的池)   │
   └──────────────────┘        └───────────────────┘
            │                            │
            └────────────┬───────────────┘

                  ┌──────▼──────┐
                  │   目标      │
                  │   进程      │
                  │  (uprobes)  │
                  └─────────────┘

工作空间结构

GhostScope 使用 Cargo workspace 进行模块化设计:

Crate用途
ghostscope主程序和运行时协调器 - 通过异步事件循环协调所有组件
ghostscope-compiler脚本编译流水线 - 通过 LLVM 将用户脚本转换为经过验证的 eBPF 字节码
ghostscope-dwarfPC 上下文 DWARF 语义引擎 - 解析源码位置、可见变量、类型布局、地址映射和编译器读取计划
ghostscope-loadereBPF 程序生命周期管理器 - 通过 Aya 处理 uprobe 附加和 RingBuf/PerfEventArray 事件传输
ghostscope-ui终端用户界面 - 实现基于 TEA (The Elm Architecture) 模式的交互式 TUI
ghostscope-protocol通信协议 - 定义 eBPF 与用户态数据交换的消息格式
ghostscope-platform平台抽象层 - 封装架构特定代码(调用约定、ABI)
ghostscope-process运行时进程解析与偏移管理——统一维护模块 Cookie 与 ASLR 段偏移,服务于 -p/-t 两种模式;提供 PID/模块枚举与偏移缓存,供 Loader/Compiler 复用

核心架构组件

1. 运行时协调器

角色:异步编排器,复用 eBPF 事件和 UI 命令。

关键职责

  • 轮询 eBPF ring buffers 获取追踪事件(非阻塞)
  • 接收来自 UI 的命令(脚本执行、trace 启用/禁用)
  • 转发事件到 UI 进行显示
  • 管理 trace 生命周期

2. GhostSession

角色:整个追踪会话的中央状态容器。

管理内容

  • DWARF 分析器(所有加载模块的调试信息)
  • Trace 管理器(活动 traces 池)
  • 目标进程信息(PID、二进制路径)
  • 配置状态

关键特性:渐进式加载,带有 UI 进度更新回调。

3. DWARF 语义引擎

角色:高性能多模块调试信息系统,以及基于 PC 上下文的语义规划器。

核心优化

  1. 并行模块加载

    • 异步并行加载所有进程模块(主程序 + 动态库)
    • 支持进度回调,为 UI 提供实时初始化反馈
    • 通过解析 /proc/PID/maps 高效发现模块
  2. 跨模块符号解析

    • 跨所有已加载模块的统一命名空间
    • 函数查找覆盖主程序和共享库
    • 支持内联函数的源码行号到地址映射
    • 跨模块边界的类型解析
  3. 内存高效缓存

    • 多级缓存存储频繁访问的符号
    • 延迟解析调试信息(按需解析)
    • 最小化具有大量调试信息的大型二进制文件的内存占用
  4. 地址转换

    • 自动处理 ASLR/PIE 地址
    • 虚拟地址到文件偏移的转换
    • 针对特定进程追踪的运行时地址映射
  5. PC 上下文读取计划

    • 在指定 probe PC 上解析局部变量、参数、全局变量和 inline 作用域
    • 向编译器输出带类型的读取计划,而不是暴露原始 DWARF 位置
    • 保留 optimized-out、需要重定位的绝对地址、value-backed 聚合等语义差异
    • 当变量可见但无法安全 lower 时,给出编译期诊断

TODO: 但是依然很慢,需要继续研究 GDB 是怎么提升解析 DWARF 性能的。

4. 编译流水线

具有类型安全的多阶段流水线:

┌──────────────────────────────────────────────────────────┐
│ 阶段 1:脚本解析                                         │
│                                                          │
│   用户脚本 (*.gs)                                        │
│         ↓                                                │
│   Pest 解析器 (PEG 语法)                                 │
│         ↓                                                │
│   抽象语法树 (AST)                                       │
└──────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────┐
│ 阶段 2:LLVM IR 生成                                     │
│                                                          │
│   AST + PC 上下文 + DWARF 读取计划                       │
│         ↓                                                │
│   计划 Lowering(变量、类型、可用性)                    │
│         ↓                                                │
│   LLVM IR(类型安全的中间表示)                          │
└──────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────┐
│ 阶段 3:eBPF 后端                                        │
│                                                          │
│   LLVM IR                                                │
│         ↓                                                │
│   LLVM BPF 后端(优化 + 代码生成)                       │
│         ↓                                                │
│   eBPF 字节码(验证器友好)                              │
└──────────────────────────────────────────────────────────┘

下图来自 Crafting Interpreters,红色路径标注了 GhostScope 的编译流程。当然,Pest 和 LLVM 已经为我们完成了绝大部分繁重的工作。

Compile Pipeline 编译流水线示意图(红色路径为 GhostScope 流程)

5. Trace 管理器

角色:管理多个独立追踪点的生命周期。

架构

  • 每个 trace 有自己的 eBPF 程序和 ring buffer
  • Traces 可以独立启用/禁用
  • 资源隔离:一个 trace 的失败不影响其他
  • 并发执行:所有 uprobes 在内核空间并行运行

6. UI 架构(TEA 模式)

模式:The Elm Architecture(Model-Update-View)

┌──────────────┐
│    Model     │  AppState (不可变的 UI 状态快照)
└──────┬───────┘

┌──────▼───────┐
│   Update     │  事件处理器 (按键 → 动作 → 状态变更)
└──────┬───────┘

┌──────▼───────┐
│    View      │  渲染 (状态 → 终端输出)
└──────────────┘

优势

  • 可测试:状态更新的纯函数
  • 可预测:相同输入总是产生相同输出
  • 可调试:可以重放事件序列
  • 可维护:清晰的数据流

通信方式:通过通道与运行时通信(发送命令,接收追踪事件)。

7. eBPF 到用户态通信

核心机制:启动时选择内核事件传输。GhostScope 在内核支持时优先使用 RingBuf,否则回退到 PerfEventArray。RingBuf 是跨 CPU 共享的多生产者缓冲区;PerfEventArray 使用相互独立的 per-CPU 缓冲区。

事件传输架构

┌───────────────────────────────────────────────────────┐
│              内核空间                                 │
│                                                       │
│  ┌────────────┐                                       │
│  │  eBPF      │  追踪事件发生                         │
│  │  程序      │         ↓                             │
│  │  (uprobe)  │  收集数据(寄存器、内存)             │
│  └─────┬──────┘         ↓                             │
│        │         序列化为协议格式                     │
│        │                ↓                             │
│        │   bpf_ringbuf_output() /                     │
│        │   bpf_perf_event_output()                    │
│        │                ↓                             │
│        └────────►┌─────────────────────┐              │
│                  │  RingBuf(共享)    │              │
│                  │  或 Perf buffers    │              │
│                  │  (per-CPU)        │              │
│                  │                     │              │
│                  │  [事件1][事件2]...  │              │
│                  └──────────┬──────────┘              │
└─────────────────────────────┼─────────────────────────┘
                              │ 内存映射

┌─────────────────────────────┼─────────────────────────┐
│              用户空间       │                         │
│                             │                         │
│  ┌──────────────────────────▼──────────┐              │
│  │  Trace Manager                       │             │
│  │  (轮询当前传输)                      │             │
│  └──────────────────────┬───────────────┘             │
│                         │                             │
│              读取事件(非阻塞)                       │
│                         ↓                             │
│  ┌──────────────────────────────────────┐             │
│  │  流式解析器                           │            │
│  │  (处理可变长度消息)                   │            │
│  └──────────────────────┬───────────────┘             │
│                         │                             │
│              解析后的追踪事件                         │
│                         ↓                             │
│  ┌──────────────────────────────────────┐             │
│  │  运行时协调器                         │            │
│  │  (转发到 UI)                          │            │
│  └──────────────────────────────────────┘             │
└───────────────────────────────────────────────────────┘

通信流程

  1. 事件生成(内核):

    • 目标指令执行时 Uprobe 触发
    • eBPF 程序收集数据(通过 DWARF 位置读取寄存器、栈、内存)
    • 根据协议格式序列化数据
    • 调用当前选择的 RingBuf 或 PerfEventArray 输出 helper
    • 输出 helper 失败时增加该 trace 的丢失计数
  2. 事件轮询(用户空间):

    • Trace 管理器通过 Aya 轮询当前选择的传输
    • 非阻塞:如果没有事件立即返回
    • RingBuf 使用共享的内存映射缓冲区;PerfEventArray 分别消费 per-CPU 缓冲区
  3. 事件解析

    • 流式解析器处理可变长度消息
    • 解析器按传输类型处理 framing,并跟踪 RingBuf 跨 chunk 的部分读取
    • 重建完整事件
  4. 事件投递

    • 解析后的事件发送到运行时协调器
    • 协调器通过通道转发到 UI
    • UI 实时更新显示
  5. 丢失报告

    • 运行时定期读取每个 trace 的 eBPF 输出失败计数
    • CLI 和 TUI 报告区间增量与累计丢失量
    • 非零计数表示对应观测区间不完整

协议格式

GhostScope 使用基于指令的协议实现灵活的追踪事件表示:

┌─────────────────────────────────────────────────────┐
│ TraceEventHeader (4 字节)                           │
│   - magic: u32 (0x43484C53 "CHLS")                  │
├─────────────────────────────────────────────────────┤
│ TraceEventMessage (24 字节)                         │
│   - trace_id: u64                                   │
│   - timestamp: u64                                  │
│   - pid: u32                                        │
│   - tid: u32                                        │
├─────────────────────────────────────────────────────┤
│ 指令序列 (可变长度)                                 │
│                                                     │
│   ┌──────────────────────────────────────┐          │
│   │ InstructionHeader (4 字节)           │          │
│   │   - inst_type: u8                    │          │
│   │   - data_length: u16                 │          │
│   │   - reserved: u8                     │          │
│   ├──────────────────────────────────────┤          │
│   │ InstructionData (可变长度)           │          │
│   │   - 根据指令类型而不同               │          │
│   └──────────────────────────────────────┘          │
│                                                     │
│   ... (更多指令) ...                                │
│                                                     │
│   ┌──────────────────────────────────────┐          │
│   │ EndInstruction (结束标记)            │          │
│   │   - total_instructions: u16          │          │
│   │   - execution_status: u8             │          │
│   └──────────────────────────────────────┘          │
└─────────────────────────────────────────────────────┘

指令类型

类型代码用途
PrintStringIndex0x01打印静态字符串(索引化)
PrintVariableIndex0x02打印带类型信息的简单变量
PrintComplexVariable0x03打印带访问路径的结构体/数组
PrintComplexFormat0x05带复杂变量的格式化打印
Backtrace0x10带栈帧地址的栈回溯
EndInstruction0xFF标记指令序列结束

Backtrace 是紧凑的栈帧数据流,不是预先渲染好的文本。compiler 会从 ghostscope-dwarf 获取 compact DWARF CFI row,并把这些 row 加载到 BPF array map;uprobe 程序只记录 module cookie 与模块内标准化 PC。用户态再根据 进程模块映射解析 raw IP,并交给 ghostscope-dwarf 查询函数、源码行号和 inline 调用链。bt 始终表示 DWARF unwind,脚本语言不会暴露 helper/fp/后端选择。

变量状态跟踪

每个变量指令都包含一个 status 字段 (u8) 指示数据获取结果:

状态含义
Ok0变量读取成功
NullDeref1尝试解引用空指针
ReadError2内存读取失败(无效地址)
AccessError3内存访问被拒绝(权限问题)
Truncated4数据被截断(超出大小限制)

这种按变量的错误报告机制允许:

  • 部分成功:即使部分变量失败,也能打印成功读取的变量
  • 精确诊断:在复杂表达式中准确定位失败点
  • 安全运行:尽管单个读取失败,eBPF 程序仍继续执行