eBPF 程序本身只是一段逻辑——它需要挂载到内核的某个检查点(Hook 点)才能被触发执行。Hook 点决定了 eBPF 程序何时运行、能看到什么数据、能做什么操作。理解不同的 Hook 机制,是选择正确的 eBPF 程序类型和编写有效追踪程序的基础。
本章将深入三大 Hook 机制:kprobe(动态内核函数追踪)、tracepoint(静态追踪点)、uprobe(用户态函数追踪),以及 USDT(用户态静态追踪点)。
一、Hook 点全景
1.1 Hook 点分类
1.2 Hook 点对比
| Hook 类型 | 稳定性 | 性能 | 灵活性 | 适用场景 |
|---|---|---|---|---|
| kprobe | 低(依赖内核函数名) | 中 | 高(任意内核函数) | 调试、快速验证 |
| tracepoint | 高(稳定 ABI) | 高 | 中(预定义追踪点) | 生产可观测性 |
| uprobe | 中(依赖函数名) | 中 | 高(任意用户函数) | 应用层追踪 |
| USDT | 高(稳定标记) | 高 | 中(预定义标记) | 应用性能分析 |
| XDP | 高 | 极高 | 中(网络专用) | 网络数据包处理 |
| TC | 高 | 高 | 中(网络专用) | 流量控制 |
| LSM | 高 | 中 | 中(安全专用) | 安全策略 |
二、kprobe:动态内核函数追踪
2.1 kprobe 的工作原理
kprobe 是 Linux 内核的动态追踪机制,允许在几乎任意内核函数的入口和返回点插入探测:
kprobe 的实现机制:
- 内联替换:将目标函数的第一条指令替换为
INT3(x86 断点指令) - 断点处理:CPU 执行到
INT3时触发异常,进入 kprobe 处理 - 单步执行:执行原始指令(保存后恢复)
- 返回探测:在函数返回时触发 kretprobe
2.2 kprobe eBPF 程序
#include <linux/bpf.h>#include <bpf/bpf_helpers.h>#include <bpf/bpf_tracing.h>
struct event { u32 pid; int ret; char comm[16];};
struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024);} events SEC(".maps");
// kprobe:在函数入口追踪SEC("kprobe/do_sys_openat2")int BPF_KPROBE(trace_open_entry, int dfd, const char *filename, int flags){ u32 pid = bpf_get_current_pid_tgid() >> 32;
// 过滤:只追踪 PID 为 1234 的进程 if (pid != 1234) return 0;
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0); if (!e) return 0;
e->pid = pid; bpf_get_current_comm(&e->comm, sizeof(e->comm)); bpf_probe_read_kernel_str(&e->filename, sizeof(e->filename), filename);
bpf_ringbuf_submit(e, 0); return 0;}
// kretprobe:在函数返回追踪SEC("kretprobe/do_sys_openat2")int BPF_KRETPROBE(trace_open_return, int ret){ u32 pid = bpf_get_current_pid_tgid() >> 32; if (pid != 1234) return 0;
// ret 是函数返回值 // 正数 = fd,负数 = 错误码 if (ret < 0) { // 记录打开失败 } return 0;}
char LICENSE[] SEC("license") = "GPL";2.3 kprobe 的优缺点
| 优点 | 缺点 |
|---|---|
| 可以追踪任意内核函数 | 依赖内核函数名,不同版本可能变化 |
| 入口和返回都能追踪 | 函数内联后可能无法追踪 |
| 无需修改内核代码 | 性能开销比 tracepoint 高 |
| 使用简单 | 不属于稳定 ABI,可能随内核版本变化 |
kprobe 依赖内核函数名,而内核函数名不属于稳定的 ABI——不同内核版本可能重命名、内联或删除函数。生产环境优先使用 tracepoint。详见第 6 章:CO-RE 了解如何处理内核版本兼容性。
三、tracepoint:静态追踪点
3.1 tracepoint 的工作原理
tracepoint 是内核开发者预先定义的静态追踪点,通过 TRACE_EVENT 宏声明:
// 内核中定义 tracepoint 示例TRACE_EVENT_FN(sys_enter_openat, TP_PROTO(struct pt_regs *regs, int dfd, const char __user *filename, int flags), TP_ARGS(regs, dfd, filename, flags),
TP_STRUCT__entry( __field(int, __syscall_nr) __field(int, dfd) __string(filename, filename) __field(int, flags) ),
TP_fast_assign( __entry->__syscall_nr = syscall_get_nr(current, regs); __entry->dfd = dfd; __assign_str(filename, filename); __entry->flags = flags; ),
TP_printk("dfd=%d filename=%s flags=%x", __entry->dfd, __get_str(filename), __entry->flags));3.2 tracepoint vs kprobe
3.3 tracepoint eBPF 程序
#include <linux/bpf.h>#include <bpf/bpf_helpers.h>
struct event { u32 pid; int dfd; int flags; char comm[16]; char filename[256];};
struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024);} events SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_openat")int trace_openat(struct trace_event_raw_sys_enter *ctx){ struct event *e;
e = bpf_ringbuf_reserve(&events, sizeof(*e), 0); if (!e) return 0;
e->pid = bpf_get_current_pid_tgid() >> 32; e->dfd = ctx->args[0]; // dfd 参数 e->flags = ctx->args[2]; // flags 参数 bpf_get_current_comm(&e->comm, sizeof(e->comm));
// 读取 filename 参数(用户态指针) const char *filename = (const char *)ctx->args[1]; bpf_probe_read_user_str(&e->filename, sizeof(e->filename), filename);
bpf_ringbuf_submit(e, 0); return 0;}
char LICENSE[] SEC("license") = "GPL";3.4 查看系统中的 tracepoint
# 列出所有可用的 tracepointsudo bpftool perf list
# 或者查看 tracepoint 文件系统ls /sys/kernel/debug/tracing/events/# bpf/ block/ cgroup/ cpuhp/ exceptions/ ext4/ filemap/# f2fs/ fs_dax/ ftrace/ huge_memory/ i2c/ initcall/# iomap/ irq/ irq_vectors/ jbd2/ kmem/ libata/# mce/ mdio/ migrate/ mmap/ module/ napi/# net/ numa/ oom/ pagemap/ power/ printk/# ras/ raw_syscalls/ rcu/ regmap/ regulator/ rpm/# rseq/ rtc/ sched/ scsi/ signal/ skb/# smbus/ sock/ spi/ sunrpc/ swiotlb/ syscalls/# task/ tcp/ thermal/ timer/ tlb/ udp/ vmscan/# vsyscall/ workqueue/ writeback/ xdp/ xfs/
# 查看特定 tracepoint 的格式sudo cat /sys/kernel/debug/tracing/events/syscalls/sys_enter_openat/format3.5 常用 tracepoint 分类
| 类别 | tracepoint 路径 | 用途 |
|---|---|---|
| 系统调用 | syscalls/sys_enter_* | 追踪系统调用 |
| 调度 | sched/sched_switch | 进程切换 |
| 调度 | sched/sched_process_exec | 进程执行 |
| 调度 | sched/sched_process_fork | 进程创建 |
| 网络 | net/netif_rx | 网络收包 |
| 网络 | tcp/tcp_probe | TCP 状态 |
| 文件系统 | ext4/ext4_readpage | 文件读取 |
| 内存 | vmscan/mm_vmscan_direct_reclaim | 内存回收 |
| 块设备 | block/block_rq_issue | I/O 请求 |
四、uprobe:用户态函数追踪
4.1 uprobe 的工作原理
uprobe 是用户态程序的动态追踪机制,允许在任意用户态函数的入口和返回点插入探测:
4.2 uprobe eBPF 程序
#include <linux/bpf.h>#include <bpf/bpf_helpers.h>#include <bpf/bpf_tracing.h>
struct event { u32 pid; u64 duration_ns; char comm[16];};
struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024);} events SEC(".maps");
// 记录函数进入时间struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 10240); __type(key, u32); // TID __type(value, u64); // 入口时间戳} start_times SEC(".maps");
// uprobe:在函数入口记录时间SEC("uprobe")int BPF_UPROBE(trace_func_entry){ u64 pid_tgid = bpf_get_current_pid_tgid(); u32 tid = (u32)pid_tgid; u64 ts = bpf_ktime_get_ns();
bpf_map_update_elem(&start_times, &tid, &ts, BPF_ANY); return 0;}
// uretprobe:在函数返回计算耗时SEC("uretprobe")int BPF_URETPROBE(trace_func_return){ u64 pid_tgid = bpf_get_current_pid_tgid(); u32 tid = (u32)pid_tgid; u64 *start_ts;
start_ts = bpf_map_lookup_elem(&start_times, &tid); if (!start_ts) return 0;
u64 duration = bpf_ktime_get_ns() - *start_ts; bpf_map_delete_elem(&start_times, &tid);
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0); if (!e) return 0;
e->pid = pid_tgid >> 32; e->duration_ns = duration; bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_ringbuf_submit(e, 0); return 0;}
char LICENSE[] SEC("license") = "GPL";4.3 挂载 uprobe
# 使用 bpftool 挂载 uprobe# 格式:binary:offset 或 binary:function_name
# 方式一:通过函数名(需要符号表)sudo bpftool prog load uprobe.bpf.o /sys/fs/bpf/uprobe \ type uprobe binary /usr/bin/myapp func my_function
# 方式二:通过偏移量sudo bpftool prog load uprobe.bpf.o /sys/fs/bpf/uprobe \ type uprobe binary /usr/bin/myapp offset 0x5d6
# 使用 bpftrace 挂载 uprobe(更简单)sudo bpftrace -e 'uprobe:/usr/bin/myapp:my_function { printf("called by %s\n", comm);}'4.4 uprobe 的限制
| 限制 | 说明 |
|---|---|
| 需要符号表 | 函数名必须在二进制文件的符号表中 |
| 内联函数 | 无法追踪被内联的函数 |
| 偏移计算 | 不同编译版本的偏移可能不同 |
| 性能开销 | 比 kprobe 更高(需要唤醒目标进程) |
| 多线程 | 同一函数被多线程调用时需注意并发 |
五、USDT:用户态静态追踪点
5.1 USDT 的工作原理
USDT(User Statically Defined Tracing)是开发者主动埋入的静态追踪点,类似于内核的 tracepoint:
// 在 C 程序中定义 USDT 探针#include <sys/sdt.h> // SystemTap DTrace 头文件
void handle_request(struct request *req){ // 标记函数入口 DTRACE_PROBE2(myapp, request_start, req->id, req->type);
// 处理请求 process_request(req);
// 标记函数完成 DTRACE_PROBE2(myapp, request_done, req->id, req->status);}5.2 USDT eBPF 程序
#include <linux/bpf.h>#include <bpf/bpf_helpers.h>
struct event { u32 pid; u64 request_id; int request_type;};
struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024);} events SEC(".maps");
SEC("usdt/myapp/request_start")int trace_request_start(struct pt_regs *ctx){ struct event *e;
e = bpf_ringbuf_reserve(&events, sizeof(*e), 0); if (!e) return 0;
e->pid = bpf_get_current_pid_tgid() >> 32; // USDT 参数通过 bpf_usdt_read_arg 读取 bpf_usdt_read_arg(1, ctx, &e->request_id); // 第1个参数 bpf_usdt_read_arg(2, ctx, &e->request_type); // 第2个参数
bpf_ringbuf_submit(e, 0); return 0;}
char LICENSE[] SEC("license") = "GPL";5.3 查看 USDT 探针
# 使用 bpftrace 列出二进制中的 USDT 探针sudo bpftrace -l 'usdt:/usr/bin/myapp:*'
# 使用 readelf 查看 .note.stapsdt 段readelf -n /usr/bin/myapp | grep stapsdt
# 使用 tplist(BCC 工具)tplist -l /usr/bin/myapp5.4 常见软件的 USDT 探针
| 软件 | USDT 探针 | 用途 |
|---|---|---|
| MySQL | query__start, query__done | SQL 查询追踪 |
| PostgreSQL | query__start, query__done | SQL 查询追踪 |
| Nginx | http__request__start | HTTP 请求追踪 |
| Python | function__entry, function__return | Python 函数追踪 |
| Node.js | http__server__request | HTTP 请求追踪 |
| Java | method__entry, method__return | Java 方法追踪 |
| Ruby | method__entry, method__return | Ruby 方法追踪 |
六、Hook 点选择指南
6.1 决策树
6.2 性能开销对比
| Hook 类型 | 每次触发开销 | 适用频率 |
|---|---|---|
| tracepoint | ~1-2μs | 百万级/秒 |
| kprobe | ~3-5μs | 十万级/秒 |
| uprobe | ~5-10μs | 万级/秒 |
| USDT | ~1-2μs | 百万级/秒 |
| XDP | ~0.1-0.5μs | 千万级/秒 |
七、动手实践
7.1 使用 bpftrace 追踪内核函数
# 追踪 do_sys_openat2 的调用(kprobe)sudo bpftrace -e 'kprobe:do_sys_openat2 { printf("PID=%d COMM=%s\n", pid, comm);}'
# 追踪 do_sys_openat2 的返回值(kretprobe)sudo bpftrace -e 'kretprobe:do_sys_openat2 { printf("ret=%d\n", retval);}'
# 追踪系统调用(tracepoint)sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s: %s\n", comm, str(args->filename));}'7.2 追踪用户态函数
# 追踪 bash 的 readline 函数sudo bpftrace -e 'uprobe:/bin/bash:readline { printf("%s typed: %s\n", comm, str(retval));}'
# 追踪 malloc 调用sudo bpftrace -e 'uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc { printf("malloc(%d) by %s\n", arg0, comm);}'7.3 使用 libbpf 挂载 kprobe
// 在用户态程序中挂载 kprobe#include <bpf/libbpf.h>
int main(){ struct myprog_bpf *skel;
skel = myprog_bpf__open_and_load(); if (!skel) return 1;
// 附加 kprobe skel->links.trace_open_entry = bpf_program__attach_kprobe(skel->progs.trace_open_entry, false, // kprobe (not kretprobe) "do_sys_openat2"); if (!skel->links.trace_open_entry) { fprintf(stderr, "Failed to attach kprobe\n"); return 1; }
// 事件循环... while (1) { ring_buffer__poll(rb, 100); }
myprog_bpf__destroy(skel); return 0;}Hook 点的选择直接影响系统性能。kretprobe 比 kprobe 开销更大(需要跟踪返回地址),tracepoint 比 kprobe 更稳定但覆盖范围有限。生产环境优先选择 tracepoint,仅在 tracepoint 不可用时退而使用 kprobe。
八、本章小结
上一章探讨了eBPF Map 数据结构。 本章详解了 eBPF 的三大 Hook 机制:
| 主题 | 核心要点 | 关键词 |
|---|---|---|
| kprobe | 动态追踪任意内核函数,灵活但不稳定,适合调试和快速验证 | kprobe |
| tracepoint | 静态追踪点,稳定 ABI,高效,适合生产环境可观测性 | tracepoint |
| uprobe | 追踪用户态函数,灵活但开销较高,适合应用层追踪 | uprobe |
| USDT | 用户态静态追踪点,稳定高效,需要开发者主动埋点 | USDT |
选择 Hook 点的核心原则:优先使用静态追踪点(tracepoint/USDT),仅在必要时使用动态追踪(kprobe/uprobe)。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






