eBPF 概念和基本原理

原文:What is eBPF and How Does it Work?

作者:Virag Mody

大约一年前,有个朋友想要用 Rust 开发一个 EVM Assembler。在他的一再要求之下,我开始帮忙编写单元测试。那时候我还不大了解操作系统的相关知识,只好开始学习一些语法和词法方面的东西。很快这个事情就无以为继了,然而我对操作系统有了一些整体了解。之后他对 eBPF 赞赏有加时,我觉得我的春天又来了。

eBPF 的门槛有点高,有 500 字的小品,也有 Cilium 铺天盖地的文档。我编写本文的目的,是针对这一新技术读者提供一个全面的入门介绍,用户可以以此为基础,进行进一步的探索。后续可以阅读 Linux Weekly NewsBrenden Gregg 的网站 以及 Cilium 文档学习更多相关知识。本文设计的内容包括:

  • eBPF 的用处
  • eBPF 的原理
  • eBPF 的实例
  • 如何开始使用 eBPF

eBPF 的用处

有了 eBPF,无需修改内核,也不用加载内核模块,程序员也能在内核中执行自定义的字节码。eBPF 和内核紧密联系,下面先介绍一些相关的基本概念。

Linux 系统分为内核空间和用户空间。内核空间是操作系统的核心,对所有硬件都具备不受限制的完整的访问能力,例如内存、存储以及 CPU 等。内核既然具备了这样的超级权限,势必需要严加保护,仅允许运行最可靠的代码。而用户空间运行的就是非内核的进程——例如 I/O、文件系统等。这些进程仅能通过内核开放的系统调用,对硬件进行有限的访问。换句话说,用户空间的程序一定要经过内核空间的过滤。

系统调用接口能够满足绝大多数需要,开发者在面对新的硬件、文件系统、网络协议甚至自定义的系统调用时,还是需要更多的弹性的。在不修改内核源码的情况下,用户代码要直接访问硬件怎么办呢?可以使用 Linux 内核模块(LKM)。用户空间一般是需要通过系统调用来访问内核空间,而 LKM 是直接加载到内核的,是内核的一部分。LKM 最有价值的特点之一,就是可以在运行时加载,不用编译内核也不用重启机器。

what is ebpf 1

图 1:LKM 的动态加载和卸载

LKM 非常有用,但是也引入了很多风险。内核和用户空间不同,要进行不同的安全考量。内核空间是为了操作系统内核这样的特权代码准备的。系统调用连接了内核和用户空间,让用户空间能够对硬件进行合适的操作。换个说法,LKM 是能够让内核崩溃的。模块和内核的紧密关系,使得安全和升级成本直线升高。

eBPF 是什么

eBPF 是一个用于访问 Linux 内核服务和硬件的新方法。这一新技术已经用于网络、出错、跟踪以及防火墙等方面。

dtrace 是一个 Solaris 和 BSD 操作系统上的动态跟踪工具,eBPF 受到 dtrace 的启发,原意是设计一个更好的 Linux 跟踪工具。跟 dtrace 不同的是,Linux 无法获取运行中系统的鸟瞰视图,它被系统调用、库调用以及函数所限制。一小撮工程师在 Berkeley Packet Filter(BPF)基础之上,构建一个内核虚拟机级别的包过滤机制,提供了类似 dtrace 的功能。2014 年第一个版本适配了 Linux 3.18,提供的功能相对较少。要使用完整的 eBPF,需要 Linux 4.4 或以上。

what is ebpf 2

上图对 eBPF 架构进行了一个简单的展示。eBPF 程序需要满足一系列的需求,才能被加载到内核。Verifier 中有一万多行代码用来对 eBPF 程序进行检查。Verifier 会遍历对 eBPF 程序在内核中可能的执行路径进行遍历,确保程序能够在不出现导致内核锁定的循环的情况下运行完成。除此之外还有其它必须满足的检查,例如有效的寄存器状态、程序大小以及越界等。安全控制方面,eBPF 和 LKM 是颇有差异的。

如果所有的检查都通过了,eBPF 程序被加载并编译到内核中,并监听特定的信号。该信号以事件的形式出现,会被传递给被加载的 eBPF 程序。一旦被触发,字节码就会根据其中的指令执行并收集信息。

所以 eBPF 到底做了什么?程序员能够在不增加或者修改内核代码的情况下,就能够在 Linux 内核中执行自定义的字节码。虽说还远不能整体取代 LKM,eBPF 程序可以自定义代码来和受保护的硬件资源进行交互,对内核的威胁最小。

eBPF 的机制

前面介绍了 eBPF 的基础架构。这些能力是由多个组件协同实现的,每一种都有自己的复杂度。

eBPF 程序剖析

事件和钩子

eBPF 程序是在内核中被事件触发的。在一些特定的指令被执行时时,这些事件会在钩子处被捕获。钩子被触发就会执行 eBPF 程序,对数据进行捕获和操作。钩子定位的多样性正是 eBPF 的闪光点之一。例如下面几种:

  • 系统调用:当用户空间程序通过系统调用执行内核功能时。
  • 功能的进入和退出:在函数退出之前拦截调用。
  • 网络事件:当接收到数据包时。
  • kprobe 和 uprobe:挂接到内核或用户函数中。

