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 编写,必须包含:

cpp 复制代码
#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 定义的宏:

cpp 复制代码
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>

例子:

cpp 复制代码
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 定义的宏:

cpp 复制代码
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:

cpp 复制代码
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函数永远无法停止:

cpp 复制代码
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/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 中的斜杠换成下划线。

例子:

cpp 复制代码
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 可以查看:

cpp 复制代码
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

cpp 复制代码
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

cpp 复制代码
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 程序的符号表:

bash 复制代码
$ 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 的编译

bash 复制代码
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 程序的程序。

bash 复制代码
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()

cpp 复制代码
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

cpp 复制代码
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 指定了允许的程序类型,例如:

cpp 复制代码
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()

cpp 复制代码
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 为例:

cpp 复制代码
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

cpp 复制代码
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 中的信息,包括:

bash 复制代码
$ 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。

cpp 复制代码
#define bpf_printk(fmt, ...)                     \
    ({                                           \
    char ____fmt[] = fmt;                        \
    bpf_trace_printk(____fmt, sizeof(____fmt),   \
                            ##__VA_ARGS__);      \
    })

查看内核日志可用:

bash 复制代码
$ 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

相关推荐
带电的小王2 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡2 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道2 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库3 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道4 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe4 小时前
Android Hook - 动态加载so库
android
居居飒5 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He7 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗8 小时前
Android笔试面试题AI答之Android基础(1)
android
qq_397562319 小时前
android studio更改应用图片,和应用名字。
android·ide·android studio