mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1421 字
4 分钟
eBPF Hook 点:kprobe/tracepoint/uprobe
2026-04-25

eBPF 程序本身只是一段逻辑——它需要挂载到内核的某个检查点(Hook 点)才能被触发执行。Hook 点决定了 eBPF 程序何时运行能看到什么数据能做什么操作。理解不同的 Hook 机制,是选择正确的 eBPF 程序类型和编写有效追踪程序的基础。

本章将深入三大 Hook 机制:kprobe(动态内核函数追踪)、tracepoint(静态追踪点)、uprobe(用户态函数追踪),以及 USDT(用户态静态追踪点)。

一、Hook 点全景#

1.1 Hook 点分类#

flowchart TB HOOK["eBPF Hook 点"] HOOK --> KERNEL["内核态 Hook"] HOOK --> USER["用户态 Hook"] HOOK --> NET["网络 Hook"] KERNEL --> KP["kprobe / kretprobe<br/>动态内核函数追踪"] KERNEL --> TP["tracepoint<br/>静态追踪点"] KERNEL --> LSM["LSM Hook<br/>安全检查点"] KERNEL --> PERF["perf_event<br/>性能计数器"] USER --> UP["uprobe / uretprobe<br/>用户态函数追踪"] USER --> USDT["USDT<br/>用户态静态追踪点"] NET --> XDP["XDP<br/>驱动层数据包处理"] NET --> TC["TC<br/>流量控制"] NET --> SKB["Socket / Skb<br/>Socket 操作"] style HOOK fill:#e8eaf6,stroke:#283593 style KERNEL fill:#e3f2fd,stroke:#1565c0 style USER fill:#e8f5e9,stroke:#2e7d32 style NET fill:#fff3e0,stroke:#e65100

1.2 Hook 点对比#

Hook 类型稳定性性能灵活性适用场景
kprobe低(依赖内核函数名)高(任意内核函数)调试、快速验证
tracepoint高(稳定 ABI)中(预定义追踪点)生产可观测性
uprobe中(依赖函数名)高(任意用户函数)应用层追踪
USDT高(稳定标记)中(预定义标记)应用性能分析
XDP极高中(网络专用)网络数据包处理
TC中(网络专用)流量控制
LSM中(安全专用)安全策略

二、kprobe:动态内核函数追踪#

2.1 kprobe 的工作原理#

kprobe 是 Linux 内核的动态追踪机制,允许在几乎任意内核函数的入口和返回点插入探测:

flowchart LR subgraph 内核函数调用 CALL["调用 do_sys_open()"] -->|"1. 触发 kprobe"| KP["kprobe 处理程序<br/>eBPF 程序"] KP -->|"2. 继续执行"| FUNC["do_sys_open() 函数体"] FUNC -->|"3. 触发 kretprobe"| KRP["kretprobe 处理程序<br/>eBPF 程序"] KRP -->|"4. 返回"| RET["返回调用者"] end style KP fill:#bbdefb,stroke:#1565c0 style KRP fill:#c8e6c9,stroke:#2e7d32

kprobe 的实现机制:

  1. 内联替换:将目标函数的第一条指令替换为 INT3(x86 断点指令)
  2. 断点处理:CPU 执行到 INT3 时触发异常,进入 kprobe 处理
  3. 单步执行:执行原始指令(保存后恢复)
  4. 返回探测:在函数返回时触发 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,可能随内核版本变化
Warning

kprobe 依赖内核函数名,而内核函数名不属于稳定的 ABI——不同内核版本可能重命名、内联或删除函数。生产环境优先使用 tracepoint。详见第 6 章:CO-RE 了解如何处理内核版本兼容性。

三、tracepoint:静态追踪点#

3.1 tracepoint 的工作原理#

tracepoint 是内核开发者预先定义的静态追踪点,通过 TRACE_EVENT 宏声明:

include/trace/events/syscalls.h
// 内核中定义 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#

flowchart TB subgraph tracepoint["tracepoint(静态)"] TP_DEF["内核开发者定义<br/>TRACE_EVENT 宏"] --> TP_STABLE["稳定 ABI<br/>参数格式不变"] TP_STABLE --> TP_FAST["快速路径<br/>静态跳转"] end subgraph kprobe["kprobe(动态)"] KP_DYN["运行时插入<br/>INT3 断点"] --> KP_UNSTABLE["不稳定<br/>函数可能变化"] KP_UNSTABLE --> KP_SLOW["较慢<br/>断点异常处理"] end style tracepoint fill:#c8e6c9,stroke:#2e7d32 style kprobe fill:#fff9c4,stroke:#f9a825

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#

# 列出所有可用的 tracepoint
sudo 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/format

3.5 常用 tracepoint 分类#

类别tracepoint 路径用途
系统调用syscalls/sys_enter_*追踪系统调用
调度sched/sched_switch进程切换
调度sched/sched_process_exec进程执行
调度sched/sched_process_fork进程创建
网络net/netif_rx网络收包
网络tcp/tcp_probeTCP 状态
文件系统ext4/ext4_readpage文件读取
内存vmscan/mm_vmscan_direct_reclaim内存回收
块设备block/block_rq_issueI/O 请求

