迫于Linux eBPF文档过少,我边学习边把对其的理解记录下来,供后来者参考。
本文是eBPF系列的第四篇:eBPF与CO-RE。
- 若对Linux tracing技术不清晰,可参考前置篇the Overview of Linux Tracing Tools
- 若对eBPF的工作流程不清晰,可参考eBPF系列一:Hello eBPF
- 篇二提供了一个采集系统调用openat2参数信息的例子,参见eBPF系列二:例子——openat2
- 篇三eBPF系列三:eBPF map展示了eBPF map如何使用
Introduction
bcc(BPF Compiler Collection)[1]能够简化eBPF程序的开发。但使用bcc编写的eBPF代码运行时编译,不仅需要kernel header,而且需携带llvm/clang相关的二进制。此外,因kernel struct的变更导致memory layout产生了变化,无法令编译生成的eBPF二进制运行在任意版本Linux kernel中,这就无法将eBPF二进制与用户态控制程序打包成二进制进行分发。
借助Linux kernel提供的BTF、bpftool与libbpf,能够将eBPF二进制与用户态控制程序封装至单个ELF中,实现CO-RE(Compile once, run everywhere),不必再担心因memory layout的变更导致eBPF二进制不再可用。只要kernel的eBPF功能具备相应的feature,它就能正常地运行在该kernel之上。并且借助BTF,编译时不必需要kernel header。[2][3]
编写能够CO-RE的eBPF代码步骤如下:
- 执行
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
导出当前运行的kernel定义的各类变量类型(v5.2添加该feature) - 在eBPF代码中声明的头文件如下[4]:
1 |
- 使用
bpftool gen skeleton <file> > <file>.skel.h
将编译生成的转换为c文件(v5.6添加该feature) - 在用户态程序代码中
#include "<file>.skel.h"
,
bpftool gen
的man page [5]上提供了一份示例代码。
Note:
- llvm-10才提供了用于eBPF的attribute
__attribute__((preserve_access_index)
[6],clang使用时需添加参数-target bpf -g
- 若eBPF中自定义的struct命名与kernel中的重复(比如
struct msg
),会导致该自定义的struct使用时出现问题
Example
例子代码在这里。
实现了一个例子:追踪系统调用connect()
的使用情况,打印使用者的进程名称目标的IPv4地址与端口。eBPF代码如下:
1 |
|
Linux kernel v5.3介绍了定义eBPF map的新语法,称为BTF-defined maps,格式如ringbuf
的定义,有两点变化:
SEC(maps)
变为SEC(.maps)
- 使用macro
__uint(type, xxx) / __type(key, xxx) / __type(value, xxx) / __uint(max_entries, xxx)
来替代原有的`.type=xxx / .key_size=xxx / .value=xxx / .max_entries=xxx
原有的eBPF map写法被称为legancy mode。
这里使用了eBPF ring buffer,它在声明时仅需要提供type与max_entries信息,其中max_entries必须页对其。其提供了与bpf_perf_event_output()
类似的API bpf_ringbuf_output()
。这里使用了更高效的API组合bpf_ringbuf_reserve() / bpf_ringbuf_submit()
。[7]。
至于SEC("tp/syscalls/sys_enter_connect")
,是SEC("tracepoint/syscalls/sys_enter_connect")
的简写,表明hook点是系统调用connect()
。
用户态控制程序关键代码如下,它能够打印来自eBPF程序的消息:
1 |
|
其中connect_kern__{open_and_load, attach, detach, destroy}()
这些都是由bpftool gen skeleton
自动生成的API,控制着eBPF程序的加载与使用,ring_buffer__{new, poll, free}()
是libbpf使用epoll封装的eBPF ring buffer API。
编译及其结果如下:
1 | $ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h |
Reference
- Linux Plumbers Conference 2018 BPF Microconference: Compile-Once Run-Everywhere BPF Programs?
- Linux Kernel Developers’ bpfconf 2019: BPF CO-RE (Compile Once - Run Everywhere)
- facebookmicrosites: HOWTO: BCC to libbpf conversion
- facebookmicrosites: BPF Portability and CO-RE
- facebookmicrosites: Enhancing the Linux kernel with BTF type information
- Brendan Gregg’s Blog: BPF binaries: BTF, CO-RE, and the future of BPF perf tools
Epilogue
第一次接触eBPF是在19年年中,那时候只觉得这东西看起来好酷炫,有意愿却没有了解。去年9月先是使用bcc写了一个探测IP数据报文的程序,后在12月利用libbpf写了个开发者测试用例,开启了我的eBPF之旅。毕竟使用C写eBPF不如bcc简单,加之测试环境限制,并不能使用完整的libbpf,只能从Linux kernel中剥离libbpf代码使用,在12月份那次编码中栽了各种跟头,遂下决心系统地学习一下eBPF的编译与原理,偶然间看到了CO-RE这个概念,遂有了此系列文章。
eBPF系列文章至此暂告一段落,自我感觉了解了这么多足够入门了。当然还有许多概念并未展现或探究,比如xdp编程、四类bpf()
枚举类型enum bpf_cmd / enum bpf_map_type / enum bpf_prog_type / enum bpf_attach_type / enum bpf_link_type
的区别与联系、eBPF的指令格式与debug,等等等等。迫于时间所限与工作暂时用不到,不能慢慢琢磨了。来日方长,有缘再看:-)