BruceFan's Blog

Stay hungry, stay foolish

0%

Cilium入门

git仓库地址:https://github.com/cilium/ebpf
我的环境:Ubuntu 20.04 64bit

安装依赖

1
2
3
$ sudo apt install clang-13 llvm-13
$ export BPF_CLANG=clang
$ git clone https://github.com/cilium/ebpf.git

编译运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ cd kprobe
$ rm *.o bpf_*.go
.
├── kprobe.c
└── main.go
$ go generate
.
├── bpf_bpfeb.go
├── bpf_bpfeb.o
├── bpf_bpfel.go
├── bpf_bpfel.o
├── kprobe.c
└── main.go
$ go build
$ sudo ./kprobe
2022/08/09 14:51:54 Waiting for events..
2022/08/09 14:51:55 sys_execve called 6 times
2022/08/09 14:51:56 sys_execve called 25 times
2022/08/09 14:51:57 sys_execve called 35 times
2022/08/09 14:51:58 sys_execve called 37 times

如果能打印出sys_execve的执行次数,说明环境搭建成功了。

第一个项目

创建项目目录

1
$ mkdir first

将Cilium eBPF example中的相关文件复制过来作为基础进行修改

1
2
3
4
$ cd ebpf/examples
$ cp -r headers /path/to/first
$ cp kprobe/main.go /path/to/first
$ cp kprobe/kprobe.c /path/to/first

修改main.go第19行

1
-I../headers -> -I./headers

go generate命令是go 1.4版本里面新添加的一个命令,当运行go generate时,它将扫描与当前包相关的源代码文件,找出所有包含”//go:generate”的特殊注释,提取并执行该特殊注释后面的命令,命令为可执行程序,形同shell下面执行。

将依赖添加到go.mod中,并进行生成和编译

1
2
3
4
5
6
7
8
9
10
11
$ cd first
$ go mod init first
$ go mod tidy
$ go generate
Compiled /home/fanrong/Computer/BPF/first/bpf_bpfel.o
Stripped /home/fanrong/Computer/BPF/first/bpf_bpfel.o
Wrote /home/fanrong/Computer/BPF/first/bpf_bpfel.go
Compiled /home/fanrong/Computer/BPF/first/bpf_bpfeb.o
Stripped /home/fanrong/Computer/BPF/first/bpf_bpfeb.o
Wrote /home/fanrong/Computer/BPF/first/bpf_bpfeb.go
$ go build

代码分析

先看一下main.go中main函数的前半部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func main() {

// Name of the kernel function to trace.
fn := "sys_execve"

// Allow the current process to lock memory for eBPF resources.
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
}

// Load pre-compiled programs and maps into the kernel.
objs := bpfObjects{}
if err := loadBpfObjects(&objs, nil); err != nil {
log.Fatalf("loading objects: %v", err)
}
defer objs.Close()

// Open a Kprobe at the entry point of the kernel function and attach the
// pre-compiled program. Each time the kernel function enters, the program
// will increment the execution counter by 1. The read loop below polls this
// map value once per second.
kp, err := link.Kprobe(fn, objs.KprobeExecve)
if err != nil {
log.Fatalf("opening kprobe: %s", err)
}
defer kp.Close()

fn中定义了kprobe附着的函数为sys_execve,并锁定当前进程 eBPF 资源的内存。
之后是调用loadBpfObjects将预先编译的 eBPF 程序和 maps 加载到内核,其定义在生成的.go文件中,最后是调用link.Kprobe进行真正的attach。
关于这个objs,其类型是bpfObjects,定义在生成的.go文件

1
2
3
4
5
6
7
// bpfSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfSpecs struct {
bpfProgramSpecs
bpfMapSpecs
}

bpfProgramSpecsbpfMapSpecs的定义分别为:

1
2
3
4
5
6
7
8
9
10
11
12
13
// bpfSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfProgramSpecs struct {
KprobeExecve *ebpf.ProgramSpec `ebpf:"kprobe_execve"`
}

// bpfMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfMapSpecs struct {
KprobeMap *ebpf.MapSpec `ebpf:"kprobe_map"`
}

kprobe_execvekprobe_map分别对应 kprobe.c 文件中定义的:

1
2
3
4
5
6
7
8
struct bpf_map_def SEC("maps") kprobe_map = {
...
};

SEC("kprobe/sys_execve")
int kprobe_execve() {
...
}

所以,Go 中的这两个名字 KprobeExecve、KprobeMap 就是根据 C 程序中的这两个名字生成过来的,规则是:首字母大写,去除下划线_并大写后一个字母。

实现新功能

利用刚刚创建的 Cilium eBPF 项目,编写一个可以监听 open 系统调用,获取 filename 的程序。首先先看一下open系统调用

1
2
3
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
|--do_sys_open(AT_FDCWD, filename, flags, mode);
|--do_sys_openat2(dfd, filename, &how);
1
2
3
static long do_sys_openat2(int dfd, const char __user *filename,
struct open_how *how) { ...

目标是获取do_sys_openat2的第二个参数filename。打开kprobe.c开始改造,将宏SEC的名字和函数名改为

1
2
SEC("kprobe/do_sys_openat2")
int kprobe_openat2(struct pt_regs *ctx) { ...

想知道当前是哪个进程进行了open系统调用,所以可以通过BPF辅助函数bpf_get_current_pid_tgid获得当前pid_tgid

1
u32 pid = bpf_get_current_pid_tgid() >> 32;

关于BPF辅助函数,可以参考文档:https://www.man7.org/linux/man-pages/man7/bpf-helpers.7.html

filename 在kprobe_openat2的第二个参数,可以通过PT_REGS_PARM2宏获取,其定义在bpf_tracing.h

1
2
3
4
5
6
#define PT_REGS_PARM1(x) ((x)->rdi)
#define PT_REGS_PARM2(x) ((x)->rsi)
#define PT_REGS_PARM3(x) ((x)->rdx)
#define PT_REGS_PARM4(x) ((x)->rcx)
#define PT_REGS_PARM5(x) ((x)->r8)
#define PT_REGS_RET(x) ((x)->rsp)

__user代表该数据在用户空间,所以需要bpf_probe_read_user_str读取

1
2
3
4
5
#include "bpf_tracing.h"
...
char filename[20];
const char *fp = (char *)PT_REGS_PARM2(ctx);
long err = bpf_probe_read_user_str(filename, sizeof(filename), fp);

之后可以通过bpf_printk将这些数据输出到/sys/kernel/debug/tracing/trace

1
bpf_printk("pid:%d,filename:%s,err:%ld",pid,filename,err);

kprobe.c改造结束了,但是使用PT_REGS_PARM2需要指定target,在main.go中,继续修改第19行为

1
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS --target=amd64 bpf kprobe.c -- -I./headers

我所使用的机器平台为amd64,所以我加上了–target=amd64,删除之前生成的文件,否则可能会在之后报错,执行go generate调用bpf2go生成,此次由于指定了target为amd64,所以生成的文件为x86版本。
接着修改main.go中对应的函数名(26和44行)

1
2
3
fn := "do_sys_openat2"
...
kp, err := link.Kprobe(fn, objs.KprobeOpenat2)

44行中的名字可以在生成的bpf_bpfel_x86.go文件中看到。
最后编译并运行

1
2
3
4
5
$ go build
$ sudo ./first
2022/08/09 17:01:03 Waiting for events..
2022/08/09 17:01:04 do_sys_openat2 called 760 times
2022/08/09 17:01:05 do_sys_openat2 called 958 times

查看输出

1
$ sudo cat /sys/kernel/debug/tracing/trace

参考
Cilium eBPF搭建与使用