Android 中ebpf 的集成和调试

news2024/9/20 5:59:06

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.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/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.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该程序的进程知道即可。

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.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

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2080219.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

安卓中回调函数的使用

在Android开发中&#xff0c;回调函数是一种常见的编程模式&#xff0c;用于在某个任务完成时异步接收通知或数据。它们通常用于处理用户界面事件、完成网络请求、数据库操作或其他长时间运行的任务。回调&#xff08;Callback&#xff09;是一种允许某段代码通知另一段代码执行…

机器人学——机械臂轨迹规划-2

直线轨迹 线段转折点速度不连续 加速度状态讨论 double dot 多段直线轨迹&#xff0c;转折点利用二次方程转为圆弧 关键步骤 第一个线段处理 Vt V0 at , 此处的V0 0 , 利用函数连续性&#xff0c;左右速度相等&#xff0c;联立求解 sgn(x):符号函数 最后一个线段…

dubbo:dubbo服务负载均衡、集群容错、服务降级、服务直连配置详解(五)

文章目录 0. 引言1. dubbo负载均衡1.1 负载均衡算法1.2. dubbo负载均衡使用1.3 自定义负载均衡策略 2. dubbo服务容错2.1 8种服务容错策略2.2 自定义容错策略 3. dubbo服务降级&#xff08;mock&#xff09;4. dubbo服务直连5. 总结 0. 引言 之前我们讲解了dubbo的基本使用&am…

内部类java

内部类就是定义在一个类里面的类&#xff0c;里面的类可以理解成&#xff08;寄生&#xff09;&#xff0c;外部类可以理解成&#xff08;寄主&#xff09;。 //外部类 public class people{//内部类public class heart{} } 内部类的使用场景、作用 1.当一个事物的内部&…

STM32学习笔记3---ADC,DMA

目录 ADC模拟数字转换器 规则组的四种转换模式 AD单通道 AD多通道 常用代码函数相关 DMA直接存储器 存取&#xff08;访问&#xff09; 两个应用 DMA存储器到存储器的转运 ADCDMA ADC模拟数字转换器 stm32数字电路&#xff0c;只有高低电平&#xff0c;无几V电压的概念…

MySQL:常用函数

MySQL&#xff1a;常用函数 日期时间函数字符串函数数学函数加密函数 在MySQL中&#xff0c;存在许多现成的函数&#xff0c;可以简化部分操作&#xff0c;本博客讲解MySQL中的常用函数。 日期时间函数 current_date current_date函数用于输出当前的日期&#xff1a; curren…

一道关于php文件包含的CTF题

一、源码 这是index.php的页面。 点击login后会发现url里多了action的参数&#xff0c;那么我们就可以通过它来获取源码。 ?actionphp://filter/readconvert.base64-encode/resourcelogin.php 再通过base64的解码可以查看源码。 index.php源码&#xff1a; <?php erro…

【编码解码】CyberChef v10.18.9

下载地址 【编码解码神器】CyberChef v10.18.9 在线地址 CyberChef (gchq.github.io) 简介 CyberChef 是一个简单易用的网页应用&#xff0c;&#xff0c;包含了四百多种在线编解码工具。它在浏览器中执行各种“网络安全”操作。这些操作包括简单的 XOR 和 Base64 编码、复…

基于单片机的无线空气质量检测系统设计

本设计以STC89C52单片机为核心&#xff0c;其中包含了温湿度检测模块、光照检测模块、PM2.5检测模块、报警电路、LCD显示屏显示电路、按键输入模块和无线传输模块来完成工作。首先&#xff0c;系统可以通过按键输入模块设置当前的时间和报警值&#xff1b;使用检测模块检测当前…

spring boot(学习笔记第十九课)

spring boot(学习笔记第十九课) Spring boot的batch框架&#xff0c;以及Swagger3(OpenAPI)整合 学习内容&#xff1a; Spring boot的batch框架Spring boot的Swagger3&#xff08;OpenAPI&#xff09;整合 1. Spring boot batch框架 Spring Batch是什么 Spring Batch 是一个…

