cilium/ebpf入门

本文通过监听ksys_write函数的调用情况来学习cilium/ebpf的编写方法,相较于bcc,cilium/ebpf对环境的依赖较少。

环境安装

cilium/ebpf是一个纯GO库,提供加载,编译和调试eBPF程序的实用程序,安装环境llvm11、clang11、go 1.7,安装命令如下:

1
sudo apt install libllvm11 llvm-11-dev libclang-11-dev

代码编写

内核函数原型如下,发起write系统调用后会执行该函数,我们通过指定pid的方式,获取调用者传输给该函数的三个参数(文件描述符、写入内容、写入数量)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;

if (f.file) {
loff_t pos, *ppos = file_ppos(f.file);
if (ppos) {
pos = *ppos;
ppos = &pos;
}
ret = vfs_write(f.file, buf, count, ppos);
if (ret >= 0 && ppos)
f.file->f_pos = pos;
fdput_pos(f);
}

return ret;
}

新建go项目

1
$ go mod init myebpf

从cilium/ebpf官方仓库(https://github.com/cilium/ebpf/)
examples目录下拷贝headers文件夹,放到新创建的go项目下。
go.mod文件中添加cilium/ebpf项目依赖。

1
2
3
4
module github.com/myebpf
go 1.17
require github.com/cilium/ebpf v0.8.2-0.20220217141816-62da0a730ab7
require golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf // indirect

创建kprob.c文件,编写bpf内核部分的代码。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// +build ignore

#include "common.h"
#include "bpf_helpers.h"
#include "bpf_tracing.h"

char __license[] SEC("license") = "Dual MIT/GPL";

struct bpf_map_def SEC("maps") kprobe_map = {
.type = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
};
struct ksys_write_event_t {
u32 pid;
u32 count;
u32 fd;
u64 ts;
char buf[20];
} __attribute__((packed));

SEC("kprobe/ksys_write")
int kprobe_execve(struct pt_regs *ctx) {

int my_pid;
asm("%0 = MY_PID ll" : "=r"(my_pid));

struct ksys_write_event_t event = {};
u64 id;
id = bpf_get_current_pid_tgid();
u32 pid = id >> 32;
if(my_pid != 0 ){
if (pid != my_pid)
return 0;
}
event.pid = pid;
event.fd = PT_REGS_PARM1(ctx);
char *filename = (char *)PT_REGS_PARM2(ctx);
event.count = PT_REGS_PARM3(ctx);
event.ts = bpf_ktime_get_ns();
bpf_probe_read(&event.buf, sizeof(event.buf), filename);
bpf_perf_event_output(ctx, &kprobe_map, BPF_F_CURRENT_CPU, &event, sizeof(event));

return 0;
}

创建main.go文件,编译并加载bpf内核部分的代码。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//go:build linux
// +build linux

// This program demonstrates attaching an eBPF program to a kernel symbol.
// The eBPF program will be attached to the start of the sys_execve
// kernel function and prints out the number of times it has been called
// every second.
package main
import (
"bytes"
"encoding/binary"
"flag"
"fmt"
"log"
"time"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/perf"
"github.com/cilium/ebpf/rlimit"
"golang.org/x/sys/unix"
)

// $BPF_CLANG and $BPF_CFLAGS are set by the Makefile.
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS --target=amd64 bpf kprobe.c -- -I./headers
const mapKey uint32 = 0

type ksysWriteEvent struct {
Pid uint32
Count uint32
Fd uint32
Ts uint64
Buf [20]byte
}
func main() {

var mypid int64
pid := flag.Int("pid", -1, "attach to pid, default is all processes")
flag.Parse()
if *pid != -1 {
mypid = int64(*pid)
} else {
mypid = 0
}
// Name of the kernel function to trace.
fn := "ksys_write"

// Allow the current process to lock memory for eBPF resources.
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
}
objs := bpfObjects{}
spec, err := loadBpf()
if err != nil {
log.Fatalf("loading objects: %s", err)
return
}
funcName := "kprobe_execve"
// 过滤pid
for i, ins := range spec.Programs[funcName].Instructions {
if ins.Reference == "MY_PID" {
spec.Programs[funcName].Instructions[i].Constant = mypid
spec.Programs[funcName].Instructions[i].Offset = 0
fmt.Printf("found the my_pid and replaced, index: %d, opCode: %d\n", i, ins.OpCode)
}
}
if err := spec.LoadAndAssign(&objs, nil); err != nil {
log.Fatalf("loading objects: %s", 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()

// Read loop reporting the total amount of times the kernel
// function was entered, once per second.
ticker := time.NewTicker(1 * time.Second)
log.Println("Waiting for events..")
rd, err := perf.NewReader(objs.KprobeMap, 4096)
if err != nil {
panic(err)
}
defer rd.Close()
fmt.Printf("%10s\t%20s\t%5s\t%5s\t%5s\n", "PID", "Content", "Fd", "COUNT", "Time")
for range ticker.C {
var event ksysWriteEvent
record, err := rd.Read()
if err != nil {
panic(err)
}
err1 := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event)
if err1 != nil {
fmt.Printf("failed to decode received data: %s\n", err)
continue
}
fmt.Printf("%10d\t%20s\t%5d\t%5d\t%10d\n", event.Pid, unix.ByteSliceToString(event.Buf[:]), event.Fd, event.Count, event.Ts)
}
}

代码中有过滤PID以及读取数据方式供大家参考。

执行编译命令:

1
$ go generate

该命令的具体内容在main.go文件的上方//go:generate注释处,可以通过编辑该行注释调整命令的执行内容。

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

例如:$BPF_CLANG环境变量系统中通常是没有的,可以将此处替换为clang,或者在当前shell中执行:

$ export BPF_CLANG=clang

如果找不到头文件,还要注意该行注释中headers文件夹的相对路径是否配置正确。

最后–target=amd64选项能够让我们使用PT_REGS_PARM1宏定义来读取内核函数的参数。

编译后生成bpf_bpfel_x86.go和bpf_bpfel_x86.o文件,如图所示。

在这里插入图片描述

运行程序

1
$ go run main.go bpf_bpfel_x86.go -pid=1235

在这里插入图片描述

至此完成程序的编写与数据采集。

iovisor/gobpf

最后附上相同内容在iovisor/gobpf环境下的写法,以供对比参考。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package main
import (
"bytes"
"encoding/binary"
"fmt"
"os"
"os/signal"
"unsafe"
"flag"
"strings"
"time"
bpf "github.com/iovisor/gobpf/bcc"
)
import "C"
var source1 string = `
#include <uapi/linux/ptrace.h>
#include <linux/fs.h>
struct ksys_write_event_t {
u32 pid;
u32 count;
u32 fd;
char buf[20];
} __attribute__((packed));
BPF_PERF_OUTPUT(ksys_write_events);
int ksys_write_probe(struct pt_regs *ctx,unsigned int fd, const char __user *buf, size_t count) {
struct ksys_write_event_t event = {};
u64 id;
id = bpf_get_current_pid_tgid();
u32 pid = id >> 32;
if (FILTER_PID)
return 0;
event.pid = pid;
event.count = count;
event.fd = fd;
bpf_probe_read(&event.buf, sizeof(event.buf), (void *)buf);

ksys_write_events.perf_submit(ctx, &event, sizeof(event));
return 0;
}
`
type ksysWriteEvent struct {
Pid uint32
Count uint32
Fd uint32
Buf [16]byte
}
func main() {

pid := flag.Int("pid", -1, "attach to pid, default is all processes")
flag.Parse()
if *pid != -1{
source1 = strings.Replace(source1, "FILTER_PID", fmt.Sprintf("%s%d", "pid != ", *pid), -1 )
}else{
source1 = strings.Replace(source1, "FILTER_PID", "0", -1 )
}
m := bpf.NewModule(source1, []string{})
defer m.Close()
probe, err := m.LoadKprobe("ksys_write_probe")
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to load ksys_write_probe: %s\n", err)
os.Exit(1)
}
if err := m.AttachKprobe("ksys_write", probe, -1); err != nil {
fmt.Fprintf(os.Stderr, "Failed to attach ksys_write_probe: %s\n", err)
os.Exit(1)
}
ksysTable := bpf.NewTable(m.TableId("ksys_write_events"), m)
channel := make(chan []byte)
perfMap, err := bpf.InitPerfMap(ksysTable, channel, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to init perf map: %s\n", err)
os.Exit(1)
}
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, os.Kill)
fmt.Printf("%10s\t%20s\t%5s\t%5s\n", "PID","Buf","COUNT","Fd")
go func() {
var event ksysWriteEvent
for {
data := <-channel
err := binary.Read(bytes.NewBuffer(data), binary.LittleEndian, &event)
if err != nil {
fmt.Printf("failed to decode received data: %s\n", err)
continue
}
filename := C.GoString((*C.char)(unsafe.Pointer(&event.Buf)))
fmt.Printf("%10d\t%20s\t%5d\t%5d\n", event.Pid, filename, event.Count, event.Fd)
}
}()
perfMap.Start()
<-sig
perfMap.Stop()
}

参考资料:
https://github.com/iovisor/gobpf
https://github.com/cilium/ebpf
https://pkg.go.dev/github.com/cilium/ebpf

PS:前往公众号“Linux工坊”,可以查看最新内容!关注即可免费领取面试&Linux技术书籍!

------ 本文结束------
  • 文章标题: cilium/ebpf入门
  • 本文作者: 你是我的阳光
  • 发布时间: 2022年03月19日 - 20:23:10
  • 最后更新: 2022年11月09日 - 16:12:50
  • 本文链接: https://szp2016.github.io/ebpf/ebpf入门/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
0%