Skip to content

Latest commit

 

History

History
188 lines (136 loc) · 7.7 KB

File metadata and controls

188 lines (136 loc) · 7.7 KB

eBPF fentry/fexit 初体验

引出问题


如何在 eBPF 中使用 fentry 捕获 unlink 系统调用 ?

这里的 fentry(function entry)和 fexit(function exit)是 eBPF 中的两种探针类型,用于在 Linux 内核函数的入口和退出处进行跟踪. 它们允许开发者在内核函数执行的特定阶段收集信息、修改参数或观察返回值. 这种跟踪和监控功能在性能分析、故障排查和安全分析等场景中非常有用.

kprobes 相比,fentryfexit 程序有更高的性能和可用性. 在这个例子中,可以直接访问函数的指针参数,就像在普通的 C 代码中一样,而不需要使用各种读取帮助程序. fexitkretprobe 程序最大的区别在于,fexit 程序可以访问函数的输入参数和返回值,而 kretprobe 只能访问返回值. 从 5.5 内核开始,fentryfexiteBPF 程序可用.

解决方案


#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(扩展的伯克利包过滤器)程序,它使用 BPFfentryfexit 探针来跟踪 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 程序


和前面几章介绍的一样,使用 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
......

方法二:使用 cilium/ebpf 编译和加载 eBPF 程序


接下来我们介绍稍微麻烦的方式,使用 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 的优点

  • 低开销: 相比于 kprobefentry 的开销更小,因为它直接注入到内核函数的入口,而不需要进行传统的断点和调试陷阱.

  • 简化调试: 可以直接拦截内核中的函数调用,在函数调用前执行用户定义的 eBPF 程序,适合用于调试或监控系统的行为.

  • 内核兼容性: fentry 依赖于 CONFIG_BPF 和内核版本(Linux 5.5 及以上),相比老版本的 kprobefentry 更加高效和现代.