个人网站免费上线

声明一下&#xff0c;小科用的是natapp&#xff0c;进行的 1.起步-下载安装 去浏览器搜索" natapp "&#xff0c;在官网下载&#xff0c;或者直接 点击下列网站 NATAPP-内网穿透 基于ngrok的国内高速内网映射工具https://natapp.cn/ 打开后下滑找到下载&#xff…

JMeter Plugins之内网插件问题解决

JMeter Plugins之内网插件问题解决 背景 在我司内部进行JMeter工具进行性能脚本开发时&#xff0c;为了提高测试效率&#xff0c;我们会用到部分JMeter提供的插件&#xff0c;但是在我司内网的情况下&#xff0c;我们如果直接点击JMeter界面右上角的插件按钮 弹出来的JMeter…

洛谷刷题(4)

P1089 [NOIP2004 提高组] 津津的储蓄计划 题目描述 津津的零花钱一直都是自己管理。每个月的月初妈妈给津津 300 元钱&#xff0c;津津会预算这个月的花销&#xff0c;并且总能做到实际花销和预算的相同。 为了让津津学习如何储蓄&#xff0c;妈妈提出&#xff0c;津津可以随…

零基础5分钟上手亚马逊云科技 - AI模型内容安全过滤

在上一篇文章中&#xff0c;小李哥带大家深入调研亚马逊云科技AI模型平台Amazon Bedrock热门开发功能&#xff0c;了解了模型平台的文字/图片生成、模型表现评估和模型内容安全审核的实践操作。这次我们将继续介绍如何利用API的形式&#xff0c;利用Python代码的形式对AI模型内…

OpenSearch的快照还原

本次测试选择把索引快照备份到Amazon S3&#xff0c;所以需要使用S3 repository plugin&#xff0c;这个插件添加了对使用 Amazon S3 作为快照/恢复存储库的支持。 OpenSearch集群自带了这个插件&#xff0c;所以无需额外安装。 由于需要和Amazon Web Services打交道&#xf…

工厂数字化转型中工业一体机起到什么作用?

近年来工厂数字化转型成为企业提升竞争力的关键路径。而在这场转型浪潮中&#xff0c;工业一体机扮演着至关重要的角色&#xff0c;它不仅是推动工厂数字化转型的关键工具&#xff0c;更是赋能企业实现更高效、智能、灵活生产的关键要素。 一、工业一体机&#xff1a;连接物理与…

CAN通信之波特率相关配置

由于 CAN 属于异步通讯&#xff0c;没有时钟信号线&#xff0c;连接在同一个总线网络中的各个节点会像串口异步通讯那样&#xff0c;节点间使用约定好的波特率进行通讯。 首先我们要明确几个概念&#xff1a; 波特率&#xff1a;can 1s传输的位数&#xff0c;其单位为bps。 T…

Vue3学习笔记之插槽

目录 前言 一、基础 (一) 默认插槽 (二) 具名插槽 (三) 作用域插槽 (四) 动态插槽 二、实战案例 前言 插槽&#xff08;Slots&#xff09;&#xff1f; 插槽可以实现父组件自定义内容传递给子组件展示&#xff0c;相当于一块画板&#xff0c;画板就是我们的子组件&…

速速报名|数据治理与数据建模workshop报名开启

由Datamodeling社区出品的「数据治理与数据建模workshop 」将在9月份正式启动上海站和深圳站。 本课程由社区特邀讲师王琤老师、黄峰老师授课&#xff0c;两位老师基于丰富的数据管理经验提炼出知识体系&#xff0c;以面对面带练的方式&#xff0c;帮助学习者快速掌握数据建模…

dp+差分数组

前言&#xff1a;怎么也没想到要用dp来做&#xff0c;并且这个题目中如果列为1的话还要特殊考虑 题目地址 #include<bits/stdc.h> using namespace std;//#define int long long const int N (int)5e3 10; int dp[N][N][2]; // 0 表示上端点&#xff0c;1表示下端点 in…