调试eBPF虚拟机

eBPF的大部分实现都在Linux内核当中,uBPF项目在用户空间实现了eBPF虚拟机,可以在用户空间模拟执行eBPF程序,添加断点,帮助开发者调试程序。eBPF虚拟机运行在内核中,但是内核可以通过GDB进行调试,因此eBPF虚拟机也可以通过GDB进行调试。

在内核调试eBPF VM有什么益处吗?

  • 调试可以很好的帮助理解其工作原理。

  • 开发者编写的程序有时无法加载进内核,调试可以帮助定位问题。

本文记录了踩完众多坑的搭建环境过程,供需要的人进行参考。

所需环境

内核镜像

通过下载内核源码编译构建内核镜像文件,源码可以通过Linux官方网站,或者清华镜像网站选择需要的版本下载。本文采用了5.4.1版本内核

编译内核镜像的步骤可以参考之前的文章,QEMU调试Linux内核环境搭建

根文件系统

使用buildroot工具,构建根文件系统。从git仓库下载源码,也可以从官网下载并解压。

1
2
3
4
git clone git://git.buildroot.net/buildroot /source/buildroot
cd buildroot
make menuconfig
make -j4

在buildroot需要进行几个关键的配置,才能在虚拟机中运行bpf程序。

  • Target options
    架构选择为x86_64,如下图。

在这里插入图片描述

  • Toolchain
    编译工具链的配置是能否在qemu虚拟机中运行ebpf程序的关键。
    在这里插入图片描述

  • Kernel
    buildroot也支持下载内核源码并编译内核镜像,建议勾不勾选,使用自己下载的内核。
    在这里插入图片描述

  • Target packages

    • Debugging, profiling and benchmark

    该目录下勾选bpftool和libbpf,其他软件根据需要勾选,建议勾选strace,可以结合strace学习bpf工作原理,同时加载程序出错时也能看到更多的错误信息。
    在这里插入图片描述

    • Libraries → Other
      勾选clang和llvm、BPF backend。
      在这里插入图片描述

在这里插入图片描述

  • Networking applications
    添加对ssh的支持,方便宿主机往虚拟机里面传输编译完成的ebpf程序。
    在这里插入图片描述
  • Filesystem images
    选择ext4 root filesystem,exact size改成256M,取消选择f2fs root filesystem。

在这里插入图片描述

  • Bootloaders
    取消U-Boot,可以加快编译速度。

在这里插入图片描述

最后别忘了选择Save保存到.config文件。

在make的过程中,buildroot需要从国外网站下载软件包,速度很慢,如果观察到下载速度慢,可以复制窗口日志中的下载地址,从其他渠道下载完成后放到buildroot/dl/目录下,再次make就能够直接从dl目录解压了。

make完成后,会在buildroot/output/images目录下生成rootfs.ext4文件。

在这里插入图片描述

eBPF程序

编写一个简单打印hello world的eBPF测试程序bpf_program.c,挂载在ext4_file_write_iter函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <linux/version.h>
#include <uapi/linux/bpf.h>
#include "bpf_helpers.h"
#define SEC(NAME) __attribute__((section(NAME), used))

struct bpf_map_def SEC("maps") my_map = {
.type = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
.key_size = sizeof(int),
.value_size = sizeof(u32),
.max_entries = 2,
};

SEC("kprobe/ext4_file_write_iter")
int bpf_prog1(struct pt_regs *ctx)
{
char msg[] = "Hello, BPF World!";
bpf_trace_printk(msg, sizeof(msg));

return 0;
}

char _license[] SEC("license") = "GPL";
u32 _version SEC("version") = LINUX_VERSION_CODE;

用户态加载程序loader.c

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "bpf_load.h"
#include <stdio.h>

int main(int argc, char **argv) {
if (load_bpf_file("bpf_program.o") != 0) {
printf("The kernel didn't load the BPF program\n");
return -1;
}

read_trace_pipe();

return 0;
}

Makefile文件,使用sed命令替换为自己的内核源码路径。

