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

做些鋪墊

本文假設讀者已掌握如下內容

  1. 熟悉 Linux 內核編譯方法

  2. 閱讀過博文 eBPF 架構優勢及其應用方向上的暢想

  3. 熟悉 Git 操作

  4. 熟悉 CMake,LLVM,Clang 等編譯工具

在博文 eBPF 架構優勢及其應用方向上的暢想中有提到 eBPF 的執行流程,這套在主機系統(泛指基於 x86 的 Linux distribution)上直接 apt install 或者源碼編譯安裝就可以了。但是在 Android 上怎麼執行呢?現在大部分 Android 設備運行在基於 arm 的處理器架構上,我們熟悉的高通,MTK,華爲海思都屬於 arm 處理器架構。x86 上的 eBPF 工具棧(如前文所述,這裏僅指 BCC&BPFTrace)程序是無法直接在 arm 處理器上執行的,需要所謂的交叉編譯技術纔可以。除了程序本身之外,它所依賴的基礎庫如 libc 也同樣需要交叉編譯才能正確工作。

運行在 Android 上的難題

  1. BCC 及 BPFTrace 使用的是基於 CMake 的編譯方式,與 Android 使用的 gradle,Android BP 的編譯系統是不一致的

  2. BCC 項目中以 python 包爲基礎,那意味着需要有 python 運行環境,這在 anroid 裏也是沒有的

可行的思路

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

  1. 沒條件就創造條件,將 BCC&BPFTrace 強行適配到 Android 編譯環境,使其可以直接運行在 Android 上下文中。中間涉及的依賴,衝突問題需要手動修改

  2. 添加中間層,由 host 端生成的的 bytecode 通過 adb 通道派發給 client 端,具體執行由 client 端的常駐進程完成

  3. Android 端運行某個 linux distribution 環境(如 Debian,Ubuntu),在手機端編譯與安裝 BCC&BPFTrace

  4. 參考 BCC&BPFTrace 設計思路,依照 Android 的架構實現一套類似功能程序,部分採用 BCC&BPFTrace 項目代碼

綜合利弊之後,本文使用方案 3。缺點就是對 Android 的環境要求比較高,它需要:

  1. 手機能夠 root,而且可以將 data 分區 remount 成可讀寫

  2. Android kernel 版本要求在 4.9 及以上

  3. 具有編譯 android kernel 的環境

步驟 1 編譯帶必要功能的內核

通過手動編內核實現以下兩個目的:

  1. 使能 eBPF 相關功能(如果已經開啓可以跳過此步驟)

  2. 獲取特定 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:根據項目情況編譯內核

  1. 如果你有完整 Android 項目代碼的話可以 make bootimage 編譯出內核

  2. 否則配置好交叉編譯環境後單獨編譯內核

關於獲取 Linux kernel 頭文件:

  1. 如果你直接使用交叉編譯環境來編譯內核的話可以忽略這段內容

  2. 如果你的內核改動比較少,算是比較”乾淨”的話可以直接使用別人已經打包好的頭文件包。不過這種情況比較少見,嵌入式的 linux 內核基本被芯片廠或手機廠有所修改

  3. 如果你是用 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)之後下載安裝社區提供的各種軟件。

  1. 首先下載 adeb 項目 git clone https://github.com/joelagnel/adeb.git

  2. 然後執行安裝命令,此時需要手機已經是 root,並且確保剩餘空間至少大於 300MB。adeb 的其他命令具體參考 reference guide,參考引用 2

  3. adeb prepare –full –kernelsrc /path/to/kernel-source // 步驟 1 中提及的 kernel 路徑

  4. 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 <[email protected]>  #
                                                         #
 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

  1. git clone https://github.com/iovisor/bcc.git // 下載 bcc 項目代碼

  2. cd bcc && rm -rf build && mkdir -p build && cd build //在 bcc 目錄下創建 build 目錄,用於代碼編譯。

  3. export CC=clang-6.0 // 設置 C 編譯器

  4. export CXX=clang++-6.0 //設置 C++編譯器

  5. cmake .. -DCMAKE_INSTALL_PREFIX=/usr //運行環境檢查

  6. make -j4 //編譯

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

  1. git clone https://github.com/iovisor/bpftrace.git // 下載 bpftrace 項目代碼

  2. cd bpftrace && rm -rf build && mkdir -p build && cd build //在 bpftrace 目錄下創建 build 目錄,用於代碼編譯。

  3. export CC=clang-6.0 // 設置 C 編譯器

  4. export CXX=clang++-6.0 //設置 C++編譯器

  5. cmake -DCMAKE_BUILD_TYPE=Debug ../ //運行環境檢查

  6. make -j4 //編譯

  7. 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); }’

寫在最後

  1. 本文介紹的方法適用於系統開發階段,因爲有了 debian,所以只要能找到源或者代碼,幾乎可以執行任何 linux 發行版上的工具

  2. 後面會陸續介紹其他比較重要的工具(Perf,glances,pidstat,stress 等),目前計劃還是基於 debian 的方案

  3. 用戶固件中不可能會有這套 debian 的程序,因爲他需要 root 運行,這是最大的缺點。個人比較認同的方案是 4,也就是實現適合用於 Android 環境的類似 BCC&BPFTrace 工具鏈,目標是用戶固件中也可直接使用 eBPF

Reference

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

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

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章