kprobe 是一个处理内核空间入口探针(kprobe)和出口(返回)探针(kretprobe)的示例。它会将 kprobe 和 kretprobe 类型的 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() 宏即是对该函数的封装。
技能五子棋
fentry 和 fexit,它们可以在函数的入口和出口处精准 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 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 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);