如何调试eBPF程序?
[bpf(2) - Linux manual page (man7.org)](https://man7.org/linux/man-pages/man2/bpf.2.html)
[bpf-helpers(7) - Linux manual page (man7.org)](https://man7.org/linux/man-pages/man7/bpf-helpers.7.html)
用户态eBPF程序与内核交互的唯一路径是bpf系统调用:
#include
int bpf(int cmd, union bpf_attr *attr, unsigned int size);
BPF 系统调用接受三个参数:
BPF_PROG_LOAD
就是加载eBPF程序.bpf系统调用看似简单,但是包括了bpf的所有功能。在内核5.15中,第一个参数cmd支持37个宏定义命令([bpf.h - include/uapi/linux/bpf.h - Linux source code (v5.15) - Bootlin](https://elixir.bootlin.com/linux/v5.15/source/include/uapi/linux/bpf.h#L838)),包括map的创建、增删改查;加载eBPF程序。。。
在内核文档中的示例:
int bpf_prog_load(enum bpf_prog_type type,
const struct bpf_insn *insns, int insn_cnt,
const char *license) {
union bpf_attr attr = {
.prog_type = type,
.insns = ptr_to_u64(insns),
.insn_cnt = insn_cnt,
.license = ptr_to_u64(license),
.log_buf = ptr_to_u64(bpf_log_buf),
.log_size = LOG_BUF_SIZE,
.log_level = 1,
};
return bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
}
//---------------
int
bpf_create_map(enum bpf_map_type map_type,
unsigned int key_size,
unsigned int value_size,
unsigned int max_entries)
{
union bpf_attr attr = {.map_type = map_type,
.key_size = key_size,
.value_size = value_size,
.max_entries = max_entries};
return bpf(BPF_MAP_CREATE, &attr, sizeof(attr));
}
可以看出,bpf程序在用户空间使用的bpf_prog_load(),bpf_create_map()
都是bpf()
系统调用的封装。
strace常用来跟踪进程执行时的系统调用和接收所接收的信号。strace可以跟踪到一个进程产生的系统调用,包括 参数,返回值,执行消耗的时间。
通过参数-ebpf
,能够过滤出bpf系统调用:
tzx@tzx:~/ebpf_monitor/libbpf/src$ sudo strace -ebpf ./network_monitor
...
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_SOCKET_FILTER, insn_cnt=2, insns=0xfffff5ee6d18, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(0, 0, 0), prog_flags=0, prog_name="", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS, prog_btf_fd=0, func_info_rec_size=0, func_info=NULL, func_info_cnt=0, line_info_rec_size=0, line_info=NULL, line_info_cnt=0, attach_btf_id=0, attach_prog_fd=0}, 116) = 8
...
bpf(BPF_BTF_LOAD, {btf="\237\353\1\0\30\0\0\0\0\0\0\0\20\0\0\0\20\0\0\0\5\0\0\0\1\0\0\0\0\0\0\1"..., btf_log_buf=NULL, btf_size=45, btf_log_size=0, btf_log_level=0}, 32) = 8
...
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_RINGBUF, key_size=0, value_size=0, max_entries=4096, map_flags=0, inner_map_fd=0, map_name="ringbuf", map_ifindex=0, btf_fd=8, btf_key_type_id=0, btf_value_type_id=0, btf_vmlinux_value_type_id=0, map_extra=0}, 72) = 9
libbpf: map 'ringbuf': created successfully, fd=9
...
load success.
bpf(BPF_OBJ_GET_INFO_BY_FD, {info={bpf_fd=9, info_len=88 => 80, info=0xfffff5ee6fd0}}, 16) = 0
^Cstrace: Process 31846 detached
利用strace追踪bpf系统调用,能够捕获到被追踪的eBPF程序进行了哪些bpf系统调用,还能看到具体的参数值以及返回值(文件描述符,可以用于上下文分析)。
bpftool是一个用于操作和调试 BPF 程序的命令行工具。它可以用于列出、加载、卸载和查询 BPF 程序、映射和事件等。其源码位于内核源码中:[bpftool « bpf « tools - kernel/git/bpf/bpf-next.git - BPF next kernel tree](https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git/tree/tools/bpf/bpftool)
常用的 bpftool 命令:
bpftool prog show
:列出所有 BPF 程序bpftool map show
:列出所有 BPF mapbpftool event show
:列出所有 BPF 事件bpftool prog load
:加载一个 BPF 程序bpftool prog unload
:卸载一个 BPF 程序在调试 BPF 程序时:
bpftool prog dump type
:显示指定类型的所有 BPF 程序的汇编代码bpftool prog tracelog
:跟踪指定的 BPF 程序的执行并显示其输出bpftool map dump id
:显示指定map的内容,以json格式输出bpftool map update name key 0xc1 0xc2 value 0xa1 0xa2
:直接操作map[Kindling - eBPF-based Cloud Native Monitoring tool (harmonycloud.cn)](http://kindling.harmonycloud.cn/)
[网易伏羲私有云基于eBPF的云原生网络可观测性探索与实践_开源_石钟浩_InfoQ精选文章](https://www.infoq.cn/article/2U1Hj6PwavlgjAsEdSCU)
近期在思考eBPF性能监控程序的开发的架构问题:
遂阅读了国内互联网公司对eBPF在APM方面的应用。“kindling是一款基于 eBPF 的云原生可观测性开源工具,旨在帮助用户更好、更快地定界(triage)云原生系统故障。通过 kindling,用户可以快速定界问题类型,比如是应用代码问题还是基础设施问题。如果是代码问题,可以借助 APM(Application Performance Monitoring 应用性能监控)监控进一步排查问题;如果是基础设施问题,那么通过分析来自内核的相关监控指标,定位故障点。”
博客(发布日期2023-03-14)以及官方文档中展示的kindling架构:
通过架构图,可以得出:
Data Receiver
,后面流向Kafka、Analyzer、ES…等组件,最后由web前端进行展示。通过阅读源码以及文档,学习真实项目中如何写eBPF程序:
eBPF开发库选型
drivers程序源码阅读:
[agent-libs/driver at kindling-dev · KindlingProject/agent-libs (github.com)](https://github.com/KindlingProject/agent-libs/tree/kindling-dev/driver)
forked from [falcosecurity/libs](https://github.com/falcosecurity/libs)
driver/bpf/maps.h
文件中,定义了全局所有的map,包括perf_map
、cpu_records
等等。
perf_map
的提交操作被封装在了driver/bpf/ring_helpers.h
的push_evt_frame
函数中
//https://github.com/KindlingProject/agent-libs/blob/e6f73c5fa3e37073cf811a42782cc40591a422f6/driver/bpf/maps.h#L24
struct bpf_map_def __bpf_section("maps") perf_map = {
.type = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
.key_size = sizeof(u32),
.value_size = sizeof(u32),
.max_entries = 0,
};
//https://github.com/KindlingProject/agent-libs/blob/e6f73c5fa3e37073cf811a42782cc40591a422f6/driver/bpf/ring_helpers.h#L45C6-L45C6
static __always_inline int push_evt_frame(void *ctx,
struct filler_data *data)
{
...
int res = bpf_perf_event_output(ctx,
&perf_map,
BPF_F_CURRENT_CPU,
data->buf,
((data->state->tail_ctx.len - 1) & SCRATCH_SIZE_MAX) + 1);//perf_map的唯一操作
...
if (res == -ENOENT || res == -EOPNOTSUPP) {
...
bpf_printk("detected hotplug event, cpu=%d\n", state->hotplug_cpu);
}
...
return PPM_SUCCESS;
}
//https://github.com/KindlingProject/agent-libs/blob/e6f73c5fa3e37073cf811a42782cc40591a422f6/driver/bpf/fillers.h#L5185C37-L5185C37
static __always_inline int bpf_cpu_analysis(void *ctx, u32 tid)
{
struct filler_data data;
int res;
res = init_filler_data(ctx, &data, false);
...
if (res == PPM_SUCCESS)
res = push_evt_frame(ctx, &data);//在逻辑函数中调用push_event
...
return 0;
}
通过map的梳理,能够看出:1. 整个内核态的eBPF程序较为复杂,所有组件协作对外提供一套完整的功能。2.各模块有较为独立的封装,除了map模块,还有filler等一系列功能模块。
总结:
后续在开发自己的ebpf程序时,需要注意以上问题。
[uapi - include/uapi - Linux source code (v5.15) - Bootlin](https://elixir.bootlin.com/linux/v5.15/source/include/uapi)
在Linux内核中,userspace api是一组函数和系统调用,用于在用户空间(userspace)和内核空间(kernelspace)之间进行通信。通过这些api,用户空间的程序可以请求内核执行某些操作,例如读取或写入文件、创建或删除进程等。
内核源码中的/include/uapi/
路径下。当内核被编译时,uapi头文件会被放置在/usr/include
目录或其他指定的目录中,以便用户空间程序可以包含它们。
例如,编写一个C程序来使用open()和read()系统调用,可以包含以下头文件:
#include
#include
这些头文件实际上是从内核源代码中的uapi目录中提取出来的。在编译时,编译器将查找这些头文件,并将它们包含在最终的可执行文件中。
通过diff
命令比较/usr/src/linux-headers-5.15.0-76/include/uapi/linux/bpf.h
&/usr/include/linux/bpf.h
可以看出,两头文件除了开头结尾的预防式声明宏定义不同,其他都一样。而后者就是在编写eBPF程序时使用的
头文件。
通过查阅资料,uapi路径是linux Kernel3.7中引入的:一个是解决 Linux Kernel 里的交叉引用;另外一个就是方便用户态的开发者,可以简单的查看 uapi 里的代码变化来确定 Linux Kernel 是否改变了系统 API。
在BCC中,有很多地方引入了uapi路径下的头文件:
由BCC的安装、使用指南可知,BCC的运行依赖于内核头文件。
# BCC官方文档中展示如何安装bcc工具
sudo apt-get install bpfcc-tools linux-headers-$(uname -r)