eBpf 在 Android 上的集成和調試
eBPF(Extended Berkeley Packet Filter )是一種新興的 linux 內核功能擴展技術,可以無需修改內核代碼,在保證安全的前提下,靈活的動態加載程序,實現對內核功能的擴展。
Android 平臺上也引入了對 eBpf 技術的支持,本文以一些典型使用場景,貫穿 eBpf 在 android 上的使用流程,展示如何在手機上集成和調試 eBpf 程序。
如下圖示,爲 bpf 的基本部署流程,在 android 上也是適用的。
一、Bpf 程序編寫
Android 的 eBpf 程序源碼,位於 system/bpfprogs,比如打開 time_in_state.c 可以看到程序總體上分爲三個部分:
-
使用 DEFINE_BPF_MAP 定義了一些 Map 數據結構,這些是用來實現用戶程序和內核互傳數據的共享緩存。
-
使用 DEFINE_BPF_PROG,定義了一個 Bpf 函數,這個函數編譯後,可以加載進內核,實現鉤子函數的功能。
-
LICENSE("GPL") 許可協議聲明。
上述中,Map 的類型,以及 Bpf 的 hook 類型,根據功能的不同有許多種類,可以參考 https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md#program-types,裏面有詳細的描述。
二、Bpf 程序生成
當以 C 語言的格式編寫一個 Bpf 程序後,通過編譯,可以得到一個 “.o” 文件。此文件是以 BTF(BPF Type Format) 字節碼編碼的元數據格式文件,並不可以直接執行,需要加載到內核中,內核進行解析執行,或者 JIT 轉換後執行。
BTF 格式文件可查看文檔:
https://www.kernel.org/doc/html/latest/bpf/btf.html
三、加載 Bpf 程序
Bpf 程序在 Android 上有嚴格的權限控制,在 bpfloader.te 中有限制 bpf 執行的 sepolicy,限定了 bpfloader 是唯一可以加載 bpf 程序的程序。
neverallow {domain -bpfloader} *:bpf { map_create prog_load };
而 bpfloader 只在手機啓動時執行一次,保證了其它模塊無法額外加載系統之外的 bpf 程序,防止對內核的安全性造成危害。
在 system/bpf/bpfloader/BpfLoader.cpp 中,bpfloader 會使用 loadAllElfObjects 遍歷 / system/etc/bpf 下 btf 格式的”.o” 文件。接着使用 android::bpf::loadProg 解析 bpf 程序文件,實現創建 Bpf 程序和相應的 Map。
Bpfloader 執行加載之後,會立即退出。Bpf 程序的生命週期管理爲引用計數,類似文件句柄 fd,當失去所有引用時,Bpf 程序和 map 等對象就會被銷燬。
爲了避免 bpf prog 和 map 對象在 bpfloader 執行之後被銷燬, 最後會通過 bpf_obj_pin 把這些 bpf 對象映射到 / sys/fs/bpf 文件節點。映射的文件節點,其命名有特定的規則,以便其它的程序能夠通過文件路徑名稱來找到對應的 bpf 程序。
**四、**Attach Bpf 程序
Bpf 程序被加載之後,並沒有附着到內核函數上,此時 bpf 程序不會有任何執行,還需要經過 attach 操作。attach 指定把 bpf 程序 hook 到哪個內核監控點上,具體有 tracepoint,kprobe 等幾十種類型。成功 attach 上的話,bpf 程序就轉換爲內核代碼的一個函數。
比如 attach task_rename 這個 tracepoint 類型,可以用
cat/sys/kernel/tracing/events/task/task_rename/format 來確認參數,使得定義的 bpf 函數和具體的 tracepoint 函數參數一致。
如果是 attach raw tracepoint,則需要自行構建參數,因爲 raw tracepoint 訪問的是事件的原始參數,未進行參數封裝,相比之下有更好一點的性能。
比如 task_rename 這個 tracepoint 中,兩種類型的參數差異:
五、Update map
當 Bpf 附着到內核函數上,起到了一個鉤子函數的作用。鉤子函數在 detach 之前,可以一直偵測內核的執行,有時候我們需要改變偵測的範圍,或者把偵測的結果上報,此時需要使用 Map,Map 是用戶監控程序和內核間數據交換的媒介。用戶態和內核態都可以使用類似的接口來訪問 Map。
典型操作
- 在 Map 中查找記錄
void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)
- 在 Map 中更新記錄
long bpf_map_update_elem(struct bpf_map *map, const void *key, const void *value, u64 flags)
- 在 Map 中刪除記錄
long bpf_map_delete_elem(struct bpf_map *map, const void *key)
六、Event 上報
一般的 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 個數相等的大小。
七、調試
實際開發中,免不了需要反覆調試的過程,遵照 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。
/* Helper macro to print out debug messages */
#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 個參數輸出,超過則會報錯。
八、結語
Bpf 可以 hook 系統調用、tracepoint 和內核函數等,其應用場景相當廣泛,目前在 Android 上的使用比較初步,還有很大的空間讓我們在實踐中進一步探索。
參考文獻:
1.https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md#program-types
2.https://man7.org/linux/man-pages/man2/bpf.2.html
3.https://man7.org/linux/man-pages/man7/bpf-helpers.7.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/_9p64p8tL2T2XjjYVJqQtg