如何在 eBPF 中使用 fentry 捕获 unlink 系统调用 ?
这里的 fentry(function entry)和 fexit(function exit)是 eBPF 中的两种探针类型,用于在 Linux 内核函数的入口和退出处进行跟踪. 它们允许开发者在内核函数执行的特定阶段收集信息、修改参数或观察返回值. 这种跟踪和监控功能在性能分析、故障排查和安全分析等场景中非常有用.
与 kprobes 相比,fentry 和 fexit 程序有更高的性能和可用性. 在这个例子中,可以直接访问函数的指针参数,就像在普通的 C 代码中一样,而不需要使用各种读取帮助程序. fexit 和 kretprobe 程序最大的区别在于,fexit 程序可以访问函数的输入参数和返回值,而 kretprobe 只能访问返回值. 从 5.5 内核开始,fentry 和 fexit 对 eBPF 程序可用.
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_endian.h>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
SEC("fentry/do_unlinkat")
int BPF_PROG(do_unlinkat, int dfd, struct filename *name)
{
pid_t pid;
pid = bpf_get_current_pid_tgid() >> 32;
bpf_printk("My internal print ==> fentry: pid = %d, filename = %s", pid, name->name);
return 0;
}
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;
bpf_printk("My internal print ==> fexit: pid = %d, filename = %s, ret = %ld", pid, name->name, ret);
return 0;
}这段程序是用 C 语言编写的 eBPF(扩展的伯克利包过滤器)程序,它使用 BPF 的 fentry 和 fexit 探针来跟踪 Linux 内核函数 do_unlinkat. 在这个教程中,将以这段程序作为示例,学会如何在 eBPF 中使用 fentry 监测捕获 unlink 系统调用.
程序包含以下部分:
-
包含头文件:包括
vmlinux.h(用于访问内核数据结构)、bpf/bpf_helpers.h(包含 eBPF 帮助函数)、bpf/bpf_tracing.h(用于eBPF跟踪相关功能). -
定义许可证:这里定义了一个名为
LICENSE的字符数组,包含许可证信息“Dual BSD/GPL”. -
定义
fentry探针:我们定义了一个名为BPF_PROG(do_unlinkat)的 fentry 探针,该探针在do_unlinkat函数的入口处被触发. 这个探针获取当前进程的 PID(进程 ID)并将其与文件名一起打印到内核日志. -
定义
fexit探针:我们还定义了一个名为BPF_PROG(do_unlinkat_exit)的fexit探针,该探针在do_unlinkat函数的退出处被触发. 与fentry探针类似,这个探针也会获取当前进程的PID并将其与文件名和返回值一起打印到内核日志.
和前面几章介绍的一样,使用 eunomia-bpf 很容易的编译和运行这个 eBPF 程序:
./ecc ./eBPF/functionEntryExit/func_entry_exit.c
INFO [ecc_rs::bpf_compiler] Compiling bpf object...
INFO [ecc_rs::bpf_compiler] Generating package json..
INFO [ecc_rs::bpf_compiler] Packing ebpf object and config into ./eBPF/functionEntryExit/package.json...
sudo ./ecli run ./eBPF/functionEntryExit/package.json
INFO [faerie::elf] strtab: 0x504 symtab 0x540 relocs 0x588 sh_offset 0x588
INFO [bpf_loader_lib::skeleton::poller] Running ebpf program...使用 sudo cat /sys/kernel/debug/tracing/trace_pipe | grep "My internal print ==> " 获取打印信息
sudo cat /sys/kernel/debug/tracing/trace_pipe | grep "My internal print ==> "
......
Chrome_IOThread-4324 [001] ...11 31197.791430: bpf_trace_printk: My internal print ==> fexit: pid = 4294, filename = /dev/shm/.com.google.Chrome.R36CqL, ret = 0
Chrome_IOThread-4324 [003] ...11 31197.792823: bpf_trace_printk: My internal print ==> fentry: pid = 4294, filename = /dev/shm/.com.google.Chrome.RsLI6V
......接下来我们介绍稍微麻烦的方式,使用 ciliu/ebpf 加载这个程序, 加载这个程序的 go 代码如下
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -cflags "-O2 -g -target bpf -D__TARGET_ARCH_x86" --go-package tool -output-dir tool Tool func_entry_exit.c -- -I/usr/include/bpf -I/usr/include/linux
package main
import (
"C"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit"
"github.com/767829413/advanced-go/eBPF/functionEntryExit/tool"
)
func main() {
// Allow the current process to lock memory for eBPF resources.
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
}
// 加载编译的 eBPF 程序
objs := tool.ToolObjects{}
if err := tool.LoadToolObjects(&objs, nil); err != nil {
log.Fatalf("loading objects: %v", err)
}
defer objs.Close()
// 将 fentry 程序附加到 do_unlinkat 函数的入口点
linkFentry, err := link.AttachTracing(link.TracingOptions{
Program: objs.DoUnlinkat,
AttachType: ebpf.AttachTraceFEntry,
})
if err != nil {
log.Fatalf("Attaching fentry program failed: %v", err)
}
defer linkFentry.Close()
// 将 fexit 程序附加到 do_unlinkat 函数的退出点
linkFexit, err := link.AttachTracing(link.TracingOptions{
Program: objs.DoUnlinkatExit,
AttachType: ebpf.AttachTraceFExit,
})
if err != nil {
log.Fatalf("Attaching fexit program failed: %v", err)
}
defer linkFexit.Close()
fmt.Println("eBPF programs successfully attached.")
// 捕获退出信号
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
<-sig
fmt.Println("Exiting program...")
}首先运行 go generate 命令, 它会生成绑定的文件,并且支持大端和小端的操作系统
go generate
Compiled /home/fangyuan/code/go/src/github.com/767829413/advanced-go/eBPF/functionEntryExit/tool/tool_bpfeb.o
Stripped /home/fangyuan/code/go/src/github.com/767829413/advanced-go/eBPF/functionEntryExit/tool/tool_bpfeb.o
Wrote /home/fangyuan/code/go/src/github.com/767829413/advanced-go/eBPF/functionEntryExit/tool/tool_bpfeb.go
Compiled /home/fangyuan/code/go/src/github.com/767829413/advanced-go/eBPF/functionEntryExit/tool/tool_bpfel.o
Stripped /home/fangyuan/code/go/src/github.com/767829413/advanced-go/eBPF/functionEntryExit/tool/tool_bpfel.o
Wrote /home/fangyuan/code/go/src/github.com/767829413/advanced-go/eBPF/functionEntryExit/tool/tool_bpfel.go然后运行 go run 命令, 查看打印流程和上述一致,这里就不重复介绍了.
sudo go run main.go
eBPF programs successfully attached.fentry 的工作原理
fentry 通过附加 eBPF 程序到内核函数的入口点,能够在函数执行之前运行指定的 BPF 代码. 它比传统的 kprobe 拦截机制具有更低的开销,因为它直接钩挂在内核函数的入口,省去了部分上下文切换的开销.
与 fexit 相对,fentry 在目标函数被调用时立即执行,而 fexit 则是在目标函数退出时执行. 二者配合使用,可以全面监控内核函数的行为.
fentry 的优点
-
低开销: 相比于
kprobe,fentry的开销更小,因为它直接注入到内核函数的入口,而不需要进行传统的断点和调试陷阱. -
简化调试: 可以直接拦截内核中的函数调用,在函数调用前执行用户定义的
eBPF程序,适合用于调试或监控系统的行为. -
内核兼容性:
fentry依赖于CONFIG_BPF和内核版本(Linux 5.5及以上),相比老版本的kprobe,fentry更加高效和现代.