uprobe与uretprobe
uprobe(用户态探针)和 uretprobe(用户态返回探针)是eBPF和Linux tracing中用于追踪用户态程序执行的两种机制:
- uprobe会在指定的用户态函数入口处插入探针,函数一调用就触发eBPF程序,常用于收集参数、触发计数或打印日志;
- uretprobe 则在该函数返回时触发,可获取返回值和执行结果。
运行eBPF程序
新建终端编译、运行eBPF程序,该程序会跟踪用户态程序内的入口和返回,将信息输出到内核的缓冲区。
1
2
3
|
cd libbpf-bootstrap/examples/c/
make uprobe
sudo ./uprobe
|

新建终端使用cat命令实时查看输出的信息。
1
|
sudo cat /sys/kernel/debug/tracing/trace_pipe
|

eBPF程序:uprobe.bpf.c
BPF_KPROBE(name, args…)宏声明一个以name为符号名的eBPF 程序,该程序作为内核kprobe或用户态uprobe的钩子入口,自动从上下文寄存器中提取被跟踪函数的参数,方便用户直接使用函数参数编写跟踪逻辑。例如在下面代码中:定义了一个名为uprobe_add的uprobe钩子入口函数, 宏将自动从寄存器(ctx)读取被跟踪函数的参数a、b,并传给函数体uprobe_add处理。BPF_KRETPROBE宏与之同理。
SEC(“uprobe”)指示是一个uprobe类型的eBPF程序,同理SEC(“uretprobe”)指示是一个uretprobe类型的eBPF程序,由于它们没有明确给出附加信息(跟踪哪个函数),后续必须在用户态程序里手动指定附加对象。相反,SEC(“uprobe//proc/self/exe:uprobed_sub”)明确指出了附加信息,因此在用户态程序里能自动完成附加。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
// 指示是一个uprobe类型的eBPF程序
SEC("uprobe")
int BPF_KPROBE(uprobe_add, int a, int b)
{
// 在/sys/kernel/debug/tracing/trace_pipe中输出日志
bpf_printk("uprobed_add ENTRY: a = %d, b = %d", a, b);
return 0;
}
// 指示是一个uretprobe类型的eBPF程序
SEC("uretprobe")
int BPF_KRETPROBE(uretprobe_add, int ret)
{
// 在/sys/kernel/debug/tracing/trace_pipe中输出日志
bpf_printk("uprobed_add EXIT: return = %d", ret);
return 0;
}
// 指示是一个uprobe类型的eBPF程序
// 同时明确指出附加(attach)信息:
// /proc/[pid]/exe是进程可执行文件的符号链接,即要跟踪的程序
// /proc/self/exe为当前进程自己的可执行文件
// uprobed_sub为该可执行文件内的符号名,即要跟踪的函数
SEC("uprobe//proc/self/exe:uprobed_sub")
int BPF_KPROBE(uprobe_sub, int a, int b)
{
// 在/sys/kernel/debug/tracing/trace_pipe中输出日志
bpf_printk("uprobed_sub ENTRY: a = %d, b = %d", a, b);
return 0;
}
// 指示是一个uretprobe类型的eBPF程序,同时明确指出附加(attach)信息
SEC("uretprobe//proc/self/exe:uprobed_sub")
int BPF_KRETPROBE(uretprobe_sub, int ret)
{
// 在/sys/kernel/debug/tracing/trace_pipe中输出日志
bpf_printk("uprobed_sub EXIT: return = %d", ret);
return 0;
}
|
补充说明:ELF文件与符号
程序编译、链接后生成最终的可执行文件,而ELF(Executable and Linkable Format)文件是Linux默认的可执行文件二进制格式,它包含机器指令、符号表、段等信息。符号表作为ELF文件的重要组成部分,用于记录函数和全局变量等符号的名称与在文件中的地址(偏移量)对应关系。
在Linux内核的/proc文件系统中,每个运行中的进程(例如/home/user/app)都会对应一个以进程PID命名的目录(例如/proc/1234/)。该目录包含若干特殊文件,如:exe(一个符号链接,指向当前进程运行的ELF可执行文件)、cmdline(记录进程的启动参数)、fd目录(保存进程当前打开的文件描述符)。其中self是一个特殊的符号链接,永远指向当前进程自己的/proc/pid目录。
用户态程序:uprobe.c
程序中有uprobed_add()和uprobed_sub()这两个用户态函数,主程序循环中反复调用这两个函数。同时加载BPF程序跟踪这两个函数的函数进入和函数返回。
若在eBPF程序中,使用SEC宏声明了附加(attach)的信息,则调用uprobe_bpf__attach()函数即可自动完成eBPF程序的附加。否则需要手动调用bpf_program__attach_uprobe_opts()函数完成eBPF程序的附加,其参数如下:
- 第一个参数指定了要附加的 eBPF 程序;
- 第二个参数自动要附加的进程:pid=0表示只附加到当前进程;PID=-1表示在该ELF文件的固定偏移处插入探针,内核会自动对当前以及将来运行的所有进程都生效,但实际上 ELF 文件本身并不会被修改,只是在内核中注册了一个探针;若传入大于0的具体 pid,则只附加到该指定进程上;
- 第三个参数传入当前进程的可执行文件路径;
- 第四个参数传入符号的相对偏移量:如果在随后传入的第五个参数中指定了符号名func_name,libbpf会自动根据ELF符号表解析出相对偏移,因此可以简单的传入0;若没有指定符号名,就需要直接传入指定函数的相对偏移;
- 第五个参数是附加选项结构体:可以指定具体要附加的符号名func_name和是否附加到函数返回点retprobe。
在C/C++里,如果一个函数比较小、简单,编译器(比如GCC、Clang)可能会自动把它内联,即把函数体的代码直接拷贝到调用处。同时编译器可能因为函数内的逻辑什么都不做,就整个把函数体优化掉(比如直接返回结构)。此时就无法被eBPF程序追踪,因此被追踪的函数需要强制禁止内联与函数体优化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
|
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "uprobe.skel.h"
// libbpf日志的回调函数
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
return vfprintf(stderr, format, args);
}
// 被跟踪的用户态函数uprobed_add
// __attribute__((noinline))可以保证编译器不会内联
__attribute__((noinline)) int uprobed_add(int a, int b)
{
// 防止编译器优化掉整个函数体
asm volatile ("");
return a + b;
}
// 被跟踪的用户态函数uprobed_sub
__attribute__((noinline)) int uprobed_sub(int a, int b)
{
asm volatile ("");
return a - b;
}
int main(int argc, char **argv)
{
struct uprobe_bpf *skel;
int err, i;
LIBBPF_OPTS(bpf_uprobe_opts, uprobe_opts);
// 设置libbpf日志的回调函数
libbpf_set_print(libbpf_print_fn);
// 载入eBPF程序
skel = uprobe_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
// 附加到uprobed_add函数
uprobe_opts.func_name = "uprobed_add";
// 设置false表示附加到uprobe,即函数入口处
uprobe_opts.retprobe = false;
// 将eBPF程序附加到uprobed_add函数的入口处
skel->links.uprobe_add = bpf_program__attach_uprobe_opts(
skel->progs.uprobe_add,
0,
"/proc/self/exe",
0,
&uprobe_opts
);
if (!skel->links.uprobe_add) {
err = -errno;
fprintf(stderr, "Failed to attach uprobe: %d\n", err);
goto cleanup;
}
// 附加到uprobed_add函数
uprobe_opts.func_name = "uprobed_add";
// 设置true表示附加到retprobe,即函数返回处
uprobe_opts.retprobe = true;
// 将eBPF程序附加到uprobed_add函数的返回处
skel->links.uretprobe_add = bpf_program__attach_uprobe_opts(
skel->progs.uretprobe_add,
-1,
"/proc/self/exe",
0,
&uprobe_opt
);
if (!skel->links.uretprobe_add) {
err = -errno;
fprintf(stderr, "Failed to attach uprobe: %d\n", err);
goto cleanup;
}
// libbpf可以自动完成为uprobed_sub函数的附加
err = uprobe_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to auto-attach BPF skeleton: %d\n", err);
goto cleanup;
}
printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
"to see output of the BPF programs.\n");
// 主循环反复调用函数uprobed_add与uprobed_sub
// 触发uprobe/uretprobe
for (i = 0;; i++) {
fprintf(stderr, ".");
uprobed_add(i, i + 1);
uprobed_sub(i * i, i);
sleep(1);
}
cleanup:
// 卸载BPF程序、清理环境
uprobe_bpf__destroy(skel);
return -err;
}
|
USDT
USDT(User Statically Defined Tracepoints)是一种用户态静态追踪点机制,允许开发者在应用程序源码中显式埋点,在编译时生成探针信息写入ELF二进制文件。运行时,eBPF等工具可以根据这些探针信息直接附加,而无需知道具体偏移或函数地址。相比uprobe,USDT更适合生产环境和长期观测,因为它由开发者预先设计,能更精准地反映关键业务逻辑,并支持传递自定义参数供追踪程序读取。
运行eBPF程序
新建终端编译、运行eBPF程序,该程序会跟踪用户态中的静态探针,并将信息输出。
1
2
3
|
cd libbpf-bootstrap/examples/c/
make usdt
sudo ./usdt
|

