周报(1.20 - 1.31)
开发分支
- axvisor: https://github.com/Iscreamx/axvisor/tree/feature/ebpf
- axebpf: https://github.com/Iscreamx/axebpf
一、已完成工作
-
axebpf 独立模块:集成 ksym、tracepoint、tp-lexer、kbpf-basic、rbpf 实现 eBPF 运行时,支持程序加载执行,Array/HashMap/LruHash/Queue 四种 Map 类型,6 个标准 helper 函数和 3 个 Hypervisor 专用 helper(获取 vm_id/vcpu_id/exit_reason)。效果:可在 Hypervisor 内核中动态加载 eBPF 字节码进行追踪,无需重新编译内核。
-
Shell trace 命令:支持 trace list/enable/disable/stat/reset/load/unload/progs/latency/verbose 等命令。效果:可通过 shell 交互式启用追踪点,附加 eBPF 程序,查看统计数据和延迟直方图。
-
Tracepoint 定义:定义了 25+ 个 VMM 追踪点,其中 VM 生命周期(vm_create/boot/shutdown)、系统初始化(vmm_init/config_load)、Shell(shell_command/init)等追踪点已完成插桩并正常工作。
-
预编译 eBPF 程序:基于 aya-ebpf 框架提供 2 个预编译程序,支持 ELF 解析和 Map 重定位,可通过
trace enable xxx --prog printk附加程序。 -
上游贡献:向 Starry-OS/kbpf-basic 提交 PR 并已合并,修复了 printf-compat 和 VaList API 在 Rust nightly >= 2025-12 上的兼容性问题。使 kbpf-basic 可在最新 nightly 工具链(2025-12-12)上正常编译。
二、待完善
| 问题 | 原因 | 解决方向 |
|---|---|---|
| vCPU 运行时追踪点不可用 | vCPU 运行循环位于 axvm crate 而非 kernel 侧 | 等待 axvm 重构后在其内部添加插桩 |
| bpf_trace_printk 简化实现 | rbpf VM 执行时无法访问 ELF .rodata 段中的格式字符串 | 需要实现内存映射机制 |
| 验证器功能有限 | rbpf 内置验证器仅检查基础安全性 | 当前依赖预编译可信程序规避风险 |
| Kprobe/Kretprobe 未实现 | 下一阶段工作 | Phase 4 计划 |
三、遇到的问题与解决方案
3.1 Rust nightly VaList API 变更
问题:kbpf-basic 依赖的 printf-compat 使用了 VaList 和 VaListImpl 类型,在 Rust nightly >= 2025-12 上编译失败,错误信息为 cannot find type VaListImpl in module core::ffi。
原因:Rust 重构了 core::ffi 模块,VaListImpl 被移除,VaList 的 API 也有变化。
解决方案:
- Fork kbpf-basic 仓库
- 更新 printf-compat 依赖到兼容新 API 的版本
- 修改 VaList 相关代码适配新 API
- 提交 PR 到上游并合并
3.2 rbpf 内存边界检查导致 Map 访问失败
问题:bpf_map_lookup_elem 返回静态 LOOKUP_BUFFER 指针后,eBPF 程序尝试解引用触发 out of bounds memory load 错误。
原因:rbpf VM 对所有内存访问进行边界检查,仅允许访问 mem(上下文)、stack(栈)和显式注册的 allowed_memory。
解决方案:
#![allow(unused)]
fn main() {
// Register LOOKUP_BUFFER as accessible memory before program execution
vm.register_allowed_memory(helpers::get_lookup_buffer_range());
}
3.3 ELF Map 重定位
问题:aya-ebpf 编译的程序包含 maps section 和 .reltracepoint 重定位表,直接加载会因为 Map FD 未修补导致运行时错误。
解决方案:实现完整的 ELF 解析流程:
- 解析
mapssection 提取 Map 定义(每个 28 字节) - 解析符号表获取 Map 名称
- 调用
maps::create()创建 Map 并获取 FD - 解析
.reltracepoint重定位表 - 修补
ld_imm64指令中的 Map FD
四、工作量统计
| 指标 | 数量 |
|---|---|
| 新增代码行数 | ~3000 行 |
| 主要模块数 | 12 个(symbols、tracepoint、runtime、maps、helpers、attach、context、programs、output、map_ops、platform、tracepoints) |
| 定义追踪点数 | 25+ 个 |
| 实现 Helper 数 | 9 个(6 标准 + 3 Hypervisor 专用) |
| 支持 Map 类型 | 4 种 |
| 上游 PR | 1 个(已合并) |
五、下阶段计划
| 优先级 | 任务 | 说明 |
|---|---|---|
| P0 | 完善文档 | 补充 API 文档、使用示例、架构说明 |
| P1 | Kprobe 支持 | 移植 Starry-OS kprobe 库,实现动态探针 |
| P1 | axvm 追踪点插桩 | 待 axvm 重构后添加 vCPU 运行时追踪点 |
| P2 | 完善 bpf_trace_printk | 实现 .rodata 内存映射,支持格式字符串解析 |
| P2 | 性能测试 | 测量追踪点开销、eBPF 执行延迟 |
六、风险与阻塞项
| 风险项 | 影响 | 缓解措施 |
|---|---|---|
| axvm 重构时间不确定 | vCPU 运行时追踪点无法上线 | 优先完成其他可用追踪点的功能验证 |
| Kprobe 架构适配复杂度 | aarch64 断点机制与 x86 不同 | 参考 Starry-OS 实现,必要时只支持单架构 |
七、个人日志
7.1 五个关键依赖仓库的作用
| 依赖 | 定位 | 在 eBPF 执行流程中的作用 |
|---|---|---|
| ksym | 符号表管理 | 提供 KallsymsMapped 解析内核符号表,支持 lookup_symbol(addr) 地址→函数名和 lookup_addr(name) 函数名→地址双向查询。用于堆栈追踪符号化和 Kprobe 目标函数定位。 |
| tracepoint (ktracepoint) | 静态追踪点框架 | 提供 define_event_trace! 宏定义追踪点,TracepointManager 管理追踪点生命周期。当追踪点触发时,调用已注册的回调函数(包括执行附加的 eBPF 程序)。 |
| tp-lexer | 过滤表达式解析 | 解析追踪点过滤表达式语法,支持条件过滤(如 vm_id == 1)。当前为 tracepoint-support feature 的依赖,后续可用于实现追踪点过滤功能。 |
| rbpf | eBPF 虚拟机 | 核心执行引擎。runtime.rs 使用 EbpfVmRaw 执行带上下文的 eBPF 字节码,通过 register_helper 注册 helper 函数,register_allowed_memory 注册可访问内存区域(如 LOOKUP_BUFFER)。程序执行后返回 r0 寄存器值。 |
| kbpf-basic | eBPF Map 实现 | 提供 bpf_map_create 创建 Map,UnifiedMap 统一抽象层。maps.rs 通过实现 KernelAuxiliaryOps trait 适配 AxVisor 内核操作(cpu_id、time_ns、write_str 等),支持 Array/HashMap/LruHash/Queue 四种 Map 类型。 |
执行流程示意:
User Shell axebpf Dependencies
│ │ │
├─ trace enable ─────────────►│ │
│ ├─ TracepointManager ──────►│ tracepoint
│ │ .enable(name) │
├─ trace enable --prog ──────►│ │
│ ├─ parse_elf_with_maps() ──►│ (ELF parsing)
│ ├─ bpf_map_create() ───────►│ kbpf-basic
│ ├─ EbpfVmRaw::new() ───────►│ rbpf
│ ├─ register_helper() ──────►│ rbpf
│ │ │
[Tracepoint fires] │ │
│ ├─ execute_with_context() ─►│ rbpf
│ │ (eBPF execution) │
│ ├─ map_lookup/update ──────►│ kbpf-basic
│ │ │
├─ trace stat ───────────────►│ │
│ ├─ iter_entries() ─────────►│ kbpf-basic
│ ├─ lookup_symbol() ────────►│ ksym (optional)
7.2 实现代码过程的权衡
| 决策点 | 选项 | 最终选择 | 权衡理由 |
|---|---|---|---|
| eBPF VM 实现 | 自己实现 vs 复用 rbpf | rbpf | rbpf 是成熟的 Rust 实现,支持 no_std,指令集完整,减少开发时间。代价是依赖外部仓库。 |
| Map 实现 | 简化 Vec 实现 vs kbpf-basic | kbpf-basic | 早期用 Vec 线性扫描(O(n)),后迁移到 kbpf-basic 获得真正的 O(1) HashMap 和 LRU 淘汰。代价是需要实现 KernelAuxiliaryOps 适配层。 |
| KernelAuxiliaryOps 适配 | 完整实现 vs 最小实现 | 最小实现(5/15 方法) | 只实现 get_unified_map_from_fd、current_cpu_id、ebpf_time_ns、ebpf_write_str 等必需方法,其他返回 NotSupported。代价是不支持 RingBuf、PerCpu Map。 |
| bpf_map_lookup_elem 返回值 | 零拷贝指针 vs 静态缓冲区 | 静态缓冲区(256B) | rbpf 内存边界检查严格,需要 register_allowed_memory 注册返回指针。零拷贝需要 fork kbpf-basic 添加 lookup_elem_ptr API。当前用静态缓冲区 + Mutex 保护,有大小限制和拷贝开销。 |
| bpf_trace_printk 实现 | 完整格式字符串解析 vs 简化打印 | 简化打印 | 格式字符串在 ELF .rodata 段,rbpf VM 执行时无法访问。完整实现需要内存映射机制(复杂度高)。当前直接打印 r1/r2/r3 参数值。 |
| 程序验证 | 完整验证器 vs 依赖可信程序 | 依赖可信预编译程序 | rbpf 内置基础验证器仅检查内存越界和指令合法性。完整验证器(如 PREVAIL)集成复杂度高。当前通过只允许加载预编译可信程序规避风险。 |
| 追踪点插桩位置 | kernel 侧 vs axvm 侧 | 混合 | vCPU 运行循环在 axvm crate 中,kernel 侧的 vcpus.rs 代码路径未执行。等待 axvm 重构后迁移插桩点。 |
7.3 收获
技术收获:
-
eBPF 执行模型理解:深入理解了 eBPF 程序的加载→验证→执行→Map 交互流程,以及 helper 函数的调用约定(r1-r5 传参,r0 返回)。
-
ELF 解析实践:实现了最小化的 ELF64 解析器,支持 section 查找、符号表解析、重定位表处理。理解了
ld_imm64指令的 Map FD 修补机制。 -
no_std 环境适配:在无标准库环境下集成多个依赖,处理了 alloc、spin lock、static-keys 等基础设施问题。
-
Rust nightly 兼容性:修复 kbpf-basic 的 VaList API 变更问题并贡献上游,理解了 Rust 不稳定特性的演进。
工程收获:
-
模块化设计:axebpf 通过 feature flags 控制功能启用(symbols/tracepoint-support/runtime),便于裁剪和测试。
-
抽象层设计:
platform.rs提供 mock/real 模式切换,map_ops.rs实现KernelAuxiliaryOps适配层,解耦了依赖库与内核实现。 -
渐进式实现:先用简化方案验证端到端流程(如 Vec 实现 Map、简化 printk),再逐步替换为生产级实现。
待深入方向:
- Kprobe 动态插桩机制(需要理解 aarch64 断点指令和异常处理)
- eBPF 验证器原理(控制流图分析、类型状态追踪)
- 零拷贝 Map 访问优化