四、uprobe:用户态函数追踪#

4.1 uprobe 的工作原理#

uprobe 是用户态程序的动态追踪机制,允许在任意用户态函数的入口和返回点插入探测:

flowchart LR subgraph 用户态进程 MAIN["main()"] -->|"调用"| MYFUNC["my_function()" "0x4005d6: push rbp"] MYFUNC -->|"返回"| MAIN end subgraph uprobe机制 UP["uprobe<br/>在 0x4005d6 插入断点"] URP["uretprobe<br/>在返回点插入探测"] end MYFUNC -->|"1. 触发"| UP MYFUNC -->|"2. 触发"| URP style UP fill:#bbdefb,stroke:#1565c0 style URP fill:#c8e6c9,stroke:#2e7d32

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/myapp

5.4 常见软件的 USDT 探针#

软件USDT 探针用途
MySQLquery__start, query__doneSQL 查询追踪
PostgreSQLquery__start, query__doneSQL 查询追踪
Nginxhttp__request__startHTTP 请求追踪
Pythonfunction__entry, function__returnPython 函数追踪
Node.jshttp__server__requestHTTP 请求追踪
Javamethod__entry, method__returnJava 方法追踪
Rubymethod__entry, method__returnRuby 方法追踪

六、Hook 点选择指南#

6.1 决策树#

flowchart TD START["选择 Hook 点"] --> Q1{"追踪内核还是用户态?"} Q1 -->|"内核"| Q2{"目标函数有 tracepoint?"} Q1 -->|"用户态"| Q3{"目标有 USDT 探针?"} Q2 -->|"有"| TP["使用 tracepoint<br/>稳定、高效"] Q2 -->|"没有"| Q4{"需要生产环境稳定性?"} Q4 -->|"是"| TP2["寻找相近的 tracepoint<br/>或添加新 tracepoint"] Q4 -->|"否"| KP[" 使用 kprobe<br/>灵活但不稳定"] Q3 -->|"有"| USDT["使用 USDT<br/>稳定、高效"] Q3 -->|"没有"| Q5{"需要生产环境稳定性?"} Q5 -->|"是"| USDT2["添加 USDT 探针到代码"] Q5 -->|"否"| UP[" 使用 uprobe<br/>灵活但不稳定"] style TP fill:#c8e6c9,stroke:#2e7d32 style USDT fill:#c8e6c9,stroke:#2e7d32 style KP fill:#fff9c4,stroke:#f9a825 style UP fill:#fff9c4,stroke:#f9a825

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;
}
Warning

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)

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

eBPF Hook 点:kprobe/tracepoint/uprobe
https://blog.souloss.com/posts/ebpf/hook-points/
作者
Souloss
发布于
2026-04-25
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
eBPF 可观测性
eBPF eBPF 最大的应用场景是可观测性——零侵入、低开销、内核级的全链路追踪。本章详解三大可观测性工具链——bpftrace(一行命令追踪内核)、BCC(Python 前端 + 丰富工具集)、Beyla(零侵入应用性能监控),并通过实战展示性能分析、分布式追踪、应用性能监控的完整工作流。
2
eBPF 与 WebAssembly
eBPF eBPF 提供内核可编程能力,WebAssembly 提供跨平台可移植性——两者的融合会带来什么?本章详解 Wasm-eBPF 项目、用户态 eBPF 运行时、eBPF 程序的 Wasm 封装,以及 eBPF + Wasm 在边缘计算、插件系统、跨平台可观测性中的应用前景。
3
eBPF 网络全景
eBPF eBPF 正在重新定义 Linux 网络栈——从连接跟踪、NAT 到 kube-proxy 替代,从 Socket Filter 到 Sk_msg 重定向,eBPF 提供了比 iptables/netfilter 更高性能、更灵活的网络方案。本章从宏观视角展示 eBPF 网络的全景,详解连接跟踪、NAT、kube-proxy 替代、Socket 层 eBPF,并对比 eBPF 与传统网络方案的架构差异。
4
eBPF 与内存管理
eBPF eBPF 程序的内存使用是生产部署中的关键考量——Map 占用多少内存?eBPF 程序的栈空间有多大?容器环境中的 eBPF 内存如何计费?本章详解 eBPF 的内存模型、Map 内存开销计算、容器内存追踪、eBPF-mm 子系统,以及内存限制下的优化策略。
5
TC:流量控制与 eBPF
eBPF TC(Traffic Control)是 Linux 内核的流量控制子系统,通过 cls_bpf 分类器可以在 TC 层挂载 eBPF 程序,实现灵活的数据包分类、修改和重定向。本章详解 TC eBPF 的架构、ingress/egress 双向处理、direct action 模式、sk_buff 操作,以及 TC 与 XDP 的选择策略。