ebpf深入理解和应用介绍


1. ebpf概述

1.1 ebpf发展历史

bpf
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诞生的目的

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 的指令。

ebpf

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程序类型决定了四件事:

  1. 可以在何处附加程序
  2. 可以调用验证程序的内核内辅助函数
  3. 是否可以直接访问网络数据包
  4. 以及作为第一个传递的对象的类型该程序的参数。

实际上,程序类型本质上定义了一个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已经涌现了一大批应用。
ebpf milestones
下面是一些bpf program type
ebpf program type

3.1 XDP

xdp

3.2 IDS

ebpf IDS

3.3 容器安全

ebpf cilium

3.4 调试

ebpf 调试

参考

  1. 深入理解 BPF:一个阅读清单
  2. eBPF 简史
  3. 几句话说清楚20:eBPF的机制
  4. 看字节跳动容器化场景下,如何实现性能优化?
  5. 实现一个基于XDP/eBPF的学习型网桥