被libbpf交叉编译折磨烂了,具体来说就是libbpf只支持gnu/glibc,但是需要用到的unwindstack库又需要ndk-clang来提供很多安卓的api,两个实在没法编到一起,只能迁移到go然后把unwindstack以cgo动态库的形式加入到项目中
整体框架
cilium-ebpf
是libbpf
的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) ..
|
在安卓上运行
这里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
|
这里cc
和cflag
字段就是指定生成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
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)) 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 { 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 objs.TargetPid.Set(uint32(*targetPid)) 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}) 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 } 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}) 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) }
|
ringbuf
的reader
读的是纯二进制数据,这里用binary
库把读到的数据重新序列化成结构体
这里基本就这样了,下篇文章补充一下如何利用采集到的数据实现栈回溯