kprobe 工作原理:函数内部插桩
kprobe 工作原理:函数内部插桩
kprobe 工作原理:函数内部插桩
本文解释了 kprobe 能够对函数内几乎任何指令地址(而不止是入口点)进行触发的底层机制。
核心原理:动态指令替换
其基本技术类似于调试器(如 GDB)设置断点的方法:在内存中修改可执行代码,插入一个陷阱(Trap)。
执行流程详解 (以 x86_64 为例)
假设我们要对 ip_rcv 函数偏移量 +4 的位置进行插桩。
1. 准备阶段 (注册)
- 定位:内核解析目标地址(例如
0xffffffff81000004)。 - 校验:内核检查该地址是否为有效的指令边界。
- 备份:将该地址处的原始指令(例如
sub rsp, 0x10)复制并保存到专门的缓冲区(称为 “instruction slot”)。
2. 替换阶段 (打点)
- 内核将目标地址处的指令替换为断点指令。
- x86_64:
0xCC(INT3)。 - ARM64:
BRK。
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:断点 (
INT3)。 - 上下文切换:保存寄存器。
- 执行:运行 eBPF。
- 上下文切换:恢复寄存器并跳转到缓冲区。
- 异常 2:单步跟踪陷阱。
- 上下文切换:跳回原代码。
这种复杂的交互过程导致 kprobe 的开销(约微秒级)显著高于 fentry(约纳秒级),后者使用简单的直接函数调用机制。
This post is licensed under CC BY 4.0 by the author.