文章目录
前言
本文参考:How to Build eBPF Programs with libbpfgo
但实际运行时出现了许多情况,因此记录分享。
写在最前:使用libbpf请将系统更新到最新版本,本文环境为Ubuntu21.04。
Ubuntu最新版本下载
一、为什么使用libbpf?
libbpf是可以在用户空间和 bpf 程序中导入的库。它为开发人员提供了一个用于加载 bpf 程序并与之交互的 API。
在使用libbpf前,先使用bcc对eBPF相关知识进行学习运行,学习曲线将更平滑。相对于bcc,libbpf与BPF CO-RE的实际编译部署的难度增大了。
然而性能优化大师 Brendan Gregg 在用 libbpf + BPF CO-RE 转换一个 BCC 工具后给出了性能对比数据:
As my colleague Jason pointed out, the memory footprint of opensnoopas CO-RE is much lower than opensnoop.py. 9 Mbytes for CO-RE vs 80 Mbytes for Python.
我们可以看到在运行时相比 BCC 版本,libbpf + BPF CO-RE 版本节约了近 9 倍的内存开销,这对于物理内存资源已经紧张的服务器来说会更友好。
二、环境搭建
大概是最困难的部分,clang相关的环境需要你的Linux系统升级至较新的版本,而go相关的环境缘于一些原因较难安装上。
1.Libbpf相关环境搭建
实际上要装很多的环境,参考了网上的文章。
sudo apt install build-essential git make libelf-dev strace tar bpfcc-tools libbpf-dev linux-headers-$(uname -r) gcc-
安装clang和llvm,当前最新版本为12。
sudo apt install clang llvm
如果存在因为某些版本问题无法安装的软件时,建议:
- 升级到最新版系统
- 使用
sudo aptitude install
代替sudo apt install
安装完成后,于命令行输入
clang -v
见到类似下图的输出则安装成功:
2.GO环境搭建
sudo apt-get golang
由于未来将使用go get
相关命令来获取包,此处建议先换源,否则会出现connection refused的情况。
使用GOPROXY,在命令行输入:
export GOPROXY=https://goproxy.io
export GO111MODULE=on
三、使用libbpfgo编译运行eBPF程序
构建基于libbpf的BPF应用需要使用BPF CO-RE包含的几个步骤:
- 生成带所有内核类型的头文件vmlinux.h;
- 使用Clang(版本10或更新版本)将BPF程序的源代码编译为.o对象文件;
- 使用 libbpfgo 将其编译为二进制文件,加载到内核中并监听输出。
步骤1:生成头文件
首先新建一个目录作为本次eBPF程序的目录,并将目录权限设置为可以新建或删除文件。
sudo mkdir libbpf-hello
sudo chmod 777 libbpf-hello
进入该目录下,生成带所有内核类型的头文件vmlinux.h
cd libbpfgo-hello/
sudo bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
看到目录下出现了vmlinux.h文件且里面有内容则成功。
步骤2:Clang将BPF程序的源代码编译为.o对象文件
首先,创建文件simple.h,内容如下:
typedef struct process_info {
int pid;
char comm[100];
} proc_info;
simple.h 包含我们想要包含在 bpf 和用户空间代码中的结构定义。
然后,创建文件simple.bpf.c,这是本次BPF程序原代码。
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include "simple.h"
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24);
} events SEC(".maps");
long ringbuffer_flags = 0;
SEC("kprobe/sys_execve")
int kprobe__sys_execve(struct pt_regs *ctx)
{
__u64 id = bpf_get_current_pid_tgid();
__u32 tgid = id >> 32;
proc_info *process;
// Reserve space on the ringbuffer for the sample
process = bpf_ringbuf_reserve(&events, sizeof(proc_info), ringbuffer_flags);
if (!process) {
return 0;
}
process->pid = tgid;
bpf_get_current_comm(&process->comm, 100);
bpf_ringbuf_submit(process, ringbuffer_flags);
return 0;
}
最后,使用Clang进行编译。
clang -g -O2 -c -target bpf -o simple.bpf.o simple.bpf.c
成功编译后,目录结构应该如下:
步骤3:使用GO编译为二进制文件并运行
(注:本阶段的一些命令需要不使用sudo
才能运行,否则连不上go的网络,我也不知道为什么…)
创建文件main.go,内容如下:
package main
import "C"
import (
"bytes"
"encoding/binary"
"fmt"
"os"
bpf "github.com/aquasecurity/tracee/libbpfgo"
)
func main() {
bpfModule, err := bpf.NewModuleFromFile("simple.bpf.o")
if err != nil {
os.Exit(-1)
}
defer bpfModule.Close()
bpfModule.BPFLoadObject()
prog, err := bpfModule.GetProgram("kprobe__sys_execve")
if err != nil {
os.Exit(-1)
}
_, err = prog.AttachKprobe("__x64_sys_execve")
if err != nil {
os.Exit(-1)
}
eventsChannel := make(chan []byte)
rb, err := bpfModule.InitRingBuf("events", eventsChannel)
if err != nil {
os.Exit(-1)
}
rb.Start()
for {
event := <-eventsChannel
pid := int(binary.LittleEndian.Uint32(event[0:4])) // Treat first 4 bytes as LittleEndian Uint32
comm := string(bytes.TrimRight(event[4:], "\x00")) // Remove excess 0's from comm, treat as string
fmt.Printf("%d %v\n", pid, comm)
}
rb.Stop()
rb.Close()
}
利用go的module工具,在目录下输入:
go mod init libbpfgo-hello
但是此时生成的go.mod是空的,为了加载所需module,需要输入:
go mod tidy
如果显示connection refused的话,只能使用*手段了,如:
proxychains go mod tidy
加载成功后,目录下将会多出go.mod与go.sum两个文件。并会有如下显示:
之后,使用go进行编译生成二进制文件。在命令行输入:
CC=gcc CGO_CFLAGS="-I /usr/include/bpf" CGO_LDFLAGS="/usr/lib/x86_64-linux-gnu/libbpf.a" go build -o libbpfgo-prog
注:文件libbpf.a的地址可能不一样,建议使用find命令找一下本机的libbpf.a文件。
编译完成后得到二进制文件 libbpfgo-prog ,文件目录结构如下:
于命令行输入以下命令运行二进制文件
sudo ./libbpfgo-prog
随着系统运行,能够以下列格式捕捉到系统的exec信息
至此,你的第一个通过libbpfgo构建的eBPF项目成功运行!
后记
关于本文代码中的详细解释,可见How to Build eBPF Programs with libbpfgo。