被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库把读到的数据重新序列化成结构体
这里基本就这样了,下篇文章补充一下如何利用采集到的数据实现栈回溯