GhostScope 架构
July 12, 2026 · View on GitHub
深入探讨基于 eBPF 的运行时追踪系统 GhostScope 的设计与实现。
规范性的保证与失败语义定义在设计保证与可信性模型中。本文档说明当前实现通过哪些机制执行这份契约。
可信观测链
GhostScope 的源码语义并不来自某一个独立组件。最终展示的值位于一条完整证据链的末端:
请求的目标范围
|
v
运行时 PID / namespace / 模块映射
|
v
模块内 probe PC + 匹配的 DWARF
|
v
基于 PC 的作用域、类型和位置计划
|
v
有界 eBPF 读取 + 带归因信息的事件
|
v
通过验证的协议记录或显式失败状态
| 阶段 | 不变量 | 保证机制与失败行为 |
|---|---|---|
| 目标选择 | SCOPE-1、IDENT-1 | Linux x86_64 构建守卫和目标 ELF 验证建立平台边界;-p/-t 模式语义、PID 过滤和感知 namespace 的进程发现共同定义允许运行的范围。 |
| 模块与调试信息 | IDENT-1、SEM-1 | 运行时映射、模块 Cookie、加载偏移和可用的调试文件身份检查把语义绑定到模块;无法验证或 loose match 的情况会被标记为证据减弱。 |
| 语义规划 | SEM-1、FAIL-1 | DWARF 引擎在 probe PC 上解析作用域、类型、位置和重定位;不支持的计划产生诊断,而不是猜测值。 |
| eBPF 执行 | SAFE-1、COST-1 | 编译器生成有界的观测程序,内核 verifier 拒绝不安全程序。 |
| 事件传输 | LOSS-1 | RingBuf 或 PerfEventArray 传输事件;输出 helper 失败会增加按 trace 维护的丢失计数。 |
| 协议与展示 | IDENT-1、FAIL-1 | Trace/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-dwarf | PC 上下文 DWARF 语义引擎 - 解析源码位置、可见变量、类型布局、地址映射和编译器读取计划 |
| ghostscope-loader | eBPF 程序生命周期管理器 - 通过 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 上下文的语义规划器。
核心优化:
-
并行模块加载
- 异步并行加载所有进程模块(主程序 + 动态库)
- 支持进度回调,为 UI 提供实时初始化反馈
- 通过解析
/proc/PID/maps高效发现模块
-
跨模块符号解析
- 跨所有已加载模块的统一命名空间
- 函数查找覆盖主程序和共享库
- 支持内联函数的源码行号到地址映射
- 跨模块边界的类型解析
-
内存高效缓存
- 多级缓存存储频繁访问的符号
- 延迟解析调试信息(按需解析)
- 最小化具有大量调试信息的大型二进制文件的内存占用
-
地址转换
- 自动处理 ASLR/PIE 地址
- 虚拟地址到文件偏移的转换
- 针对特定进程追踪的运行时地址映射
-
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 已经为我们完成了绝大部分繁重的工作。
编译流水线示意图(红色路径为 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) │ │
│ └──────────────────────────────────────┘ │
└───────────────────────────────────────────────────────┘
通信流程
-
事件生成(内核):
- 目标指令执行时 Uprobe 触发
- eBPF 程序收集数据(通过 DWARF 位置读取寄存器、栈、内存)
- 根据协议格式序列化数据
- 调用当前选择的 RingBuf 或 PerfEventArray 输出 helper
- 输出 helper 失败时增加该 trace 的丢失计数
-
事件轮询(用户空间):
- Trace 管理器通过 Aya 轮询当前选择的传输
- 非阻塞:如果没有事件立即返回
- RingBuf 使用共享的内存映射缓冲区;PerfEventArray 分别消费 per-CPU 缓冲区
-
事件解析:
- 流式解析器处理可变长度消息
- 解析器按传输类型处理 framing,并跟踪 RingBuf 跨 chunk 的部分读取
- 重建完整事件
-
事件投递:
- 解析后的事件发送到运行时协调器
- 协调器通过通道转发到 UI
- UI 实时更新显示
-
丢失报告:
- 运行时定期读取每个 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 │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
指令类型:
| 类型 | 代码 | 用途 |
|---|---|---|
| PrintStringIndex | 0x01 | 打印静态字符串(索引化) |
| PrintVariableIndex | 0x02 | 打印带类型信息的简单变量 |
| PrintComplexVariable | 0x03 | 打印带访问路径的结构体/数组 |
| PrintComplexFormat | 0x05 | 带复杂变量的格式化打印 |
| Backtrace | 0x10 | 带栈帧地址的栈回溯 |
| EndInstruction | 0xFF | 标记指令序列结束 |
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) 指示数据获取结果:
| 状态 | 值 | 含义 |
|---|---|---|
| Ok | 0 | 变量读取成功 |
| NullDeref | 1 | 尝试解引用空指针 |
| ReadError | 2 | 内存读取失败(无效地址) |
| AccessError | 3 | 内存访问被拒绝(权限问题) |
| Truncated | 4 | 数据被截断(超出大小限制) |
这种按变量的错误报告机制允许:
- 部分成功:即使部分变量失败,也能打印成功读取的变量
- 精确诊断:在复杂表达式中准确定位失败点
- 安全运行:尽管单个读取失败,eBPF 程序仍继续执行