1
sed -i 's;/kernel-src;/home/szp/linux-5.4.1;' ./Makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
CLANG = clang
EXECABLE = monitor-exec
BPFCODE = bpf_program
BPFTOOLS = /home/szp/linux-5.4.1/samples/bpf
BPFLOADER = $(BPFTOOLS)/bpf_load.c
CCINCLUDE += -I/home/szp/linux-5.4.1/tools/testing/selftests/bpf
LOADINCLUDE += -I/home/szp/linux-5.4.1/samples/bpf
LOADINCLUDE += -I/home/szp/linux-5.4.1/tools/lib
LOADINCLUDE += -I/home/szp/linux-5.4.1/tools/perf
LOADINCLUDE += -I/home/szp/linux-5.4.1/tools/include
LIBRARY_PATH = -L/usr/local/lib64
BPFSO = -lbpf
CFLAGS += $(shell grep -q "define HAVE_ATTR_TEST 1" /home/szp/linux-5.4.1/tools/perf/perf-sys.h \
&& echo "-DHAVE_ATTR_TEST=0")
.PHONY: clean $(CLANG) bpfload build
clean:
rm -f *.o *.so $(EXECABLE)
build: ${BPFCODE.c} ${BPFLOADER}
$(CLANG) -O2 -target bpf -c $(BPFCODE:=.c) $(CCINCLUDE) -I/home/szp/linux-5.4.1/include/ -o ${BPFCODE:=.o}
bpfload: build
clang $(CFLAGS) -o $(EXECABLE) -lelf $(LOADINCLUDE) $(LIBRARY_PATH) $(BPFSO) \
$(BPFLOADER) loader.c
$(EXECABLE): bpfload
.DEFAULT_GOAL := $(EXECABLE)

在宿主机编译eBPF代码,生成bpf_program.o文件和monitor-exec文件。
在这里插入图片描述

GDB

使用vscode可以将gdb调试可视化,推荐使用。所需插件

  • Remote-SSH
  • C/C++
  • GDB Debug

使用远程资源管理器打开内核源码根目录,在“运行(Debug)”菜单下添加配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"version": "0.2.0",
"configurations": [
{
"name": "kernel debug",
"type": "cppdbg",
"request": "launch",
"miDebuggerServerAddress": "127.0.0.1:1234",
"program": "${workspaceFolder}/vmlinux",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "为 gdb 启用整齐打印",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "将反汇编风格设置为 Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
]
}
]
}

启动调试即可远程连接GDB进行调试。

启动QEMU

进入内核源码根目录下,使用buildroot构建的根文件系统启动QEMU虚拟机,将虚拟机ssh端口映射到宿主机2222。

1
sudo qemu-system-x86_64 -kernel arch/x86/boot/bzImage --enable-kvm -s -smp 1 -nic user,hostfwd=tcp::2222-:22 -boot c -m 2049M -hda ../buildtool/output/images/rootfs.ext4 -append "root=/dev/sda rw console=ttyS0,115200 acpi=off nokaslr" -serial stdio -display none

使用root用户登录虚拟机,配置网卡与ip地址:

1
2
ifconfig eth0 10.0.2.15
ifconfig eth0 up

在这里插入图片描述

修改/etc/ssh/sshd_config文件,将如下两项改为yes:

1
2
PermitRootLogin yes
PermitEmptyPasswords yes

现在就可以使用ssh登录该虚拟机了:

1
2
3
4
ssh -p 2222 root@127.0.0.1

scp -P 2222 monitor-exec root@127.0.0.1:/tmp/
scp -P 2222 bpf_program.o root@127.0.0.1:/tmp/

使用scp将bpf可执行文件拷贝到虚拟机中。

在这里插入图片描述

执行程序

在kernel/bpf/syscall.c:bpf_prog_load()函数中添加断点,启动GDB调试并运行bpf程序,就可以打印变量,进行调试。虽然内核优化掉了很多变量,但是还是可以通过现有的变量和函数调用栈直观的学习bpf程序的加载过程,map初始化过程。

内核打印的调试日志

vscode调试面板

参考资料:
https://youtu.be/W6rgaghycFI

PS:前往公众号“Linux工坊”,可以查看最新内容!关注即可免费领取面试&Linux技术书籍!

------ 本文结束------
  • 文章标题: 调试eBPF虚拟机
  • 本文作者: 你是我的阳光
  • 发布时间: 2022年03月13日 - 10:48:04
  • 最后更新: 2022年11月09日 - 16:12:55
  • 本文链接: https://szp2016.github.io/ebpf/调试eBPF虚拟机/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
0%