Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

AxVisor 统一探针调试系统设计

1. 背景与动机

当前 axebpf 模块提供了基本的 eBPF 追踪能力:rbpf 虚拟机、25+ 个 VMM 静态 tracepoint、EL2 kprobe/kretprobe,以及 shell 命令接口。但有几个问题:

  • 命名混淆:现有 kprobe 实际探测的是 EL2 hypervisor 代码,而非 guest 内核
  • 无法观测 guest:无法对 guest VM 内核函数设置探针
  • 无法联调:缺乏跨 VMM/Guest 的统一调试视角

目标是构建一个统一的探针调试系统,支持 hprobe(VMM 探针)、kprobe(guest 探针)、tracepoint(静态探针)的联调,所有探针共享 VMM 侧的单一 eBPF 虚拟机。

2. 探针体系

按特权级和观察方向划分为三类:

2.1 hprobe(Hypervisor Probe)— 自省探针

  • 探测 VMM 自身代码(EL2),即当前 kprobe/kretprobe 的重命名
  • 实现机制不变:BRK 注入 + 指令槽单步执行
  • 包含 hprobe(入口)和 hretprobe(返回)

2.2 kprobe(Kernel Probe)— 跨特权级探针

  • 探测 guest VM 内核代码(EL1),VMM 作为外部观察者
  • 本质上类比 Linux uprobe:高特权级透过地址空间边界探测低特权级代码
  • 两种实现模式:Stage-2 fault(非侵入,默认)和 BRK 注入(低延迟)
  • 包含 kprobe(入口)和 kretprobe(返回)
  • 全局注册,可选限定到特定 VM ID
  • 符号解析分阶段:先支持手动地址,后支持加载 guest 符号表

2.3 tracepoint — 静态探针(仅 VMM 侧)

  • 保持现有 25+ 个 VMM tracepoint 不变
  • Guest 侧不加 tracepoint,guest 观测完全通过 kprobe 和uprobe

2.4 uprobe — 预留接口

2.5 与 Linux 的类比

LinuxAxVisor特权级关系
内核 (EL1)VMM/Hypervisor (EL2)观察者
用户态进程 (EL0)Guest VM (EL1/EL0)被观察者
kprobe (内核探测自己)hprobe (VMM 探测自己)自省
uprobe (内核探测用户态)kprobe (VMM 探测 guest 内核)跨特权级向下探测

3. 核心架构 — 单一 eBPF VM + 多源事件汇聚

所有探针类型最终汇聚到 VMM 侧的同一个执行路径:

┌──────────────────────────────────────────────────┐
│                    VMM (EL2)                     │
│                                                  │
│  ┌──────────┐  ┌──────────┐  ┌───────────────┐   │
│  │  hprobe  │  │tracepoint│  │    kprobe     │   │
│  │ BRK @ EL2│  │  Static  │  │ S2 Fault/BRK  │   │
│  └────┬─────┘  └─────┬────┘  └───────┬───────┘   │
│       │              │               │           │
│       └─────────────┬┘───────────────┘           │
│                     ▼                            │
│          ┌─────────────────────┐                 │
│          │    TraceContext     │                 │
│          │ - probe_type        │                 │
│          │ - vm_id (0=host)    │                 │
│          │ - cpu_id / vcpu_id  │                 │
│          │ - regs (EL2 or EL1) │                 │
│          │ - timestamp         │                 │
│          └─────────┬───────────┘                 │
│                    ▼                             │
│          ┌─────────────────────┐                 │
│          │   rbpf VM Engine    │                 │
│          │   Helpers + Maps    │                 │
│          └─────────────────────┘                 │
└──────────────────────────────────────────────────┘

几个设计要点:

  • TraceContext 统一:扩展现有 TraceContext,增加 probe_type(hprobe/kprobe/tracepoint)和 vm_id 字段。vm_id=0 表示 host/VMM 自身,vm_id>0 表示 guest。
  • 寄存器上下文区分:hprobe 传入 EL2 的 TrapFrame,kprobe 传入 guest 的 EL1 寄存器状态(从 vCPU 上下文中提取)。eBPF 程序通过统一的偏移访问,但实际内容因 probe_type 而异。
  • eBPF helper 扩展:现有的 bpf_get_current_vm_id()(ID 100)等 helper 在 kprobe 上下文中返回实际 VM ID 而非硬编码 0。

3.1 探针类型枚举

#![allow(unused)]
fn main() {
pub enum ProbeType {
    Hprobe,          // VMM function entry
    Hretprobe,       // VMM function return
    Kprobe,          // Guest kernel function entry
    Kretprobe,       // Guest kernel function return
    Tracepoint,      // VMM static probe
}
}

4. kprobe(Guest 探针)实现

4.1 模式一:Stage-2 fault(默认,非侵入)

