BPF示例在64位ARM上的调试

eBPF调试工具

eBPF作为Linux内核中调试功能强大的子系统,相应的应用层调试工具有很多,例如bpftool/bpftrace,以及提供了LuaPython调试接口的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主机上安装clangllvm相关的工具链:

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交叉编译准备依赖库了。这里只需交叉编译zlibelfutils;这两个开源库的编译笔者在上一篇文章中已涉及,不再重复;值得强调的是这两个库的安装路径为/opt/libbpf。此外,在编译samples/bpf的过程中,内核的编译脚本会自动编译tools/lib/bpf并生成静态库libbpf.aBPF示例中的调试应用程序会链接到该静态库。

编译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示例

笔者复制了两个文件到设备上,分别为tracex1tracex1_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 -1eBPF汇编指令:

$ 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内核接口在不断变化,出现这种问题也是正常的;从侧面反映出活跃的内核开发活动一直在进行中。

笔者尝试调试tracex2tracex2_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的内核调试功能了。

下面是一个内核端bpf创建map的C语言示例: ```c #include <linux/bpf.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/syscall.h> #include <linux/if_ether.h> #include <linux/ip.h> #include <linux/tcp.h> #define MAP_TYPE BPF_MAP_TYPE_HASH #define KEY_SIZE sizeof(__u32) #define VALUE_SIZE sizeof(__u64) #define MAP_SIZE 256 int main(int argc, char **argv) { int map_fd, i, ret; __u32 key; __u64 value; struct bpf_insn prog[] = { BPF_MOV64_REG(BPF_REG_1, BPF_REG_10), BPF_LD_MAP_FD(BPF_REG_2, map_fd), BPF_MOV32_IMM(BPF_REG_3, 1), BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, -ETH_HLEN), BPF_STX_MEM(BPF_W, BPF_REG_2, BPF_REG_1, BPF_REG_3), BPF_MOV64_REG(BPF_REG_1, BPF_REG_10), BPF_LD_MAP_FD(BPF_REG_2, map_fd), BPF_MOV32_IMM(BPF_REG_3, 1), BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, -ETH_HLEN + offsetof(struct iphdr, protocol)), BPF_LD_ABS(BPF_W, BPF_REG_4), BPF_STX_MEM(BPF_W, BPF_REG_2, BPF_REG_1, BPF_REG_3), BPF_MOV64_REG(BPF_REG_1, BPF_REG_10), BPF_LD_MAP_FD(BPF_REG_2, map_fd), BPF_MOV32_IMM(BPF_REG_3, 1), BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, -ETH_HLEN + sizeof(struct iphdr)), BPF_LD_ABS(BPF_W, BPF_REG_4), BPF_STX_MEM(BPF_W, BPF_REG_2, BPF_REG_1, BPF_REG_3), BPF_MOV64_REG(BPF_REG_1, BPF_REG_10), BPF_LD_MAP_FD(BPF_REG_2, map_fd), BPF_MOV32_IMM(BPF_REG_3, 1), BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, -ETH_HLEN + sizeof(struct iphdr) + offsetof(struct tcphdr, dest)), BPF_LD_ABS(BPF_W, BPF_REG_4), BPF_STX_MEM(BPF_W, BPF_REG_2, BPF_REG_1, BPF_REG_3), BPF_MOV64_IMM(BPF_REG_0, 0), BPF_EXIT_INSN(), }; struct bpf_program prog_struct = { .len = sizeof(prog) / sizeof(struct bpf_insn), .insn = prog, }; map_fd = syscall(__NR_bpf, MAP_TYPE, KEY_SIZE, VALUE_SIZE, MAP_SIZE, 0); if (map_fd < 0) { perror("Failed to create map"); exit(1); } ret = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, &prog_struct, prog_struct.len, "GPL", 0, NULL, 0); if (ret < 0) { perror("Failed to load program"); exit(1); } printf("Successfully created map with fd %d and program fd %d\n", map_fd, ret); close(map_fd); close(ret); return 0; } ``` 这个示例程序创建了一个类型为HASH、键大小为4字节、值大小为8字节、大小为256的map,并通过BPF程序将目的端口地址的值存储在map中。它使用了Linux系统调用`bpf()`创建map,并使用`bpf_prog_load()`将BPF程序加载到内核中。在完成后,它关闭了map和BPF程序的文件描述符。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值