动态探针让你在任意内核/用户函数上挂钩——不需要重新编译,不需要重启。
一、先看图
flowchart TD
KPROBE[kprobe<br/>内核函数探针] --> INT3[int3 断点<br/>或 ftrace]
UPROBE[uprobe<br/>用户态探针] --> BKPT[断点指令<br/>替换用户代码]
FPROBE[fprobe<br/>基于 ftrace] --> FTRACE[ftrace 框架<br/>批量高效]
INT3 --> HANDLER[回调函数<br/>或 BPF 程序]
BKPT --> HANDLER
FTRACE --> HANDLER
classDef kern fill:#388bfd22,stroke:#388bfd,color:#adbac7;
classDef user fill:#3fb95022,stroke:#3fb950,color:#adbac7;
class KPROBE,INT3,FPROBE,FTRACE kern
class UPROBE,BKPT user
class HANDLER kern
二、kprobe
2.1 原理
- 保存目标地址的原始指令
- 替换为
int3(x86)断点 - 触发时 → 执行 pre_handler → 单步执行原始指令 → 执行 post_handler
2.2 使用
# 通过 tracefs
echo 'p:myprobe do_sys_openat2 filename=+0(%si):string' > \
/sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable
cat /sys/kernel/debug/tracing/trace2.3 kretprobe
echo 'r:myret do_sys_openat2 $retval' > \
/sys/kernel/debug/tracing/kprobe_events捕获函数返回值 → 通过替换返回地址实现。
三、fprobe(推荐)
基于 ftrace → 不需要 int3 → 更高效:
struct fprobe fp = {
.entry_handler = my_handler,
};
register_fprobe(&fp, "do_sys_openat2", NULL);fprobe 支持批量注册 → 适合 BPF 大规模追踪。
四、uprobe
4.1 原理
- 在 ELF 文件中找到目标函数偏移
- 运行时替换为断点指令
- 触发时 → 陷入内核 → 执行回调 → 恢复
4.2 使用
echo 'p:myuprobe /usr/lib/libc.so.6:malloc size=%di' > \
/sys/kernel/debug/tracing/uprobe_events
echo 1 > /sys/kernel/debug/tracing/events/uprobes/myuprobe/enable4.3 USDT
User Statically-Defined Tracing → 应用程序预埋探针:
bpftrace -e 'usdt:/usr/lib/libc.so.6:memory_malloc_retry { printf("retry\n"); }'五、BPF 联动
SEC("kprobe/do_sys_openat2")
int BPF_KPROBE(trace_open, int dfd, struct filename *name)
{
bpf_printk("open: %s\n", name->name);
return 0;
}BPF + kprobe/uprobe 是最常用的动态追踪组合。
六、性能开销
| 探针类型 | 单次开销(x86_64) |
|---|---|
| kprobe (int3) | ~100-200ns |
| fprobe (ftrace) | ~30-50ns |
| uprobe | ~1-3μs |
| tracepoint | ~50-100ns |
uprobe 开销最高 → 涉及用户态/内核态切换。
七、安全限制
# 非 root 用户限制
sysctl kernel.perf_event_paranoid=2 # 限制 perf
sysctl kernel.kptr_restrict=1 # 隐藏内核指针lockdown 模式 → kprobe 可能被禁用。
八、常见陷阱
- kprobe 在内联函数上无效 → 函数被内联后没有入口点
- kretprobe 有最大并发限制(
maxactive) - uprobe 在短生命周期进程上开销大
- 内核更新可能改变函数名/签名
九、观察
cat /sys/kernel/debug/kprobes/list # 已注册 kprobe
cat /sys/kernel/debug/tracing/uprobe_events
# bpftrace 示例
bpftrace -e 'kprobe:vfs_read { @[comm] = count(); }'
bpftrace -e 'kretprobe:vfs_read { @bytes = hist(retval); }'
bpftrace -e 'uprobe:/usr/bin/bash:readline { printf("readline\n"); }'十、小结
- kprobe = 内核函数动态探针(int3 断点)
- fprobe = 基于 ftrace 的高效替代
- uprobe = 用户态函数探针(开销较高)
- USDT = 预埋的用户态静态探针
- BPF + 探针 = 最强大的动态追踪组合
参考文献
kernel/kprobes.ckernel/trace/fprobe.ckernel/events/uprobes.cDocumentation/trace/kprobetrace.rst
工具
bpftracebpftool- tracefs kprobe_events/uprobe_events
上一篇:eBPF 核心 下一篇:kdump 与 crash
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。
【操作系统百科】交换
swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。
【操作系统百科】Slab/SLUB 分配器
buddy 只管页粒度(4K+),内核大多数对象只有几十到几百字节。slab/SLUB 在 buddy 之上做对象级缓存。本文讲 slab 历史、SLUB 接手、SLOB 退场、kmem_cache、per-CPU cache、KASAN 集成。
【操作系统百科】用户态分配器
glibc malloc、tcmalloc、jemalloc、mimalloc 各有哲学。本文讲 arena、thread cache、size class、madvise 返还策略、碎片与 RSS 膨胀、如何根据负载选分配器。