一文解決eBpf在Android上的集成和調試
eBPF(Extended Berkeley Packet Filter )是一種新興的linux內核功能擴展技術,可以無需修改內核代碼,在保證安全的前提下,靈活的動態(tài)加載程序,實現(xiàn)對內核功能的擴展。
Android平臺上也引入了對eBpf技術的支持,本文以一些典型使用場景,貫穿eBpf在android上的使用流程,展示如何在手機上集成和調試eBpf程序。
如下圖示,為bpf的基本部署流程,在android上也是適用的。

一、Bpf程序編寫
Android的eBpf程序源碼,位于system/bpfprogs,比如打開time_in_state.c可以看到程序總體上分為三個部分:
使用DEFINE_BPF_MAP定義了一些Map數(shù)據(jù)結構,這些是用來實現(xiàn)用戶程序和內核互傳數(shù)據(jù)的共享緩存。
使用DEFINE_BPF_PROG,定義了一個Bpf函數(shù),這個函數(shù)編譯后,可以加載進內核,實現(xiàn)鉤子函數(shù)的功能。
LICENSE("GPL") 許可協(xié)議聲明。
上述中,Map的類型,以及Bpf的hook類型,根據(jù)功能的不同有許多種類,可以參考https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md#program-types,里面有詳細的描述。
二、Bpf程序生成
當以C語言的格式編寫一個Bpf程序后,通過編譯,可以得到一個 “.o” 文件。此文件是以BTF(BPF Type Format) 字節(jié)碼編碼的元數(shù)據(jù)格式文件,并不可以直接執(zhí)行,需要加載到內核中,內核進行解析執(zhí)行,或者JIT轉換后執(zhí)行。
BTF格式文件可查看文檔:
https://www.kernel.org/doc/html/latest/bpf/btf.html
三、加載Bpf程序
Bpf程序在Android上有嚴格的權限控制,在bpfloader.te 中有限制bpf執(zhí)行的sepolicy,限定了bpfloader是唯一可以加載bpf程序的程序。
neverallow { domain -bpfloader } *:bpf { map_create prog_load };
而bpfloader只在手機啟動時執(zhí)行一次,保證了其它模塊無法額外加載系統(tǒng)之外的bpf程序,防止對內核的安全性造成危害。
在system/bpf/bpfloader/BpfLoader.cpp中,bpfloader會使用loadAllElfObjects遍歷/system/etc/bpf下btf格式的”.o”文件。接著使用android::bpf::loadProg解析bpf程序文件,實現(xiàn)創(chuàng)建Bpf程序和相應的Map。
Bpfloader執(zhí)行加載之后,會立即退出。Bpf程序的生命周期管理為引用計數(shù),類似文件句柄fd,當失去所有引用時,Bpf程序和map等對象就會被銷毀。
為了避免bpf prog和map對象在bpfloader執(zhí)行之后被銷毀, 最后會通過bpf_obj_pin把這些bpf對象映射到/sys/fs/bpf文件節(jié)點。映射的文件節(jié)點,其命名有特定的規(guī)則,以便其它的程序能夠通過文件路徑名稱來找到對應的bpf程序。
【文章福利】小編推薦自己的Linux內核技術交流群:【749907784】整理了一些個人覺得比較好的學習書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)? ? ?


四、Attach Bpf程序
Bpf程序被加載之后,并沒有附著到內核函數(shù)上,此時bpf程序不會有任何執(zhí)行,還需要經過attach操作。attach指定把bpf程序hook到哪個內核監(jiān)控點上,具體有tracepoint,kprobe等幾十種類型。成功attach上的話,bpf程序就轉換為內核代碼的一個函數(shù)。
比如attach task_rename 這個tracepoint類型,可以用
cat/sys/kernel/tracing/events/task/task_rename/format來確認參數(shù),使得定義的bpf 函數(shù)和具體的tracepoint 函數(shù)參數(shù)一致。
如果是attach raw tracepoint,則需要自行構建參數(shù),因為raw tracepoint 訪問的是事件的原始參數(shù),未進行參數(shù)封裝,相比之下有更好一點的性能。
比如task_rename 這個tracepoint中,兩種類型的參數(shù)差異:

五、Update map
當Bpf附著到內核函數(shù)上,起到了一個鉤子函數(shù)的作用。鉤子函數(shù)在detach之前,可以一直偵測內核的執(zhí)行,有時候我們需要改變偵測的范圍,或者把偵測的結果上報,此時需要使用Map,Map是用戶監(jiān)控程序和內核間數(shù)據(jù)交換的媒介。用戶態(tài)和內核態(tài)都可以使用類似的接口來訪問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數(shù)據(jù),需要我們主動去讀取里面的數(shù)據(jù)。有時候,希望有數(shù)據(jù)時,能得到通知,而不是輪詢去讀取。此時,可以通過perf event map實現(xiàn)偵聽數(shù)據(jù)變化的功能。內核數(shù)據(jù)能夠存儲到自定義的數(shù)據(jù)結構中,并且通過 perf 事件ring緩存發(fā)送和廣播到用戶空間進程。
perf event map的構建流程:

上面構建流程完成后,用戶態(tài)和內核態(tài),就存在了event fd關聯(lián)。接著用戶態(tài)使用epoll來持續(xù)偵聽fd上的通知,而fd實際上是映射到了緩存,所以當偵聽到變化時,就可以到緩存中讀取具體的數(shù)據(jù)。

在內核中,則通過
bpf_perf_event_output(ctx,&events,BPF_F_CURRENT_CPU, &data, sizeof(data));
來通知數(shù)據(jù)。
BPF_F_CURRENT_CPU 參數(shù)指定了使用當前cpu的索引值來訪問event map中的fd,進而往fd對應的緩存填充數(shù)據(jù),這樣可以避免多cpu同時傳遞數(shù)據(jù)的同步問題,也解釋了上面event map初始化時,為何需要創(chuàng)建與cpu個數(shù)相等的大小。
七、調試
實際開發(fā)中,免不了需要反復調試的過程,遵照bpf的原理,在android上重新部署一個bpf程序可以采用如下步驟。
Push 新的bpf.o 文件到/system/etc/bpf/ 中。
舊版本的bpf程序和map的映射文件仍然存在,需要進入/sys/fs/bpf,rm掉映射文件。舊bpf由于沒有了引用,就會被銷毀。
然后再次執(zhí)行/./system/bin/bpfloader,bpfloader就能夠和開機時一樣,把新的bpf.o再次加載起來。
注意:bpfloader在加載時打印的log太多,會觸發(fā)ratelimiting,有時候發(fā)現(xiàn)bpfloader不能加載新的bpf程序,也不能查到有報錯的信息??梢韵扔?#34;echo on > /proc/sys/kernel/printk_devkmsg" 指令關閉ratelimiting,此時就能正常發(fā)現(xiàn)錯誤了。
在成功掛載bpf程序之后,還需要確認其在內核中執(zhí)行的情況,使用bpf_printk輸出內核log。
查看內核日志可用:
$ echo 1 > /sys/kernel/tracing/tracing_on
$ cat /sys/kernel/tracing/trace_pipe
注意:bpf程序雖然用C 代碼格式書寫,但其最終為內核驗證執(zhí)行,會有許多安全和能力方面的限制,典型的如bpf_printk,只支持3個參數(shù)輸出,超過則會報錯。
八、結語
Bpf 可以hook 系統(tǒng)調用、tracepoint和內核函數(shù)等,其應用場景相當廣泛,目前在Android上的使用比較初步,還有很大的空間讓我們在實踐中進一步探索。
原文作者:內核工匠
