1. ebpf概述
1.1 ebpf发展历史
BPF,及伯克利包过滤器Berkeley Packet Filter,最初构想提出于 1992 年,其目的是为了提供一种过滤包的方法,并且要避免从内核空间到用户空间的无用的数据包复制行为。它最初是由从用户空间注入到内核的一个简单的字节码构成,它在那个位置利用一个校验器进行检查 —— 以避免内核崩溃或者安全问题 —— 并附着到一个套接字上,接着在每个接收到的包上运行。几年后它被移植到 Linux 上,并且应用于一小部分应用程序上(例如,tcpdump)。其简化的语言以及存在于内核中的即时编译器(JIT),使 BPF 成为一个性能卓越的工具。
在 2013 年,Alexei Starovoitov 对 BPF 进行彻底地改造,并增加了新的功能,改善了它的性能。这个新版本被命名为 eBPF (意思是 “extended BPF”),与此同时,将以前的 BPF 变成 cBPF(意思是 “classic” BPF)。新版本出现了如映射和尾调用tail call这样的新特性,并且 JIT 编译器也被重写了。新的语言比 cBPF 更接近于原生机器语言。并且,在内核中创建了新的附着点。
1.2 ebpf诞生的目的
用Linux Kernel Module来做一个类比说明eBPF诞生的目的。
Kernel Module的主要目的就是让用户可以通过这种机制,实现对内核的“赋能”,动态添加一些内核本身不支持的功能,比如硬件的驱动能力,新的文件系统或是系统调用。当然也可以融合到现有的内核处理流程中,比如在netfilter的某个hook点中添加包处理方法等。
Kernel Module的优点:
- 动态添加/删除,无需重新编译内核
- 减小内核体积
缺点:
- 一旦出现BUG可能导致内核直接崩溃
- 增加内核攻击面,影响内核安全
eBPF要做的事情也非常类似,但它想要克服Kernel Module的缺点,即确保执行的代码绝对安全。
为了达到这一目的,eBPF在内核中实现了一个虚拟机执行用户的指令。与Kernel Module直接在真实的物理硬件上执行用户的指令不同,eBPF提供给用户一个虚拟的RISC处理器,以及一组相关的指令。用户可以直接用这组指令编写程序。同时,程序在下发到该虚拟机之前也会经过eBPF的检查,比如会不会进入无限循环,会不会访问不合法的内存地址等等。只有在通过检查之后才可以进入执行的环节。
对eBPF来说,和Kernle Module一样,也是通过特定的Hook点监听内核中的特定事件,进而执行用户定义的处理。这些Hook点包括:
- 静态tracepoint
- 动态内核态探针(Dynamic Kernel probes)
- 动态用户态探针(Dynamic User Probes)
- 其他hook点
针对主要是监控、跟踪使用的eBPF应用来说,主要通过这种方式取得内核运行时的一些参数和统计信息。例如,系统调用的参数值、返回值,通过eBPF map将得到的信息送给用户态程序,进而在用户态完成后处理流程。
另外一类应用则直接在一些内核处理流程中加入自己的处理逻辑,例如XDP,就是在网卡驱动和内核协议栈之间插入了eBPF扩展的网包过滤、转发功能。
2. ebpf深入理解
参考:全面介绍eBPF
eBPF 是一个在内核中运行的虚拟机,它可以去运行用户。在用户态实现的这种 eBPF 的代码,在内核以本地代码的形式和速度去执行,它可以跟内核的 Trace 系统相结合,给我们提供了几乎无限的可观测性。
eBPF 的基本原理——它所有的接口都是通过 BPF 系统调用来跟内核进行交互,eBPF 程序通过 LVM 和 Cline 进行编译,产生 eBPF 的字节码,通过 BPF 系统调用,加载到内核,验证代码的安全性,从而通过 JIT 实时的转化成 Native 的 X86 的指令。
2.1 eBPF内核验证程序
允许用户空间代码在内核中运行存在固有的安全性和稳定性风险。因此,在加载每个eBPF程序之前,会对它们进行大量检查。
第一个测试确保eBPF程序终止,并且不包含任何可能导致内核锁定的循环。这是通过对程序的控制流程图(CFG)进行深度优先搜索来检查的。严格禁止无法到达的指令;包含无法访问的指令的任何程序都将无法加载。
第二阶段涉及更多内容,并且要求验证程序一次模拟一次eBPF程序的执行。在执行每条指令之前和之后都要检查虚拟机状态,以确保寄存器和堆栈状态有效。禁止跳越边界,访问超范围数据也是如此。
最后,验证者使用eBPF程序类型来限制可以从eBPF程序调用哪些内核功能以及可以访问哪些数据结构。例如,某些程序类型被允许直接访问网络数据包数据。
2.2 bpf()系统调用
我们可以通过bpf()系统调用加载程序:
1 2 3 4 5 6 | /* cmd:BPF_PROG_LOAD bpf_attr:允许数据可以在内核和用户空间传输 size:bpf_attr的长度 */ int bpf(int cmd, union bpf_attr *attr, unsigned int size); |
2.3 eBPF程序类型
BPF_PROG_LOAD的BPF程序类型决定了四件事:
- 可以在何处附加程序
- 可以调用验证程序的内核内辅助函数
- 是否可以直接访问网络数据包
- 以及作为第一个传递的对象的类型该程序的参数。
实际上,程序类型本质上定义了一个API,内核支持的当前eBPF程序类型集为:
1 2 3 4 5 6 7 8 9 10 11 12 13 | BPF_PROG_TYPE_SOCKET_FILTER:网络数据包过滤器 BPF_PROG_TYPE_KPROBE:确定是否应触发kprobe BPF_PROG_TYPE_SCHED_CLS:网络流量控制分类器 BPF_PROG_TYPE_SCHED_ACT:网络流量控制操作 BPF_PROG_TYPE_TRACEPOINT:确定是否应触发跟踪点 BPF_PROG_TYPE_XDP:从设备驱动程序接收路径运行的网络数据包筛选器 BPF_PROG_TYPE_PERF_EVENT:确定是否应该触发性能事件处理程序 BPF_PROG_TYPE_CGROUP_SKB:用于控制组的网络数据包过滤器 BPF_PROG_TYPE_CGROUP_SOCK:用于控制组的网络数据包筛选器,允许修改套接字选项 BPF_PROG_TYPE_LWT_ *:用于轻型隧道的网络数据包过滤器 BPF_PROG_TYPE_SOCK_OPS:用于设置套接字参数的程序 BPF_PROG_TYPE_SK_SKB:网络数据包过滤器,用于在套接字之间转发数据包 BPF_PROG_CGROUP_DEVICE:确定是否应该允许设备操作 |
2.4 eBPF数据结构
eBPF程序的主要数据结构是eBPF map,一种key-value数据结构。Maps通过bpf()系统调用创建和操作。
有不同类型的Map:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | BPF_MAP_TYPE_HASH:哈希表 BPF_MAP_TYPE_ARRAY:数组映射,已针对快速查找速度进行了优化,通常用于计数器 BPF_MAP_TYPE_PROG_ARRAY:对应eBPF程序的文件描述符数组;用于实现跳转表和子程序以处理特定的数据包协议 BPF_MAP_TYPE_PERCPU_ARRAY:每个CPU的阵列,用于实现延迟的直方图 BPF_MAP_TYPE_PERF_EVENT_ARRAY:存储指向struct perf_event的指针,用于读取和存储perf事件计数器 BPF_MAP_TYPE_CGROUP_ARRAY:存储指向控制组的指针 BPF_MAP_TYPE_PERCPU_HASH:每个CPU的哈希表 BPF_MAP_TYPE_LRU_HASH:仅保留最近使用项目的哈希表 BPF_MAP_TYPE_LRU_PERCPU_HASH:每个CPU的哈希表,仅保留最近使用的项目 BPF_MAP_TYPE_LPM_TRIE:最长前缀匹配树,适用于将IP地址匹配到某个范围 BPF_MAP_TYPE_STACK_TRACE:存储堆栈跟踪 BPF_MAP_TYPE_ARRAY_OF_MAPS:地图中地图数据结构 BPF_MAP_TYPE_HASH_OF_MAPS:地图中地图数据结构 BPF_MAP_TYPE_DEVICE_MAP:用于存储和查找网络设备引用 BPF_MAP_TYPE_SOCKET_MAP:存储和查找套接字,并允许使用BPF辅助函数进行套接字重定向 |
可以使用bpf_map_lookup_elem()和 bpf_map_update_elem()函数从eBPF或用户空间程序访问所有Map.
2.5 如何用C编写eBPF程序
利用高级语言书写 BPF 逻辑并经由编译器生成出伪代码来并不是什么新鲜的尝试,比如 libpcap 就是在代码中内嵌了一个小型编译器来分析 tcpdump 传入的 filter expression 从而生成 BPF 伪码的。只不过长久以来该功能一直没有能被独立出来或者做大做强,究其原因,主要还是由于传统的 BPF 所辖领域狭窄,过滤机制也不甚复杂,就算是做的出来,估计也不堪大用。
然而到了 eBPF 的时代,情况终于发生了变化:现行的伪指令集较之过去已经复杂太多,再用纯汇编的开发方式已经不合时宜,于是,自然而然的,利用 C 一类的高级语言书写 BPF 伪代码的呼声便逐渐高涨了起来。
目前,支持生成 BPF 伪代码的编译器只有 llvm 一家,即使是通篇使用 gcc 编译的 Linux 内核,samples 目录下的 bpf 范例也要借用 llvm 来编译完成。
2.6 BCC(BPF Compiler Collection)
虽然现在可以用 C 来实现 BPF,但编译出来的却仍然是 ELF 文件,开发者需要手动析出真正可以注入内核的代码。这部分工作多少有些麻烦,如果可以有一个通用的方案一步到位的生成出 BPF 代码就好了。
于是就有人设计了 BPF Compiler Collection(BCC),BCC 是一个 python 库,但是其中有很大一部分的实现是基于 C 和 C++的,python 只不过实现了对 BCC 应用层接口的封装而已。
使用 BCC 进行 BPF 的开发仍然需要开发者自行利用 C 来设计 BPF 程序——但也仅此而已,余下的工作,包括编译、解析 ELF、加载 BPF 代码块以及创建 map 等等基本可以由 BCC 一力承担,无需多劳开发者费心。
3. ebpf应用
利用ebpf已经涌现了一大批应用。
下面是一些bpf program type
3.1 XDP
3.2 IDS
3.3 容器安全
3.4 调试
参考
- 深入理解 BPF:一个阅读清单
- eBPF 简史
- 几句话说清楚20:eBPF的机制
- 看字节跳动容器化场景下,如何实现性能优化?
- 实现一个基于XDP/eBPF的学习型网桥