
作者:Hcamael@知道創宇404實驗室日期:2022年11月4日
最近在研究eBPF,做一下學習筆記。
起因
我的手機的內核版本是4.19,沒有開啟BTF,但是BPF是開啟了的,接着我繼續查看ecapture的文檔,說如果內核沒有開啟BTF,需要使用make nocore編譯,在github上有提供直接編譯好的nocore安卓版,但是測試還是運行不了。接着自己編譯了一波,但是仍然失敗,感覺可能得嚴格按文檔所述,需要內核版本大於等於5.4。

AndroidBPF demo
而且大部分能搜到的中文資料,都是一堆廢話,或者一堆ctrl+c, ctrl+v的文章,實際有用的太少了。安卓官方的資料中也只有一個簡單的demo,而且使用的是Android.bp進行編譯的,還需要本地搭建AOSP環境。
使用AOSP環境編譯程序
# 初始化一下環境變量$ source build/envsetup.sh # 初始化一下你想編譯哪個版本的android程序$ lunch aosp_crosshatch-userdebug通過這個demo,能看出來,android下使用BPF程序的步驟如下:
首先把編譯好的bpf.o程序放到/system/etc/bpf/目錄下,這就要求我們需要有/system目錄的可寫權限,但是在我的手機上,就算有root權限了,system目錄也沒辦法寫。所以我把手機的系統從MIUI12,刷成了evolution x系統,然後通過adb shell mount -o rw,remount /來重新掛載根目錄,這樣就能寫/system/etc/bpf目錄了。使用bpfloader程序,會自動加載/system/etc/bpf目錄下的*.o文件,然後會在/sys/fs/bpf目錄生成相應的prog_xxx和map_xx文件。我們自己的loader文件需要通過/sys/fs/bpf目錄下的那兩個文件來和BPF程序進行交互。

