在Android中开发eBPF程序学习总结(一)
最近在研究eBPF,做一下学习笔记。
起因
其实是想学习一下ecapture是怎么实现的,但是实际在我xiaomi 10
手机上测试的过程中(已经有root权限)发现,并没办法运行,因为ecapture
需要内核开启CONFIG_DEBUG_INFO_BTF
,这个配置信息可以通过/proc/config.gz
中来查看是否开启。
我的手机的内核版本是4.19,没有开启BTF,但是BPF是开启了的,接着我继续查看ecapture
的文档,说如果内核没有开启BTF
,需要使用make nocore
编译,在github上有提供直接编译好的nocore
安卓版,但是测试还是运行不了。接着自己编译了一波,但是仍然失败,感觉可能得严格按文档所述,需要内核版本大于等于5.4。
那么我的手机就没办法运行BPF程序了吗?接着,就开启了我的研究。
Android BPF demo
在网上搜相关的学习资料,BPF相关的资料本身就挺少的,再过滤一下只限制Android平台,就更少了。
而且大部分能搜到的中文资料,都是一堆废话,或者一堆ctrl+c, ctrl+v的文章,实际有用的太少了。安卓官方的资料中也只有一个简单的demo,而且使用的是Android.bp进行编译的,还需要本地搭建AOSP环境。
AOSP环境搭建
这破环境真是绝了,挂上daili,我装了一个晚上还没好(速度也有4Mb/s了)。然后第二天搜到了能换国内源,下面放一下我的搭建环境的命令:
1 | $ apt-get install -y repo |
简单的几句命令就好了,但是要注意,内存建议大于16G,硬盘最好200G以上。
使用AOSP环境编译程序
1 | # 初始化一下环境变量 |
接着后续的测试代码可以参考测试代码,该文章中的代码,在我测试的过程中,没有啥问题,是能正常运行的,但是在第一次编译的时候,可能是AOSP架构的问题,会把整个项目都先编译一次,我安卓也搞的不多,也不知道如果只编译指定项目。不过在第一编译之后,后续只需要使用m name
,就可以只编译指定项目了。也是因为要编译整个项目,如果内存小于16G,是会编译失败的,如果本身内存不够,可以增加一下交换分区的大小。
Android上的BPF
通过这个demo,能看出来,android下使用BPF程序的步骤如下:
首先把编译好的bpf.o程序放到
/system/etc/bpf/
目录下,这就要求我们需要有/system
目录的可写权限,但是在我的手机上,就算有root权限了,system
目录也没办法写。所以我把手机的系统从MIUI12,刷成了evolution x
系统,然后通过adb shell mount -o rw,remount /
来重新挂载根目录,这样就能写/system/etc/bpf
目录了。
使用bpfloader
程序,会自动加载/system/etc/bpf
目录下的*.o
文件,然后会在/sys/fs/bpf
目录生成相应的prog_xxx
和map_xx
文件。
我们自己的loader文件需要通过/sys/fs/bpf
目录下的那两个文件来和BPF程序进行交互。
深入研究Android下的BPF
我根据Linux下的eBPF文件的资料,自己写了一个DEMO:
BPF程序bpftest.c
1 |
|
map映射
DEFINE_BPF_MAP
是对map相关操作的一个宏定义,可以参考:bpf_helpers.h
1 |
比如我上面的代码为:DEFINE_BPF_MAP(execve_map, ARRAY, uint32_t, struct event_execv, 256);
我的map_name为execve_map
,所以这个宏定义帮我定义了bpf_execve_map_update_elem
这类的函数,帮我定义了结构体:
1 | struct bpf_map_def SEC("maps") execve_map = { |
并且在/sys/fs/bpf
目录下生成的map文件的结构为:map_(bpf文件名)_(定义的map_name)
,假如我编译的bpf文件名为:bpftest.o
,放到/system/etc/bpf/
目录下,那么在/sys/fs/bpf
目录下生成的为:map_bpftest_execve_map
。
map可以理解为,内核中的BPF和用户态之间的接口,在内存中是以键值对的形式存在的,按我理解,key和value的类型也是可以自己定义的,可以是int,指针,字符串,或者结构体,因为对于BPF来说,key和value就是内存中的一段值,只需要定义好key和value的size就好了,而在上面的结构体中就定义了key和value的大小。
用户态的loader可以通过/sys/fs/bpf/map_bpftest_execve_map
和BPF程序来交换数据。
BPF函数编写
这块知识的文章挺多的,在BPF的函数定义的上头都需要有一个SEC("xxxx")
,在最开始的demo中还有另一个写法,以下两种写法是等同的:
1 | SEC("tracepoint/sched/sched_switch") |
SEC里面的字符串是为了定义下面的函数是什么类型的BPF程序,因为BPF程序也有很多中类型,比如kprobe, kretprobe, uprobe, uretprobe, tracepoint......
。
具体都有啥,可以参见:libbpf.c
再低一点的版本这个结构体的名字叫section_names
,不过在我研究了一波之后,我感觉不能通过内核版本来确定我们可以用哪个section
,需要通过/sys/kernel/debug/
目录下的情况来确定,但是安卓手机上的情况却有一些不同,目录为: /sys/kernel/tracing/
,比如我上面代码中的:SEC("tracepoint/raw_syscalls/sys_enter")
,是因为有以下目录:/sys/kernel/tracing/events/raw_syscalls/sys_enter/
,并且struct execve_args
结构体是来源于:/sys/kernel/tracing/events/raw_syscalls/sys_enter/format
目前这种方式我觉得只适用于tracepoint
,其他的还没研究到,后续研究到了再补充。
再android上,/sys/fs/bpf/prog_xx
的命名方式为:prog_(文件名)_(section名)_(分类,分类名之类的)
比如我的代码中,文件名为bpftest
,section名为tracepoint
,tracepoint的分类为raw_syscalls
,分类名为sys_enter
,所以最后得到的文件为:/sys/fs/bpf/prog_bpftest_tracepoint_raw_syscalls_sys_enter
BPF相关函数
bpf的相关函数可以参考bpf_helper_defs.h
文件,比如上述的bpf_get_current_pid_tgid
,表示获取触发该BPF的程序的pid,bpf_get_current_uid_gid
是获取用户的gid,bpf_get_current_comm
是获取程序名,还有其他的可以自行去看这个头文件的定义。
日志调试
BPF提供一个bpf_trace_printk
函数来打印调试信息,在android下,可以使用atrace命令来读取。
并且我通过strace对atrace进行跟踪发现,其实只需要执行下面两句命令:
1 | $ echo 1 > /sys/kernel/tracing/tracing_on |
我在想,通过这个调试信息,好想也能把BPF的数据传送给用户态的loader程序。
参考
在Android中开发eBPF程序学习总结(一)