fentry/fexit
运行eBPF程序
新建终端编译、运行eBPF程序,该程序会调用bpf_printk()辅助函数,将信息输出到内核的缓冲区。
1
2
3
|
cd libbpf-bootstrap/examples/c/
make fentry
sudo ./fentry
|

新建终端使用cat命令实时查看输出的信息。
1
|
sudo cat /sys/kernel/debug/tracing/trace_pipe
|
再新建终端新建、删除文件,可以观察到文件的删除动作被捕获显示。
1
2
3
|
cd ~
touch test_file
rm -rf test_file
|

eBPF程序:fentry.bpf.c
eBPF程序需要追踪文件的删除操作,其在内核中的相关函数为do_unlinkat()函数,查询Linux源码可获知该函数具有dfd与name两个输入参数、返回值为一个int类型的值。

fentry和fexit是Linux eBPF中专门用于内核函数跟踪的,它们分别在内核函数的入口和出口(返回之前)被调用。fentry 在函数刚开始执行时触发,主要用于记录调用上下文、输入参数或做统计。fexit则在函数返回前触发,可以同时获取输入参数和返回值(kretprobe只能获得返回值),用于分析执行结果、测量延迟等。
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
|
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
// 指示该BPF程序附加到内核函数do_unlinkat的入口
// BPF程序的函数签名为do_unlinkat
// dfd与name是内核函数do_unlinkat的输入参数
SEC("fentry/do_unlinkat")
int BPF_PROG(do_unlinkat, int dfd, struct filename *name)
{
pid_t pid;
pid = bpf_get_current_pid_tgid() >> 32;
// 输出进程PID与要删除的文件名
bpf_printk("fentry: pid = %d, filename = %s", pid, name->name);
return 0;
}
// 指示该BPF程序附加到内核函数do_unlinkat的出口
// BPF程序的函数签名为do_unlinkat_exit
// dfd与name是内核函数do_unlinkat的输入参数、ret是函数的返回值
SEC("fexit/do_unlinkat")
int BPF_PROG(do_unlinkat_exit, int dfd, struct filename *name, long ret)
{
pid_t pid;
pid = bpf_get_current_pid_tgid() >> 32;
// 输出进程PID、要删除的文件名、删除操作的返回状态
bpf_printk("fexit: pid = %d, filename = %s, ret = %ld\n", pid, name->name, ret);
return 0;
}
|
用户态程序:fentry.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
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
|
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "fentry.skel.h"
// libbpf日志输出回调函数
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
return vfprintf(stderr, format, args);
}
// 标识是否停止程序
static volatile sig_atomic_t stop;
// 终止信号回调函数
void sig_int(int signo)
{
stop = 1;
}
int main(int argc, char **argv)
{
struct fentry_bpf *skel;
int err;
// 设置libbpf日志输出的回调函数
libbpf_set_print(libbpf_print_fn);
// 载入并加载eBPF程序
skel = fentry_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
// 根据SEC自动完成eBPF程序的附加
err = fentry_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
// 注册终止信号的回调函数
if (signal(SIGINT, sig_int) == SIG_ERR) {
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");
// 主循环保持程序不退出
while (!stop) {
fprintf(stderr, ".");
sleep(1);
}
cleanup:
// 退出并清理环境
fentry_bpf__destroy(skel);
return -err;
}
|
kprobe/kretprobe
运行eBPF程序
新建终端编译、运行eBPF程序,该程序会调用bpf_printk()辅助函数,将信息输出到内核的缓冲区。
1
2
3
|
cd libbpf-bootstrap/examples/c/
make kprobe
sudo ./kprobe
|

新建终端使用cat命令实时查看输出的信息。
1
|
sudo cat /sys/kernel/debug/tracing/trace_pipe
|
再新建终端新建、删除文件,可以观察到文件的删除动作被捕获显示。
1
2
3
|
cd ~
touch test_file
rm -rf test_file
|

eBPF程序:kprobe.bpf.c
传统的kprobe/kretprobe方式,是通过动态符号表(kallsyms)找到函数地址,在入口或返回处插入探针,但只能通过pt_regs提供的寄存器信息来获取参数,需要程序员手动解析,并且难以保证类型安全。因此在代码中只能用函数bpf_probe_read_kernel()对参数进行安全读取,代码中使用的BPF_CORE_READ()宏即是对该函数的封装。
BTF attach则依赖于内核暴露的BTF类型信息:在加载eBPF程序时,验证器根据目标函数的BTF描述,直接检查eBPF程序签名是否与真实内核函数完全一致,并将函数真实的参数传递给eBPF程序,从而保证类型安全和可维护性。这种机制的典型代表是fentry和fexit,它们可以在函数的入口和出口处精准attach,并且不需要手动解析寄存器或做复杂的偏移计算,内核在运行时会直接把真实的参数和返回值传递给 eBPF 程序。由于省去了寄存器快照和动态解析的开销,BTF attach带来了更低的性能开销和更高的稳定性。
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
|
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
// 指示该BPF程序附加到内核函数do_unlinkat的入口
// BPF程序的函数签名为do_unlinkat
// dfd与name是内核函数do_unlinkat的输入参数
// 注意此处使用的是kprobe方式,宏为BPF_KPROBE
SEC("kprobe/do_unlinkat")
int BPF_KPROBE(do_unlinkat, int dfd, struct filename *name)
{
pid_t pid;
const char *filename;
// 获取当前进程的PID
pid = bpf_get_current_pid_tgid() >> 32;
// 获取要删除的文件名
// 注意kprobe中需要手动读取
// 从指针name指向的结构体filename中,读取它name字段的值
filename = BPF_CORE_READ(name, name);
// 输出进程PID与要删除的文件名
bpf_printk("KPROBE ENTRY pid = %d, filename = %s\n", pid, filename);
return 0;
}
// 指示该BPF程序附加到内核函数do_unlinkat的出口
// BPF程序的函数签名为do_unlinkat_exit
// dfd与name是内核函数do_unlinkat的输入参数、ret是函数的返回值
SEC("kretprobe/do_unlinkat")
int BPF_KRETPROBE(do_unlinkat_exit, long ret)
{
pid_t pid;
pid = bpf_get_current_pid_tgid() >> 32;
// 输出进程PID、要删除的文件名、删除操作的返回状态
bpf_printk("KPROBE EXIT: pid = %d, ret = %ld\n", pid, ret);
return 0;
}
|
用户态程序:kprobe.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
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
|
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "kprobe.skel.h"
// libbpf日志输出回调函数
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
return vfprintf(stderr, format, args);
}
// 标识是否停止程序
static volatile sig_atomic_t stop;
// 终止信号回调函数
static void sig_int(int signo)
{
stop = 1;
}
int main(int argc, char **argv)
{
struct kprobe_bpf *skel;
int err;
// 设置libbpf日志输出的回调函数
libbpf_set_print(libbpf_print_fn);
// 载入并加载eBPF程序
skel = kprobe_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
// 根据SEC自动完成eBPF程序的附加
err = kprobe_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
// 注册终止信号的回调函数
if (signal(SIGINT, sig_int) == SIG_ERR) {
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");
// 主循环保持程序不退出
while (!stop) {
fprintf(stderr, ".");
sleep(1);
}
cleanup:
// 退出并清理环境
kprobe_bpf__destroy(skel);
return -err;
}
|