eBPF(Extended Berkeley Packet Filter )是一种新兴的linux内核功能扩展技术,可以无需修改内核代码,在保证安全的前提下,灵活的动态加载程序,实现对内核功能的扩展。
Android平台上也引入了对eBpf技术的支持,本文以一些典型使用场景,贯穿eBpf在android上的使用流程,展示如何在手机上集成和调试eBpf程序。
如下图示,为bpf的基本部署流程,在android上也是适用的。
一、Bpf程序编写
Android的eBpf程序源码,位于system/bpfprogs,比如打开time_in_state.c可以看到程序总体上分为三个部分:
- 使用DEFINE_BPF_MAP定义了一些Map数据结构,这些是用来实现用户程序和内核互传数据的共享缓存。
- 使用DEFINE_BPF_PROG,定义了一个Bpf函数,这个函数编译后,可以加载进内核,实现钩子函数的功能。
- LICENSE("GPL") 许可协议声明。
上述中,Map的类型,以及Bpf的hook类型,根据功能的不同有许多种类,可以参考https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md#program-types,里面有详细的描述。
二、Bpf程序生成
当以C语言的格式编写一个Bpf程序后,通过编译,可以得到一个 “.o” 文件。此文件是以BTF(BPF Type Format) 字节码编码的元数据格式文件,并不可以直接执行,需要加载到内核中,内核进行解析执行,或者JIT转换后执行。
BTF格式文件可查看文档:
https://www.kernel.org/doc/html/latest/bpf/btf.html
三、加载Bpf程序
Bpf程序在Android上有严格的权限控制,在bpfloader.te 中有限制bpf执行的sepolicy,限定了bpfloader是唯一可以加载bpf程序的程序。
neverallow { domain -bpfloader } *:bpf { map_create prog_load };
而bpfloader只在手机启动时执行一次,保证了其它模块无法额外加载系统之外的bpf程序,防止对内核的安全性造成危害。
在system/bpf/bpfloader/BpfLoader.cpp中,bpfloader会使用loadAllElfObjects遍历/system/etc/bpf下btf格式的”.o”文件。接着使用android::bpf::loadProg解析bpf程序文件,实现创建Bpf程序和相应的Map。
Bpfloader执行加载之后,会立即退出。Bpf程序的生命周期管理为引用计数,类似文件句柄fd,当失去所有引用时,Bpf程序和map等对象就会被销毁。
为了避免bpf prog和map对象在bpfloader执行之后被销毁, 最后会通过bpf_obj_pin把这些bpf对象映射到/sys/fs/bpf文件节点。映射的文件节点,其命名有特定的规则,以便其它的程序能够通过文件路径名称来找到对应的bpf程序。
资料直通车:Linux内核源码技术学习路线+视频教程内核源码
学习直通车:Linux内核源码内存调优文件系统进程管理设备驱动/网络协议栈
四、Attach Bpf程序
Bpf程序被加载之后,并没有附着到内核函数上,此时bpf程序不会有任何执行,还需要经过attach操作。attach指定把bpf程序hook到哪个内核监控点上,具体有tracepoint,kprobe等几十种类型。成功attach上的话,bpf程序就转换为内核代码的一个函数。
比如attach task_rename 这个tracepoint类型,可以用
cat/sys/kernel/tracing/events/task/task_rename/format来确认参数,使得定义的bpf 函数和具体的tracepoint 函数参数一致。
如果是attach raw tracepoint,则需要自行构建参数,因为raw tracepoint 访问的是事件的原始参数,未进行参数封装,相比之下有更好一点的性能。
比如task_rename 这个tracepoint中,两种类型的参数差异:
五、Update map
当Bpf附着到内核函数上,起到了一个钩子函数的作用。钩子函数在detach之前,可以一直侦测内核的执行,有时候我们需要改变侦测的范围,或者把侦测的结果上报,此时需要使用Map,Map是用户监控程序和内核间数据交换的媒介。用户态和内核态都可以使用类似的接口来访问Map。
典型操作
- 在Map中查找记录
void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)
- 在Map中更新记录
long bpf_map_update_elem(struct bpf_map *map, const void *key, const void *value, u64 flags)
- 在Map中删除记录
long bpf_map_delete_elem(struct bpf_map *map, const void *key)
六、Event 上报
一般的Map数据,需要我们主动去读取里面的数据。有时候,希望有数据时,能得到通知,而不是轮询去读取。此时,可以通过perf event map实现侦听数据变化的功能。内核数据能够存储到自定义的数据结构中,并且通过 perf 事件ring缓存发送和广播到用户空间进程。
perf event map的构建流程:
上面构建流程完成后,用户态和内核态,就存在了event fd关联。接着用户态使用epoll来持续侦听fd上的通知,而fd实际上是映射到了缓存,所以当侦听到变化时,就可以到缓存中读取具体的数据。
在内核中,则通过
bpf_perf_event_output(ctx,&events,BPF_F_CURRENT_CPU, &data, sizeof(data));
来通知数据。
BPF_F_CURRENT_CPU 参数指定了使用当前cpu的索引值来访问event map中的fd,进而往fd对应的缓存填充数据,这样可以避免多cpu同时传递数据的同步问题,也解释了上面event map初始化时,为何需要创建与cpu个数相等的大小。
七、调试
实际开发中,免不了需要反复调试的过程,遵照bpf的原理,在android上重新部署一个bpf程序可以采用如下步骤。
- Push 新的bpf.o 文件到/system/etc/bpf/ 中。
- 旧版本的bpf程序和map的映射文件仍然存在,需要进入/sys/fs/bpf,rm掉映射文件。旧bpf由于没有了引用,就会被销毁。
- 然后再次执行/./system/bin/bpfloader,bpfloader就能够和开机时一样,把新的bpf.o再次加载起来。
注意:bpfloader在加载时打印的log太多,会触发ratelimiting,有时候发现bpfloader不能加载新的bpf程序,也不能查到有报错的信息。可以先用"echo on > /proc/sys/kernel/printk_devkmsg" 指令关闭ratelimiting,此时就能正常发现错误了。
在成功挂载bpf程序之后,还需要确认其在内核中执行的情况,使用bpf_printk输出内核log。
/* Helper macro to print out debug messages */
#define bpf_printk(fmt, ...) \
({ \
char ____fmt[] = fmt; \
bpf_trace_printk(____fmt, sizeof(____fmt), \
##__VA_ARGS__); \
})
查看内核日志可用:
$ echo 1 > /sys/kernel/tracing/tracing_on
$ cat /sys/kernel/tracing/trace_pipe
注意:bpf程序虽然用C 代码格式书写,但其最终为内核验证执行,会有许多安全和能力方面的限制,典型的如bpf_printk,只支持3个参数输出,超过则会报错。
八、结语
Bpf 可以hook 系统调用、tracepoint和内核函数等,其应用场景相当广泛,目前在Android上的使用比较初步,还有很大的空间让我们在实践中进一步探索。