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 的类比
| Linux | AxVisor | 特权级关系 |
|---|---|---|
| 内核 (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 内存的执行权限。
- 注册:VMM 记录目标 GVA(guest 虚拟地址),通过 guest 页表(TTBR0_EL1/TTBR1_EL1)翻译为 GPA(guest 物理地址),然后将该 GPA 所在页面的 Stage-2 映射标记为不可执行(XN=1)
- 触发:guest vCPU 执行到该页面 → Stage-2 Permission Fault → 陷入 EL2
- 处理:VMM 的 fault handler 检查 fault 地址是否匹配已注册的 kprobe:
- 匹配:构建 TraceContext(从 vCPU 上下文提取 EL1 寄存器),执行 eBPF 程序,临时恢复页面执行权限,单步执行一条指令后重新标记 XN
- 不匹配:走正常 Stage-2 fault 流程
- 优点:不修改 guest 内存,guest 完全无感知
- 缺点:页面粒度(4KB),同一页面内多个函数会频繁触发 fault;单步执行需要额外处理
4.2 模式二:BRK 注入(高级,低延迟)
类似 Linux uprobe,直接修改 guest 内核内存。
- 注册:GVA → GPA → HVA(通过 Stage-2 页表),将目标指令替换为
BRK #kprobe_guest_brk_imm(使用与 hprobe 不同的立即数以区分来源) - 触发:guest 执行 BRK → EL1 异常 → 因 HCR_EL2.TGE 或路由配置陷入 EL2
- 处理:VMM 识别为 guest kprobe,执行 eBPF,恢复原指令并单步
- 优点:指令级精度,开销低
- 缺点:侵入 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)寄存器:
- 在 kprobe 触发时记录原始 LR
- 将 LR 修改为一个特殊的 trampoline 地址(VMM 预先在 guest 内存中映射的一段 BRK 指令)
- Guest 函数返回时执行 BRK 陷入 EL2
- 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_path或system_map_path - VM 启动时 VMM 加载该文件,为每个 VM 维护独立的
ksym::SymbolTable - 用户按符号名注册:
trace kprobe vm0:schedule - 复用现有
ksymcrate,每个 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_type、vm_id字段) - 建立
probe/registry.rs统一探针注册表 - Shell 命令从
trace kprobe改为trace hprobe - 更新 feature gate:
kprobe→hprobe - 验收标准:现有 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 程序并输出
阶段三:增强功能
按优先级排列:
- kretprobe(guest):LR 劫持 + trampoline
- BRK 注入模式:完整 GVA→HVA 翻译 + guest 指令修改
- Guest 符号表加载:
VmSymbolRegistry+ VM 配置集成 bpf_get_current_vm_id()真实实现:对接 axvm 的 VM 上下文追踪