ebpf on Android (Google pixel 3)
android使用ebpf
目前Android上使用ebpf的方式大概可以分为以下三种
使用adeb编译
adeb类似于linux deploy等,利用chroot技术在安卓手机上运行一个Debian虚拟机,可以利用这个环境安装BCC(eBPF的一个工具库)等。这个方法测试可行,但是比较麻烦,还要解决很多依赖问题,体验不是很好。
完全下载AOSP项目,增加代码后进行编译
需要根据AOSP的环境编译BPF的测试程序(而不需要编译整个项目),这种方法没特别复杂,只是占用相当多的空间且完全无用,非常不优雅。
手动编译
通过编写Makefile来替换谷歌的Soong编译,目前只找到一篇博客为 Android 平台编译 eBPF 程序,但只能编译bpf的内核态程序,并不能编译用户态来读取map的cli程序,因此不予考虑。
本文主要讲解如何使用第二种方法来在android上实现ebpf的使用。
环境
- Google pixel3 with Magisk
- user版本,所以后续想要向/system文件夹下写文件不能使用
disable-verify
命令,采用magisk方法。
- user版本,所以后续想要向/system文件夹下写文件不能使用
- Arch Linux (任何一个Linux发行版应该都可以)
KPROBES选项编译
eBPF 作为内核的一个功能,它的启用需要依赖特定的内核编译配置,如果某个内核没有开启某编译选项,eBPF 可能就用不了;在 Android 内核碎片化的年代,想要搞到能完整支持 eBPF 的设备,那是挺难的;如果想用 eBPF 这个功能,只有自己去改内核代码然后自己编译,然而你会遇到各种设备驱动问题,比如触屏失灵,Wifi 不工作等等等等。。当你好不容易在自己设备上折腾好,想要在别的设备上运行时,哦豁,它不支持 BTF,你的 eBPF 程序跑不了。GKI 通过统一核心内核,把其他功能(如 SoC,ODM 等提供的)从内核剥离并提供稳定接口(KMI),一举解决了碎片化问题。并且,Google 强制要求,Android 12 以上版本的设备,出厂必须使用 GKI 内核。更重要的是,GKI 内核的编译选项,完整支持 eBPF 的几乎所有功能!!也就是说,你拿到一个 GKI 的设备,无需自己编译内核代码,它必然支持 eBPF;你在一个 GKI 设备上编写 的 eBPF 程序,可以轻松地拓展到其他的 GKI 设备![6]
由于我们使用的Google Pixel3,上面的版本是Android11,所以还是需要苦逼的自己打开一些相关的编译选项。参考博客Pixel 3 Kernel打开KPROBES编译选项在编译选项中加上CONFIG_KPROBES选项重新编译kernel,然后临时刷入Google Pixel 3(使用永久刷入会出现一些校验不通过的问题,慎重!),否则后面运行bpf程序会报以下错误:
ioctl(PERF_EVENT_IOC_SET_BPF): Bad file descriptor
AOSP环境配置
下载repo
curl https://mirrors.tuna.tsinghua.edu.cn/git/git-repo -o repo
chmod +x repo
下载源码
export REPO_URL='https://mirrors.tuna.tsinghua.edu.cn/git/git-repo/'
python3 repo init -u https://aosp.tuna.tsinghua.edu.cn/platform/manifest -b android-11.0.0_r45
repo sync
编写BPF代码
按照Google官方文档,我们需要三个文件
Android eBPF C执行程序(内核态执行)
Bpf程序在Android上有严格的权限控制,在bpfloader.te 中有限制bpf执行的sepolicy,限定了bpfloader是唯一可以加载bpf程序的程序。
Android eBPF C加载程序(用户态执行),用于激活追踪点
Bpf程序被加载之后,并没有附着到内核函数上,此时bpf程序不会有任何执行,还需要经过attach操作。attach指定把bpf程序hook到哪个内核监控点上,具体有tracepoint,kprobe等几十种类型。成功attach上的话,bpf程序就转换为内核代码的一个函数。
Android.bp
三个文件参见目录。
编译BPF代码
打开AOSP环境external目录,创建mybpf目录。将bpf_test.c,bpf_cli.cpp和Android.bp移到mybpf目录下。
在 AOSP 环境下,执行以下命令
# 获取lunch等命令 source build/envsetup.sh # 这里主要是针对整体构建,由于我们只build自己的文件,所以有没有这一步无所谓,以防万一,还是lunch上好。 lunch aosp_crosshatch-user cd external/my_bpf/ m bpf_test.o m bpf_cli.cpp
上传BPF代码
代码均处于out/target/product/croesshatch/system/bin/
下
bpf_cli程序
直接通过adb push,push到
/data/local/tmp
目录下bpf_test.o程序
需要上传到
/system/etc/bpf
下,但是由于system是只读的,而且我是用remount也无法重新挂载,于是借助Magisk进行挂载。下载Magisk示例模块
下载zip文件:https://github.com/victor141516/httpcanary-magisk/releases/tag/v1
解压缩后,更改customize.sh的RPLACE变量为
/system/etc/bpf
在模块文件夹下创建文件夹
system/etc/bpf
文件夹,并把bpf_test.o
放进去,Magisk加载模块后重启会把当前目录下的bpf_test.o
自动放到系统的/system/etc/bpf
中打包文件为zip,push到手机
/sdcard/Download
,然后打开Magisk刷入该模块。zip打包时不要包含顶层目录,直接在模块文件夹下通过
zip -r httpcanary-magisk.zip ./*
命令进行打包
重新启动手机(此时可能会导致之前临时刷入的内核恢复到之前的版本,因此这一步完成之后需要重新刷入dtb文件),发现/sys/fs/bpf
出现关于bpf_test的map和prog文件。
运行bpf_cli程序
$ adb shell /data/bpf_cli
last PID running on CPU 0 is 4371
last PID running on CPU 0 is 4371
last PID running on CPU 0 is 4371
... ....
last PID running on CPU 0 is 4371
last PID running on CPU 0 is 1833
last PID running on CPU 0 is 0
... ...
参考:
包含了作者的ebpf经验和总结
附录
bpf_test.c
#include <linux/bpf.h>
#include <stdbool.h>
#include <stdint.h>
#include <bpf_helpers.h>
DEFINE_BPF_MAP(cpu_pid_map, ARRAY, int, uint32_t, 1024);
struct switch_args {
unsigned long long ignore;
char prev_comm[16];
int prev_pid;
int prev_prio;
long long prev_state;
char next_comm[16];
int next_pid;
int next_prio;
};
// SEC("tracepoint/sched/sched_switch")
DEFINE_BPF_PROG("tracepoint/sched/sched_switch", AID_ROOT, AID_NET_ADMIN, tp_sched_switch) (struct switch_args* args) {
// int tp_sched_switch(struct switch_args* args) {
int key;
uint32_t val;
key = bpf_get_smp_processor_id();
val = args->next_pid;
bpf_cpu_pid_map_update_elem(&key, &val, BPF_ANY);
return 0;
}
// char _license[] SEC("license") = "GPL";
LICENSE("Apache 2.0");
bpf_cli.cpp
#include <android-base/macros.h>
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include <bpf/BpfMap.h>
#include <bpf/BpfUtils.h>
#include <libbpf_android.h>
int main() {
constexpr const char tp_prog_path[] = "/sys/fs/bpf/prog_bpf_test_tracepoint_sched_sched_switch";
constexpr const char tp_map_path[] = "/sys/fs/bpf/map_bpf_test_cpu_pid_map";
// Attach tracepoint and wait for 4 seconds
int mProgFd = bpf_obj_get(tp_prog_path);
// int mMapFd = bpf_obj_get(tp_map_path);
bpf_attach_tracepoint(mProgFd, "sched", "sched_switch");
sleep(1);
android::bpf::BpfMap<int, int> myMap(tp_map_path);
while(1) {
usleep(40000);
// Read the map to find the last PID that ran on CPU 0
// android::bpf::BpfMap<int, int> myMap(mMapFd);
printf("last PID running on CPU %d is %d\n", 0, *myMap.readValue(0));
}
exit(0);
}
Android.bp
bpf {
name: "bpf_test.o",
srcs: ["bpf_test.c"],
cflags: [
"-Wall",
"-Werror",
],
}
cc_binary {
name: "bpf_cli",
cflags: [
"-Wall",
"-Werror",
"-Wthread-safety",
],
clang: true,
shared_libs: [
"libcutils",
"libbpf_android",
"libbase",
"liblog",
"libnetdutils",
"libbpf",
],
srcs: [
"bpf_cli.cpp",
],
}