eBPF调试工具
eBPF作为Linux内核中调试功能强大的子系统,相应的应用层调试工具有很多,例如bpftool/bpftrace,以及提供了Lua和Python调试接口的bcc;内核信息收集、性能分析的开源工具SystemTap也用到了内核的eBPF
功能。不过笔者希望在嵌入式设备上使用eBPF
提供的调试功能,一种方法是使用开源的SDK(例如yocto,笔者未尝试过)自动化构建这些调试工具(否则就需要手动交叉编译);另一种方法是将某个支持arm64架构的Linux发行版(例如运行debian系统的树莓派)提供eBPF
组件剥离出来,集成到嵌入式arm64/Linux设备上进行调试;最后一种方法是参照Linux内核提供的BPF
示例(代码路径为samples/bpf
)编写C代码实现所需的调试功能。
本文记录了笔者在PC机上为arm64/Linux
设备交叉编译BPF
示例的过程,及该过程中遇到的问题和简单的解决方法;笔者用到的Linux内核版本为v5.4.123
。
相关依赖库的准备
首先,按照文档linux-5.4.123/samples/bpf/README.rst
的要求,在x86_64/Linux
主机上安装clang及llvm相关的工具链:
sudo apt install clang llvm
该工具链不属交叉编译器,仅用于生成BTF的调试文件;BTF
文件本身是ELF
文件,其中包含了eBPF
相关的调试信息,使用BTF
可以实现一定程度上的“一次编译,处处运行”。之后,配置嵌入式设备的内核源码,并编译内核:
alias lmake='make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-'
lmake menuconfig
lmake headers_install
lmake Image
Linux内核的头文件安装及编译是为了让内核源码中生成相应的头文件,否则后面的samples/bpf
编译会出错。接下来就需要为samples/bpf
交叉编译准备依赖库了。这里只需交叉编译zlib和elfutils;这两个开源库的编译笔者在上一篇文章中已涉及,不再重复;值得强调的是这两个库的安装路径为/opt/libbpf
。此外,在编译samples/bpf
的过程中,内核的编译脚本会自动编译tools/lib/bpf
并生成静态库libbpf.a
;BPF
示例中的调试应用程序会链接到该静态库。
编译samples/bpf中的所有示例
在编译BPF
示例前,需要修改相关的Makefile,笔者的修改如下:
diff --git a/samples/bpf/Makefile b/samples/bpf/Makefile
index 6d1df7117..f8b1e04e6 100644
--- a/samples/bpf/Makefile
+++ b/samples/bpf/Makefile
@@ -171,6 +171,7 @@ always += ibumad_kern.o
always += hbm_out_kern.o
always += hbm_edt_kern.o
+KBUILD_HOSTCFLAGS += -I/opt/libbpf/include
KBUILD_HOSTCFLAGS += -I$(objtree)/usr/include
KBUILD_HOSTCFLAGS += -I$(srctree)/tools/lib/bpf/
KBUILD_HOSTCFLAGS += -I$(srctree)/tools/testing/selftests/bpf/
@@ -180,7 +181,7 @@ KBUILD_HOSTCFLAGS += -DHAVE_ATTR_TEST=0
HOSTCFLAGS_bpf_load.o += -I$(objtree)/usr/include -Wno-unused-variable
-KBUILD_HOSTLDLIBS += $(LIBBPF) -lelf
+KBUILD_HOSTLDLIBS += $(LIBBPF) -lelf -L/opt/libbpf/lib -Wl,-rpath-link=/opt/libbpf/lib
HOSTLDLIBS_tracex4 += -lrt
HOSTLDLIBS_trace_output += -lrt
HOSTLDLIBS_map_perf_test += -lrt
diff --git a/tools/build/Makefile.build b/tools/build/Makefile.build
index cd72016c3..7eeadf644 100644
--- a/tools/build/Makefile.build
+++ b/tools/build/Makefile.build
@@ -87,10 +87,6 @@ quiet_cmd_host_ld_multi = HOSTLD $@
cmd_host_ld_multi = $(if $(strip $(obj-y)),\
$(HOSTLD) -r -o $@ $(filter $(obj-y),$^),rm -f $@; $(HOSTAR) rcs $@)
-ifneq ($(filter $(obj),$(hostprogs)),)
- host = host_
-endif
-
# Build rules
$(OUTPUT)%.o: %.c FORCE
$(call rule_mkdir)
diff --git a/tools/lib/bpf/Makefile b/tools/lib/bpf/Makefile
index 9758bfa59..5c2b4ce78 100644
--- a/tools/lib/bpf/Makefile
+++ b/tools/lib/bpf/Makefile
@@ -191,7 +191,8 @@ $(OUTPUT)libbpf.so: $(OUTPUT)libbpf.so.$(LIBBPF_VERSION)
$(OUTPUT)libbpf.so.$(LIBBPF_VERSION): $(BPF_IN_SHARED)
$(QUIET_LINK)$(CC) --shared -Wl,-soname,libbpf.so.$(LIBBPF_MAJOR_VERSION) \
- -Wl,--version-script=$(VERSION_SCRIPT) $^ -lelf -o $@
+ -Wl,--version-script=$(VERSION_SCRIPT) $^ -lelf -o $@ \
+ -L$(prefix)/lib -Wl,-rpath-link=$(prefix)/lib
@ln -sf $(@F) $(OUTPUT)libbpf.so
@ln -sf $(@F) $(OUTPUT)libbpf.so.$(LIBBPF_MAJOR_VERSION)
@@ -199,7 +200,8 @@ $(OUTPUT)libbpf.a: $(BPF_IN_STATIC)
$(QUIET_LINK)$(RM) $@; $(AR) rcs $@ $^
$(OUTPUT)test_libbpf: test_libbpf.cpp $(OUTPUT)libbpf.a
- $(QUIET_LINK)$(CXX) $(INCLUDES) $^ -lelf -o $@
+ $(QUIET_LINK)$(CXX) $(INCLUDES) $^ -lelf -o $@ \
+ -L$(prefix)/lib -Wl,-rpath-link=$(prefix)/lib
$(OUTPUT)libbpf.pc:
$(QUIET_GEN)sed -e "s|@PREFIX@|$(prefix)|" \
上面的一些修改在上一篇文章中也出现过。最后就是非常重要的一步,编译samples/bpf
;在Linux内核源码根目录下执行:
lmake M=samples/bpf prefix=/opt/libbpf \
FEATURE_CHECK_CFLAGS-libelf='-I/opt/libbpf/include' \
FEATURE_CHECK_LDFLAGS-libelf='-L/opt/libbpf/lib -Wl,-rpath-link=/opt/libbpf/lib' \
EXTRA_CFLAGS='-Wall -fPIC -O2 -mcpu=cortex-a53 -I/opt/libbpf/include'
该命令执行完成后,就可以在arm64/Linux
设备上运行BPF
示例了。下面笔者记录调试过程中的两个问题。
运行samples/bpf示例
笔者复制了两个文件到设备上,分别为tracex1
和tracex1_kern.o
;前者用于加载后者到Linux内核并调试,后者是由clang
编译器生成的BTF
文件。调试结果如下:
# export LD_LIBRARY_PATH=/opt/libbpf/lib::/opt/libbpf/lib64
# ./tracex1
invalid relo for insn[4].code 0x85
bpf_load_program() err=22
last insn is not an exit or jmp
processed 0 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
last insn is not an exit or jmp
processed 0 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
结果为BTF
文件加载失败,使用llvm-objdump
反汇编tracex1_kern.o
可以看到其中存在call -1
的eBPF
汇编指令:
$ llvm-objdump -d tracex1_kern.o
tracex1_kern.o: file format ELF64-BPF
Disassembly of section kprobe/__netif_receive_skb_core:
0000000000000000 bpf_prog1:
0: 79 13 00 00 00 00 00 00 r3 = *(u64 *)(r1 + 0)
1: bf a1 00 00 00 00 00 00 r1 = r10
2: 07 01 00 00 e8 ff ff ff r1 += -24
3: b7 02 00 00 08 00 00 00 r2 = 8
4: 85 10 00 00 ff ff ff ff call -1
5: b7 06 00 00 00 00 00 00 r6 = 0
6: 7b 6a f0 ff 00 00 00 00 *(u64 *)(r10 - 16) = r6
7: 79 a3 e8 ff 00 00 00 00 r3 = *(u64 *)(r10 - 24)
8: 07 03 00 00 10 00 00 00 r3 += 16
这一定程度上意味着缺失符号的引用,通常是未定义的函数。查看tracex1_kern.c
源码可知,bpf_prog1
函数调用了bpf_probe_read_kernel
:
/* linux-5.4.123/samples/bpf/tracex1_kern.c */
SEC("kprobe/__netif_receive_skb_core")
int bpf_prog1(struct pt_regs *ctx)
{
/* attaches to kprobe __netif_receive_skb_core,
* looks for packets on loobpack device and prints them
*/
char devname[IFNAMSIZ];
struct net_device *dev;
struct sk_buff *skb;
int len;
/* non-portable! works for the given kernel only */
bpf_probe_read_kernel(&skb, sizeof(skb), (void *)PT_REGS_PARM1(ctx));
dev = _(skb->dev);
len = _(skb->len);
而搜索整个Linux内核源码,该函数未定义,只能搜索到一次:
$ git grep -n bpf_probe_read_kernel
samples/bpf/tracex1_kern.c:32: bpf_probe_read_kernel(&skb, sizeof(skb), (void *)PT_REGS_PARM1(ctx));
这说明samples/bpf
下的某些示例是不可用的。实际上,Linux内核提供的eBPF
内核接口在不断变化,出现这种问题也是正常的;从侧面反映出活跃的内核开发活动一直在进行中。
笔者尝试调试tracex2
及tracex2_kern.o
,结果又出现了异常:
# ./tracex2
failed to create kprobe 'sys_write' error 'No such file or directory'
出现该问题的原因是,在arm64/Linux
设备上,系统调用write
在内核中的函数名不为sys_write
,而为__arm64_sys_write
;可以通过读取/proc/kallsyms
确认:
# cat /proc/kallsyms | grep -e sys_write
ffffffc0102a1d28 T __arm64_sys_writev
ffffffc0102a2090 T __arm64_compat_sys_writev
ffffffc0102a4630 T ksys_write
ffffffc0102a46f0 T __arm64_sys_write
ffffffc010346738 t proc_sys_write
这一命名的差异源于Linux内核对不同CPU架构的兼容机制。修tracex2_kern.c
代码:
diff --git a/samples/bpf/tracex2_kern.c b/samples/bpf/tracex2_kern.c
index 5e11c20ce..0c5330fb2 100644
--- a/samples/bpf/tracex2_kern.c
+++ b/samples/bpf/tracex2_kern.c
@@ -76,7 +76,7 @@ struct bpf_map_def SEC("maps") my_hist_map = {
.max_entries = 1024,
};
-SEC("kprobe/sys_write")
+SEC("kprobe/__arm64_sys_write")
int bpf_prog3(struct pt_regs *ctx)
{
long write_size = PT_REGS_PARM3(ctx);
后再次编译生成tracex2_kern.o
,在设备上运行可以得到正确的示例运行结果:
# ./tracex2
location 0xa94153f352800020 count 1
location 0xa94153f352800020 count 2
location 0xa94153f352800020 count 3
location 0xa94153f352800020 count 4
pid 1 cmd procd uid 0
syscall write() stats
byte_size : count distribution
1 -> 1 : 0 | |
2 -> 3 : 0 | |
4 -> 7 : 0 | |
8 -> 15 : 0 | |
16 -> 31 : 0 | |
32 -> 63 : 0 | |
64 -> 127 : 0 | |
128 -> 255 : 0 | |
256 -> 511 : 0 | |
512 -> 1023 : 0 | |
1024 -> 2047 : 0 | |
2048 -> 4095 : 0 | |
4096 -> 8191 : 0 | |
8192 -> 16383 : 0 | |
16384 -> 32767 : 0 | |
32768 -> 65535 : 0 | |
65536 -> 131071 : 0 | |
131072 -> 262143 : 0 | |
262144 -> 524287 : 0 | |
524288 -> 1048575 : 0 | |
1048576 -> 2097151 : 0 | |
2097152 -> 4194303 : 0 | |
4194304 -> 8388607 : 0 | |
8388608 -> 16777215 : 0 | |
16777216 -> 33554431 : 0 | |
33554432 -> 67108863 : 0 | |
67108864 -> 134217727 : 0 | |
134217728 -> 268435455 : 0 | |
268435456 -> 536870911 : 0 | |
536870912 -> 1073741823 : 0 | |
1073741824 -> 2147483647 : 0 | |
2147483648 -> 4294967295 : 0 | |
4294967296 -> 8589934591 : 0 | |
8589934592 -> 17179869183 : 0 | |
17179869184 -> 34359738367 : 0 | |
34359738368 -> 68719476735 : 0 | |
68719476736 -> 137438953471 : 0 | |
137438953472 -> 274877906943 : 0 | |
274877906944 -> 549755813887 : 0 | |
549755813888 -> 1099511627775 : 0 | |
1099511627776 -> 2199023255551 : 0 | |
2199023255552 -> 4398046511103 : 0 | |
4398046511104 -> 8796093022207 : 0 | |
8796093022208 -> 17592186044415 : 0 | |
17592186044416 -> 35184372088831 : 0 | |
35184372088832 -> 70368744177663 : 0 | |
70368744177664 -> 140737488355327 : 0 | |
140737488355328 -> 281474976710655 : 0 | |
281474976710656 -> 562949953421311 : 0 | |
562949953421312 -> 1125899906842623 : 0 | |
1125899906842624 -> 2251799813685247 : 0 | |
2251799813685248 -> 4503599627370495 : 0 | |
4503599627370496 -> 9007199254740991 : 0 | |
9007199254740992 -> 18014398509481983 : 0 | |
18014398509481984 -> 36028797018963967 : 0 | |
36028797018963968 -> 72057594037927935 : 0 | |
72057594037927936 -> 144115188075855871 : 0 | |
144115188075855872 -> 288230376151711743 : 0 | |
288230376151711744 -> 576460752303423487 : 0 | |
576460752303423488 -> 1152921504606846975 : 0 | |
1152921504606846976 -> 2305843009213693951 : 0 | |
2305843009213693952 -> 4611686018427387903 : 0 | |
-4611686018427387904 -> 9223372036854775807 : 0 | |
0 -> 0 : 1 |************************************* |
......
注意到这两个问题,可以帮助加深对BPF
功能的了解,方便在嵌入式设备上学习BPF
的内核调试功能了。