如何在 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
更加高效和现代.