Android 中ebpf 的集成和调试

server/2024/9/25 8:29:17/

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 the bpf(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 字节码,然后被发送给内核,内核中有个verifier,当认为该字节码不安全会拒绝。如果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.henum 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.cDEFINE_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.hstatic inline void waitForProgsLoaded() {// infinite loop until success with 5/10/20/40/60/60/60... delayfor (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/PROGNAMEPROGTYPE为 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段,详细看第一个参数;

注意:

第一个参数 SECTION_NAME在bpfloader 加载该程序时会创建一个用来pin 的prog 文件,格式为:/sys/fs/bpf/<pin_subdir|prefix>/prog_<objName>_<sectionName>其中<sectionName>就是将SECTION_NAME 中的斜杠换成下划线。

例子:

frameworks/native/services/gpuservice/bpfprogs/gpuMem.cDEFINE_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该程序的进程知道即可。

kprobeHooks 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.
tracepointHooks 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.
skfilterProgram functions as a networking socket filter.
schedclsProgram functions as a networking traffic classifier.
cgroupskb, cgroupsockProgram runs whenever processes in a CGroup create an AF_INET or AF_INET6 socket.

更多的type 可以查看:

system/bpf/libbpf_android/Loader.cppsectionType 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           OffsetSize              EntSize          Flags  Link  Info  Align[ 0]                   NULL             0000000000000000  000000000000000000000000  0000000000000000           0     0     0[ 1] .strtab           STRTAB           0000000000000000  000011390000000000000110  0000000000000000           0     0     1[ 2] .text             PROGBITS         0000000000000000  000000400000000000000000  0000000000000000  AX       0     0     4[ 3] tracepoint/gpu_me PROGBITS         0000000000000000  000000400000000000000100  0000000000000000  AX       0     0     8[ 4] .reltracepoint/gp REL              0000000000000000  000011000000000000000030  0000000000000010   I      15     3     8[ 5] maps              PROGBITS         0000000000000000  000001400000000000000078  0000000000000000   A       0     0     4[ 6] .maps.gpu_mem_tot PROGBITS         0000000000000000  000001b80000000000000010  0000000000000000  WA       0     0     8[ 7] progs             PROGBITS         0000000000000000  000001c8000000000000005c  0000000000000000   A       0     0     4[ 8] bpfloader_min_ver PROGBITS         0000000000000000  000002240000000000000004  0000000000000000  WA       0     0     4[ 9] bpfloader_max_ver PROGBITS         0000000000000000  000002280000000000000004  0000000000000000  WA       0     0     4[10] size_of_bpf_map_d PROGBITS         0000000000000000  000002300000000000000008  0000000000000000  WA       0     0     8[11] size_of_bpf_prog_ PROGBITS         0000000000000000  000002380000000000000008  0000000000000000  WA       0     0     8[12] license           PROGBITS         0000000000000000  00000240000000000000000b  0000000000000000  WA       0     0     1[13] .BTF              PROGBITS         0000000000000000  0000024c0000000000000da7  0000000000000000           0     0     4[14] .llvm_addrsig     LOOS+0xfff4c03   0000000000000000  000011300000000000000009  0000000000000000   E       0     0     1[15] .symtab           SYMTAB           0000000000000000  00000ff80000000000000108  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.0default_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.teneverallow { 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.cppint 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 themfor (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.cppconst 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.cppint 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) { //加载异常,是否设置了criticalif (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.cppvoid 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_totalint 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_mapauto 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.cDEFINE_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/formatname: 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


http://www.ppmy.cn/server/110034.html

相关文章

WiFi标签注册(电脑版)

安装WiFi-Tool工具 需要windows系统电脑并且有WiFi功能 下载软件安装包&#xff1a;http://a.picksmart.cn:8088/picksmart/app/WiFi-Tool-Setup-V1.0.37.zip 配置操作流程 登录WiFi标签管理系统到设备管理-产品管理&#xff0c;复制“产品ApiKey”参数&#xff0c;打开“WiFi-…

uniapp小程序怎么判断滑动的方向

项目场景&#xff1a; 获取手机上手指滑动的距离超过一定距离 来操作一些逻辑 解决方案&#xff1a; 在uniapp中&#xff0c;可以通过监听触摸事件来判断滑动的方向。常用的触摸事件包括touchstart, touchmove, 和 touchend。通过这些事件的参数&#xff0c;可以计算出用户的滑…

KTV开台源码--SAAS本地化及未来之窗行业应用跨平台架构

一、ktv开台源码 function 未来之窗_人工智能_KTV开台(title,桌台id,类型id,类型名称){var 未来之窗app_通用ID"未来之城激光加工机";var 未来之窗_人工智能_内容 tpl_未来之窗_模板_KTV开单;CyberWin_Dialog.layer(未来之窗_人工智能_内容,{type:"frame",…

Python基础性知识(中部分)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言1、Python中的语句1.1 顺序语句1.2 条件语句1.3 循环语句1.3.1 while循环1.3.2 for循环1.3.3 break与continue语句 1.4 综合三大语句制作小游戏--人生重开模拟器…

研究生深度学习入门的十天学习计划------第五天

第5天&#xff1a;深度学习中的模型评估与调优 目标&#xff1a; 掌握深度学习模型的评估方法&#xff0c;理解如何通过超参数调优和模型集成来提升模型性能。 5.1 模型评估指标 在深度学习中&#xff0c;不同的任务需要不同的评估指标来衡量模型的性能。常见的评估指标包括…

【C#】Visual Studio2017 MSDN离线安装

1. 运行Visual Studio Installer 在Windows的开始菜单中直接搜索 2. 单击“修改”按钮 3. 依次点击&#xff0c;单个组件 - 代码工具 - Help Viewer - 修改&#xff0c;开始安装 4. 下载速度慢解决方法 修改IPv4 DNS 参考&#xff1a;visual studio下载慢解决方法&#xf…

HarmonyOS NEXT应用开发: 常用页面模板

正文内容 我只写了几个我认为比较常用的界面 登录 首页 个人中心 然后尽量没有拆分代码&#xff0c;也没有使用公共变量&#xff0c;这样方便大家有需要的&#xff0c;可以快速直接复制使用&#xff0c;然后再根据自己实际项目情况进行拆分提取。 登录页面 运行效果如下 代码…

【轻松学EntityFramework Core】--数据迁移

本文将深入探讨EF Core的数据迁移功能&#xff0c;帮助您快速掌握数据库的创建、更新及版本控制&#xff0c;实现数据库与应用程序的无缝集成。无论您是初学者还是有经验的开发者&#xff0c;本文都将带您轻松驾驭EF Core的数据迁移。 一、什么是数据迁移&#xff1f; 数据迁…