Post

kprobe 工作原理:函数内部插桩

kprobe 工作原理:函数内部插桩

kprobe 工作原理:函数内部插桩

本文解释了 kprobe 能够对函数内几乎任何指令地址(而不止是入口点)进行触发的底层机制。

核心原理:动态指令替换

其基本技术类似于调试器(如 GDB)设置断点的方法:在内存中修改可执行代码,插入一个陷阱(Trap)。

执行流程详解 (以 x86_64 为例)

假设我们要对 ip_rcv 函数偏移量 +4 的位置进行插桩。

1. 准备阶段 (注册)

  • 定位:内核解析目标地址(例如 0xffffffff81000004)。
  • 校验:内核检查该地址是否为有效的指令边界。
  • 备份:将该地址处的原始指令(例如 sub rsp, 0x10)复制并保存到专门的缓冲区(称为 “instruction slot”)。

2. 替换阶段 (打点)

  • 内核将目标地址处的指令替换为断点指令
  • x86_640xCC (INT3)。
  • ARM64BRK

3. 触发阶段 (陷阱)

  • 当 CPU 执行到该地址命中 INT3 时,会暂停正常执行。
  • 触发 断点异常 (#BP)
  • 控制权转移给内核的异常处理程序(do_int3)。

4. 回调执行

  • 处理程序识别出该异常是由已注册的 kprobe 引起的。
  • 保存当前 CPU 寄存器状态 (pt_regs)。
  • 执行关联的 eBPF 程序(或其他内核处理函数)。

5. 恢复原始指令 (单步执行)

这是最关键的部分:如何在不破坏逻辑的情况下运行被覆盖掉的原始指令 (sub rsp, 0x10)?

  • 非原位执行 (Out-of-Line):内核将 CPU 的指令指针 (RIP) 指向之前保存原始指令的备份缓冲区
  • 单步模式:内核开启 CPU 的陷阱标志位 (TF)。
  • CPU 执行缓冲区中的那一条原始指令。

6. 返回正常流程

  • 执行完那条指令后,陷阱标志位触发另一个异常(调试异常)。
  • 内核捕获此异常,关闭陷阱标志位。
  • 内核计算原函数中下一条指令的地址。
  • 内核将 RIP 设置回原函数流,继续执行,就像什么都没发生过一样。

可视化对比

插桩前:

0x100: push rbp
0x101: mov rbp, rsp
0x104: sub rsp, 0x10  <-- 目标位置
0x108: mov rax, 1

插桩后:

0x100: push rbp
0x101: mov rbp, rsp
0x104: INT3           <-- 被替换为 0xCC
0x105: ...
0x108: mov rax, 1

为什么它比较慢?

如上所述,单次 kprobe 触发涉及:

  1. 异常 1:断点 (INT3)。
  2. 上下文切换:保存寄存器。
  3. 执行:运行 eBPF。
  4. 上下文切换:恢复寄存器并跳转到缓冲区。
  5. 异常 2:单步跟踪陷阱。
  6. 上下文切换:跳回原代码。

这种复杂的交互过程导致 kprobe 的开销(约微秒级)显著高于 fentry(约纳秒级),后者使用简单的直接函数调用机制。

This post is licensed under CC BY 4.0 by the author.