1. BPF 简介
BPF,是Berkeley Packet Filter
的简称,最初构想提出于 1992 年。是一种网络分流器和数据包过滤器,允许早操作系统级别捕获和过滤计算机网络数据包。它为数据链路层提供了一个原始接口,允许发送和接收原始链路层数据包,并允许用户空间进程提供一个过滤程序
来制定它想要接收哪些数据包。BPF 仅返回通过进程提供的过滤器的数据包。这样避免了将不需要的数据包从操作系统内核复制到进程,从而大大提高了性能。
过滤程序
采用了虚拟机指令的形式,通过JIT
(just-in-time) 机制在内核中将其解释或编译成机器码。
2. eBPF 简介
在 2013 年,Alexei Starovoitov 对 BPF 进行彻底地改造,这个新版本被命名为 eBPF (extended BPF),与此同时,将以前的 BPF 变成 cBPF (classic BPF)。新版本出现了如映射
和尾调用 (tail call)
这样的新特性,并且 JIT 编译器也被重写了。新的语言比 cBPF 更接近于原生机器语言。并且,在内核中创建了新的附着点。
Extended Berkeley Packet Filter (eBPF) is an
in-kernel virtual machine
that runs user-supplied eBPF programs to extend kernel functionality. These programs can be hooked to probes or events in the kernel and used to collect useful kernel statistics, monitor, and debug. A program is loaded into the kernel using thebpf(2)
syscall and is provided by the user as a binary blob of eBPF machine instructions.
更多的 eBPF 的内部构建和架构,可以参考:Linux Extended BPF (eBPF) Tracing Tools
可观测工具拥有BPF 代码来执行某些特定操作:测量延迟、汇总一个柱状图、抓取堆栈traces 等。
BPF 代码会被编译成 BPF 字节码,然后被发送给内核,内核中有个验证器当认为该字节码不安全会拒绝。如果BPF 的字节码被接受,则可以将其附加到不同的event sources,包括:
- kprobes,内核动态跟踪;
- uprobes,用户级别动态跟踪;
- tracepoints,内核静态跟踪;
- perf_events,定时采样和PMCs;
BPF 有两种方法将测量数据传递回用户空间:
- 每个事件的details;
- 通过一个BPF map;
BPF maps 可以实现数组、关联数组和柱状图,并且适当传递汇总信息。
3. Android 中BPF 使用
涉及代码:
kernel/bpf/
external/libbpf/
system/bpf/
可以将框架图分成如下几个使用过程:
- 用户端 eBPF 程序源码开发;
- 用户端 eBPF 程序源码编译;
- 用户端 eBPF 字节码加载;
- 内核端对字节码验证;
- 将 eBPF 程序 attach 到内核钩子函数;
- 通过钩子函数调用 eBPF程序中的
the_prog
函数,更新map; - 内核侦测,通过map 或event map 与用户层交互;
4. eBPF 程序开发
在 Android 启动时,所有的 eBPF 程序位于 /system/etc/bpf/
目录下都会被加载。这些 eBPF 程序会通过 Android.bp
编译到/system/etc/bpf/
,成为system image 的一部分。
4.1 eBPF 的编写
eBPF 程序用C 编写,必须包含:
#include <bpf_helpers.h>
/* Define one or more maps in the maps section, for example
* define a map of type array int -> uint32_t, with 10 entries
*/
DEFINE_BPF_MAP(name_of_my_map, ARRAY, int, uint32_t, 10);
/* this also defines type-safe accessors:
* value * bpf_name_of_my_map_lookup_elem(&key);
* int bpf_name_of_my_map_update_elem(&key, &value, flags);
* int bpf_name_of_my_map_delete_elem(&key);
* as such it is heavily suggested to use lowercase *_map names.
* Also note that due to compiler deficiencies you cannot use a type
* of 'struct foo' but must instead use just 'foo'. As such structs
* must not be defined as 'struct foo {}' and must instead be
* 'typedef struct {} foo'.
*/
DEFINE_BPF_PROG("PROGTYPE/PROGNAME", AID_*, AID_*, PROGFUNC)(..args..) {
<body-of-code
... read or write to MY_MAPNAME
... do other things
>
}
LICENSE("GPL"); // or other license
4.1.1 bpf_helpers.h
eBPF 程序所有的宏定义和函数声明都在头文件 bpf_helpers.h
中,所以,必须要包含该头文件。
4.1.2 宏 DEFINE_BPF_MAP
在bpf_helpers.h 头文件中,可以看到很多map 定义的宏:
frameworks/libs/net/common/native/bpf_headers/include/bpf/bpf_helpers.h
#define DEFINE_BPF_MAP(the_map, TYPE, KeyType, ValueType, num_entries) \
DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
DEFAULT_BPF_MAP_UID, AID_ROOT, 0600)
#define DEFINE_BPF_MAP_RO(the_map, TYPE, KeyType, ValueType, num_entries, gid) \
DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
DEFAULT_BPF_MAP_UID, gid, 0440)
#define DEFINE_BPF_MAP_GWO(the_map, TYPE, KeyType, ValueType, num_entries, gid) \
DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
DEFAULT_BPF_MAP_UID, gid, 0620)
#define DEFINE_BPF_MAP_GRO(the_map, TYPE, KeyType, ValueType, num_entries, gid) \
DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
DEFAULT_BPF_MAP_UID, gid, 0640)
#define DEFINE_BPF_MAP_GRW(the_map, TYPE, KeyType, ValueType, num_entries, gid) \
DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
DEFAULT_BPF_MAP_UID, gid, 0660)
从代码可以看到 DEFINE_BPF_MAP 使用默认 AID_ROOT,而其他的则是使用了指定的 gid,并且指定了map 节点的mode 值。最终调用的都是 DEFINE_BPF_MAP_UGM
这个宏定义。
关注该宏函数的:
- 第一个参数
the_map
:定义的一个 struct bpf_map_def 变量的名称,该变量符号位于maps
段,详细可以查看 bpf_helpers.h。该名称用来通知 BPF loader 将要创建的map 类型以及参数; - 第二个参数
TYPE
:这个用以表明 map 的类型,会与BPF_MAP_TYPE_
拼接成实际的bpf_map_type
,详细的可以查看 uapi/linux/bpf.h 中enum bpf_map_type
; - 第三个参数
KeyType
:map 数据中key 数据的类型,例如int 或 uint64_t; - 第四个参数
ValueType
:map 数据中value 数据的类型,例如 uint64_t; - 第五个参数
num_entries
:map 数据的条目数;
注意:
第一个参数 the_map
是结构体变量名称,该变量被放到代码段 maps
,在bpfloader 加载该程序时会创建一个map 文件,格式为:/sys/fs/bpf/<pin_subdir|prefix>/map_<objName>_<mapName>
例子:
frameworks/native/services/gpuservice/bpfprogs/gpuMem.c
DEFINE_BPF_MAP_GRO(gpu_mem_total_map, HASH, uint64_t, uint64_t, GPU_MEM_TOTAL_MAP_SIZE,
AID_GRAPHICS);
使用的 DEFINE_BPF_MAP_GRO
定义一个全局变量 const struct bpf_map_def gpu_mem_total_map。
该变量使用的map类型为 BPF_MAP_TYPE_HASH
,键值对是 uint64_t :uint64_t,这样的条目有 GPU_MEM_TOTAL_MAP_SIZE
个。
在bpfloader 加载该程序时会创建一个map 文件,格式为:/sys/fs/bpf/map_
gpuMem
_
gpu_mem_total_map
其中prefix 为空、objName 为gpuMem、mapName为结构体变量名称 gpu_mem_total_map
4.1.3 宏 DEFINE_BPF_PROG
在bpf_helpers.h 头文件中,可以看到很多prog 定义的宏:
frameworks/libs/net/common/native/bpf_headers/include/bpf/bpf_helpers.h
#define DEFINE_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv) \
DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
false)
#define DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, \
max_kv) \
DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, true)
// programs requiring a kernel version >= min_kv
#define DEFINE_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv) \
DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF, \
false)
#define DEFINE_OPTIONAL_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv) \
DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF, \
true)
// programs with no kernel version requirements
#define DEFINE_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, 0, KVER_INF, false)
#define DEFINE_OPTIONAL_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, 0, KVER_INF, true)
主要考虑是对kernel version 的要求,内核版本要求[min_kv, max_kv)
注意最后一个参数 optional:
frameworks/libs/net/common/native/bpf_headers/include/bpf/bpf_helpers.h
// Programs (here used in the sense of functions/sections) marked optional are allowed to fail
// to load (for example due to missing kernel patches).
// The bpfloader will just ignore these failures and continue processing the next section.
//
// A non-optional program (function/section) failing to load causes a failure and aborts
// processing of the entire .o, if the .o is additionally marked critical, this will result
// in the entire bpfloader process terminating with a failure and not setting the bpf.progs_loaded
// system property. This in turn results in waitForProgsLoaded() never finishing.
//
// ie. a non-optional program in a critical .o is mandatory for kernels matching the min/max kver.
当标记optional,表示允许load 该eBPF 程序失败;
当没有标记optional,会依赖程序是否设定 CRITICAL
,一旦标记该flag,当加载该eBPF 程序失败的时,会结束整个加载过程,且bpfloader 结束属性 bpf.progs_loaded
不会设置。这就导致了 waitForProgsLoaded
函数永远无法停止:
frameworks/libs/net/common/native/bpf_headers/include/bpf/WaitForProgsLoaded.h
static inline void waitForProgsLoaded() {
// infinite loop until success with 5/10/20/40/60/60/60... delay
for (int delay = 5;; delay *= 2) {
if (delay > 60) delay = 60;
if (android::base::WaitForProperty("bpf.progs_loaded", "1", std::chrono::seconds(delay)))
return;
ALOGW("Waited %ds for bpf.progs_loaded, still waiting...", delay);
}
}
回到宏函数 DEFINE_BPF_PROG,关注该宏函数的参数:
- 第一个参数
SECTION_NAME
:用以定义bpf 函数 the_prog() 的 section,详细看第四个参数,且该参数细分为PROGTYPE/PROGNAME
,PROGTYPE
为 eBPF程序的代码类型,PROGNAME
为eBPF程序名称(与type 绑定); - 第二个参数
prog_uid
:bpf 程序的uid; - 第三个参数
prog_gid
:bpf 程序的gid; - 第四个参数
the_prog
:共两个作用:- 定义一个全局 struct bpf_prog_def 变量,变量名为
the_prog##_def
,该变量符号位于progs
段; - 定义一个函数 the_prog(),该函数符号位于
SECTION_NAME
段,详细看第一个参数;
- 定义一个全局 struct bpf_prog_def 变量,变量名为
注意:
第一个参数 SECTION_NAME
在bpfloader 加载该程序时会创建一个用来pin 的prog 文件,格式为:/sys/fs/bpf/<pin_subdir|prefix>/prog_<objName>_<sectionName>
,其中<sectionName>就是将SECTION_NAME
中的斜杠换成下划线。
例子:
frameworks/native/services/gpuservice/bpfprogs/gpuMem.c
DEFINE_BPF_PROG("tracepoint/gpu_mem/gpu_mem_total", AID_ROOT, AID_GRAPHICS, tp_gpu_mem_total)
(struct gpu_mem_total_args* args) {
...
}
使用 DEFINE_BPF_PROG
定义bpf 函数 tp_gpu_mem_total()
,该函数位于代码段中tracepoint/gpu_mem/gpu_mem_total
段,该函数有个参数 struct gpu_mem_total_args*
.
另外,程序的type是 tracepoint
,那么 tp_gpu_mem_total
会被挂接在跟踪点。那么,PROGNAME
中的gpu_mem 是跟踪子系统的名称,gpu_mem_total 是跟踪的events 名称。详细可以查看:trace/events.txt
在bpfloader 在加载该程序时,会创建一个用来pin 的prog 文件,格式为:/sys/fs/bpf/prog_
gpuMem
_
tracepoint_gpu_mem_gpu_mem_total
PROGTYPE
可以是下表中任意一个,当不是这里列举的类型,则认为该程序没有严格的命名协定,那么PROGNAME
只需要被attach该程序的进程知道即可。
kprobe | Hooks PROGFUNC onto at a kernel instruction using the kprobe infrastructure. PROGNAME must be the name of the kernel function being kprobed. Refer to the kprobe kernel documentation for more information about kprobes. |
tracepoint | Hooks PROGFUNC onto a tracepoint. PROGNAME must be of the format SUBSYSTEM/EVENT. For example, a tracepoint section for attaching functions to scheduler context switch events would be SEC("tracepoint/sched/sched_switch"), where sched is the name of the trace subsystem, and sched_switch is the name of the trace event. Check the trace events kernel documentationfor more information about tracepoints. |
skfilter | Program functions as a networking socket filter. |
schedcls | Program functions as a networking traffic classifier. |
cgroupskb, cgroupsock | Program runs whenever processes in a CGroup create an AF_INET or AF_INET6 socket. |
更多的type 可以查看:
system/bpf/libbpf_android/Loader.cpp
sectionType sectionNameTypes[] = {
{"bind4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_BIND},
{"bind6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_BIND},
{"cgroupskb/", BPF_PROG_TYPE_CGROUP_SKB, BPF_ATTACH_TYPE_UNSPEC},
{"cgroupsock/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_ATTACH_TYPE_UNSPEC},
{"connect4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_CONNECT},
{"connect6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_CONNECT},
{"egress/", BPF_PROG_TYPE_CGROUP_SKB, BPF_CGROUP_INET_EGRESS},
{"getsockopt/", BPF_PROG_TYPE_CGROUP_SOCKOPT, BPF_CGROUP_GETSOCKOPT},
{"ingress/", BPF_PROG_TYPE_CGROUP_SKB, BPF_CGROUP_INET_INGRESS},
{"kprobe/", BPF_PROG_TYPE_KPROBE, BPF_ATTACH_TYPE_UNSPEC},
{"kretprobe/", BPF_PROG_TYPE_KPROBE, BPF_ATTACH_TYPE_UNSPEC},
{"lwt_in/", BPF_PROG_TYPE_LWT_IN, BPF_ATTACH_TYPE_UNSPEC},
{"lwt_out/", BPF_PROG_TYPE_LWT_OUT, BPF_ATTACH_TYPE_UNSPEC},
{"lwt_seg6local/", BPF_PROG_TYPE_LWT_SEG6LOCAL, BPF_ATTACH_TYPE_UNSPEC},
{"lwt_xmit/", BPF_PROG_TYPE_LWT_XMIT, BPF_ATTACH_TYPE_UNSPEC},
{"perf_event/", BPF_PROG_TYPE_PERF_EVENT, BPF_ATTACH_TYPE_UNSPEC},
{"postbind4/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET4_POST_BIND},
{"postbind6/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET6_POST_BIND},
{"recvmsg4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_RECVMSG},
{"recvmsg6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_RECVMSG},
{"schedact/", BPF_PROG_TYPE_SCHED_ACT, BPF_ATTACH_TYPE_UNSPEC},
{"schedcls/", BPF_PROG_TYPE_SCHED_CLS, BPF_ATTACH_TYPE_UNSPEC},
{"sendmsg4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_SENDMSG},
{"sendmsg6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_SENDMSG},
{"setsockopt/", BPF_PROG_TYPE_CGROUP_SOCKOPT, BPF_CGROUP_SETSOCKOPT},
{"skfilter/", BPF_PROG_TYPE_SOCKET_FILTER, BPF_ATTACH_TYPE_UNSPEC},
{"sockops/", BPF_PROG_TYPE_SOCK_OPS, BPF_CGROUP_SOCK_OPS},
{"sysctl", BPF_PROG_TYPE_CGROUP_SYSCTL, BPF_CGROUP_SYSCTL},
{"tracepoint/", BPF_PROG_TYPE_TRACEPOINT, BPF_ATTACH_TYPE_UNSPEC},
{"uprobe/", BPF_PROG_TYPE_KPROBE, BPF_ATTACH_TYPE_UNSPEC},
{"uretprobe/", BPF_PROG_TYPE_KPROBE, BPF_ATTACH_TYPE_UNSPEC},
{"xdp/", BPF_PROG_TYPE_XDP, BPF_ATTACH_TYPE_UNSPEC},
};
4.1.4 宏 LICENSE
frameworks/libs/net/common/native/bpf_headers/include/bpf/bpf_helpers.h
#define LICENSE(NAME) \
unsigned int _bpfloader_min_ver SECTION("bpfloader_min_ver") = BPFLOADER_MIN_VER; \
unsigned int _bpfloader_max_ver SECTION("bpfloader_max_ver") = BPFLOADER_MAX_VER; \
size_t _size_of_bpf_map_def SECTION("size_of_bpf_map_def") = sizeof(struct bpf_map_def); \
size_t _size_of_bpf_prog_def SECTION("size_of_bpf_prog_def") = sizeof(struct bpf_prog_def); \
char _license[] SECTION("license") = (NAME)
eBPF 程序开发必须要指定 LICENSE
,否则无法加载。
系统会使用 LICENSE
宏不仅用来验证该程序是否与内核的 licese 兼容。
该宏还定义了几个全局变量,位于不同的section。
4.1.5 宏 CRITICAL
frameworks/libs/net/common/native/bpf_headers/include/bpf/bpf_helpers.h
#define CRITICAL(REASON) char _critical[] SECTION("critical") = (REASON)
指定 CRITICAL
宏的程序必须要加载成功,否则会终止 bpfloader。
4.1.6 符号表
这里参考gpuMem.o 来确认eBPF 程序的符号表:
$ readelf -S gpuMem.o
There are 16 section headers, starting at offset 0x1250:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .strtab STRTAB 0000000000000000 00001139
0000000000000110 0000000000000000 0 0 1
[ 2] .text PROGBITS 0000000000000000 00000040
0000000000000000 0000000000000000 AX 0 0 4
[ 3] tracepoint/gpu_me PROGBITS 0000000000000000 00000040
0000000000000100 0000000000000000 AX 0 0 8
[ 4] .reltracepoint/gp REL 0000000000000000 00001100
0000000000000030 0000000000000010 I 15 3 8
[ 5] maps PROGBITS 0000000000000000 00000140
0000000000000078 0000000000000000 A 0 0 4
[ 6] .maps.gpu_mem_tot PROGBITS 0000000000000000 000001b8
0000000000000010 0000000000000000 WA 0 0 8
[ 7] progs PROGBITS 0000000000000000 000001c8
000000000000005c 0000000000000000 A 0 0 4
[ 8] bpfloader_min_ver PROGBITS 0000000000000000 00000224
0000000000000004 0000000000000000 WA 0 0 4
[ 9] bpfloader_max_ver PROGBITS 0000000000000000 00000228
0000000000000004 0000000000000000 WA 0 0 4
[10] size_of_bpf_map_d PROGBITS 0000000000000000 00000230
0000000000000008 0000000000000000 WA 0 0 8
[11] size_of_bpf_prog_ PROGBITS 0000000000000000 00000238
0000000000000008 0000000000000000 WA 0 0 8
[12] license PROGBITS 0000000000000000 00000240
000000000000000b 0000000000000000 WA 0 0 1
[13] .BTF PROGBITS 0000000000000000 0000024c
0000000000000da7 0000000000000000 0 0 4
[14] .llvm_addrsig LOOS+0xfff4c03 0000000000000000 00001130
0000000000000009 0000000000000000 E 0 0 1
[15] .symtab SYMTAB 0000000000000000 00000ff8
0000000000000108 0000000000000018 1 2 8
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)
4.2 eBPF 的编译
package {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_native_license"
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["frameworks_native_license"],
}
bpf {
name: "gpuMem.o",
srcs: ["gpuMem.c"],
btf: true,
cflags: [
"-Wall",
"-Werror",
],
}
将bpf 的 C 程序文件编译成 gpuMem.o,并生成到 /system/etc/bpf/gpuMem.o
,在系统启动的时候会自动加载 /system/etc/bpf/*.o
到内核中。
5. eBPF 程序加载
BPF 程序在Android 上有严格的权限控制,在bpfloader.te 中有明确sepolicy,限定了 bpfloader 是唯一可以加载 bpf 程序的程序。
system/sepolicy/private/bpfloader.te
neverallow { domain -bpfloader } *:bpf { map_create prog_load };
neverallow { domain -bpfloader } fs_bpf_loader:bpf *;
neverallow { domain -bpfloader } fs_bpf_loader:file *;
...
通过bpfloader.rc 可知bpfloader 只会在系统起来的时候运行一次,这样保证了其他模块无法额外加载系统之外的 BPF 程序,防止对内核的安全性造成危害。
5.1 main()
system/bpf/bpfloader/BpfLoader.cpp
int main(int argc, char** argv) {
...
// Create all the pin subdirectories
// (this must be done first to allow selinux_context and pin_subdir functionality,
// which could otherwise fail with ENOENT during object pinning or renaming,
// due to ordering issues)
for (const auto& location : locations) {
if (createSysFsBpfSubDir(location.prefix)) return 1;
}
if (createSysFsBpfSubDir("loader")) return 1;
// Load all ELF objects, create programs and maps, and pin them
for (const auto& location : locations) {
if (loadAllElfObjects(location) != 0) {
ALOGE("=== CRITICAL FAILURE LOADING BPF PROGRAMS FROM %s ===", location.dir);
ALOGE("If this triggers reliably, you're probably missing kernel options or patches.");
ALOGE("If this triggers randomly, you might be hitting some memory allocation "
"problems or startup script race.");
ALOGE("--- DO NOT EXPECT SYSTEM TO BOOT SUCCESSFULLY ---");
sleep(20);
return 2;
}
}
int key = 1;
int value = 123;
android::base::unique_fd map(
android::bpf::createMap(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 2, 0));
if (android::bpf::writeToMapEntry(map, &key, &value, BPF_ANY)) {
ALOGE("Critical kernel bug - failure to write into index 1 of 2 element bpf map array.");
return 1;
}
if (android::base::SetProperty("bpf.progs_loaded", "1") == false) {
ALOGE("Failed to set bpf.progs_loaded property");
return 1;
}
return 0;
}
bpfloader 的 main 函数主要操作:
- 轮询全局数组变量 locations,根据每个location 中的prefix 在
/sys/fs/bpf/
根目录下创建 pin 子目录; - 手动创建
/sys/fs/bpf/loader/
目录,用于触发 genfscon规则; - 调用
loadAllElfObjects
函数,轮询所有location 指定的目录下*.o
文件,调用bpf::loadProg
函数挂载所有的 BPF 程序,实现创建BPF 程序和相应的Map; - 设置属性
bpf.progs_loaded
为1,标记 bpfloader 加载程序完成;
5.1.1 数组变量 locations
system/bpf/bpfloader/BpfLoader.cpp
const android::bpf::Location locations[] = {
...
// Core operating system
{
.dir = "/system/etc/bpf/",
.prefix = "",
.allowedDomainBitmask = domainToBitmask(domain::platform),
.allowedProgTypes = kPlatformAllowedProgTypes,
.allowedProgTypesLength = arraysize(kPlatformAllowedProgTypes),
},
// Vendor operating system
{
.dir = "/vendor/etc/bpf/",
.prefix = "vendor/",
.allowedDomainBitmask = domainToBitmask(domain::vendor),
.allowedProgTypes = kVendorAllowedProgTypes,
.allowedProgTypesLength = arraysize(kVendorAllowedProgTypes),
},
};
省略的部分都是网络相关的 location,这里重点关注Android 中的bpf 程序所在目录 /system/etc/bpf
每个location 指定了允许的程序类型,例如:
constexpr bpf_prog_type kPlatformAllowedProgTypes[] = {
BPF_PROG_TYPE_KPROBE,
BPF_PROG_TYPE_PERF_EVENT,
BPF_PROG_TYPE_SOCKET_FILTER,
BPF_PROG_TYPE_TRACEPOINT,
BPF_PROG_TYPE_UNSPEC, // Will be replaced with fuse bpf program type
};
即/system/etc/bpf
目录中的bpf 程序类型只能是kprobe、perf_event、socket_filter、tracepoint;
5.2 loadAllElfObjects()
system/bpf/bpfloader/BpfLoader.cpp
int loadAllElfObjects(const android::bpf::Location& location) {
int retVal = 0;
DIR* dir;
struct dirent* ent;
if ((dir = opendir(location.dir)) != NULL) {
while ((ent = readdir(dir)) != NULL) {
string s = ent->d_name;
if (!EndsWith(s, ".o")) continue; //轮询所有的*.o 文件
string progPath(location.dir);
progPath += s; //获取 *.o 的路径
bool critical;
int ret = android::bpf::loadProg(progPath.c_str(), &critical, location);
if (ret) { //加载异常,是否设置了critical
if (critical) retVal = ret;
ALOGE("Failed to load object: %s, ret: %s", progPath.c_str(), std::strerror(-ret));
} else { //加载成功
ALOGI("Loaded object: %s", progPath.c_str());
}
}
closedir(dir);
}
return retVal;
}
核心函数是loadProg
,主要是加载各个 ELF 文件,读取 /system/etc/bpf/
下所有的 *.o
文件,然后加载到内核中。
为了避免 bpf prog和 map 对象在 bpfloader执行之后被销毁, 最后会通过 bpf_obj_pin
函数把这些bpf对象映射到 /sys/fs/bpf 文件节点,确保bpfloader 退出后,bpf 程序依然可以正常执行。
6. attach eBPF 程序
Bpf程序被加载之后,并没有附着到内核函数上,此时bpf程序不会有任何执行,还需要经过attach操作。attach指定把 bpf 程序 hook到哪个内核监控点上,例如 tracepoint、kprobe 等。
成功 attach 上的话,bpf 程序就转换为内核代码的一个函数。
这里用 GpuMem 为例:
frameworks/native/services/gpuservice/gpumem/GpuMem.cpp
void GpuMem::initialize() {
// 一直等待,知道bpfloader 成功加载完 bpf 程序
bpf::waitForProgsLoaded();
errno = 0;
//确认该程序是否加载成功,如果成功会有 prog文件节点,通过bpf 系统调用(cmd: BPF_OBJ_GET)获取到句柄
//对于gpuMem,该prog 的节点为 /sys/fs/bpf/prog_gpuMem_tracepoint_gpu_mem_gpu_mem_total
int fd = bpf::retrieveProgram(kGpuMemTotalProgPath);
if (fd < 0) {
ALOGE("Failed to retrieve pinned program from %s [%d(%s)]", kGpuMemTotalProgPath, errno,
strerror(errno));
return;
}
// Attach the program to the tracepoint, and the tracepoint is automatically enabled here.
errno = 0;
int count = 0;
//调用 bpf_attach_tracing_event()函数将该程序节点 attach 到tracepoint上
//tracepoint 的节点名称为 /sys/kernel/tracing/events/<tp_category>/<tp_name>
//tp_category 和tp_name就是 kGpuMemTraceGroup、kGpuMemTotalTracepoint
//至此,eBPF程序与钩子函数进行了绑定,当调用钩子函数时则会触发该 eBPF 程序定义的 the_prog函数
while (bpf_attach_tracepoint(fd, kGpuMemTraceGroup, kGpuMemTotalTracepoint) < 0) {
if (++count > kGpuWaitTimeout) {
ALOGE("Failed to attach bpf program to %s/%s tracepoint [%d(%s)]", kGpuMemTraceGroup,
kGpuMemTotalTracepoint, errno, strerror(errno));
return;
}
// Retry until GPU driver loaded or timeout.
sleep(1);
}
// Use the read-only wrapper BpfMapRO to properly retrieve the read-only map.
errno = 0;
//调用系统调用(cmd: BPF_OBJ_GET)获取 map 的句柄
//对于gpu来说该 map 节点为/sys/fs/bpf/map_gpuMem_gpu_mem_total_map
auto map = bpf::BpfMapRO<uint64_t, uint64_t>(kGpuMemTotalMapPath);
if (!map.isValid()) {
ALOGE("Failed to create bpf map from %s [%d(%s)]", kGpuMemTotalMapPath, errno,
strerror(errno));
return;
}
setGpuMemTotalMap(map);
mInitialized.store(true);
}
7. eBPF 程序运行
当钩子函数触发时,会调用 eBPF 程序指定的the_prog
函数,还是以gpu_mem 举例,最终调用的是tp_gpu_mem_total
:
frameworks/native/services/gpuservice/bpfprogs/gpuMem.c
DEFINE_BPF_PROG("tracepoint/gpu_mem/gpu_mem_total", AID_ROOT, AID_GRAPHICS, tp_gpu_mem_total)
(struct gpu_mem_total_args* args) {
uint64_t key = 0;
uint64_t cur_val = 0;
uint64_t* prev_val = NULL;
/* The upper 32 bits are for gpu_id while the lower is the pid */
key = ((uint64_t)args->gpu_id << 32) | args->pid;
cur_val = args->size;
if (!cur_val) {
bpf_gpu_mem_total_map_delete_elem(&key);
return 0;
}
prev_val = bpf_gpu_mem_total_map_lookup_elem(&key);
if (prev_val) {
*prev_val = cur_val;
} else {
bpf_gpu_mem_total_map_update_elem(&key, &cur_val, BPF_NOEXIST);
}
return 0;
}
7.1 参数args
参数 args 是event 中的信息,包括:
$ cat /sys/kernel/tracing/events/gpu_mem/gpu_mem_total/format
name: gpu_mem_total
ID: 657
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;
field:uint32_t gpu_id; offset:8; size:4; signed:0;
field:uint32_t pid; offset:12; size:4; signed:0;
field:uint64_t size; offset:16; size:8; signed:0;
print fmt: "gpu_id=%u pid=%u size=%llu", REC->gpu_id, REC->pid, REC->size
前面 8 个字节是common 信息,对应args 中的ignore;
后面的参数对应 args 中的 gpu_id、pid、size;
7.2 更新map
上面的函数中出现了几个更新map 的函数:
- bpf_gpu_mem_total_map_delete_elem() :删除map 中某个key 项数据
- bpf_gpu_mem_total_map_lookup_elem():读取 map 中该key 对应的value 指针;
- bpf_gpu_mem_total_map_update_elem():在 map中添加一个key-value 数据;
这三个函数的声明在 bpf_helper.h 中,在使用 DEFINE_BPF_MAP 时定义,最终对应于内核函数:
- bpf_map_delete_elem()
- bpf_map_lookup_elem()
- bpf_map_update_elem()
8. Event map 上报
一般的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个数相等的大小。
9. 调试
实际开发中,免不了需要反复调试的过程,遵照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。
#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个参数输出,超过则会报错。
参考:
eBPF 全面介绍
理解Android eBPF
Linux内核观测技术BPF
https://blog.csdn.net/hudongliang2006nb/article/details/136474370