kprobe 是一个处理内核空间入口探针(kprobe)和出口(返回)探针(kretprobe)的示例。它会将 kprobekretprobe 类型的 BPF 程序分别附加到 do_unlinkat() 函数上,并使用 bpf_printk() 记录进程标识符(PID)、文件名以及返回结果。

运行

1
2
3
$ ./kprobe 
libbpf: loading object 'kprobe_bpf' from buffer
...
1
2
3
$ cat /sys/kernel/debug/tracing/trace_pipe
<...>-47878 [001] ...21 108736.735085: bpf_trace_printk: KPROBE ENTRY pid = 47878, filename = test_file
<...>-47878 [001] ...21 108736.735174: bpf_trace_printk: KPROBE EXIT: pid = 47878, ret = 0
1
2
3
$ cd ~
$ touch test_file
$ rm -rf test_file

The BPF side

SEC(“kprobe/do_unlinkat”)

1
2
3
4
5
6
7
8
9
10
11
SEC("kprobe/do_unlinkat")
int BPF_KPROBE(do_unlinkat, int dfd, struct filename *name)
{
pid_t pid;
const char *filename;

pid = bpf_get_current_pid_tgid() >> 32;
filename = BPF_CORE_READ(name, name);
bpf_printk("KPROBE ENTRY pid = %d, filename = %s\n", pid, filename);
return 0;
}

可以看到,与 fentry 相比,此处需要使用 BPF_CORE_READ(name, name) 获取 filename

传统的五子棋

传统的 kprobe/kretprobe 方式,是通过动态符号表(kallsyms)找到函数地址,在入口或返回处插入探针,但只能通过 pt_regs 提供的寄存器信息来获取参数,需要程序员手动解析,并且难以保证类型安全。因此在代码中只能用函数 bpf_probe_read_kernel() 对参数进行安全读取,代码中使用的 BPF_CORE_READ() 宏即是对该函数的封装。

技能五子棋

fentryfexit,它们可以在函数的入口和出口处精准 attach,并且不需要手动解析寄存器或做复杂的偏移计算,内核在运行时会直接把真实的参数和返回值传递给 eBPF 程序。

SEC(“kretprobe/do_unlinkat”)

1
2
3
4
5
6
7
8
9
SEC("kretprobe/do_unlinkat")
int BPF_KRETPROBE(do_unlinkat_exit, long ret)
{
pid_t pid;

pid = bpf_get_current_pid_tgid() >> 32;
bpf_printk("KPROBE EXIT: pid = %d, ret = %ld\n", pid, ret);
return 0;
}

可以看到,与fexit 相比,kretprobe 只能访问返回值。

The user-space side

用户态程序与 fentry 几乎相同。

open_and_load

1
2
3
4
5
6
/* Open load and verify BPF application */
skel = kprobe_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}

attach

1
2
3
4
5
6
/* Attach tracepoint handler */
err = kprobe_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}

while

1
2
3
4
while (!stop) {
fprintf(stderr, ".");
sleep(1);
}

cleanup

1
2
cleanup:
kprobe_bpf__destroy(skel);