利用 Stage-2 页表控制 guest 内存的执行权限。

  1. 注册:VMM 记录目标 GVA(guest 虚拟地址),通过 guest 页表(TTBR0_EL1/TTBR1_EL1)翻译为 GPA(guest 物理地址),然后将该 GPA 所在页面的 Stage-2 映射标记为不可执行(XN=1)
  2. 触发:guest vCPU 执行到该页面 → Stage-2 Permission Fault → 陷入 EL2
  3. 处理:VMM 的 fault handler 检查 fault 地址是否匹配已注册的 kprobe:
    • 匹配:构建 TraceContext(从 vCPU 上下文提取 EL1 寄存器),执行 eBPF 程序,临时恢复页面执行权限,单步执行一条指令后重新标记 XN
    • 不匹配:走正常 Stage-2 fault 流程
  4. 优点:不修改 guest 内存,guest 完全无感知
  5. 缺点:页面粒度(4KB),同一页面内多个函数会频繁触发 fault;单步执行需要额外处理

4.2 模式二:BRK 注入(高级,低延迟)

类似 Linux uprobe,直接修改 guest 内核内存。

  1. 注册:GVA → GPA → HVA(通过 Stage-2 页表),将目标指令替换为 BRK #kprobe_guest_brk_imm(使用与 hprobe 不同的立即数以区分来源)
  2. 触发:guest 执行 BRK → EL1 异常 → 因 HCR_EL2.TGE 或路由配置陷入 EL2
  3. 处理:VMM 识别为 guest kprobe,执行 eBPF,恢复原指令并单步
  4. 优点:指令级精度,开销低
  5. 缺点:侵入 guest 内存,需要处理 I-cache 一致性(跨 CPU IPI flush)

两种模式通过注册时的参数选择:

trace kprobe vm0:0xffff800080012340          # 默认 Stage-2 fault
trace kprobe vm0:0xffff800080012340 --inject  # BRK 注入模式

4.3 kretprobe(Guest 返回探针)

需要劫持 guest EL1 函数的 LR(x30)寄存器:

  1. 在 kprobe 触发时记录原始 LR
  2. 将 LR 修改为一个特殊的 trampoline 地址(VMM 预先在 guest 内存中映射的一段 BRK 指令)
  3. Guest 函数返回时执行 BRK 陷入 EL2
  4. VMM 捕获返回值和执行时间后恢复原始 LR

对 eBPF 程序暴露的接口与 hretprobe 一致:

probe_type: kretprobe
regs.x0:   返回值
duration:   函数执行耗时(entry 到 return)

5. 地址翻译与符号解析

5.1 地址翻译链

GVA (Guest Virtual Address)
 │  guest 页表 (TTBR1_EL1, 由 vCPU 上下文持有)
 ▼
GPA (Guest Physical Address)
 │  Stage-2 页表 (VTTBR_EL2, 由 axaddrspace 管理)
 ▼
HPA (Host Physical Address)
 │  phys_to_virt() (线性映射)
 ▼
HVA (Host Virtual Address)  ← VMM 可直接读写
  • Stage-2 fault 模式只需 GPA,用于修改 Stage-2 页表权限
  • BRK 注入模式需要完整链到 HVA,用于读写 guest 指令

新增 GuestAddressTranslator 组件,负责 walk guest 页表(从 vCPU 上下文读取 TTBR1_EL1,按 AArch64 四级页表格式逐级翻译)。

5.2 符号解析(分阶段)

阶段一(MVP):用户直接指定 GVA 地址。

trace kprobe vm0:0xffff800080012340