深入研究Android下的BPF
BPF程序bpftest.c
#include <linux/bpf.h>#include <stdbool.h>#include <stdint.h>#include <bpf_helpers.h>#include <string.h>#define MAX_ARGV 128;#define bpf_printk(fmt, args...) bpf_trace_printk(fmt, sizeof(fmt), ##args)struct event_execv{ uint32_t pid; uint32_t gid; char cmd[80];};DEFINE_BPF_MAP(execve_map, ARRAY, uint32_t, struct event_execv, 256);struct execve_args{ short common_type; char common_flags; char common_preempt_count; int common_pid; long __syscall_nr; unsigned long args[6];};SEC("tracepoint/raw_syscalls/sys_enter")int trace_execve_event(struct execve_args *ctx){ struct event_execv event; uint32_t key = 1; int comm; char trace_buf[] = "[Debug] pid = %d, gid = %d, comm=%s\n"; memset(&event, 0, sizeof(event)); event.pid = bpf_get_current_pid_tgid(); event.gid = bpf_get_current_uid_gid(); bpf_execve_map_update_elem(&key, &event, BPF_ANY); comm = bpf_get_current_comm(&event.cmd, sizeof(event.cmd)); if (comm != 0) { return -1; } event.cmd[79] = 0; bpf_printk(trace_buf, event.pid, event.gid, event.cmd); bpf_execve_map_update_elem(&key, &event, BPF_ANY); return 0;}LICENSE("GPL");map映射DEFINE_BPF_MAP是對map相關操作的一個宏定義,可以參考:bpf_helpers.h
#define DEFINE_BPF_MAP_NO_ACCESSORS(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \ struct bpf_map_def SEC("maps") the_map = { \ .type = BPF_MAP_TYPE_##TYPE, \ .key_size = sizeof(TypeOfKey), \ .value_size = sizeof(TypeOfValue), \ .max_entries = (num_entries), \ };#define DEFINE_BPF_MAP(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \ DEFINE_BPF_MAP_NO_ACCESSORS(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \ \ static inline __always_inline __unused TypeOfValue* bpf_##the_map##_lookup_elem( \ TypeOfKey* k) { \ return unsafe_bpf_map_lookup_elem(&the_map, k); \ }; \ \ static inline __always_inline __unused int bpf_##the_map##_update_elem( \ TypeOfKey* k, TypeOfValue* v, unsigned long long flags) { \ return unsafe_bpf_map_update_elem(&the_map, k, v, flags); \ }; \ \ static inline __always_inline __unused int bpf_##the_map##_delete_elem(TypeOfKey* k) { \ return unsafe_bpf_map_delete_elem(&the_map, k); \ };我的map_name為execve_map,所以這個宏定義幫我定義了bpf_execve_map_update_elem這類的函數,幫我定義了結構體:
struct bpf_map_def SEC("maps") execve_map = { .type = BPF_MAP_TYPE_##TYPE, .key_size = sizeof(TypeOfKey), .value_size = sizeof(TypeOfValue), .max_entries = (num_entries), };並且在/sys/fs/bpf目錄下生成的map文件的結構為:map_(bpf文件名)_(定義的map_name),假如我編譯的bpf文件名為:bpftest.o,放到/system/etc/bpf/目錄下,那麼在/sys/fs/bpf目錄下生成的為:map_bpftest_execve_map。
map可以理解為,內核中的BPF和用戶態之間的接口,在內存中是以鍵值對的形式存在的,按我理解,key和value的類型也是可以自己定義的,可以是int,指針,字符串,或者結構體,因為對於BPF來說,key和value就是內存中的一段值,只需要定義好key和value的size就好了,而在上面的結構體中就定義了key和value的大小。
用戶態的loader可以通過/sys/fs/bpf/map_bpftest_execve_map和BPF程序來交換數據。
這塊知識的文章挺多的,在BPF的函數定義的上頭都需要有一個SEC("xxxx"),在最開始的demo中還有另一個寫法,以下兩種寫法是等同的:
SEC("tracepoint/sched/sched_switch")int tp_sched_switch(struct switch_args* args){......}DEFINE_BPF_PROG("tracepoint/sched/sched_switch", AID_ROOT, AID_NET_ADMIN, tp_sched_switch) (struct switch_args* args) {......}SEC裡面的字符串是為了定義下面的函數是什麼類型的BPF程序,因為BPF程序也有很多種類型,比如kprobe, kretprobe, uprobe, uretprobe, tracepoint......。
具體都有啥,可以參見:libbpf.c
再低一點的版本這個結構體的名字叫section_names,不過在我研究了一波之後,我感覺不能通過內核版本來確定我們可以用哪個section,需要通過/sys/kernel/debug/目錄下的情況來確定,但是安卓手機上的情況卻有一些不同,目錄為:/sys/kernel/tracing/,比如我上面代碼中的:SEC("tracepoint/raw_syscalls/sys_enter"),是因為有以下目錄:/sys/kernel/tracing/events/raw_syscalls/sys_enter/,並且struct execve_args結構體是來源於:/sys/kernel/tracing/events/raw_syscalls/sys_enter/format
目前這種方式我覺得只適用於tracepoint,其他的還沒研究到,後續研究到了再補充。
在android上,/sys/fs/bpf/prog_xx的命名方式為:prog_(文件名)_(section名)_(分類,分類名之類的)
比如我的代碼中,文件名為bpftest,section名為tracepoint,tracepoint的分類為raw_syscalls,分類名為sys_enter,所以最後得到的文件為:/sys/fs/bpf/prog_bpftest_tracepoint_raw_syscalls_sys_enter
BPF相關函數
bpf的相關函數可以參考bpf_helper_defs.h文件,比如上述的bpf_get_current_pid_tgid,表示獲取觸發該BPF的程序的pid,bpf_get_current_uid_gid是獲取用戶的gid,bpf_get_current_comm是獲取程序名,還有其他的可以自行去看這個頭文件的定義。
BPF提供一個bpf_trace_printk函數來打印調試信息,在android下,可以使用atrace命令來讀取。
並且我通過strace對atrace進行跟蹤發現,其實只需要執行下面兩句命令:
$ echo 1 > /sys/kernel/tracing/tracing_on$ cat /sys/kernel/tracing/trace_pipe
參考
https://github.com/ehids/ecapture
https://zhuanlan.zhihu.com/p/482266243
https://github.com/omnirom/android_system_bpf/blob/0706429da9a9fb15d93d8ed8300af77410311a69/progs/include/bpf_helpers.h
https://elixir.bootlin.com/linux/v5.10.150/source/tools/lib/bpf/libbpf.c#L8319
作者名片
END