辅助函数

eBPF 程序被触发时,会调用辅助函数。这些特别的函数让 eBPF 能够有访问内存的丰富功能。例如 Helper 能够执行一系列的任务:

  • 在数据表中对键值对进行搜索、更新以及删除。
  • 生成伪随机数。
  • 搜集和标记隧道元数据。
  • 把 eBPF 程序连接起来,这个功能被称为 tail call
  • 执行 Socket 相关任务,例如绑定、获取 Cookie、数据包重定向等。

这些助手函数必须是内核定义的,换句话说,eBPF 程序的调用能力是受到一个白名单限制的。这个名单很长,并且还在持续增长之中。

Map

要在 eBPF 程序和内核以及用户空间之间存储和共享数据,eBPF 需要使用 Map。正如其名,Map 是一种键值对。Map 能够支持多种数据结构,eBPF 程序能够通过辅助函数在 Map 中发送和接收数据。

执行 eBPF 程序

加载和校验

所有 eBPF 程序都是以字节码的形式执行的,因此需要有办法把高级语言编译成这种字节码。eBPF 使用 LLVM 作为后端,前端可以介入任何语言。因为 eBPF 使用 C 编写的,所以前端使用的是 Clang。但在字节码被 Hook 之前,必须通过一系列的检查。在一个类似虚拟机的环境下用内核 Verifier阻止带有循环、权限不正确或者导致崩溃的程序运行。如果程序通过了所有的检查,字节码会使用 bpf() 系统调用被载入到 Hook 上。

JIT 编译器

校验结束后,eBPF 字节码会被 JIT 编译器转译成本地机器码。eBPF 是 64 位编码,共有 11 个寄存器,因此 eBPF 和 x86、ARM 以及 arm64 等硬件都能紧密对接。虽然 eBPF 受到 VM 限制,JIT 过程保障了它的运行性能。

what-is-ebpf-3.png

总结

上面的概念们放在一起,eBPF 程序通过安全检查后插入钩子,被事件触发之后,程序会启动执行,用辅助函数和 Map 来对数据进行存储和操作。下一届我们来研究一下它们的协同方式。

一个例子

在 Gravitational 有一个叫做 Teleport 的开源项目,其中使用了 eBPF 程序进行跟踪和网络操作。有的组织希望知道 SSH 会话中发生了什么,Teleport 提供 SSH 访问途径的同时,加入了对用户行为的记录。可以通过对命令编码、在 Shell 脚本中运行命令的方式来进行混淆,从而阻碍对会话的记录。

Teleport 4.2 中,我们引入了会话记录,其中用了三个 eBPF 程序来获取 SSH 会话,并将其转化为结构化的事件。

例如 echo Y3VybCBodHRwOi8vd3d3LmV4YW1wbGUuY29tCg== | base64 --decode | sh,我们能够在终端抓取到这个命令,但是这并无意义,用户已经对命令进行了混淆,但是有了 eBPF,我们就能知道,用户试图隐藏的是 curl

{
  "event": "session.command",
  "path": "/bin/sh",
  "program": "sh",
  "argv": [],
  "login": "centos",
  "user": "jsmith"
}
{
  "event": "session.command",
  "path": "/bin/base64",
  "program": "base64",
  "argv": [
    "--decode"
  ],
  "login": "centos",
  "user": "jsmith"
}
{
  "event": "session.command",
  "path": "/bin/curl",
    "argv": [
    "http://www.example.com"
  ],
  "program": "curl",
  "return_code": 0,
  "login": "centos",
  "user": "jsmith"
}
{
  "event": "session.network",
  "program": "curl",
  "src_addr": "172.31.43.104",
  "dst_addr": "93.184.216.34",
  "dst_port": 80,
  "login": "centos",
  "user": "jsmith",
  "version": 4
}

借助 eBPF 的能力,我们把这种混淆行为转换为事件流,便于导出和分析。Teleport 用 execsnoopopensnoop 以及 tcpconnect 来恢复这些事件。特别会关注的是 tcpconnect,它最终将信息以 JSON 的形式返回来。

what-is-ebpf-4.png

tcpconnect 会跟踪 TCP 连接。像 Teleport 这样用 SSH 证书管理访问的工具来说,必须要知道 TCP 连接的发起情况。tcpconnect 能跟踪 connect() 系统调用,该调用会在 Socket 上初始化一个连接。要对这个情况进行跟踪,tcpconnect 在内核中插入了一个 kprobe,能够动态进入任何例程:

# initialize
BPF b = BPF(text=bpf_text) b.attach_kprobe(event="tcp_v4_connect", fn_name="trace_connect_entry") b.attach_kretprobe(event="tcp_v4_connect", fn_name="trace_connect_v4_return")

程序被触发以后,tcpconnect 会开始输出信息,下表展示的就是这样的信息:

$ ./tcpconnect
PID   COMM  SADDR             DADDR           DPORT
-----------------------------------------------------
2315  curl  172.31.43.104     93.184.216.34   80