新建终端使用cat命令实时查看输出的信息。
1
|
sudo cat /sys/kernel/debug/tracing/trace_pipe
|

查询USDT
要跟踪的setjmp是libc里的函数,因此在指定的libc.so.6里列出所有USDT探针,确认是否存在setjump相关的探针。
1
|
sudo bpftrace -l 'usdt:/usr/lib/x86_64-linux-gnu/libc.so.6:*' | grep setjmp
|

读取libc.so.6文件中的NOTE段内容,NOTE段通常包含调试信息、静态探针描述等元数据。其中Provider标识程序名、Name标识探测点的名称、Location记录了探测点的位置(在程序中的地址偏移)、Arguments为探针的参数列表(格式为<大小>@<寄存器或内存位置>)。
1
|
readelf --notes /usr/lib/x86_64-linux-gnu/libc.so.6 | grep -A10 setjmp
|

参数列表中:
- 8@%rdi:第一个参数占8字节,位于%rdi寄存器;
- -4@%esi:第二个参数占4字节,位于%esi寄存器;
- 8@%rax:第三个参数占8字节,位于%rax寄存器。
eBPF程序:usdt.bpf.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/usdt.bpf.h>
// 定义一个全局变量,用于记录要跟踪的进程
pid_t my_pid = 0;
// 指示libbpf将该eBPF程序自动附加到libc.so.6里名为libc:setjmp的USDT探针
// libc:setjmp中的libc为提供者(provider)、setjump为探针名称(name)
// 该eBPF程序的函数签名为usdt_auto_attach
SEC("usdt/libc.so.6:libc:setjmp")
int BPF_USDT(usdt_auto_attach, void *arg1, int arg2, void *arg3)
{
pid_t pid = bpf_get_current_pid_tgid() >> 32;
if (pid != my_pid)
return 0;
bpf_printk("USDT auto attach to libc:setjmp: arg1 = %lx, arg2 = %d, arg3 = %lx", arg1, arg2, arg3);
return 0;
}
// 指示该eBPF程序将附加到USDT探针,由于没有明确指示附加到哪,需要在用户态程序中手动指定
// 该eBPF程序的函数签名为usdt_manual_attach
SEC("usdt")
int BPF_USDT(usdt_manual_attach, void *arg1, int arg2, void *arg3)
{
bpf_printk("USDT manual attach to libc:setjmp: arg1 = %lx, arg2 = %d, arg3 = %lx", arg1, arg2, arg3);
return 0;
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";
|
用户态程序:usdt.c
用户态进程周期性执行setjump,进而触发附加的eBPF程序。jmp_buf是一个用来存储进程当前执行上下文的数据结构,setjmp()函数则负责将该上下文保存到该数据结构。
eBPF程序附加到USDT探针同样支持自动附加与手动附加两种形式,手动附加的方式需要使用bpf_program__attach_usdt()函数,其参数与uprobe基本一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
|
#include <signal.h>
#include <unistd.h>
#include <setjmp.h>
#include <linux/limits.h>
#include "usdt.skel.h"
// 标识程序是否需要退出
static volatile sig_atomic_t exiting;
// 存储进程当前的上下文
static jmp_buf env;
// 接收到退出信号后的回调函数
static void sig_int(int signo)
{
exiting = 1;
}
// libbpf日志打印的回调函数
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
return vfprintf(stderr, format, args);
}
// 将当前上下文进行保存
static void usdt_trigger()
{
setjmp(env);
}
int main(int argc, char **argv)
{
struct usdt_bpf *skel;
int err;
// 设置libbpf的日志打印回调函数
libbpf_set_print(libbpf_print_fn);
// 载入eBPF程序
skel = usdt_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
// 为eBPF程序中的my_pid变量赋值
skel->bss->my_pid = getpid();
// 加载eBPF程序
err = usdt_bpf__load(skel);
if (!skel) {
fprintf(stderr, "Failed to load BPF skeleton\n");
return 1;
}
// 为eBPF程序usdt_manual_attach进行手动附加
// 参数一:要附加的eBPF程序
// 参数二:要附加的进程为本进程
// 参数三:进程的可执行文件路径
// 参数四:USDT探针的提供者名字
// 参数五:通常为NULL
skel->links.usdt_manual_attach = bpf_program__attach_usdt(
skel->progs.usdt_manual_attach,
getpid(),
"libc.so.6",
"libc",
"setjmp",
NULL
);
if (!skel->links.usdt_manual_attach) {
err = errno;
fprintf(stderr, "Failed to attach BPF program `usdt_manual_attach`\n");
goto cleanup;
}
// eBPF程序usdt_auto_attach已声明附加的USDT探针,自动完成附加
err = usdt_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
// 注册信号处理回调函数sig_int
if (signal(SIGINT, sig_int) == SIG_ERR) {
err = errno;
fprintf(stderr, "can't set signal handler: %s\n", strerror(errno));
goto cleanup;
}
printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
"to see output of the BPF programs.\n");
// 主程序循环运行以触发setjump的调用
while (!exiting) {
usdt_trigger();
fprintf(stderr, ".");
sleep(1);
}
cleanup:
// 退出eBPF程序、清理eBPF环境
usdt_bpf__destroy(skel);
return -err;
}
|