阶段二:支持加载 guest 内核符号表。

  • 在 VM 配置文件(configs/vms/*.toml)中新增可选字段 kallsyms_pathsystem_map_path
  • VM 启动时 VMM 加载该文件,为每个 VM 维护独立的 ksym::SymbolTable
  • 用户按符号名注册:trace kprobe vm0:schedule
  • 复用现有 ksym crate,每个 VM 一个实例,存储在 VmSymbolRegistry
trace kprobe vm0:schedule            # 按符号名
trace kprobe vm0:schedule+0x20       # 符号名 + 偏移
ksym vm0 list                        # 列出 VM 0 的符号
ksym vm0 lookup schedule             # 查询 VM 0 中的符号地址
ksym vm0 load <path>                 # 手动加载符号表

6. 模块组织与文件结构

6.1 目录结构

modules/axebpf/src/
├── lib.rs                      # 模块入口,feature 门控,init()
├── context.rs                  # TraceContext(扩展:probe_type, vm_id)
├── runtime.rs                  # eBPF VM 封装(不变)
├── maps.rs / map_ops.rs        # eBPF map(不变)
├── helpers.rs                  # 标准 helper(不变)
├── symbols.rs                  # VMM 符号表(不变)
├── attach.rs                   # 程序附着管理(不变)
├── output.rs / macros.rs       # 输出与宏(不变)
│
├── probe/                      # 统一探针框架
│   ├── mod.rs                  # ProbeType 枚举,ProbeManager trait
│   ├── registry.rs             # 全局探针注册表(统一管理所有类型)
│   │
│   ├── hprobe/                 # 从现有 kprobe_* 重命名
│   │   ├── mod.rs
│   │   ├── manager.rs          # ← kprobe_manager.rs
│   │   ├── handler.rs          # ← kprobe_handler.rs
│   │   └── ops.rs              # ← kprobe_ops.rs
│   │
│   └── kprobe/                 # Guest 探针
│       ├── mod.rs
│       ├── manager.rs          # Guest kprobe 注册/生命周期
│       ├── handler.rs          # Stage-2 fault / BRK 处理
│       ├── addr_translate.rs   # GVA→GPA→HVA 地址翻译
│       └── guest_symbols.rs    # 每 VM 符号表管理
│
├── tracepoints/                # 不变
│   ├── vmm.rs                  # 25+ VMM tracepoint
│   ├── shell.rs
│   ├── registry.rs
│   ├── stats.rs
│   ├── histogram.rs
│   └── hypervisor_helpers.rs   # 扩展:真实 vm_id 返回
│
├── programs/                   # 不变
│   ├── bytecode.rs
│   └── registry.rs
│
├── cache.rs                    # I-cache 刷新(hprobe/kprobe 共用)
├── insn_slot.rs                # 指令槽分配(hprobe 使用)
└── page_table.rs               # EL2 页表权限修改(hprobe 使用)

6.2 Feature 门控

[features]
default = ["symbols", "tracepoint-support", "runtime", "axhal"]
hprobe = ["tracepoint-support", "dep:kprobe"]    # 原 kprobe feature 重命名
guest-kprobe = ["hprobe"]                         # 依赖 hprobe 基础设施

hprobe 复用现有 kprobe crate 依赖。guest-kprobe 是新增 feature,依赖 hprobe 因为共享 cache/insn_slot 等基础设施。

7. Shell 命令接口

7.1 命令体系

# === hprobe (VMM 探针) ===
trace hprobe <symbol>                    # 注册并启用 hprobe
trace hprobe <symbol> --ret              # hretprobe
trace unhprobe <symbol>                  # 移除

# === kprobe (Guest 探针) ===
trace kprobe vm<id>:<addr>               # 按地址,Stage-2 fault 模式
trace kprobe vm<id>:<addr> --inject      # 按地址,BRK 注入模式
trace kprobe vm<id>:<symbol>             # 按符号名(需加载符号表)
trace kprobe vm<id>:<symbol> --ret       # kretprobe
trace unkprobe vm<id>:<addr|symbol>      # 移除

# === tracepoint (不变) ===
trace enable <subsys>:<event>
trace disable <subsys>:<event>

# === 符号查询 ===
ksym list                               # VMM 符号
ksym lookup <name>                       # VMM 符号查找
ksym vm<id> list                         # Guest 符号
ksym vm<id> lookup <name>                # Guest 符号查找
ksym vm<id> load <path>                  # 手动加载 Guest 符号表

# === 统一状态查看 ===
trace list                               # 列出所有活跃探针
trace stat                               # 统计信息

7.2 联调工作流示例

追踪 guest 调度引发的 VM Exit:

# 1. 在 VMM 侧挂 tracepoint,观察 vcpu_run_exit 事件
trace enable vmm:vcpu_run_exit

# 2. 在 guest 内核的 schedule() 入口挂 kprobe
trace kprobe vm0:schedule

# 3. 在 VMM 侧的 exit handler 挂 hprobe
trace hprobe handle_vcpu_exit

# 4. 查看所有活跃探针
trace list
  [hprobe]     handle_vcpu_exit        hits: 0
  [kprobe]     vm0:schedule            hits: 0  mode: s2fault
  [tracepoint] vmm:vcpu_run_exit       enabled

# 5. 运行 VM,触发事件后查看统计
trace stat

三种探针协同工作,eBPF 程序通过 bpf_get_current_vm_id() 关联事件,可以在 map 中构建跨层的因果链(guest schedule → VM Exit → VMM handle_vcpu_exit 的时间线)。

8. 实现阶段

阶段一:hprobe 重构 + 基础设施

  • 将现有 kprobe_* 文件重命名为 hprobe_*,建立 probe/ 目录结构
  • 定义 ProbeType 枚举和统一 TraceContext(增加 probe_typevm_id 字段)
  • 建立 probe/registry.rs 统一探针注册表
  • Shell 命令从 trace kprobe 改为 trace hprobe
  • 更新 feature gate:kprobehprobe
  • 验收标准:现有 hprobe 功能完全不退化,所有现有测试通过

阶段二:Guest kprobe — Stage-2 fault 模式

  • 实现 GuestAddressTranslator(walk guest 页表,GVA→GPA)
  • 实现 Stage-2 页表权限修改(XN 位控制)
  • 实现 guest kprobe manager(注册、启用、禁用生命周期)
  • 在 Stage-2 fault handler 中增加 kprobe 匹配逻辑
  • 从 vCPU 上下文提取 EL1 寄存器构建 TraceContext
  • 支持 trace kprobe vm<id>:<addr> 命令(手动地址)
  • 验收标准:能在 guest 内核函数入口触发 eBPF 程序并输出

阶段三:增强功能

按优先级排列:

  1. kretprobe(guest):LR 劫持 + trampoline
  2. BRK 注入模式:完整 GVA→HVA 翻译 + guest 指令修改
  3. Guest 符号表加载:VmSymbolRegistry + VM 配置集成
  4. bpf_get_current_vm_id() 真实实现:对接 axvm 的 VM 上下文追踪