做些鋪墊
本文假設讀者已掌握如下內容
熟悉 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 <[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
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