Featured image of post eBPF例程——内核态函数跟踪

eBPF例程——内核态函数跟踪

基于libbpf编写的eBPF程序,用于追踪的是内核里当删除文件时会调用的do_unlinkat()函数,案例一演示了如何使用fentry/fexit来追踪Linux内核函数的调用和返回,案例二演示了如何使用kprobe/kretprobe来追踪Linux内核函数的调用和返回。

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;
}
Licensed under CC BY-NC-SA 4.0
皖ICP备2025083746号-1
公安备案 陕公网安备61019002003315号



使用 Hugo 构建
主题 StackJimmy 设计