基于eBPF的嵌入式應用調試
筆者之前寫過幾篇有關于使用eBPF調試Linux內核和應用的博客,其中提到,在嵌入式設備上使用BCC或bpftrace是不可行的;主要原因在于嵌入式設備的資源有限,而這兩個調試工具依賴python
/clang
/llvm
等庫,需要將腳本編譯為eBPF
字節碼加載入Linux內核來執行。然而,隨著嵌入式設備的性能越來越強,板載資源越來越多,在嵌入式設備上運行bpftrace
工具已成為可能。本文整理了筆者為raspberrypi 4
設備編譯bpftrace
工具的過程,按照這些操作,筆者為工作中的嵌入式設備也制作了可用的bpftrace
調試工具。
另外,22.04版本的Ubuntu
系統自帶的bpftrace
已經不能用了,主要的原因是它不帶符號表(strip -s
);本文的內容也可用于PC
端的bpftrace
工具編譯構建:
root@ubuntu:/usr/sbin# uname -a
Linux ubuntu 5.15.0-97-generic #107-Ubuntu SMP Wed Feb 7 13:26:48 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
root@ubuntu:/usr/sbin# ./execsnoop.bt
Attaching 3 probes...
ERROR: Could not resolve symbol: /proc/self/exe:BEGIN_trigger
編譯使用的設備
首先,筆者使用的raspberrypi 4
可插入兩張TF卡,兩張卡分別安裝了debian
系統和筆者編譯的openwrt
系統,二者的Linux內核版本大致相同,均為6.1版本。其次,樹莓派設備的內存大小為2GB,這一點非常重要;因為bcc/bpftrace
是由C++編寫的,編譯過程非常耗內存(筆者使用clang編譯器構建)。最后,筆者是為了在openwrt
系統中使用bpftrace
,因此要求在構建openwrt
系統時,選擇glibc庫。
零,安裝clang編譯器
編譯在是樹莓派的debian系統上完成的。需要按照apt.llvm.org的說明操作來安裝,筆者選擇了安裝clang-10
編譯器:
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
./llvm.sh 10
之后,我們需要在debian安裝其他依賴庫(而不編譯所有的依賴庫,否則耗時太久了)。以下需要安裝的庫,是在raspberrypi/openwrt
系統上缺少的,后面會直接復制到安裝有openwrt
系統的TF卡上:
sudo apt install flex bison zlib1g-dev liblzma-dev libbz2-dev cmake \libzstd-dev dwarves build-essential python3-setuptools libpython3-dev
一,編譯elfutils
筆者下載了最新版本的elfutils,它提供了libelf
依賴庫,該軟件包的配置、編譯操作如下:
../configure --prefix=/opt/bpftrace CC=clang-10 CXX=clang++-10 \CFLAGS='-Wall -fPIC -O2 -D_GNU_SOURCE -ggdb -fno-omit-frame-pointer -Wno-unused-parameter' \CXXFLAGS='-Wall -fPIC -O2 -D_GNU_SOURCE -ggdb -fno-omit-frame-pointer -Wno-unused-parameter' \--disable-nls --enable-libdebuginfod=dummy --disable-debuginfod --with-zlib --with-lzma \--with-bzlib --with-zstd LDFLAGS='-Wl,-rpath=/opt/bpftrace/lib'
make && sudo make install
以上配置完成后,再執行make && sudo make install
即可安裝。
二,編譯libbpf庫
這里需要強調的是,依筆者的經驗,libbpf
庫以及bcc、bpftrace版本的選擇,不能任意;一定要根據Linux內核版本來選擇。例如,樹苺派的Linux內核版本為6.1,那么確認該版本內核的(初始)發布日期(是2023年初發布的),然后選擇時間上相近的libbpf
庫。這里筆者選擇的版本為libbpf-1.2.2
。然而工作中,筆者因使用的嵌入式設備比較老,使用的版本為0.1.1
。
該庫的編譯操作如下:
cd $HOME ; tar -zxf libbpf-1.2.2.tar.gz
cd libbpf-1.2.2/src
make V=1 PREFIX=/opt/bpftrace CC=clang-10 CFLAGS='-fPIC -O2 -ggdb -Wall -fno-omit-frame-pointer -I/opt/bpftrace/include' LDFLAGS='-L/opt/bpftrace/lib -Wl,-rpath=/opt/bpftrace/lib'
sudo make V=1 PREFIX=/opt/bpftrace CC=clang-10 CFLAGS='-fPIC -O2 -ggdb -Wall -fno-omit-frame-pointer -I/opt/bpftrace/include' LDFLAGS='-L/opt/bpftrace/lib -Wl,-rpath=/opt/bpftrace/lib' install
注意,以上編譯的庫在debian/arm64
系統下會被安裝到/opt/bpftrace/lib64
路徑下,需要移到/opt/bpftrace/lib
路徑下,并修改/opt/bpftrace/lib/pkgconfig/libbpf.pc
文件。
三,安裝cereal頭文件
早期的bpftrace
工具不依賴該庫,但現在是需要了,筆者選擇了cereal-1.3.2.tar.gz
版本:
mkdir -p build_aarch64 && cd build_aarch64
cmake -DCMAKE_BUILD_TYPE=release -DCMAKE_INSTALL_PREFIX=/opt/bpftrace \-DCMAKE_C_COMPILER=clang-10 -DCMAKE_CXX_COMPILER=clang++-10 \-DCMAKE_C_FLAGS='-fPIC -O2 -D_GNU_SOURCE -ggdb -fno-omit-frame-pointer' \-DCMAKE_CXX_FLAGS='-fPIC -O2 -D_GNU_SOURCE -ggdb -fno-omit-frame-pointer' \-DBUILD_DOC=OFF -DBUILD_SANDBOX=OFF -DSKIP_PERFORMANCE_COMPARISON=ON -DBUILD_TESTS=OFF ..
sudo make install
注意,該庫以頭文件的形式提供,以上不會有動態庫編譯生成。
四,編譯bcc
bcc的版本為v0.27.0,筆者已驗證最新版本的bcc
編譯得到的bpftrace
存在異常。需要注意的是,筆者下載的源代碼文件名為bcc-src-with-submodule.tar.gz
。以下是配置、編譯安裝的操作:
mkdir -p build_aarch64 && cd build_aarch64
cmake -DCMAKE_BUILD_TYPE=release -DCMAKE_INSTALL_PREFIX=/opt/bpftrace \-DCMAKE_C_COMPILER=clang-10 -DCMAKE_CXX_COMPILER=clang++-10 \-DCMAKE_C_FLAGS='-fPIC -O2 -D_GNU_SOURCE -ggdb -fno-omit-frame-pointer -I/opt/bpftrace/include' \-DCMAKE_CXX_FLAGS='-fPIC -O2 -D_GNU_SOURCE -ggdb -fno-omit-frame-pointer -I/opt/bpftrace/include' \-DCMAKE_EXE_LINKER_FLAGS='-L/opt/bpftrace/lib -Wl,-rpath=/opt/bpftrace/lib' \-DCMAKE_MODULE_LINKER_FLAGS='-L/opt/bpftrace/lib -Wl,-rpath=/opt/bpftrace/lib' \-DCMAKE_SHARED_LINKER_FLAGS='-L/opt/bpftrace/lib -Wl,-rpath=/opt/bpftrace/lib' \-DENABLE_LLVM_SHARED=ON -DENABLE_TESTS=OFF -DRUN_LUA_TESTS=OFF -DENABLE_LIBDEBUGINFOD=OFF ..
make && sudo make install
編譯完成后,筆者刪除了所有的靜態庫(因為筆者希望所有的庫是動態鏈接的):sudo rm -rf /opt/bpftrace/lib/*.a
。注意,這個編譯過程比較耗時,大約持繼在一小時以上。因rasbperrypi 4
內存有2GB,上面可以make -j2
來編譯加速。
五,編譯bpftrace
筆者選擇的bpftrace
版本為v0.17.1
(正如上面提到的,筆者驗證最新版本的bpftrace
在arm64/linux-6.1
系統下運行不正常。配置、編譯安裝的操作如下:
mkdir -p build_aarch64 && cd build_aarch64
cmake -DCMAKE_BUILD_TYPE=release -DCMAKE_INSTALL_PREFIX=/opt/bpftrace \-DCMAKE_C_COMPILER=clang-10 -DCMAKE_CXX_COMPILER=clang++-10 \-DCMAKE_C_FLAGS='-fPIC -O2 -D_GNU_SOURCE -ggdb -fno-omit-frame-pointer -I/opt/bpftrace/include -I${HOME}/libbpf-1.2.2/include -I${HOME}/libbpf-1.2.2/include/uapi' \-DCMAKE_CXX_FLAGS='-fPIC -O2 -D_GNU_SOURCE -ggdb -fno-omit-frame-pointer -I/opt/bpftrace/include -I${HOME}/libbpf-1.2.2/include -I${HOME}/libbpf-1.2.2/include/uapi' \-DCMAKE_EXE_LINKER_FLAGS='-L/opt/bpftrace/lib -Wl,-rpath=/opt/bpftrace/lib' \-DCMAKE_MODULE_LINKER_FLAGS='-L/opt/bpftrace/lib -Wl,-rpath=/opt/bpftrace/lib' \-DCMAKE_SHARED_LINKER_FLAGS='-L/opt/bpftrace/lib -Wl,-rpath=/opt/bpftrace/lib' \-DUSE_SYSTEM_BPF_BCC=ON -DSTATIC_LINKING=OFF -DALLOW_UNSAFE_PROBE=ON -DBUILD_TESTING=OFF ..
make && sudo make install
需要注意的是,上面的編譯選項,直接指定了libbpf-1.2.2
頭文件包含的路徑。至此,筆者的編譯就結束了,接下來將編譯結果復制到安裝有openwrt
系統的TF卡中。
在raspberrypi/openwrt系統下使用bpftrace
首先,除了/opt/bpftrace
外,筆者從樹莓派的debian系統中復制了以下庫到openwrt
系統:
root@OpenWrt:/lib/aarch64-linux-gnu# ls -lh -t | head -n 8
-rw-r--r-- 1 root root 64.8M Sep 9 22:40 libLLVM-10.so.1
-rw-r--r-- 1 root root 65.0K Sep 9 22:42 libbz2.so.1.0
-rw-r--r-- 1 root root 27.7M Sep 9 22:41 libclang-10.so.1
-rw-r--r-- 1 root root 179.1K Sep 9 22:43 libedit.so.2
-rw-r--r-- 1 root root 30.2K Sep 9 22:42 libffi.so.6
-rw-r--r-- 1 root root 130.2K Sep 9 22:41 liblzma.so.5
-rw-r--r-- 1 root root 158.6K Sep 9 22:43 libtinfo.so.5
-rw-r--r-- 1 root root 405.8K Sep 9 22:41 libzstd.so.1
然后,筆者為樹莓派的內核使能了相關的內核選項,詳見此處的詳細說明。還有,bpftrace
的正常運行,可能還需要訪問內核的頭文件,這就需要使能CONFIG_IKHEADERS=m
,在樹莓派啟動后,手動加載該內核模塊:insmod kheaders.ko
。最后,筆者在樹莓派的openwrt
系統下使用bpftrace
的結果如下:
root@OpenWrt:~# insmod kheaders.ko
module is already loaded - kheaders
root@OpenWrt:~# uname -a
Linux OpenWrt 6.1.50 #0 SMP Fri Sep 1 21:45:47 2023 aarch64 GNU/Linux
root@OpenWrt:~# /opt/bpftrace/bin/bpftrace --info
SystemOS: Linux 6.1.50 #0 SMP Fri Sep 1 21:45:47 2023Arch: aarch64Buildversion: v0.17.1LLVM: 10.0.1unsafe uprobe: nobfd: nolibdw (DWARF support): yeslibbpf: failed to find valid kernel BTF
Kernel helpersprobe_read: yesprobe_read_str: yesprobe_read_user: yesprobe_read_user_str: yesprobe_read_kernel: yesprobe_read_kernel_str: yesget_current_cgroup_id: yessend_signal: yesoverride_return: noget_boot_ns: yesdpath: noskboutput: noKernel featuresInstruction limit: 1000000Loop support: yesbtf: nomap batch: yesuprobe refcount (depends on Build:bcc bpf_attach_uprobe refcount): yesMap typeshash: yespercpu hash: yesarray: yespercpu array: yesstack_trace: yesperf_event_array: yesProbe typeskprobe: yestracepoint: yesperf_event: yeskfunc: noiter:task: noiter:task_file: nokprobe_multi: noraw_tp_special: yesroot@OpenWrt:~# cd /opt/bpftrace/share/bpftrace/tools/
root@OpenWrt:/opt/bpftrace/share/bpftrace/tools# ls
bashreadline.bt gethostlatency.bt runqlen.bt tcpdrop.bt
biolatency.bt killsnoop.bt setuids.bt tcplife.bt
biosnoop.bt loads.bt ssllatency.bt tcpretrans.bt
biostacks.bt mdflush.bt sslsnoop.bt tcpsynbl.bt
bitesize.bt naptime.bt statsnoop.bt threadsnoop.bt
capable.bt old swapin.bt undump.bt
cpuwalk.bt oomkill.bt syncsnoop.bt vfscount.bt
dcsnoop.bt opensnoop.bt syscount.bt vfsstat.bt
doc pidpersec.bt tcpaccept.bt writeback.bt
execsnoop.bt runqlat.bt tcpconnect.bt xfsdist.bt
root@OpenWrt:/opt/bpftrace/share/bpftrace/tools# /opt/bpftrace/bin/bpftrace ./execsnoop.bt
libbpf: failed to find valid kernel BTF
libbpf: failed to find valid kernel BTF
Attaching 3 probes...
TIME(ms) PID ARGS
26138 2490 /etc/init.d/firewall reload
26168 2502 readlink /etc/init.d/firewall
26170 2503 basename /etc/init.d/firewall
26172 2504 flock -n 1000
26174 2505 flock 1000
26178 2506 readlink /etc/init.d/firewall
26181 2507 readlink /etc/init.d/firewall
26183 2508 basename /etc/init.d/firewall
26186 2509 flock -n 1000
26188 2510 fw4 reload
26191 2511 flock -x 1000
26193 2512 rm -f /var/run/fw4.state
26195 2513 utpl -S /usr/share/firewall4/main.uc
26195 2514 nft -f /dev/stdin
26303 2515 sh -c /usr/sbin/nft --terse --json list flowtables inet
26305 2515 /usr/sbin/nft --terse --json list flowtables inet
26393 2516 utpl -S /usr/share/firewall4/main.uc
29341 2517 /etc/init.d/firewall status
29371 2529 readlink /etc/init.d/firewall
29373 2530 basename /etc/init.d/firewall
29375 2531 flock -n 1000
29377 2532 flock 1000
29381 2533 readlink /etc/init.d/firewall
29384 2534 basename /etc/init.d/firewall
29387 2537 jsonfilter -e @["firewall"]
29388 2539 jshn -w
29391 2540 ubus call service list { "name": "firewall" }
29397 2543 jsonfilter -e $.instances
63078 2544 /etc/init.d/network status
63108 2556 readlink /etc/init.d/network
63110 2557 basename /etc/init.d/network
63112 2558 flock -n 1000
63114 2559 flock 1000
63118 2560 readlink /etc/init.d/network
63121 2561 basename /etc/init.d/network
63124 2564 jsonfilter -e @["network"]
63125 2566 jshn -w
63128 2567 ubus call service list { "name": "network" }
63134 2570 jsonfilter -e $.instances
63137 2571 jsonfilter -s { "instance1": { "running": true, "pid": 769, "command": [ "\/sbin\/netifd" ], "term_timeout": 5,
"limits": { "core": "unlimited" }, "respawn": { "threshold": 3600, "timeout": 5, "retry": 5 } } } -e $[*].running
^C
root@OpenWrt:/opt/bpftrace/share/bpftrace/tools#
至此,可以說明我們編譯的bpftrace
工具可以正常工作了。