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

周报(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 完全不同。

解决方案

  1. 在 arm_vcpu 的 current_el_sync_handler 中添加 BRK 指令识别逻辑
  2. 检查 ESR_EL2.EC == 0x3C(BRK from AArch64)
  3. 将 TrapFrame 指针和大小传递给 axebpf 的 kprobe handler
  4. 通过回调函数更新 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实现函数返回探针的栈帧管理
P1axvm 追踪点插桩待 axvm 重构后添加 vCPU 运行时追踪点
P2x86_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
└─────────────────┘

执行流程

  1. CPU 执行到 BRK #4,触发同步异常(EC=0x3C, ISS=0x4)
  2. 异常处理器执行 eBPF 程序,然后将 PC 设置为指令槽地址
  3. 异常返回后执行指令槽中的原始指令
  4. 执行到 BRK #6,再次触发异常(EC=0x3C, ISS=0x6)
  5. 异常处理器将 PC 设置为原始函数的下一条指令(original_pc + 4)
  6. 异常返回,继续正常执行

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_EL1ESR_EL2, FAR_EL2
异常向量表VBAR_EL1VBAR_EL2
上下文结构内核 pt_regsHypervisor TrapFrame
页表基址寄存器TTBR0_EL1/TTBR1_EL1TTBR0_EL2
任务上下文current_taskper-CPU 状态
追踪目标内核函数VMM 代码路径

适配要点

  1. 异常处理入口不同,需要在 arm_vcpu 的 current_el_sync_handler 中添加 BRK 识别逻辑
  2. TrapFrame 结构不同,需要正确提取 PC 和传递寄存器上下文给 eBPF 程序
  3. 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 收获

技术收获

  1. 软件单步机制:深入理解了 kprobe 使用双 BRK 指令实现软件单步的原理,比硬件单步更简单可控。

  2. Trait 抽象设计:学习了 KprobeAuxiliaryOps trait 如何将平台相关操作抽象出来,使 kprobe 库可以跨平台复用。

  3. 页表动态修改:掌握了运行时修改 .text 段权限的方法,包括页表项修改、TLB 刷新、I-cache 刷新的完整流程。

  4. AArch64 异常模型:深入理解了 EL2 异常处理流程,ESR_EL2.EC/ISS 字段编码,以及异常返回机制。

  5. Per-CPU 编程模式:理解了在无任务抽象的 Hypervisor 环境中如何管理执行状态。

工程收获

  1. 跨 crate feature 管理:学习了 Cargo feature 的依赖传递机制,理解了 feature = ["dep/feature"] 语法的作用。

  2. 链接器脚本定制:掌握了 linker section 的定义方法,理解了如何为指令槽预留可执行内存区域。

待深入方向

  • Kretprobe trampoline 机制:如何劫持函数返回地址并在返回时执行 eBPF 程序
  • 递归 kprobe 的重入保护:当 eBPF 程序或 helper 函数本身触发 kprobe 时如何避免死循环
  • x86_64 适配:INT3 单字节指令的处理、VMX root mode 异常处理差异