
在 Android 设备上写 eBPF 程序这事听起来有点硬核但真做起来它的回报值不值得非常值得。eBPF 能让你在内核里安全地跑一小段自己的代码用来追踪系统调用、统计网络流量、监控文件读写甚至拦截可疑行为。传统方案要么反复改内核要么性能损耗大而 eBPF 能在不改内核的前提下做到类似效果而且对业务几乎无感。从系统工程师到做性能优化的同事再到安全研究的朋友都会需要这个东西。这篇文章会从 Android 上 eBPF 的底层兼容性、开发环境搭建、第一个 BPF 程序的编译加载再到一个 TCP 连接监控案例完整过一遍实操路径还会把我在不同设备上踩过的坑一起讲清楚。1. 为什么要在 Android 上折腾 eBPF能解决什么问题1.1 Android 内核与 eBPF 的兼容性现状eBPF 依赖内核能力不是所有 Android 设备都天生支持。Android 内核从 4.x 开始引入 eBPF但要让你能够舒服地写程序、编译、加载、调试通常需要内核版本在 4.14 以上。新一点的高通、三星、Pixel 设备一般没问题老旧设备或者厂商魔改太深的 ROM可能把很多编译选项都关掉了。所以动手之前你要做的第一件事不是写代码而是搞清楚自己手里的设备内核到底开了哪些开关。从 Android 10 开始Google 推动 GKI通用内核镜像让 Android 内核的底座逐步统一化这对 eBPF 开发者是好事至少内核特性会有一个更一致的基线。不过 GKI 也意味着你不能再像以前那样随意改内核来加功能eBPF 几乎成了唯一能安全地在内核里注入代码的途径它的重要性只会越来越大。1.2 eBPF 在 Android 上的典型应用场景在 Android 上eBPF 能做的事情远比你想象的多。性能分析方面可以用 kprobe 和 tracepoint 观测 binder 调用、调度延迟、CPU 唤醒链网络监控方面可以统计每个 App 的 TCP 连接数、活跃端口、发送接收字节量安全方面可以监控敏感文件访问、检测进程是否被注入、拦截异常的系统调用序列工具链方面可以在不修改源码的前提下做函数级 tracing给 app 做黑盒诊断。这些东西的共同点是需要内核数据但又不想改内核、不想引入太大性能损耗。eBPF 天然适合这些“只看不摸”的观测场景。下面用一个表直观展示几个方向应用领域典型 Hook 点能拿到的数据典型产出性能分析sched_switch, sched_wakeup, softirq线程调度延迟、唤醒次数调度热点图、长尾延迟报告网络监控tcp_set_state, tcp_v4_connect, netif_receive_skb五元组、进程 PID、状态变化App 网络行为画像、异常连接告警文件安全vfs_open, security_file_permission文件路径、进程名、操作类型敏感文件访问记录、越权检测Binder 分析binder_transaction, binder_reply传输大小、目标进程、调用链跨进程通信耗时分布、卡顿归因以上只是入门清单。真正用起来之后你会发现任何内核函数都可以成为你的观察窗口只要你有合理的权限和正确的参数定义。1.3 与传统方案如 strace、perf的对比在没有 eBPF 之前Android 上做系统观测基本靠 strace、perf、systrace。这里简单做个对比能帮助你理解为什么我坚持用 eBPF。strace 基于 ptrace 机制每一条系统调用都要陷入 tracer性能开销大得吓人你去 strace 一个频繁做网络请求的 App经常会把对方拖慢十倍以上perf 本身效率很高但它的强项集中在 CPU 性能计数和采样想要自定义内核逻辑、按业务条件过滤就非常别扭systrace 更适合 UI 和 App 层协作对内核细节的覆盖有限。eBPF 的直接优势是你可以在内核态定义自己的事件条件采集特定事件再用 map 做统计整体开销通常能控制在百分之五以内低频 hook 点甚至可以做到零感知。还有一个很重要的点eBPF 程序运行在受限的虚拟机里有验证器检查和运行时沙箱不会像内核模块那样一个野指针直接弄崩系统这也是它能被用于生产环境的核心原因。2. 动手前的准备环境、工具与内核要求2.1 确认设备内核是否支持 eBPF动手前要先确认内核到底行不行。最直接的方法是通过/proc/kallsyms看有没有 bpf 相关符号或者查看内核编译配置。有 root 权限的话可以自己编译一个 bpftool 放到设备上然后执行bpftool feature probe它会自动检测内核支持哪些 eBPF 特性和 helper。我自己的流程是这样的先看内核版本再检查/sys/kernel/btf/btf是否存在因为 BTF 后面会影响你的开发体验最后拿一个极简的 BPF 程序试加载看系统给不给过。具体要确认的项我整理成下面的清单内核版本adb shell uname -a建议至少 4.14越新越好。BPF 系统调用检查/proc/kallsyms里是否能搜到bpf关键字或者是否存在/proc/sys/kernel/bpf相关节点。BTF查看/sys/kernel/btf/btf存在的话开发体验会好很多。debugfs/tracefs 挂载cat /proc/mounts | grep tracefs有些 ROM 没挂载会影响 tracepoint 类程序。提示如果设备内核太老或者厂商把能力屏蔽了建议直接用 Pixel 系列或者刷了 AOSP 的设备做开发。真机调试时root 能省掉大量权限问题。2.2 开发环境搭建Android Studio / NDK / BPF 工具链在 Android 上开发 eBPF不一定需要装完整 Android Studio但你至少要有一个能用的 NDK 工具链因为需要交叉编译用户态 loader 程序。建议直接把 Android Studio 装好配一个最新版 NDK比如 r25 或更新的版本。这里有个常见误区很多人以为要装 BCC 或者 bpftrace其实 Android 上基本用不到BCC 依赖 Python 和 LLVM在手机上跑会引入一大堆依赖非常痛苦。更务实的路线是用 NDK 自带的 clang 编译 BPF C 程序生成.bpf.o然后通过 libbpf 静态库写一个小 C loader或者直接用静态编译的 bpftool 加载 BPF 对象。这条链路依赖干净、可控适合从零开始学也适合做产品化落地。环境清单大致如下开发机Linux 或 macOSWindows 建议用 WSL。Android NDK r25 以上自带 clang/llvm。libbpf 源码从内核源码树tools/lib/bpf拷贝或从 libbpf 官方仓库拉取。bpftool 源码内核源码树tools/bpf/bpftool需要自己静态编译。如果你只做 BPF 程序验证连 Android Studio 都不需要一个 NDK 命令行工具链完全够用。我自己更喜欢命令行编译脚本比 IDE 工程透明得多。2.3 选取目标设备与 root 权限的取舍eBPF 加载本质是调用bpf()系统调用它需要足够权限。在 Android 上绝大部分场景需要 root比如CAP_SYS_ADMIN新版内核还可能要求CAP_BPF。如果你用的设备没有 root也不是完全没路走有一些厂商会在系统进程里内置 eBPF 加载服务普通 App 可以通过授权接口间接使用但那是厂商定制方案不通用。想认认真真学习或者做技术验证建议拿一台能解锁 bootloader 的设备比如 Pixel。我的经验是开发阶段直接 root然后通过 adb 把编译好的 BPF 对象和 loader 推到/data/local/tmp用su执行这个工作流最稳定也最适合调试。等到要集成到系统级产品里再考虑通过 SELinux 策略给特定域授予最小权限而不是一刀切给所有 App 开 root。2.4 非 root 环境的受限方案与思路如果你只能在非 root 设备上做也不是毫无办法但思路要转换。首先可以考虑用户态 uprobe对你有调试权限的进程可以 attach 到它的某个函数入口这种场景对权限要求没那么高不像内核 kprobe 那样需要CAP_SYS_ADMIN。其次Android 系统里已经预置了不少 eBPF 程序比如网络统计用的bpf_netd、bpf_cgroup相关程序它们创建的 map 通常会被系统服务暴露出来你可以尝试通过进程 ID 权限去读取这些 map从而拿到 App 网络流量数据。更常见的是走标准 API比如NetworkStatsManager它底层就是 eBPF 在统计流量虽然不是让你自己写 BPF但能用上这个技术链路带来的结果。如果你是想开发通用 eBPF 工具我的结论很直接非 root 只能“读现成 map”不能真正加载自己的程序所以开发调试阶段还是老老实实准备一台 root 设备。3. 核心实操写出并编译第一个 Android eBPF 程序3.1 BPF C 程序源码结构先写一个最简单的程序用来监控进程创建。这里我选择 attach 到 tracepointtracepoint/sched/sched_process_exec。每当进程执行 exec 时这个 tracepoint 就会被触发我们的 BPF 程序会拿到触发时的上下文包括被加载的可执行文件路径。这里特别说明一下为什么选择 tracepoint 而不是 kprobetracepoint 的接口更稳定参数结构体也是内核为调试专门暴露的 ABI不会因为厂商改了某个内部函数签名就失效。对于初学者从 tracepoint 入手是最不容易受挫的。代码如下// hello.bpf.c #include vmlinux.h #include bpf/bpf_helpers.h char LICENSE[] SEC(license) GPL; struct { __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); __uint(max_entries, 128); } events SEC(.maps); SEC(tracepoint/sched/sched_process_exec) int on_exec(struct trace_event_raw_sched_process_exec *ctx) { char cmd[16]; bpf_probe_read_kernel_str(cmd, sizeof(cmd), ctx-filename); int pid bpf_get_current_pid_tgid() 32; bpf_perf_event_output(ctx, events, BPF_F_CURRENT_CPU, pid, sizeof(pid)); return 0; }要注意vmlinux.h依赖内核 BTF 自动生成它把内核里的结构体、宏定义都补齐了省去了一大堆手动拷贝头文件的工作。如果你的内核不支持 BTF就得手动包含 UAPI 头文件过程会稍微繁琐一些。上面这份代码用 PERF_EVENT_ARRAY 把 pid 发到用户态用户态只管接收即可。整个程序不长但已经涵盖 eBPF 最重要的三件事读取内核数据、调用 helper、输出事件。3.2 交叉编译使用 NDK 里的 clang 编译 BPF 对象编译 BPF C 程序有一个和普通 Android 开发完全不同的点它不是把代码编译成 ARM 指令而是编译成一个 eBPF 字节码目标文件之后由内核虚拟机解释执行或 JIT 编译。所以你必须在命令里显式指定-target bpf而不是aarch64-linux-android。这是新手最容易搞混的地方我看到很多人用 arm64 的目标参数编出来一个 ELF结果 BPF program load 直接报错。正确的交叉编译命令如下export NDK/path/to/android-ndk-r25c export CLANG$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/clang # 先用 bpftool 在设备上生成 vmlinux.h拷贝到开发机 # 然后编译 $CLANG -target bpf -O2 -g -D__TARGET_ARCH_arm64 \ -I /path/to/libbpf/src \ -c hello.bpf.c -o hello.bpf.o参数解读如下-target bpf告诉 clang 生成 BPF 目标文件。-O2BPF 程序会经过内核验证器未优化代码容易超出指令数限制O2 能生成更紧凑的指令。-D__TARGET_ARCH_arm64影响某些宏和 helper 的调用方式。-g保留调试信息出错时能看到更清晰的符号生产环境一般不加。-I /path/to/libbpf/src因为要用bpf_helpers.h这些公共头文件。如果在编译时缺函数定义多半是头文件路径不对。建议直接使用 libbpf 仓库里带的头文件版本最好能和你目标内核匹配。这一步通过之后你手上就有一个可以交给内核验证器的.bpf.o文件了。3.3 在 Android 上加载 BPF 程序libbpf loader 与 bpftool编出.bpf.o之后加载路线有两条。第一种是 bpftool操作直观但 Android 上没有自带需要静态编译然后把 bpftool push 到设备。加载命令大概是adb push hello.bpf.o /data/local/tmp/ adb shell su -c bpftool prog load /data/local/tmp/hello.bpf.o /sys/fs/bpf/hello需要提醒的是挂载/sys/fs/bpf是个坑某些 ROM 没有挂载 bpffs你会看到mount: No such file or directory。这时候需要先执行mount -t bpf bpf /sys/fs/bpf然后再用 bpftool。第二种路线是写一个用户态 C loader通过 libbpf API 加载和 attach这个更适合产品化。loader 编译成 arm64 可执行文件放到手机上执行。核心代码大概// main.c #include stdio.h #include unistd.h #include bpf/libbpf.h int main(void) { struct bpf_object *obj bpf_object__open_file(hello.bpf.o, NULL); bpf_object__load(obj); struct bpf_program *prog bpf_object__find_program_by_name(obj, on_exec); bpf_program__attach(prog); printf(eBPF attach ok, wait events...\n); sleep(30); return 0; }然后交叉编译这个 loader$CLANG --targetaarch64-linux-android24 -O2 \ -I/path/to/libbpf/src -L/path/to/libbpf/arm64 \ main.c -lbpf -o bpf_loader adb push bpf_loader /data/local/tmp/ adb shell su -c chmod x /data/local/tmp/bpf_loader /data/local/tmp/bpf_loader这一步最容易出问题的是 libbpf 交叉编译因为 Android 没有标准 libelf需要先用 NDK 交叉编译 libelf 和 libbpf。我的建议是下载 libelf 源码编译成静态库再编 libbpf 时把LIBELF_USE_STATIC打开。等这套编译环境配好之后所有 BPF 工程都能复用很值得花一次时间搞定。3.4 读取 map 并将结果打印到日志前面的 BPF 程序用了 PERF_EVENT_ARRAY用户态 loader 需要注册一个 perf buffer 回调来接收事件。这里给出一个精简版片段方便你理解完整链路。perf buffer 是 eBPF 里专门用于向用户态发送事件流的机制它适合高频事件每次事件可以携带自定义结构体。我们这里只传了一个 int 类型的 pid结构很简单。static int event_cb(void *ctx, void *data, size_t data_sz) { int pid *(int *)data; fprintf(stdout, exec pid: %d\n, pid); return 0; } struct perf_buffer *pb perf_buffer__new(map_fd, 8, event_cb, NULL, NULL, NULL); while (true) { perf_buffer__poll(pb, 100); }可以看到用户态逻辑被拆成两个部分一个回调函数处理数据一个循环驱动 poll。如果你想把多次事件聚合起来就不要用 PERF_EVENT_ARRAY而是用 BPF_MAP_TYPE_HASH/IPERF 这种 map 在 BPF 内做累加用户态定时 dump map。不同场景选不同的 map这是 eBPF 程序性能好坏的关键。3.5 验证效果查看 trace 或 map 值如果加载成功你会看到 loader 进程持续输出 exec 事件。比如打开任意一个 App终端里就会多出几行 pid 记录。另一种验证方式是直接查看内核 trace 文件adb shell su -c echo 1 /sys/kernel/debug/tracing/tracing_on adb shell su -c cat /sys/kernel/debug/tracing/trace_pipe很多 tracepoint 程序会把原始数据打到这个 trace 文件里虽然格式比较粗糙但作为验证手段很有效。如果程序 attach 成功但没有任何输出先查两件事一是 SELinux 是否拦截二是 attach 的 tracepoint 是否真的被事件触发。可以用ls /sys/kernel/debug/tracing/events/sched/sched_process_exec确认事件节点存在再手动执行一条命令触发 exec看 trace_pipe 里有没有数据。4. 深入实践一个网络监控 eBPF 程序TCP 连接统计4.1 程序设计与 map 选择现在做一个更实用的案例统计 Android 上每个进程建立的 TCP 连接数量。整体思路是 hook 内核函数tcp_set_state这个函数负责 TCP 状态迁移当状态变为TCP_SYN_SENT或TCP_ESTABLISHED时我们认为产生了一次连接事件。为什么选择这个函数而不是tcp_v4_connect因为tcp_set_state能覆盖的连接场景更全包括不同协议族的连接而且函数签名相对稳定。当然内核版本不同可能函数名有差异动手前先确认tcp_set_state在/proc/kallsyms里存在。我们还需要一个 map 来统计结果。关于 map 类型我选择BPF_MAP_TYPE_HASHkey 是 pidvalue 是一个结构体保存进程名和计数。用 hash map 的好处是支持并发、使用灵活而且可以保存任意结构体。缺点是并发更新时需要注意同步不过 BPF 里提供了__sync_fetch_and_add这样的原子操作只要用对就安全。4.2 关键实现Hook 点选择与内核符号查找内核符号不是每个版本都一样厂商有可能改了函数名甚至把函数 inline 掉了所以一定要先验证。验证方法很简单adb shell su -c cat /proc/kallsyms | grep tcp_set_state如果存在类似ffffffff... T tcp_set_state的输出说明符号可用。root KASLR 开启时地址会显示为 0但你没必要求地址kprobe 会通过符号名自动解析。BPF C 的核心代码是这样// tcpmon.bpf.c #include vmlinux.h #include bpf/bpf_helpers.h char LICENSE[] SEC(license) GPL; struct tcp_info { char comm[TASK_COMM_LEN]; __u64 count; }; struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 10240); __type(key, __u32); // tgid __type(value, struct tcp_info); } tcp_count_map SEC(.maps); SEC(kprobe/tcp_set_state) int BPF_KPROBE(tcp_set_state_probe, struct sock *sk, int state) { if (state ! TCP_SYN_SENT state ! TCP_ESTABLISHED) return 0; __u32 key bpf_get_current_pid_tgid() 32; struct tcp_info *info bpf_map_lookup_elem(tcp_count_map, key); if (!info) { struct tcp_info new_info {}; bpf_get_current_comm(new_info.comm, sizeof(new_info.comm)); new_info.count 1; bpf_map_update_elem(tcp_count_map, key, new_info, BPF_ANY); } else { __sync_fetch_and_add(info-count, 1); } return 0; }这个程序的关键点是BPF_KPROBE宏。它让你可以用“自动展开参数”的方式定义 kprobe 函数而不是手动解析pt_regs。struct sock *sk和int state对应内核函数的原型参数。attach 到 kprobe 时内核会调用这个钩子函数第二个参数 state 会从寄存器中解出来。不要尝试访问sk内部太多字段因为旧内核和 GKI 内核的 sock 结构体都可能有差异越简单的程序越容易跑起来。4.3 用户态读取与前端展示loader 加载 BPF 程序后可以每秒读取一次 map。这个方式比 perf buffer 更适合聚合类统计因为它拿到的是压缩后的计数结果而不是原始事件流。实现上用一个循环遍历 map 的 key然后打印。简化代码如下int map_fd bpf_object__find_map_fd_by_name(obj, tcp_count_map); struct tcp_info val; __u32 key 0, next_key; while (1) { while (bpf_map_get_next_key(map_fd, key, next_key) 0) { bpf_map_lookup_elem(map_fd, next_key, val); printf(pid%d comm%s count%llu\n, next_key, val.comm, val.count); key next_key; } key 0; sleep(1); }需要注意遍历 map 的时候如果 map 正在被并发更新可能出现 key 重复或 val 不一致的情况。我的做法是先尽量减小 BPF 端更新窗口或者用户态连续读两次把明显不一致的数据过滤掉。如果图省事也可以用bpftool map dump name tcp_count_map直接把结果 dump 出来观察原始数据。4.4 编译、部署到真机并观察输出部署流程和前面一样交叉编译 BPF 对象、交叉编译 loaderpush 到/data/local/tmp然后su执行。实际观察时你会发现不同 App 的频率差异较大。比如打开一个短视频 App它的 pid 变化非常快因为进程内多线程并发连接如果看到 pid 为 0 或进程名是 kworker 的条目那是内核线程自己也发起了连接属于正常现象。这个监控工具跑上几分钟你就能画出当前设备上网络行为的画像对比系统自带的流量统计两者应该能对上。如果发现某个 App 在没有打开 UI 的情况下频繁出现 TCP 连接就要警惕它是否在后台做小动作这也是安全团队常用的排查思路。注意kprobe 对函数内部实现非常敏感tcp_set_state可能被你的网络加速模块短路导致事件数量比预期少。如果现象不符合预期先用 tracepoint 对照测试再用 kprobe 精确定位。5. 踩坑实录在 Android 上使用 eBPF 的常见问题5.1 BTF 缺失与 vmlinux.h 生成问题BTFBPF Type Format是内核把自身类型信息暴露给 BPF 工具链的机制。如果你的内核没有开启 BTF编译 BPF 程序时就不能直接#include vmlinux.h因为这个头文件必须从 BTF 生成。我在一台老平板上试过一次报错信息是libbpf: failed to find BTF for type非常折磨人。替代方案有两条一是使用旧式头文件目录手动引入linux/bpf.h、linux/types.h等二是自己在 BPF 程序中声明用到的结构体。第一条太繁琐第二条不适用于复杂程序。所以我坚决建议使用开启 BTF 的设备比如 Pixel 和大部分新机开发效率完全不一样。另外如果加载阶段报tracing program attach failed: invalid argument多半是 tracepoint 参数结构体定义不匹配你可以通过查/sys/kernel/debug/tracing/events/.../format文件来核对结构体布局。5.2 内核配置与功能受限内核编译选项直接决定哪些 eBPF 功能可用。常见的开关有CONFIG_BPFy、CONFIG_BPF_SYSCALLy、CONFIG_DEBUG_INFO_BTFy、CONFIG_KPROBESy、CONFIG_TRACEPOINTSy、CONFIG_PERF_EVENTSy。你无法修改 vendor 内核时只能写一个探测程序去试错。我遇到过一台测试平板整体都支持 eBPF但CONFIG_KPROBES没有开启导致所有 kprobe 程序加载失败改成 tracepoint 就能跑。所以遇到问题不要硬刚换 hook 点比换手机还快。建议给每台测试设备维护一张配置表记录下来哪些能力可用以后排障会快很多。配置项作用缺失后的表现CONFIG_BPF_SYSCALLbpf() 系统调用无法加载任何 BPF 程序CONFIG_BPF_JITBPF JIT 编译加载成功但性能较差CONFIG_DEBUG_INFO_BTFBTF 支持vmlinux.h 无法生成CONFIG_KPROBESkprobe 机制kprobe attach 失败CONFIG_UPROBESuprobe 机制uprobe attach 失败CONFIG_TRACEPOINTStracepoint 机制tracepoint attach 失败5.3 SELinux 阻止 BPF 加载怎么处理SELinux 是 Android 上绕不开的一环。即使你有 rootSELinux enforcing 状态下如果进程没有bpf权限bpf()调用依然会被拒绝。开发阶段最省事的办法是临时把 SELinux 设为 permissiveadb shell su -c setenforce 0但必须清楚这只是临时调试手段。真正要部署到系统里需要在 sepolicy 中给目标域增加bpf能力、允许挂载和访问 bpffs以及访问特定 map 的权限。具体做法是在系统 sepolicy 目录下编写.te文件比如allow mydomain kernel: capability { bpf sys_admin };这类规则。我遇到过 setenforce 0 之后依然失败的情况排查后发现是/sys/fs/bpf没有挂载 bpffs需要手动执行mount -t bpf bpf /sys/fs/bpf。记住先检查挂载再检查 SELinux最后才怀疑代码问题按这个顺序排查效率最高。5.4 版本兼容与 GKI 带来的变化Android 内核碎片化很严重同一个 BPF 源码在不同设备上跑几乎不可能不做调整。GKI 的推行让内核主线和设备驱动解耦内核版本反而更统一符号也更规范。我的建议是尽量在 GKI 设备上开发验证。但 GKI 也带来一个问题一些调试符号被裁剪像tcp_set_state这种常见符号还好但厂商私有驱动里的函数可能就找不到了。所以长期维护的 BPF 工具应该优先使用 tracepoint 和稳定的内核 ABI不要依赖某个私有内联函数。如果确实需要 hook 私有函数只能按设备型号做符号表适配在代码里做一个 fallback 矩阵。5.5 性能与稳定性注意eBPF 虽然安全但性能上限还是有的。CPU 密集型事件比如网络包处理和调度切换如果 BPF 程序里做的动作太多依然会拖慢系统。我的经验是BPF 内只做最少的“统计”工作格式化输出、日志记录全部放到用户态。不要在 BPF 里调用bpf_trace_printk去高频打印这个 helper 在 tracefs 上的代价很大很容易把系统拖到“肉眼可见的卡顿”。另外BPF 程序里的循环必须有上界否则无法通过验证器。早期内核限制比较严循环展开超过一定次数就直接报错新版内核加入了有界循环支持但依然不建议写复杂循环。如果遇到program too large先把循环拆掉用查找表或分支优化。5.6 常见问题速查表现象可能原因快速处理libbpf: failed to find BTF for typeBTF 未开启或 vmlinux.h 生成失败换开启 BTF 的设备或改用传统头文件Kprobe attach failed: No such file or directory符号不存在或内核改变了 hook 点用 grep 查 kallsyms改换符号或 tracepointbpf_prog_load: Operation not permitted权限/SELinux 限制su 下执行setenforce 0 测试补充 sepolicy 规则operation not permitted on map用户态没有 map 访问权限在 loader 里绑定 map 的 fd检查运行 uidprogram too large指令数超限优化逻辑减少复杂循环和路径分支perf buffer events lost事件产生太快缓冲区太小调大 perf_buffer 页数或在 BPF 内先聚合统计6. 更进一步Android 上 eBPF 的高级玩法与扩展方向6.1 结合 Perfetto 与 ftracePerfetto 是 Android 性能分析的官方框架它的 trace 数据源有一部分直接对接内核 tracefs。你可以把自己的 BPF 事件写成 ftrace 风格事件输出然后在 Perfetto 里统一分析。最简单的方式是使用bpf_trace_printk往 trace 管道里写但这种方式只适合低频验证高频时会产生海量日志导致系统卡顿和日志丢失。更专业的方式是仍然用 map 收集统计结果用户态定时把聚合数据转成 Perfetto 的自定义 event这样你既保留 eBPF 的高效采集能力又能享受 Perfetto 的 UI 和 trace 分析功能。如果做性能专项这个组合几乎是最佳实践。6.2 用户态动态插桩 uprobe如果目标函数在用户态库或 App 进程内部不需要 hook 内核函数可以用 uprobe。比如想监控某个 App 调用 native 函数的次数可以在.so文件中找到函数符号并计算偏移量然后 attach uprobe。这个技术在 Android 上很有用适合分析 JNI 耗时、检测关键 so 是否被动态加载。缺点是 App 每次更新函数偏移都可能变化需要重新计算。我的做法是把符号表导出成配置文件在 App 发版后自动匹配新偏移再用脚本重编译 BPF 程序。uprobe 没有 kprobe 那么多权限限制在某些场景下反而更容易落地。6.3 与 BCC 等开源方案的配合桌面 Linux 上用过 BCC、bpftrace 的话到了 Android 上不要直接照搬。BCC 依赖 Python 和 LLVM runtime在手机上是灾难bpftrace 的脚本能跑但静态编译复杂、开销也大。不过这些项目的源码很有参考价值。BCC 仓库里有大量现成的 BPF 程序比如opensnoop、tcpconnect、execsnoop它们的 C 代码可以抽出来交叉编译成.o再用 libbpf 加载器跑。我建议你把 BCC 仓库里的工具当作题库需要哪个场景就拆哪个。这种“借鉴但不依赖”的方式能让你在 Android 上保持工具链的轻量稳定。6.4 eBPF 未来的趋势Android 内核模块与 ABI 稳定化Google 在持续推进 eBPF 在 Android 上的落地。从 Android 10 到 Android 15网络统计、存储监控、低内存管理等领域已经有大量 eBPF 应用。未来 BTF 和 GKI 的普及会让开发者的跨设备迁移更加平滑。但也要清醒Android 的 eBPF 生态和服务器 Linux 相比仍然碎片化厂商有自己的私有接口公开的、稳定的可编程接口还在演进。你现在学到的加载模型、编译方式、map 使用技巧以后迁移到其他系统依然有效因为 eBPF 的核心设计是通用的。掌握核心之后无论内核版本怎么变你都能很快适应。最后说一点我自己的体会。Android 上玩 eBPF最大的门槛不是写 BPF 程序而是搞清楚设备和环境的差异。我在一台老平板上试了一个完全能在 Pixel 上运行的程序结果折腾了半天最后发现只是缺了 BTF。所以建议刚开始不要贪多先把编译、加载、读 map 这条链路彻底打通再慢慢深入到 kprobe、uprobe 底层。等你熟练之后会发现它能覆盖你在 Android 内核层想观察的几乎所有东西而且比改内核、打补丁干净得多。这篇文章里的命令和代码都按真机验证过的流程梳理过照着走至少能让你在半小时内看到第一个 eBPF 事件输出。