所有这些数据都是用辅助函数收集而来。如果看看 Python 代码,会发现 tcpconnect 试用了来自 bcc 的 BPF 库的辅助函数来对上述输出内容进行格式化。

...
struct ipv4_data_t data4 = {.pid = pid, .ip = ipver}; 
data4.saddr = skp->__sk_common.skc_rcv_saddr; 
data4.daddr = skp->__sk_common.skc_daddr; 
data4.dport = ntohs(dport); 
bpf_get_current_comm(&data4.task, sizeof(data4.task));
...

eBPF 入门

行文至此,我希望读者已经对 eBPF 有了一个最基础的了解,知道了他的重要性以及基本原理。是时候浏览更多技术文档和文章了。本文中提供了不少链接,不过这里最推荐的是 Quinten Monnet 的博客。

自行编写代码,开发自己的 eBPF 可能有点难。但是很多开源的开发工具链正在涌现,简化了很多 eBPF 的相关场景。下面介绍几个最流行的:

  • BCC:BCC 是一个工具包用于创建高效的内核跟踪和处理程序,并包含了很多有用的工具和示例。BCC 简化了 BPF 程序的开发,内核指令使用 C 指令(包含了 LLVM 的封装),前端使用的是 Python 和 LUA。BCC 有很多用途,例如性能分析和网络流量控制。BCC 还为其它程序提供了 API。

  • bpftrace:BPFtrace 是一个高级跟踪语言,用 LLVM 作为后端把脚本编译为 BPF 字节码,并用 BCC 和 Linux BPF 系统进行交互,并支持现有的 Linux 跟踪能力:kprobe、uprobe 以及 tracepoint

  • GoC/C++ 以及 Rust 的通用库。

结论

eBPF 还是个很新鲜的技术,让程序员在不修改内核空间的情况下,能够在内核中执行自定义的字节码并从内核函数中获取更多信息。原本这些目标需要通过系统调用或内核模块来完成,eBPF 降低了所需的复杂度和危险性。简单来说,eBPF 的工作流程:

  • 把 eBPF 程序编译成字节码。
  • 在载入到 Hook 之前,在虚拟机中对程序进行校验。
  • 把程序附加到内核之中,被特定事件触发。
  • JIT 编译。
  • 在程序被触发时,调用辅助函数处理数据。
  • 在用户空间和内核空间之间使用键值对共享数据。

推荐阅读

相关链接

  • What is eBPF and How Does it Work?https://gravitational.com/blog/what-is-ebpf/
  • Virag Modyhttps://www.linkedin.com/in/virag-mody-650974a9
  • EVM Assemblerhttps://medium.com/mycrypto/the-ethereum-virtual-machine-how-does-it-work-9abac2b7c9e
  • Ciliumhttps://cilium.io/
  • Linux Weekly Newshttps://lwn.net/Articles/740157/
  • Brenden Gregg 的网站http://www.brendangregg.com/index.html
  • Cilium 文档https://docs.cilium.io/en/stable/bpf/
  • Linux 内核模块(LKM)https://tldp.org/LDP/lkmpg/2.6/html/lkmpg.html
  • what is ebpf 1image/what-is-ebpf-1.png
  • what is ebpf 2images/what-is-ebpf-2.png
  • Verifierhttps://github.com/torvalds/linux/blob/master/kernel/bpf/verifier.c
  • 名单https://man7.org/linux/man-pages/man7/bpf-helpers.7.html
  • LLVMhttps://llvm.org/
  • Clanghttps://clang.llvm.org/
  • 内核 Verifierhttps://elixir.bootlin.com/linux/latest/source/kernel/bpf/verifier.c
  • what-is-ebpf-3.pngimages/what-is-ebpf-3.png
  • Teleporthttps://gravitational.com/teleport
  • Teleport 4.2https://gravitational.com/blog/teleport-release-4-2
  • what-is-ebpf-4.pngimages/what-is-ebpf-4.png
  • Python 代码https://github.com/iovisor/bcc/blob/ec3747ed6b16f9eec36a204dfbe3506d3778dcb4/tools/tcpconnect.py
  • bcc 的 BPF 库https://github.com/iovisor/bcc/blob/master/src/cc/export/helpers.h
  • Quinten Monnethttps://qmonnet.github.io/whirl-offload/2016/09/01/dive-into-bpf/
  • BCChttps://github.com/iovisor/bcc
  • bpftracehttps://github.com/ajor/bpftrace
  • Gohttps://github.com/iovisor/gobpf
  • C/C++https://github.com/libbpf/libbpf
  • Rusthttps://github.com/redsift/redbpf
  • SCP - Familiar, Simple, Insecure, and Slowhttps://gravitational.com/blog/scp-familiar-simple-insecure-slow/
  • Greed is Good: Creating Teleport’s Discovery Protocolhttps://gravitational.com/blog/teleport-discovery-protocol/
  • Gracefully Restarting a Go Program Without Downtimehttps://gravitational.com/blog/golang-ssh-bastion-graceful-restarts/
Avatar
崔秀龙

简单,是大师的责任;我们凡夫俗子,能做到清楚就很不容易了。

comments powered by Disqus
上一页