顶不住了还是go吧 - ebpf-go框架之cilium-ebpf

被libbpf交叉编译折磨烂了,具体来说就是libbpf只支持gnu/glibc,但是需要用到的unwindstack库又需要ndk-clang来提供很多安卓的api,两个实在没法编到一起,只能迁移到go然后把unwindstack以cgo动态库的形式加入到项目中

整体框架

cilium-ebpflibbpf的go语言封装,用c写完bpf内核程序后,使用bpf2go工具将其转换为go框架代码,然后再使用go build将其嵌入到go语言用户程序中,剩下想加其他的功能就用c编成动态库之后挂在go程序下

构建脚本

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
TC:= /home/sgsg/ebpf/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin
CC:=$(TC)/aarch64-linux-android35-clang
CXX:=$(TC)/aarch64-linux-android35-clang++
AR:=$(TC)/llvm-ar

APP:=stackunwinder-go

stack_src:= $(wildcard stack/*.cpp)

ANDROID_SYSROOT:= $(TC)/../sysroot
stack_LDPATH:= -L/home/sgsg/ebpf/libunwindstack/build -L/home/sgsg/ebpf/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib
stack_LDFLAGS:= $(stack_LDPATH) -lunwindstack -lbase -ldexfile_stub -llzma -lprocinfo -lziparchive -llog -lz
stack_INCLUDES:= -I$(ANDROID_SYSROOT)/usr/include/aarch64-linux-android -I$(ANDROID_SYSROOT)/usr/include -I/home/sgsg/ebpf/libunwindstack/libunwindstack/include
stack_CXXFLAGS:= -std=c++20 -O2 -Wall -fPIC

app: $(APP)


stackHelp.so: $(stack_src)
$(CXX) $(stack_CXXFLAGS) -shared -o $@ $^ $(stack_LDFLAGS) $(stack_INCLUDES)


.PHONY: clean
clean:
rm -f stackHelp.so
rm -f $(wildcard stackunwinder/probes__bpfel.*)
rm -f $(APP)
rm -f linker/libwrapper.a
rm -f linker/wrapper.o

BPF_CLANG:=$(TC)/aarch64-linux-android35-clang
BPF_CFLAGS:=-O2 -g -target bpf

BPF_SKEL:=stackunwinder/probes__bpfel.go

$(BPF_SKEL): $(wildcard kernel/*.c)
@echo "=== Generating BPF Go bindings ==="
@echo "Using BPF_CLANG: $(BPF_CLANG)"
@echo "Using BPF_CFLAGS: $(BPF_CFLAGS)"
cd stackunwinder && \
BPF_CLANG="$(BPF_CLANG)" \
BPF_CFLAGS="$(BPF_CFLAGS)" \
go run github.com/cilium/ebpf/cmd/bpf2go \
-go-package stackunwinder \
-target bpfel \
-cc "$(BPF_CLANG)" \
-cflags "$(BPF_CFLAGS)" \
-type sysEnterData \
probes_ ../kernel/probes.c
cd ..
@echo "=== BPF Go bindings generated successfully ==="

WRAPPER_CFLAGS:=-I./linker -O2 -c -g -Wall -fPIC

linker/libwrapper.o: linker/wrapper.c linker/wrapper.h
$(CC) $(WRAPPER_CFLAGS) -c linker/wrapper.c -o linker/wrapper.o
linker/libwrapper.a: linker/wrapper.o
$(AR) rcs $@ linker/wrapper.o


$(APP): linker/libwrapper.a main.go $(BPF_SKEL) stackHelp.so
@echo [include settings] $(stack_INCLUDES)
@echo [ld settings] $(stack_LDFLAGS)
CGO_ENABLED=1 \
CC=$(CC) \
CXX=$(CXX) \
CGO_CFLAGS="-I./linker" \
CGO_LDFLAGS="-L./linker -lwrapper" \
GOOS=android \
GOARCH=arm64 \
go build -o $@ .
cp $(APP) ..


# CGO_CFLAGS="-I./linker"
# CGO_LDFLAGS="-L./linker"
在安卓上运行

这里GOOS要选android不然他会去链接pthread但是安卓的这玩意是集成到libc里的,然后不出所料cilium的系统检查会因为GOOS不是linux把程序拦下来(幽默),这里要去platform.go中把isLinux这个变量改了才行

加上android这一项,然后就不会报平台不兼容错误了

这里几个目标的构建基本是独立的,这篇文章主要关注bpf探针的部分,下篇文章会将怎么添加栈回溯功能

生成bpf框架

这里使用bpf2go生成框架,网上有不少教程是把命令写到go generate里,这里把它拆出来写在makefile里,基本大差不差

1
2
3
4
5
6
7
go run github.com/cilium/ebpf/cmd/bpf2go \
-go-package stackunwinder \
-target bpfel \
-cc "$(BPF_CLANG)" \
-cflags "$(BPF_CFLAGS)" \
-type sysEnterData \
probes_ ../kernel/probes.c

这里cccflag字段就是指定生成bpf目标文件时使用的cc和cflag,然后go-package字段是指定生成的go框架文件的包名,target是指定目标的端序,默认是大小端序都生成一份,这里我们只编译安卓的版本所以指定小端序即可,然后type字段是告诉bpf2go哪些bpf程序中的c类型要生成go声明,否则我们在go用户程序中是用不了这些类型的,比如这里的sysEnterData

1
2
3
4
5
6
7
8
struct sysEnterData {
u64 regs[31], pc, sp;
char stackData[16384];
uint64_t stackSize;
u8 comm[32];
u8 argBuf[3][512];
u64 syscall_id;
};

然后probes_是指定的框架名称,这个和后续自动生成的bpf对象和一些框架函数的名称会有关

最后是指定源文件 probes.c

构建可执行文件
1
2
3
4
5
6
7
CC=$(CC) \
CXX=$(CXX) \
CGO_CFLAGS="-I./linker" \
CGO_LDFLAGS="-L./linker -lwrapper" \
GOOS=android \
GOARCH=arm64 \
go build -o $@ .

这里因为后续还要添加一些cgo的功能,所以制定了CC之类的,实际上只要指定GOOS,GOARCH这两项即可构建,bpf目标文件会自动被嵌入到go产物中

bpf探针代码

bpf探针部分使用的仍然是libbpf的内容,和之前一样写即可

go用户程序

go部分代码如下,这里和libbpf的内容其实非常类似,只不过换成了go语言,个人感觉cilium封装完的api相对更简单一点,但同时给用户的自由度也低一点

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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package stackunwinder

// #include "../linker/wrapper.h"
import "C"

import (
"bytes"
"encoding/binary"
"flag"
"fmt"
"log"
"os"
"path"
"unsafe"

"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/ringbuf"
"github.com/cilium/ebpf/rlimit"
)
func initLibs(){
exe, _ := os.Executable()
exeDir := path.Dir(exe)
C.setupLibEnv(C.CString(exeDir)) // initialize the C library
if isDebug{ C.test_CGO(12345)}
}
var ProbeObjs *probes_Objects=nil
func Main(){
var(
targetPid = flag.Uint("pid", 0, "target pid")
_isDebug = flag.Bool("d", false, "enable debug mode")
)
flag.Parse()
setDebugMode(*_isDebug)
initLibs()
debug("target pid %d \n", *targetPid)

if err:=rlimit.RemoveMemlock(); err != nil { // remove kernel memory lock limit
log.Fatal(err)
}
objs:=probes_Objects{}
if err:=loadProbes_Objects(&objs,nil); err != nil {
log.Fatalf("loading objects: %v", err)
}
defer objs.Close()
ProbeObjs=&objs // save the probes objects globally
objs.TargetPid.Set(uint32(*targetPid)) // set target pid in eBPF program


debug("self pid: %d\n", uint32(os.Getpid()))
debug("bpf obj loaded\n")

sysEnterTp,err:=link.AttachTracing(link.TracingOptions{Program: objs.SysEnter,AttachType:ebpf.AttachTraceRawTp}) // attach to sys_enter
if err!=nil{
log.Fatal(err)
}
defer sysEnterTp.Close()

if isDebug{log.Printf("tp attached\n")}


sysEnterRb,err:=ringbuf.NewReader(objs.SysEnterRb)
if(err!=nil){
log.Fatal()
}
defer sysEnterRb.Close()

debug("sysEnter ringbuf reader created\n")

var sysEnterData probes_SysEnterData
for{
data,err:=sysEnterRb.Read()
if err!=nil{
log.Printf("reading err: %v", err)
continue
}
if err:=binary.Read(bytes.NewBuffer(data.RawSample), binary.LittleEndian, &sysEnterData); err != nil {
log.Printf("reading event err: %v", err)
continue
}
// log.Printf("pid: %d nr: %d\n", sysEnterData.Pid, sysEnterData.SyscallId)
switch sysEnterData.SyscallId {
case getSyscallId(objs.OPENAT):
fmt.Printf("pid: [%d] comm: [%s] openat: %s \n",*targetPid, sysEnterData.Comm, sysEnterData.ArgBuf[0])
case getSyscallId(objs.READ):
fmt.Printf("pid: [%d] comm: [%s] read: %s \n",*targetPid, sysEnterData.Comm, sysEnterData.ArgBuf[0])
case getSyscallId(objs.WRITE):
fmt.Printf("pid: [%d] comm: [%s] write: %s \n",*targetPid, sysEnterData.Comm, sysEnterData.ArgBuf[0])
default:
fmt.Printf("pid: [%d] comm: [%s] syscall %d\n", *targetPid, sysEnterData.Comm, sysEnterData.SyscallId)
}

data,err=sysEnterRb.Read()
if err!=nil {
log.Printf("reading sysEnter err: %v", err)
continue
}
if err:=binary.Read(bytes.NewBuffer(data.RawSample), binary.LittleEndian, &sysEnterData); err != nil {
log.Printf("reading sysEnterData err: %v", err)
continue
}
debug("stacksize %d\n",sysEnterData.StackSize)
debug("pc %x\n",sysEnterData.Pc)
debug("sp %x\n",sysEnterData.Sp)
var tmp C.struct_Data
for i := 0; i < 31; i++ {
tmp.regs[i] = C.uint64_t(sysEnterData.Regs[i])
}
tmp.pc=C.uint64_t(sysEnterData.Pc)
tmp.sp=C.uint64_t(sysEnterData.Sp)
for i := range tmp.stackData {
tmp.stackData[i] = C.char(sysEnterData.StackData[i])
}
tmp.stackSize=C.uint64_t(sysEnterData.StackSize)
C.unwind(C.int(*targetPid), (*C.struct_Data)(unsafe.Pointer(&tmp)))
}
}

这里我们只关注基本的加载部分

1
2
3
4
5
objs:=probes_Objects{}
if err:=loadProbes_Objects(&objs,nil); err != nil {
log.Fatalf("loading objects: %v", err)
}
defer objs.Close()

这里的probes就是和我们上面指定的框架名相关,我们初始化一个bpf框架对象,这个对象是我们和bpf对象交互的唯一途径,然后调用自动生产的load函数初始化这个框架

1
objs.TargetPid.Set(uint32(*targetPid))

这里是对bpf程序中的全局变量做设置,直接调用Set就行,注意到这里要严格保证go和c值的内存布局是相同的,否则会出问题

1
2
3
4
5
sysEnterTp,err:=link.AttachTracing(link.TracingOptions{Program: objs.SysEnter,AttachType:ebpf.AttachTraceRawTp}) // attach to sys_enter
if err!=nil{
log.Fatal(err)
}
defer sysEnterTp.Close()

这里是链接到bpf程序,这里是一个btf_tp的例子,选择Tracing类型,然后把配置通过TracingOptions传进去

1
2
3
4
5
sysEnterRb,err:=ringbuf.NewReader(objs.SysEnterRb)
if(err!=nil){
log.Fatal()
}
defer sysEnterRb.Close()

这里是初始化一个rb的reader,没啥好说的

1
2
3
4
5
6
7
8
9
var sysEnterData probes_SysEnterData
data,err:=sysEnterRb.Read()
if err!=nil{
log.Printf("reading err: %v", err)
continue
}
if err:=binary.Read(bytes.NewBuffer(data.RawSample), binary.LittleEndian, &sysEnterData); err != nil {
log.Printf("reading event err: %v", err)
}

ringbufreader读的是纯二进制数据,这里用binary库把读到的数据重新序列化成结构体

这里基本就这样了,下篇文章补充一下如何利用采集到的数据实现栈回溯

顶不住了还是go吧 - ebpf-go框架之cilium-ebpf

https://sgsgsama.github.io/ctf/ebpf/ebpf0x4/

Author

SGSG

Posted on

2025-08-13

Updated on

2025-08-25

Licensed under