软件性能工程(8)-eBPF on Android

    技术2022-07-10  160

    做些铺垫

    本文假设读者已掌握如下内容

    熟悉 Linux 内核编译方法

    阅读过博文 eBPF 架构优势及其应用方向上的畅想

    熟悉 Git 操作

    熟悉 CMake,LLVM,Clang 等编译工具

    在博文 eBPF 架构优势及其应用方向上的畅想中有提到 eBPF 的执行流程,这套在主机系统(泛指基于 x86 的 Linux distribution)上直接 apt install 或者源码编译安装就可以了。但是在 Android 上怎么执行呢?现在大部分 Android 设备运行在基于 arm 的处理器架构上,我们熟悉的高通,MTK,华为海思都属于 arm 处理器架构。x86 上的 eBPF 工具栈(如前文所述,这里仅指 BCC&BPFTrace)程序是无法直接在 arm 处理器上执行的,需要所谓的交叉编译技术才可以。除了程序本身之外,它所依赖的基础库如 libc 也同样需要交叉编译才能正确工作。

    运行在 Android 上的难题

    BCC 及 BPFTrace 使用的是基于 CMake 的编译方式,与 Android 使用的 gradle,Android BP 的编译系统是不一致的

    BCC 项目中以 python 包为基础,那意味着需要有 python 运行环境,这在 anroid 里也是没有的

    可行的思路

    思考可行的思路,可以使用的方案如下

    没条件就创造条件,将 BCC&BPFTrace 强行适配到 Android 编译环境,使其可以直接运行在 Android 上下文中。中间涉及的依赖,冲突问题需要手动修改

    添加中间层,由 host 端生成的的 bytecode 通过 adb 通道派发给 client 端,具体执行由 client 端的常驻进程完成

    Android 端运行某个 linux distribution 环境(如 Debian,Ubuntu),在手机端编译与安装 BCC&BPFTrace

    参考 BCC&BPFTrace 设计思路,依照 Android 的架构实现一套类似功能程序,部分采用 BCC&BPFTrace 项目代码

    综合利弊之后,本文使用方案 3。缺点就是对 Android 的环境要求比较高,它需要:

    手机能够 root,而且可以将 data 分区 remount 成可读写

    Android kernel 版本要求在 4.9 及以上

    具有编译 android kernel 的环境

    步骤 1 编译带必要功能的内核

    通过手动编内核实现以下两个目的:

    使能 eBPF 相关功能(如果已经开启可以跳过此步骤)

    获取特定 kernel 的头文件。BCC 会用到此头文件中的结构体来解析 eBPF 的返回数据

    1:开启以下内核配置到项目_defconfig 文件中

    CONFIG_BPF=y CONFIG_BPF_JIT=y CONFIG_HAVE_BPF_JIT=y CONFIG_BPF_EVENTS=y CONFIG_KPROBES=y CONFIG_KPROBE_EVENT=y CONFIG_UPROBES=y CONFIG_UPROBE_EVENT=y CONFIG_DEBUG_PREEMPT=y CONFIG_PREEMPTIRQ_EVENTS=y CONFIG_FTRACE_SYSCALLS=y

    2:根据项目情况编译内核

    如果你有完整 Android 项目代码的话可以 make bootimage 编译出内核

    否则配置好交叉编译环境后单独编译内核

    关于获取 Linux kernel 头文件:

    如果你直接使用交叉编译环境来编译内核的话可以忽略这段内容

    如果你的内核改动比较少,算是比较”干净”的话可以直接使用别人已经打包好的头文件包。不过这种情况比较少见,嵌入式的 linux 内核基本被芯片厂或手机厂有所修改

    如果你是用 Android 树来编译内核的话,需要手动编译头文件因为 Android 的打包结构并不保留头文件。编译方法如下:

    cd to kernel tree export ARCH=arm64 export CROSS_COMPILE=aarch64-linux-gnu- (任意交叉编译器都可以) make boardname_defconfig make -j6

    步骤 2 安装 debian-arm 到 Android

    Github 有个叫 adeb 项目,它的功能是将 debian-arm 整个固件 push 到 Android 设备的/data 目录下,然后并通过本地 shell 的配合实现了 debian 环境下 shell。也就是 debian 能支持的功能他都能支持,只是没有屏幕,只能通过终端控制。当然也支持 apt 命令,通过修改源(apt source)之后下载安装社区提供的各种软件。

    首先下载 adeb 项目 git clone https://github.com/joelagnel/adeb.git

    然后执行安装命令,此时需要手机已经是 root,并且确保剩余空间至少大于 300MB。adeb 的其他命令具体参考 reference guide,参考引用 2

    adeb prepare –full –kernelsrc /path/to/kernel-source // 步骤 1 中提及的 kernel 路径

    adeb shell

    即可进入到基于 debian 运行环境的 shell。他相比 Android 区别在于只是利用了 Android 中运行的 linux kernel 而其他标准库之类(bionic,linker 等)都替换成 debian 所提供的 libc 及 linker。安装过程及运行结果如下:

    $  adeb prepare --full --kernelsrc ./msm-4.9_valina_sdm845 |--------------| | adeb: v0.99g | |--------------| 16:01:15 - INFO    : Looking for device.. 16:01:15 - INFO    : Preparing device... 16:01:15 - INFO    : Doing a full install. 16:01:15 - INFO    : 16:01:15 - INFO    : Downloading Androdeb from the web... 16:01:15 - INFO    : 16:01:15 - INFO    : No repository URL provided in enviromnent. Attempting to auto-detect it 16:01:15 - INFO    : Detected URL: github.com/joelagnel/adeb/   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current                                  Dload  Upload   Total   Spent    Left  Speed 100   610    0   610    0     0    589      0 --:--:--  0:00:01 --:--:--   589 100  295M  100  295M    0     0   400k      0  0:12:36  0:12:36 --:--:--  541k Archive:  /tmp/tmp.A2epoY9AOr/androdeb-fs.tgz.zip   inflating: /tmp/tmp.A2epoY9AOr/androdeb-fs.tgz 16:01:53 - INFO    : Building and updating kernel headers from kernel source dir (./msm-4.9_valina_sdm845) 16:01:58 - INFO    : Using archive at /tmp/tmp.A2epoY9AOr/androdeb-fs.tgz for filesystem preparation 16:01:58 - INFO    : Pushing filesystem to device.. 16:01:09 - INFO    : Pushing addons to device.. 16:01:10 - INFO    : Unpacking filesystem in device.. 16:01:30 - INFO    : Storing kernel headers into androdeb /kernel-headers/ 16:01:57 - INFO    : All done! Run "adeb shell" to enter environment $  adeb shell ########################################################## # Welcome to androdeb environment running on Android!    # # Questions to: Joel Fernandes <joel@joelfernandes.org>  #                                                          #  Try running vim, gcc, clang, bcc, git, make, perf etc   #    or apt-get install something.                         # ##########################################################

    AOSP 项目源码仓库中的 external 目录 下面谷歌已经集成了 adeb 项目,如果有 AOSP 源码的话可以直接使用项目中的源码。这部分更新还未集成到 Android P,应该是会随着 Android Q 一起发布

    Hello world

    例 1:IO 请求

    root@localhost:/usr/share/bcc/tools# vfsstat TIME         READ/s  WRITE/s CREATE/s   OPEN/s  FSYNC/s 08:45:23:        98      369        0        5        0 08:45:24:       287      261        0      144        0 08:45:25:       115      129        0       49        0 08:45:26:       253      125        0       92        0 08:45:27:       326      272        0       74        0 08:45:28:       217      229        0       61        0

    vfsstat(Virtual FileSystem Stats)可以查看下发到虚拟文件系统层的所有 IO 请求,如果看到以上结果就说明大功告成啦!过程中如果出现问题的话可以参考引用 3。我自己遇到过 kernel head 不匹配与没有开启 eBPF 导致的错误 预编译好的 bcc 工具集目录在 /usr/share/bcc/tools, 目前将近有 100 多个小工具

    例 2:TCP 掉包时的内核路径

    root@localhost:/usr/share/bcc/tools# tcpdrop TIME     PID    IP SADDR:SPORT          > DADDR:DPORT          STATE (FLAGS) 08:54:36 3257   6  ::ffff:172.28.140.149:80 > ::ffff:183.60.137.144:46922 ESTABLISHED (ACK)         tcp_drop+0x0         tcp_rcv_established+0x2d4         tcp_v4_do_rcv+0x198         tcp_v4_rcv+0xb54         ip_local_deliver_finish+0x10c         ip_local_deliver+0x108         ip_rcv_finish+0x168         ip_rcv+0x344         __netif_receive_skb_core+0x5a8         __netif_receive_skb+0x38         process_backlog+0xd0         net_rx_action+0x258         __softirqentry_text_start+0x15c         do_softirq+0x70         netif_rx_ni+0x80         hdd_rx_packet_cbk+0x438         $x+0x310         $x+0x1e8         kthread+0xf4         ret_from_fork+0x10

    查看 TCP 掉包时的内核路径以推测掉包原因(TCP 掉包路径非常多,只能打印堆栈来诊断了)

    步骤 3(可选) 源码编译安装最新版 BCC 与 BPFTrace

    默认的安装方式虽然简单但是所使用的工具版本比较老旧,为了体验最新功能可以下载最新代码并编译安装。需要提示的是以下操作都是在 adeb shell 中执行,也就是所有操作都在手机端完成,包括源码下载,编译与安装

    安装 BCC

    git clone https://github.com/iovisor/bcc.git // 下载 bcc 项目代码

    cd bcc && rm -rf build && mkdir -p build && cd build //在 bcc 目录下创建 build 目录,用于代码编译。

    export CC=clang-6.0 // 设置 C 编译器

    export CXX=clang++-6.0 //设置 C++编译器

    cmake .. -DCMAKE_INSTALL_PREFIX=/usr //运行环境检查

    make -j4 //编译

    make install //安装

    安装后的 tools 路径为”/usr/share/bcc/tools”,运行 cachestat 检查下是否安装成功

    可能出现的错误

    BCC 20190129 版本中运行上面命令时会出现如下错误:

    Traceback (most recent call last):   File "/usr/share/bcc/tools/cachestat", line 20, in <module>     from bcc import BPF   File "/usr/lib/python2.7/dist-packages/bcc/__init__.py", line 30, in <module>     from .syscall import syscall_name   File "/usr/lib/python2.7/dist-packages/bcc/syscall.py", line 387, in <module>     raise Exception("ausyscall: command not found") Exception: ausyscall: command not found

    解决方法:

    安装 auditd 程序

    apt install auditd

    如果提示没有找到 auditd 命令的话,需要手动更新下 source list。推荐将 “deb http://ftp.de.debian.org/debian stretch main” 添加到”/etc/apt/sources.list”文件后执行更新。

    apt update

    安装 BPFTrace

    git clone https://github.com/iovisor/bpftrace.git // 下载 bpftrace 项目代码

    cd bpftrace && rm -rf build && mkdir -p build && cd build //在 bpftrace 目录下创建 build 目录,用于代码编译。

    export CC=clang-6.0 // 设置 C 编译器

    export CXX=clang++-6.0 //设置 C++编译器

    cmake -DCMAKE_BUILD_TYPE=Debug ../ //运行环境检查

    make -j4 //编译

    make install //安装

    可能出现的错误 1

    无法找到”BPF_FUNC_get_current_cgroup_id”定义!

    错误原因是我用的 4.9 内核中还没有这个定义,是 commit 22110ad25b51b0e1f1ece4fcdf21a3738391f018 中引入的功能。如果你的内核也是 4.9,或者提示没有定义的话可以单笔回退这个提交。

    git revert 22110ad25b51b0e1f1ece4fcdf21a3738391f018

    可能出现的错误 2:

    无法找到”bpf_create_map”,是否使用”bcc_createmap”替代?

    这是因为 bpftrace 依赖 bcc 的库函数,而这个库函数中使用的是 bcc 开头。规避办法是将 bpf 相关调用修改成 bcc_,修改如下:

    diff --git a/src/attached_probe.cpp b/src/attached_probe.cpp index 1837b6a..de9c6e0 100644 --- a/src/attached_probe.cpp +++ b/src/attached_probe.cpp @@ -331,7 +331,7 @@ void AttachedProbe::load_prog()    for (int attempt=0; attempt<3; attempt++)    { -    progfd_ = bpf_prog_load(progtype(probe_.type), namep, +    progfd_ = bcc_prog_load(progtype(probe_.type), namep,          reinterpret_cast<struct bpf_insn*>(insns), prog_len, license,          kernel_version(attempt), log_level, log_buf, log_buf_size);      if (progfd_ >= 0) diff --git a/src/map.cpp b/src/map.cpp index 5cfd442..6a452b4 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -46,7 +46,7 @@ Map::Map(const std::string &name, const SizedType &type, const MapKey &key, int    int value_size = type.size;    int flags = 0; -  mapfd_ = bpf_create_map(map_type, name.c_str(), key_size, value_size, max_entries, flags); +  mapfd_ = bcc_create_map(map_type, name.c_str(), key_size, value_size, max_entries, flags);    if (mapfd_ < 0)    {      std::cerr << "Error creating map: '" << name_ << "'" << std::endl; @@ -80,7 +80,7 @@ Map::Map(enum bpf_map_type map_type)      std::cerr << "invalid map type" << std::endl;      abort();    } -  mapfd_ = bpf_create_map(map_type, name.c_str(), key_size, value_size, max_entries, flags); +  mapfd_ = bcc_create_map(map_type, name.c_str(), key_size, value_size, max_entries, flags);    if (mapfd_ < 0)    {

    安装后的 tools 路径为”/usr/local/share/bpftrace/tools”,运行如下命令验证安装结果:

    bpftrace -e ‘kprobe:do_nanosleep { printf(“PID %d sleeping…\n”, pid); }’

    写在最后

    本文介绍的方法适用于系统开发阶段,因为有了 debian,所以只要能找到源或者代码,几乎可以执行任何 linux 发行版上的工具

    后面会陆续介绍其他比较重要的工具(Perf,glances,pidstat,stress 等),目前计划还是基于 debian 的方案

    用户固件中不可能会有这套 debian 的程序,因为他需要 root 运行,这是最大的缺点。个人比较认同的方案是 4,也就是实现适合用于 Android 环境的类似 BCC&BPFTrace 工具链,目标是用户固件中也可直接使用 eBPF

    Reference

    “eBPF super powers on ARM64 and Android.pdf” by Joel Fernandes

    https://github.com/joelagnel/adeb/blob/master/README.md

    https://github.com/joelagnel/adeb/blob/master/BCC.md

    https://github.com/iovisor/bcc#tools

    Processed: 0.035, SQL: 9