0%

eBPF初见

更高,更快,更强

0x0 前情提要

毕业工作后,eBPF技术是我在工作中学习并成功使用的一项技术,成功使用这个技术让我非常有成就感。我认为这项技术大有可为,在此与大家分享。

eBPF并不是一个很简单的东西,学习起来会遇到坑,会吃力,但是我觉得,当一个人离开他的舒适区的时候,就意味着他的舒适区变大了。

本文作为eBPF系列文章的开头,主要简单给大家介绍eBPF技术,相关内容也是取自网上。

0x1 什么是eBPF

eBPF全称是extended Berkeley Packet Filter,它的前身是cBPF(classic BPF),诞生于伯克利大学。在1992年时,Steven McCanne和Van Jacobson在一篇论文中提出了BPF技术,这种技术可以在Unix内核中实现网络数据包过滤,并且其性能比当时最快的数据包过滤技术还快20倍,我们工作中常用的tcpdump工具中的libpcap就是基于BPF技术的。

cBPF可以在内核协议栈中进行高效的数据包处理操作。由于运行在内核中,避免了用户空间和内核空间的频繁切换,从而提升了性能。

但是由于BPF指令集的限制和功能的局限性,它无法满足更加复杂和灵活的数据包处理的需要。

eBPF的概念应该是在Linux 3.15 ~ 3.18版本引入的,由于网上有说3.15也有说3.18,我也不知道哪个是正确的。

eBPF的到来是BPF技术的一个转折点,以往BPF只能针对网络数据包进行处理,而eBPF变成了内核里的一个非常强大的子系统,可以实现包括网络监控、安全过滤、性能分析等功能。

并且,eBPF的出现让内核功能扩展变得更加的方便。

以往如果需要给内核添加功能,往往需要完全重新编译内核代码。而eBPF技术,允许用户将需要添加的功能编译为bytecode,由eBPF的加载器通过JIT翻译成机器语言加载到内核中,在不用重新编译内核代码的情况下,将功能添加到内核中。

0x2 eBPF有什么实际应用

  • 网络性能优化:eBPF可以实现自定义的TCP拥塞控制,网络流量压缩等功能,基于eBPF的XDP技术提供了超高性能的网络通道
  • 网络监控:在内核态收集并分析网络数据包,可以帮助分析网络流量
  • 安全审计:eBPF可以监控Linux内核函数的调用,检测并记录恶意软件或攻击行为

0x3 学习和使用eBPF技术需要什么

对于个人来说:

  1. 对于计算机操作系统知识要有一定的了解
  2. 需要了解eBPF技术的基本概念和原理,了解eBPF运行的流程
  3. 必须会使用C语言,可选go、python
  4. 不要怕失败,主动在网上搜寻答案的心

对于使用eBPF,需要满足包括但不限于以下的硬件环境

  1. 支持eBPF技术的Linux内核版本(一般来说现在发行版的Linux内核都是默认开启该功能的支持的)
    • Ubuntu 20.10+
    • Fedora 31+
    • RHEL 8.2+
    • Debian 11+
  2. root权限,一般情况下,加载eBPF程序进入内核需要root权限
  3. 开发过程中,你可能会需要使用到内核头文件

0x4 eBPF技术架构

eBPF技术架构

这张图总体是比较直观的,对于开发人员来说,首先我们需要用C语言编写一份eBPF的程序,程序代码经过Clang/LLVM 编译器后生成 eBPF bytecode字节码,然后我们需要用root权限,将eBPF bytecode加载进内核,一般来说,会有一个 verifier验证器 存在,这个验证器主要是为了验证我们编写的程序是否有非法的内存访问,防止我们的程序把内核搞崩了。

如果验证器觉得我们编写的应用没有毛病,就会通过JIT编译器将eBPF字节码转换为我们机器可以执行的代码。

图中的MAPS是用于存放数据的,这是一个内核态和用户态都可以访问的内存空间,最终用户态的程序可以从MAPS里获取到内核中eBPF程序运行的数据,并进行结果输出。

0x5 eBPF CO-RE特性

eBPF技术紧密与内核相关,但是eBPF技术的开发者们不希望eBPF像cBPF一样受限于内核版本,因为Linux的API不稳定,不同版本之间可能会有很大的差别。希望eBPF开发编译一次后,能在不同的内核版本上直接运行。

所以eBPF提出了CO-RE的概念(Compile Once - Run Everywhere),要使用CO-RE特性需要Linux内核支持BTF 特性,从而eBPF程序在运行并访问内核中的结构体时,可以根据BTF提供的信息自动实现内存地址的偏移映射,从而实现一个eBPF程序可以在多个不同内核版本的机器上正常运行。

这个特性我暂时没有成功实践,目前大概知道需要以下的一些前提条件

  • Linux内核编译时配置支持暴露BTF格式的数据结构
  • Clang编译器编译eBPF程序时,将对内核数据结构的访问记录以及重定位信息保存在ELF文件的section中
  • BPF Loader程序,可以在加载的时候通过读取内核BTF,和eBPF的重定位信息来修正访问的信息,完成最终的重定位。
  • libbpf支持对eBPF暴露Kconfig或者配置struct flavor机制来兼容不同的内核数据结构改名或含义不同的情况

0x6 eBPF开发框架推荐

eBPF技术的学习曲线确实比较陡峭,个人推荐两个库,可以让学习eBPF更轻松

1. BCC (BPF Compiler Collection)

项目地址:https://github.com/iovisor/bcc
这套工具集合了eBPF开发需要的很多工具包,并且支持通过Python开发。下面放一段简单的代码,每当系统内核调用execve函数时,就会在控制台打印Hello World

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from bcc import BPF

program = """
int hello(void *ctx) {
bpf_trace_printk("Hello World!");
return 0;
}
"""

# 创建一个待绑定的函数 hello,后面我们会将它绑定到一个内核函数的执行上
b = BPF(text=program)

# 调用helper function获取函数名对应的系统调用
syscall = b.get_syscall_fnname("execve")
# 进行kprobe绑定
b.attach_kprobe(event=syscall, fn_name="hello")
# 打印eBPF程序的执行情况
b.trace_print()

代码摘自https://www.torch-fan.site/2023/04/11/eBPF使用/

这段代码中,首先通过bcc工具引入BPF

1
2
3
4
5
6
program = """
int hello(void *ctx) {
bpf_trace_printk("Hello World!");
return 0;
}
"""

然后program定义为了一串字符串,里面的内容就是c语言的eBPF代码,然后通过BCC库中BPF方法,将C代码即时编译并加载进内核,attach_kprobe方法指定了自定义eBPF程序在内核中挂载的位置,对于execve系统调用事件来说,每当这个系统函数被执行的时候,我们自定义挂载上去的eBPF程序也会被顺带一起执行。

ebpf-go库

项目地址:https://github.com/cilium/ebpf
这个库是纯go语言的,对于本身熟悉go语言的人来说会更友好。


不知道今天的内容能不能让大家对eBPF技术有一个简单的了解,后续我会基于一些实际的应用场景和我踩过的坑推出具体的分享。