要使用github.com/cilium/ebpf(ebpf-go),必需是在linux操作系统环境下,在Mac OS或windows下都是无法使用ebpf的。但我们可以安装个linux虚拟机!例如Ubuntu。作为开发ebpf项目的开发环境。
Mac系统推荐Parallels Desktop这款虚拟机软件,装个虚拟机非常简单,缺点就是收费。
本篇基于Ubuntu 22.04 系统,arm64 CPU架构 做的实验。
开发环境准备
首先我们需要根据ebpf-go官方文档列出的编译环境来准备我们的开发环境。
总结要求如下:
- 要求系统的linux内核版本>=5.7。
- 要求安装clang,并且clang的版本>=11。如果安装的clang不包含llvm,那么还需要安装llvm。
- 如果我们用的是Debian/Ubuntu系统,那么我们需要安装libbpf-dev。
- 如果我们用的是Debian/Ubuntu系统,需要使用
sudo ln -sf /usr/include/asm-generic/ /usr/include/asm
创建软连接,不然编译的时候会出现找不到asm/*.h
头文件。 - 要求go语言版本号 >= ebpf-go的go.mod中声明使用的go版本。
当我们准备好Linux虚拟机后,并且安装go之后(建议直接安装go的最新版本),根据要求安装工具。
先检查确认一下内核版本大于5.7:
$ uname -r
安装clang和llvm:
sudo apt install clang
sudo apt install llvm
验证clang版本:
$ clang --version
安装libbpf-dev:
sudo apt install libbpf-dev
创建软链接,按说明去创建这个软链可以跑官方给的例子:
sudo ln -sf /usr/include/asm-generic/ /usr/include/asm
开始实现demo
首先整理需求编写ebpf c程序代码。例如,我们实现用ebpf-go 拦截vfs_read系统函数调用,获取文件名并输出。
c代码(vfs-trace.c):
//go:build ignore
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <linux/ptrace.h>
char __license[] SEC("license") = "Dual MIT/GPL"; // 这句必须
SEC("kprobe/vfs_read")
int kprobe_vfs_read(struct pt_regs *ctx) {
struct file *f = (struct file *)PT_REGS_PARM1(ctx); // PT_REGS_PARM1方法为bpf/bpf_tracing.h头文件中定义的方法,用于获取拦截的方法的第一个参数,如果想获取第二个参数就是PT_REGS_PARM2,第三个就是PT_REGS_PARM3,...
struct path path;
bpf_probe_read(&path,sizeof(struct path),&f->f_path);
struct dentry dentry;
bpf_probe_read(&dentry,sizeof(struct dentry),path.dentry);
char filename[256];
bpf_probe_read(filename, sizeof(filename), dentry.d_name.name);
bpf_printk("file name: %s \n",filename);
return 0;
}
其中PT_REGS_PARM1这个宏定义在
bpf_tracing.h
头文件中。
初始化项目,创建一个go项目目录,例如:vfs-ebpf-trace,将刚刚生成的c代码文件也放到这个目录下。
进入目录按顺序执行下面命令,用ebpf-go初始化go项目。
cd vfs-ebpf-trace
go mod init vfs-ebpf-trace
go mod tidy
## 请先确保安装的go版本跟ebpf-go用的go版本一样新
go get github.com/cilium/ebpf/cmd/bpf2go
编写gen.go文件:
根据cpu架构修改
-target arm64
,通过-I/usr/include
指定所有头文件的根目录在/usr/include
,代码中使用#include <bpf/bpf_tracing.h>
就是找/usr/include/bpf/bpf_tracing.h
文件。
package main
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -cflags $BPF_CFLAGS -target arm64 vfstrace vfs-trace.c -- -I/usr/include
编译c程序生成go代码:
go generate
编译可能会报错:
In file included from ~/wujiuye/goprojects/src/vfs-ebpf-trace/vfs-trace.c:3:
/usr/include/linux/ip.h:21:10: fatal error: 'asm/byteorder.h' file not found
#include <asm/byteorder.h>
^~~~~~~~~~~~~~~~~
1 error generated.
Error: can't execute clang: exit status 1
exit status 1
gen.go:3: running "go": exit status 1
进入/usr/include/asm-generic/
目录查看发现确实没有byteorder.h
这个头文件,但是我从另一个目录找到了,就是aarch64-linux-gnu
。
参考文档的sudo ln -sf /usr/include/asm-generic/ /usr/include/asm
,我改成sudo ln -sf /usr/include/aarch64-linux-gnu/ /usr/include/asm
就不会报找不到byteorder.h
了,但是还是会出现很多头文件找不到的情况。
记得改之前先rm -rf /usr/include/asm。
/usr/include/string.h:26:10: fatal error: 'bits/libc-header-start.h' file not found
#include <bits/libc-header-start.h>
^~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.
Error: can't execute clang: exit status 1
exit status 1
gen.go:3: running "go": exit status 1
/usr/include/features.h:461:12: fatal error: 'sys/cdefs.h' file not found
# include <sys/cdefs.h>
^~~~~~~~~~~~~
1 error generated.
Error: can't execute clang: exit status 1
exit status 1
gen.go:3: running "go": exit status 1
/usr/include/features.h:485:10: fatal error: 'gnu/stubs.h' file not found
#include <gnu/stubs.h>
^~~~~~~~~~~~~
1 error generated.
Error: can't execute clang: exit status 1
exit status 1
gen.go:3: running "go": exit status 1
我按同样的方法,给这些目录创建软链接到aarch64-linux-gnu
。
sudo ln -sf /usr/include/aarch64-linux-gnu/asm /usr/include/asm
sudo ln -sf /usr/include/aarch64-linux-gnu/bits /usr/include/bits
sudo ln -sf /usr/include/aarch64-linux-gnu/gnu /usr/include/gnu
按照这个方案就可以正常编译了,如果还出现错误,根据错误提示看看是不是自己写的c代码抛的错,如果是,就很好解决了。
如果你的是X86_64(amd64)的Ubuntu,也应该会遇到这样的问题,把aarch64-linux-gnu换成x86_64-linux-gnu即可:
sudo ln -sf /usr/include/x86_64-linux-gnu/asm /usr/include/asm
sudo ln -sf /usr/include/x86_64-linux-gnu/bits /usr/include/bits
sudo ln -sf /usr/include/x86_64-linux-gnu/gnu /usr/include/gnu
最后,我们的 eBPF C 代码已经编译完成,Go 脚手架也已生成,剩下的工作就是编写Go代码,负责加载程序并将其附加到 Linux 内核的钩子上。
mian.go的实现:
package main
import (
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit"
"log"
"os"
"os/signal"
"time"
)
func main() {
// 删除内核的资源限制。 内核版本 < 5.11
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal("Removing memlock:", err)
}
// 加载编译好的eBPF ELF并将其加载到内核中。
var objs fusetraceObjects
if err := loadFusetraceObjects(&objs, nil); err != nil {
log.Fatal("Loading eBPF objects:", err)
}
defer objs.Close()
// 使用kprobe挂载
kp, err := link.Kprobe("vfs_read", objs.KprobeVfsRead, nil)
if err != nil {
log.Fatalf("opening kprobe: %s", err)
}
defer kp.Close()
// 监听中断信号退出进程
stop := make(chan os.Signal, 5)
signal.Notify(stop, os.Interrupt)
for {
select {
case <-stop:
log.Print("Received signal, exiting..")
return
}
}
}
编译运行:
go build
./vfs-ebpf-trace
三个命令连起来:
go generate && go build && ./vfs-ebpf-trace
查看日记输出:
bpf_printk打印的日记并不在控制台输出,而是输出到/sys/kernel/debug/tracing/trace_pipe
文件。
sudo cat /sys/kernel/debug/tracing/trace_pipe
如果查看日记报错:cat: /sys/kernel/debug/tracing/trace_pipe: Device or resource busy
,这是因为文件被其它进程占用了,可能是我们前一次ssh查看没有关闭,然后ssh超时,我们打开新的窗口,原来的进程还占用着这个文件,所以就会报这个错。
使用lsof命令查出来是哪个进程占用的这个文件,然后kill掉进程即可。
root@vultr:~# sudo lsof /sys/kernel/debug/tracing/trace_pipe
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
cat 122966 root 3r REG 0,12 0 12385 /sys/kernel/debug/tracing/trace_pipe
root@vultr:~# kill -9 122966
踩坑记
头文件找不到问题
关于编译报错找不到头文件的问题,上述做法只适合文章中的案例,如果我们再用到一些其它的头文件,比如<linux/fs.h>
,可能还是会遇到各种各样的问题。
我们可以使用下面这个大杀招。
通过使用bpftool命令,根据当前内核版本生成一个囊括所有结构体的头文件。
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
然后将vmlinux.h这个文件放到/usr/include
目录下:
mv ./vmlinux.h /usr/include
最后修改c程序代码的导入头文件部分:
#include <vmlinux.h>
//#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
//#include <linux/ptrace.h>
bpf/
开头的,是我们安装libbpf-dev得到的头文件,不是系统的,所以vmlinux.h
替代不了。而linux/
开头的是系统提供的,include了vmlinux.h
之后就不需要了,所以注释掉。
读数据优化
在案例中,我们要读取f->path.dentry->d_name.name,直接这样调用是不行的,所以我们使用了好几次bpf_probe_read,才读取到文件名。
SEC("kprobe/vfs_read")
int kprobe_vfs_read(struct pt_regs *ctx) {
struct file *f = (struct file *)PT_REGS_PARM1(ctx);
struct path path;
bpf_probe_read(&path,sizeof(struct path),&f->f_path);
struct dentry dentry;
bpf_probe_read(&dentry,sizeof(struct dentry),path.dentry);
char filename[256];
bpf_probe_read(filename, sizeof(filename), dentry.d_name.name);
bpf_printk("file name: %s \n",filename);
}
我们可以使用bpf_core_read.h
这个头文件定义的BPF_CORE_READ这个宏,支持链式读取。
我们安装libbpf-dev后,这个头文件在/usr/include/bpf/bpf_core_read.h
这个头文件中。
....
#include <bpf/bpf_core_read.h> // 引入这个头文件
SEC("kprobe/vfs_read")
int kprobe_vfs_read(struct pt_regs *ctx) {
struct file *f = (struct file *)PT_REGS_PARM1(ctx);
const unsigned char *filename;
filename = BPF_CORE_READ(&f->f_path,dentry,d_name.name);
bpf_printk("file name: %s \n",filename);
}
这里我们使用BPF_CORE_READ一步到位读文件名,解决了需要调用多次bpf_probe_read的问题。
案例中,BPF_CORE_READ(&f->f_path,dentry,d_name.name)
是从f_path(结构体)中读取dentry(结构体),再从dentry中读取d_name(结构体),并获取d_name的name字段(const unsigned char *)。
其它问题
1
验证点有没有正确,可以使用bpftrace工具。
例如:
sudo bpftrace -e 'kprobe:vfs_open {printf("hello")}'
2
如果go mod tidy
遇到下载依赖超时,如:
github.com/go-quicktest/qt: github.com/go-quicktest/[email protected]: Get "https://proxy.golang.org/github.com/go-quicktest/qt/@v/v1.101.0.zip": dial tcp 142.250.204.113:443: i/o timeout
可以将虚拟机的网络从“共享网络”切换到“侨接网络WIFI”。或者自己检查一下网络不通的原因、网络是否能打开海外的网站等。
3
如果运行遇到Removing memlock:failed to set memlock rlimit: operation not permitted
错误,需要我们以root权限启动。
如果你是在虚拟机里面装goland开发工具来run项目,那么需要以root权限来启动goland。
sudo ./goland.sh
4
如果运行出现这个错误:
Loading eBPF objects:field KprobeFuseFileWrite: program kprobe_fuse_file_write: load program: invalid argument: cannot call GPL-restricted function from non-GPL compatible program (13 line(s) omitted)
参照例子,我们需要修改我们的c程序代码,添加下面这行:
char __license[] SEC("license") = "Dual MIT/GPL";
需要重新执行go generate
。
5
如果我们不想安装虚拟机,也可以在云上启动个虚拟机,然后本地开发远程编译运行,goland提供远程编译运行的支持,可以参考这个文章:https://blog.51cto.com/u_6192297⁄5357296
6
关于linux的系统函数,可以在这个网站搜索,还能根据自己系统的内核版本去搜索。可以用来找某个函数的都有哪些参数,参数的类型是什么。 https://elixir.bootlin.com/linux/v5.10.218/source/fs/open.c#L928