周报(2.1 - 2.7)
开发分支
- axvisor: https://github.com/Iscreamx/axvisor/tree/feature/ebpf
- axebpf: https://github.com/Iscreamx/axebpf
一、已完成工作
-
Kprobe 基础设施:Starry-OS 的 kprobe 库设计用于 EL1 宏内核环境,而 AxVisor 作为 Type-1 Hypervisor 运行在 EL2,异常处理入口和寄存器上下文完全不同,无法直接复用。本周实现了 EL2 环境下的 kprobe 断点捕获机制:在 arm_vcpu 的 EL2 同步异常处理中识别 BRK 指令 (EC=0x3C),将 Hypervisor 的 TrapFrame 转换为 eBPF 程序可访问的寄存器上下文,执行完成后恢复正常执行流。效果:可在任意 Hypervisor 内核函数入口处动态执行 eBPF 程序。
-
符号表集成:实现内核符号表加载机制,使用页对齐结构嵌入 kallsyms 二进制数据满足 ksym 库要求,获取内核代码段边界用于地址校验。效果:kprobe 可通过符号名自动解析函数地址,无需手动查找。
-
Shell ksym 命令:新增内核符号查找命令,支持地址到符号名的双向解析,以及模糊搜索功能。便于定位 kprobe 挂载点。
-
Shell trace kprobe 命令:扩展 trace 命令组,新增 kprobe/kretprobe/unkprobe 子命令,支持按程序名自动加载预编译 eBPF 程序,trace list 扩展显示已注册 kprobe 的符号名、地址、命中次数、程序 ID。效果:可通过 shell 交互式管理动态探针。
-
axebpf 子模块化:将 axebpf 改为 git submodule 引入。效果:axebpf 可独立版本管理。
-
axvm kprobe feature 传递:arm_vcpu 是 axvm 的依赖,需要通过 feature flag 控制 kprobe 功能是否编译,因此在 axvm 中添加 kprobe feature 并向下传递到 arm_vcpu。
-
链接器脚本更新:tracepoint 库使用特殊的 linker section 在链接时收集所有静态追踪点定义,需要在 axplat-aarch64-dyn 的链接器脚本中添加 tracepoint 和 __static_keys 段的起止符号定义。效果:支持 tracepoint 静态插桩点的自动收集,__static_keys 为后续静态分支优化做准备。
二、待完善
| 问题 | 原因 | 解决方向 |
|---|---|---|
| 递归调用异常 | 执行稍复杂的 eBPF 程序时出现 Error: too many nested calls (max: 8) | 可能是递归 kprobe 触发,待调试 |
| Kretprobe 未完整验证 | 函数返回探针需要栈帧管理机制 | 当前仅完成命令接口框架 |
| x86_64 架构支持待实现 | 当前仅完成 aarch64 EL2 环境的适配 | x86_64 需要适配 VMX root mode 的异常处理 |
三、遇到的问题与解决方案
3.1 EL2 与 EL1 异常处理差异
问题:Starry-OS kprobe 库基于 EL1 异常处理设计,直接移植到 AxVisor 后无法正常工作。
原因:AxVisor 运行在 EL2,使用 ESR_EL2/FAR_EL2 等寄存器,异常向量表入口和 TrapFrame 结构与 EL1 完全不同。
解决方案:
- 在 arm_vcpu 的
current_el_sync_handler中添加 BRK 指令识别逻辑 - 检查 ESR_EL2.EC == 0x3C(BRK from AArch64)
- 将 TrapFrame 指针和大小传递给 axebpf 的 kprobe handler
- 通过回调函数更新 PC 实现单步执行
3.2 符号表页对齐要求
问题:ksym 库要求 kallsyms 二进制数据必须页对齐,直接使用 include_bytes! 嵌入会导致初始化失败。
解决方案:
#![allow(unused)]
fn main() {
#[repr(C, align(4096))]
struct AlignedKallsyms<const N: usize> {
data: [u8; N],
}
static KALLSYMS_ALIGNED: AlignedKallsyms<{ include_bytes!("../../kallsyms.bin").len() }> = ...;
}
3.3 Feature Flag 跨 crate 传递
问题:kprobe 功能需要同时在 kernel、axvm、arm_vcpu 三个 crate 中启用,feature 依赖链复杂。
解决方案:在 axvm 的 Cargo.toml 中声明 kprobe = ["arm_vcpu/kprobe"],实现 feature 自动向下传递。
四、工作量统计
| 指标 | 数量 |
|---|---|
| 新增代码行数 | ~500 行 |
| 修改子模块数 | 3 个(arm_vcpu、axvm、axplat-aarch64-dyn) |
| 新增 Shell 命令 | 4 个(ksym、kprobe、kretprobe、unkprobe) |
| 新增 eBPF 程序 | 3 个(kprobe_args、kprobe_simple、kprobe_noop) |
五、下阶段计划
| 优先级 | 任务 | 说明 |
|---|---|---|
| P0 | 调试递归调用问题 | 分析 nested calls 错误原因,可能需要过滤 kprobe 自身调用 |
| P1 | 完善 Kretprobe | 实现函数返回探针的栈帧管理 |
| P1 | axvm 追踪点插桩 | 待 axvm 重构后添加 vCPU 运行时追踪点 |
| P2 | x86_64 架构支持 | 适配 VMX root mode 的异常处理机制 |
六、风险与阻塞项
| 风险项 | 影响 | 缓解措施 |
|---|---|---|
| 递归 kprobe 触发 | 复杂 eBPF 程序无法执行 | 分析调用链,添加重入保护 |
| axvm 重构时间不确定 | vCPU 运行时追踪点无法上线 | 优先完成其他可用追踪点的功能验证 |
七、个人日志
7.1 Kprobe 库架构深度分析
本周深入学习了 Starry-OS 的 kprobe 库实现,理解了其核心架构:
7.1.1 软件单步执行机制
kprobe 使用基于 BRK 指令的软件单步机制,而非硬件单步(避免 SPSR.SS 的复杂性):
Original Code: After Instrumentation:
┌─────────────────┐ ┌─────────────────┐
│ func: │ │ func: │
│ original_insn │ ────────► │ BRK #4 │ ← Main Breakpoint
│ next_insn │ │ next_insn │
└─────────────────┘ └─────────────────┘
Instruction Slot (.text.kprobe_slots):
┌─────────────────┐
│ original_insn │ ← Original instruction copied here
│ BRK #6 │ ← Single-step completion marker
└─────────────────┘
执行流程:
- CPU 执行到
BRK #4,触发同步异常(EC=0x3C, ISS=0x4) - 异常处理器执行 eBPF 程序,然后将 PC 设置为指令槽地址
- 异常返回后执行指令槽中的原始指令
- 执行到
BRK #6,再次触发异常(EC=0x3C, ISS=0x6) - 异常处理器将 PC 设置为原始函数的下一条指令(original_pc + 4)
- 异常返回,继续正常执行
7.1.2 KprobeAuxiliaryOps Trait
kprobe 库通过 KprobeAuxiliaryOps trait 抽象平台相关操作,需要实现以下方法:
| 方法 | 作用 | AxVisor 实现 |
|---|---|---|
copy_memory | 复制内存(用于保存/恢复原始指令) | 检测目标是否在指令槽区域,若是则先修改页表权限 |
set_writeable_for_address | 临时使 .text 段可写 | 调用 page_table::set_kernel_text_writable,执行后恢复并刷新 I-cache |
alloc_kernel_exec_memory | 分配可执行内存(指令槽) | 从预分配的 .text.kprobe_slots 段分配 8 字节槽位 |
free_kernel_exec_memory | 释放指令槽 | 恢复只读权限并归还槽位 |
insert_kretprobe_instance_to_task | 保存 kretprobe 返回地址 | 使用 per-CPU 栈存储(Hypervisor 无传统任务概念) |
pop_kretprobe_instance_from_task | 恢复 kretprobe 返回地址 | 从 per-CPU 栈弹出 |
7.1.3 指令槽管理
指令槽是一段预分配的可执行内存区域,用于存放被替换的原始指令:
.text.kprobe_slots (在链接器脚本中定义):
┌────────────────────────────────────────────────────────┐
│ Slot 0: [4B original_insn][4B BRK #6] = 8 bytes │
│ Slot 1: [4B original_insn][4B BRK #6] = 8 bytes │
│ ... │
│ Slot N: [4B original_insn][4B BRK #6] = 8 bytes │
└────────────────────────────────────────────────────────┘
关键实现细节:
- 槽位大小固定为 8 字节(原始指令 4B + BRK #6 4B)
- 使用位图管理槽位分配状态
- 写入前需修改页表使其可写,写入后恢复只读并刷新 I-cache
7.1.4 页表权限修改
在 AArch64 中,.text 段默认是只读可执行的。插入 BRK 指令需要临时修改页表:
#![allow(unused)]
fn main() {
// 修改页表项的 AP (Access Permission) 位
// AP[2:1] = 00: EL1 RW, EL0 无访问
// AP[2:1] = 10: EL1 RO, EL0 无访问
fn set_kernel_text_writable(addr: usize, len: usize, writable: bool) -> bool {
// 1. 遍历页表找到对应 PTE
// 2. 修改 AP 位
// 3. 刷新 TLB (TLBI)
// 4. 如果是写入后,还需刷新 I-cache (IC IVAU)
}
}
7.2 Kprobe 在 Hypervisor 中的特殊性
与传统 OS 内核的 kprobe 实现相比,Hypervisor 环境下存在以下差异:
| 方面 | 传统 OS (EL1) | Hypervisor (EL2) |
|---|---|---|
| 异常寄存器 | ESR_EL1, FAR_EL1 | ESR_EL2, FAR_EL2 |
| 异常向量表 | VBAR_EL1 | VBAR_EL2 |
| 上下文结构 | 内核 pt_regs | Hypervisor TrapFrame |
| 页表基址寄存器 | TTBR0_EL1/TTBR1_EL1 | TTBR0_EL2 |
| 任务上下文 | current_task | per-CPU 状态 |
| 追踪目标 | 内核函数 | VMM 代码路径 |
适配要点:
- 异常处理入口不同,需要在 arm_vcpu 的
current_el_sync_handler中添加 BRK 识别逻辑 - TrapFrame 结构不同,需要正确提取 PC 和传递寄存器上下文给 eBPF 程序
- Hypervisor 无传统任务概念,kretprobe 实例需使用 per-CPU 存储
7.3 Per-CPU 状态管理
Hypervisor 没有传统 OS 的任务/线程概念,kprobe 执行过程中的状态需要按 CPU 核心存储:
#![allow(unused)]
fn main() {
// 每个 CPU 核心独立的状态存储
const MAX_CPUS: usize = 8;
static ORIGINAL_PC: [AtomicUsize; MAX_CPUS] = [...];
static RETPROBE_STACKS: [Mutex<Vec<RetprobeInstance>>; MAX_CPUS] = [...];
fn save_original_pc(pc: usize) {
let cpu = platform::cpu_id();
ORIGINAL_PC[cpu].store(pc, Ordering::SeqCst);
}
}
这确保了多核环境下 kprobe 的正确性:每个 CPU 独立追踪自己的执行状态。
7.4 收获
技术收获:
-
软件单步机制:深入理解了 kprobe 使用双 BRK 指令实现软件单步的原理,比硬件单步更简单可控。
-
Trait 抽象设计:学习了
KprobeAuxiliaryOpstrait 如何将平台相关操作抽象出来,使 kprobe 库可以跨平台复用。 -
页表动态修改:掌握了运行时修改 .text 段权限的方法,包括页表项修改、TLB 刷新、I-cache 刷新的完整流程。
-
AArch64 异常模型:深入理解了 EL2 异常处理流程,ESR_EL2.EC/ISS 字段编码,以及异常返回机制。
-
Per-CPU 编程模式:理解了在无任务抽象的 Hypervisor 环境中如何管理执行状态。
工程收获:
-
跨 crate feature 管理:学习了 Cargo feature 的依赖传递机制,理解了
feature = ["dep/feature"]语法的作用。 -
链接器脚本定制:掌握了 linker section 的定义方法,理解了如何为指令槽预留可执行内存区域。
待深入方向:
- Kretprobe trampoline 机制:如何劫持函数返回地址并在返回时执行 eBPF 程序
- 递归 kprobe 的重入保护:当 eBPF 程序或 helper 函数本身触发 kprobe 时如何避免死循环
- x86_64 适配:INT3 单字节指令的处理、VMX root mode 异常处理差异