Android 中ebpf 的集成和调试

2024-08-27 22:52
文章标签 android 调试 集成 ebpf

本文主要是介绍Android 中ebpf 的集成和调试,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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 字节码,然后被发送给内核,内核中有个验证器当认为该字节码不安全会拒绝。如果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

 

这篇关于Android 中ebpf 的集成和调试的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1113014

相关文章

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

ASIO网络调试助手之一:简介

多年前,写过几篇《Boost.Asio C++网络编程》的学习文章,一直没机会实践。最近项目中用到了Asio,于是抽空写了个网络调试助手。 开发环境: Win10 Qt5.12.6 + Asio(standalone) + spdlog 支持协议: UDP + TCP Client + TCP Server 独立的Asio(http://www.think-async.com)只包含了头文件,不依

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

计算机毕业设计 大学志愿填报系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

🍊作者:计算机编程-吉哥 🍊简介:专业从事JavaWeb程序开发,微信小程序开发,定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事,生活就是快乐的。 🍊心愿:点赞 👍 收藏 ⭐评论 📝 🍅 文末获取源码联系 👇🏻 精彩专栏推荐订阅 👇🏻 不然下次找不到哟~Java毕业设计项目~热门选题推荐《1000套》 目录 1.技术选型 2.开发工具 3.功能

【区块链 + 人才服务】区块链集成开发平台 | FISCO BCOS应用案例

随着区块链技术的快速发展,越来越多的企业开始将其应用于实际业务中。然而,区块链技术的专业性使得其集成开发成为一项挑战。针对此,广东中创智慧科技有限公司基于国产开源联盟链 FISCO BCOS 推出了区块链集成开发平台。该平台基于区块链技术,提供一套全面的区块链开发工具和开发环境,支持开发者快速开发和部署区块链应用。此外,该平台还可以提供一套全面的区块链开发教程和文档,帮助开发者快速上手区块链开发。

android-opencv-jni

//------------------start opencv--------------------@Override public void onResume(){ super.onResume(); //通过OpenCV引擎服务加载并初始化OpenCV类库,所谓OpenCV引擎服务即是 //OpenCV_2.4.3.2_Manager_2.4_*.apk程序包,存

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

vscode中文乱码问题,注释,终端,调试乱码一劳永逸版

忘记咋回事突然出现了乱码问题,很多方法都试了,注释乱码解决了,终端又乱码,调试窗口也乱码,最后经过本人不懈努力,终于全部解决了,现在分享给大家我的方法。 乱码的原因是各个地方用的编码格式不统一,所以把他们设成统一的utf8. 1.电脑的编码格式 开始-设置-时间和语言-语言和区域 管理语言设置-更改系统区域设置-勾选Bata版:使用utf8-确定-然后按指示重启 2.vscode