什么是 ebpf

eBPF是一项革命性的技术,起源于Linux内核,它可以在特权上下文中(如操作系统内核)运行沙盒程序。 它用于安全有效地扩展内核的功能,而无需更改内核源代码或加载内核模块。

从历史上看,由于内核具有监督和控制整个系统的特权,操作系统一直是实现可观察性、安全性和网络功能的理想场所。

同时,由于操作系统内核的核心地位和对稳定性和安全性的高要求,操作系统内核很难发展。因此,与在操作系统之外实现的功能相比,操作系统级别的创新速度传统上要低一些。

eBPF 从根本上改变了这个公式。它允许沙箱程序在操作系统中运行,这意味着应用程序开发人员可以在运行时中运行 eBPF 程序向操作系统添加额外的功能。

然后,操作系统保证安全性和执行效率,就像在即时(JIT)编译器和验证引擎的帮助下进行了本地编译一样。这导致了一波基于ebpf的项目,涵盖了广泛的用例,包括下一代网络、可观察性和安全功能。

今天,eBPF被广泛用于驱动各种用例:在现代数据中心和云原生环境中提供高性能网络和负载平衡,以低开销提取细粒度的安全可观察性数据,帮助应用程序开发人员跟踪应用程序,提供性能故障排除的见解,预防性应用程序和容器运行时安全性强制执行,等等。可能性是无限的,eBPF 正在开启的创新才刚刚开始。

ebpf.io

eBPF.io是每个人就 eBPF 主题进行学习和协作的地方。eBPF 是一个开放的社区,每个人都可以参与和分享。无论您是想阅读 eBPF 的第一个介绍,寻找进一步的阅读材料,还是迈出成为主要 eBPF 项目贡献者的第一步.

ebpf 和 BPF 代表什么

BPF 最初代表伯克利包过滤,但是现在 eBPF(扩展 BPF)可以做的不仅仅是包过滤,这个缩写不再有意义了。eBPF 现在被认为是一个独立的术语,不代表任何东西。在 Linux 源代码中,术语 BPF 持续存在,在工具和文档中,术语 BPF 和eBPF 通常可以互换使用。最初的 BPF 有时被称为 cBPF(经典 BPF),以区别于eBPF。

1. 介绍 eBPF

下面的章节是对eBPF的快速介绍。如果您想了解更多关于 eBPF 的信息,请参阅 eBPF & XDP Reference Guide.

1.1 关于 hook(钩子)

eBPF 程序是事件驱动的,当内核或应用程序通过某个钩子点时运行。预定义的钩子包括系统调用、函数入口/退出、内核跟踪点、网络事件等。

如果没有针对特定需求的预定义钩子,则可以创建内核探针(kprobe)或用户探针(uprobe),以便在内核或用户应用程序的几乎任何位置附加 eBPF 程序。

1.2 eBPF程序是如何编写的?

在很多情况下,eBPF 不是直接使用,而是通过 Cilium、bcc 或 bpftrace 等项目间接使用,这些项目提供了 eBPF 之上的抽象,不需要直接编写程序,而是提供了特定意图的定义的能力,然后用 eBPF 实现。

如果不存在更高层次的抽象,则需要直接编写程序。Linux 内核期望 eBPF 程序以字节码的形式加载。虽然直接编写字节码当然是可能的,但更常见的开发实践是利用像 LLVM 这样的编译器套件将伪 c 代码编译成 eBPF 字节码。

1.3 加载器和校验器架构

确定所需的钩子(函数|位置)后,可以使用 bpf 系统调用将 eBPF 程序加载到Linux 内核中。这通常是使用一个可用的 eBPF 库来完成的。下面将介绍可用的开发工具链。

当程序被加载到Linux内核中时,它在被附加到所请求的钩子上之前要经过两个步骤:

1.3.1 校验

验证步骤确保 eBPF 程序可以安全运行。它验证程序是否满足几个条件,例如:

  • 加载 eBPF 程序的进程拥有所需的功能(特权)。除非启用非特权 eBPF,否则只有特权进程才能加载 eBPF 程序。
  • 该程序不会崩溃或以其他方式损害系统。
  • 程序总是运行到完成(即程序不会永远处于循环中,从而阻碍进一步的处理)。

1.3.2 JIT 编译

JIT (Just-in-Time)编译步骤将程序的通用字节码转换为特定于机器的指令集,以优化程序的执行速度。这使得eBPF程序像本地编译的内核代码或作为内核模块加载的代码一样有效地运行。

1.3.3 Maps

eBPF 程序的一个重要方面是共享收集的信息和存储状态的能力。为此目的,eBPF程序可以利用 eBPF 映射的概念来存储和检索各种数据结构中的数据。eBPF 映射既可以从 eBPF 程序访问,也可以通过系统调用从用户空间中的应用程序访问。

下面是支持的映射类型的不完整列表,以帮助理解数据结构的多样性。对于各种映射类型,可以使用共享类型或者每个 cpu 的粒度的数据。

  • Hash tables, Arrays
  • LRU (Least Recently Used)
  • Ring Buffer
  • Stack Trace
  • LPM (Longest Prefix match)
  • ..

1.3.4 Helper 方法调用

eBPF 程序不能调用任意的内核函数。允许这样做会将 eBPF 程序绑定到特定的内核版本,并且会使程序的兼容性复杂化。相反,eBPF 程序可以将函数调用转换为辅助函数,这是内核提供的一种众所周知且稳定的 API。

可用的 helper 调用集在不断发展。可用的 helper 用示例:

  • Generate random numbers
  • Get current time & date
  • eBPF map access
  • Get process/cgroup context
  • Manipulate network packets and forwarding logic

1.3.4 Tail & Function Calls

eBPF 程序可与尾部和函数调用的概念组合。函数调用允许在 eBPF 程序中定义和调用函数。尾部调用可以调用和执行另一个 eBPF 程序并替换执行上下文,类似于 execve() 系统调用对常规进程的操作方式。

2. eBPF Safety

权力越大,责任越大。

eBPF 是一项非常强大的技术,现在运行在许多关键软件基础设施组件的核心。在eBPF 的开发过程中,当考虑将 eBPF 包含到 Linux 内核中时,eBPF 的安全性是最关键的方面。eBPF 的安全性通过以下几层来保证:

2.1 Required Privileges (特权控制)

除非启用了非特权 eBPF,否则所有打算将 eBPF 程序加载到 Linux 内核中的进程必须以特权模式(root)运行,或者需要 CAP_BPF 功能。这意味着不受信任的程序不能加载 eBPF 程序。

如果启用了非特权 eBPF,则非特权进程可以加载某些 eBPF 程序,这些程序的功能集减少,并且对内核的访问受限。

2.2 Verifier (校验)

如果一个进程被允许加载一个 eBPF 程序,那么所有的程序仍然要通过 eBPF 验证器。eBPF 验证器确保程序本身的安全性。这意味着,例如:

  • 程序经过验证以确保它们始终运行到完成,例如 eBPF 程序可能永远不会阻塞或永远处于循环中。eBPF 程序可能包含所谓的有界循环,但只有当验证者能够确保循环包含保证为真的退出条件时,程序才被接受。
  • 程序不能使用任何未初始化的变量或越界访问内存。
  • 程序必须符合系统的大小要求。不可能加载任意大的 eBPF 程序。
  • 程序必须具有有限的复杂性。验证者将评估所有可能的执行路径,并且必须能够在配置的最高复杂性限制范围内完成分析。

验证器是一种安全工具,用于检查程序是否可以安全运行。它不是一个检查程序正在做什么的安全工具。

4. Hardening (强化)

内核领域,hardening 指通过一系列配置、打补丁与编译 / 运行时强化措施,最小化内核攻击面、降低可利用性,提升其面对漏洞与攻击时的抗攻击性与稳健性。

常见技术与措施:

  • 及时补丁与热修复:第一时间修补内核漏洞,支持不重启更新(如 live patching)。
  • 内存与控制流保护:ASLR、stack canary、CFI 等,增加利用难度。
  • 访问控制与强制访问控制:POSIX capabilities、SELinux/AppArmor(LSM)等,实现最小权限。
  • 系统调用与边界限制:seccomp、冻结 / 删除不必要系统调用、限制 ptrace 等。
  • 容器 / 命名空间隔离:namespaces、cgroups、seccomp 等,隔离进程与资源。
  • 编译期加固:Pax/grsecurity 等加固补丁,强化栈 / 堆 / 符号防护。
  • 启动与信任链:可信引导、度量与验证,确保内核与模块来源可信。

结论:内核 hardening 是围绕 "最小攻击面 + 最大化利用难度" 的系统化工程,通过配置、补丁、编译与运行时防护,提升内核安全性与抗攻击性。

在成功完成验证后,eBPF 程序将根据程序是从特权进程还是非特权进程加载而运行一个强化过程。这一步包括:

  • Program execution protection: 保存 eBPF 程序的内核内存受到保护并设置为只读。如果出于任何原因,无论是内核错误还是恶意操作,试图修改 eBPF 程序,内核将崩溃,而不是允许它继续执行损坏/被操纵的程序。

  • Mitigation against Spectre:有观点认为,中央处理器(CPU)可能会错误预测分支指令,并产生可观察到的副作用,这些副作用可以通过侧信道提取出来。举几个例子:eBPF 程序会屏蔽内存访问,以便在临时指令下将访问重定向到受控区域;验证器还会跟踪只有在推测执行下才可访问的程序路径;而即时编译器(JIT 编译器)会在尾调用无法转换为直接调用的情况下生成返回指针。

  • Constant blinding: 代码中的所有常量都经过了混淆处理,以防止 JIT spray 攻击。这可以阻止攻击者将可执行代码注入常量,因为如果存在其他内核漏洞,攻击者可能会借此跳转到 eBPF 程序的内存区域来执行代码。

5. Abstracted Runtime Context

  • eBPF 程序无法直接访问任意内核内存。
  • 位于程序上下文范围之外的数据和数据结构必须通过 eBPF 辅助函数来访问。这确保了数据访问的一致性,并使此类访问受 eBPF 程序权限的限制,例如,只有与程序类型相关的数据结构才能被读取或(有时)被修改,前提是验证器能够在加载时确保不会发生越界访问的情况;
  • 或者运行中的 eBPF 程序仅被允许修改某些数据结构的数据,前提是这种修改是安全的。
  • eBPF 程序不能随意修改内核中的数据结构。

6. Why eBPF? 为什么 eBPF 会存在?

6.1 可编程性

我们先从一个类比说起。你还记得 GeoCities(地球村)吗?20 年前,网页几乎完全是用静态标记语言(HTML)编写的。一个网页本质上就是一份文档,需要借助浏览器这类应用程序才能显示。再看看如今的网页,它们已经发展成功能完备的应用程序,而且基于网络的技术已经取代了绝大多数用需编译语言编写的应用程序。是什么推动了这一演变呢?

简短的答案是:JavaScript(JavaScript,简称 JS)的出现带来了可编程性。这一技术引发了一场巨大变革,使得浏览器逐步演变成了近乎独立的操作系统。

这种演变为何会发生?原因在于,开发者不再像以往那样受限于用户所使用的特定浏览器版本。他们无需再费力说服标准制定机构 "有必要新增某个 HTML 标签";相反,必要构建模块的存在,使得底层浏览器的创新节奏与运行于其上的应用程序实现了 "解耦"。当然,这种说法略显简化 ------ 毕竟 HTML 本身也在不断发展,并为这一变革的成功作出了贡献,但单靠 HTML 自身的演进,并不足以推动网页技术实现如此巨大的飞跃。

在借鉴这个案例并将其应用于 eBPF 之前,我们先来看看 JavaScript 问世过程中至关重要的几个关键方面:

  • 安全性(Safety): 不可信代码(Untrusted code)运行在用户的浏览器中。这一问题通过对 JavaScript 程序进行 "沙箱隔离"(sandboxing),并对浏览器数据的访问权限进行抽象化处理得以解决。

  • 持续交付(Continuous Delivery): 程序逻辑的演进需无需频繁发布新浏览器版本即可实现。这一需求通过提供功能完备的底层构建模块(low-level building blocks)得以满足 ------ 这些模块足以支持开发者构建任意所需的逻辑。

  • 性能(Performance): 实现可编程性的同时,必须将额外开销(overhead)降至最低。这一目标通过引入即时编译(Just-in-Time,简称 JIT)编译器得以达成。 以上所有特性,在 eBPF(扩展伯克利包过滤器)中都能找到完全对应的设计,且背后的原因完全一致。

7. eBPF 对 Linux 内核的影响

现在让我们回到 eBPF 的话题。要理解 eBPF 为 Linux 内核带来的可编程性影响,先从宏观层面了解 Linux 内核的架构,以及它如何与应用程序和硬件进行交互,会大有帮助。

Linux 内核的主要作用是对硬件或虚拟硬件进行抽象处理,并提供一套统一的 API(即系统调用,system calls),使应用程序能够运行并实现资源共享。为实现这一目标,内核中维护着大量子系统与层级结构,以分配上述职责。通常情况下,每个子系统都支持一定程度的配置,从而满足用户的不同需求。但如果需要的功能无法通过配置实现,就必须对内核进行修改;从历史经验来看,此时通常有两种选择:

方式1. 通过修改内核源码的方式:(Native Support)

  1. 修改内核源代码,并说服 Linux 内核社区证明该修改的必要性。
  2. 等待数年,直至包含该修改的新内核版本成为通用版本(或:普及开来)。

方式2: 修改内核模块

  1. 编写一个内核模块
  2. 定期修复该模块 ------ 因为每个内核版本都可能导致其无法正常工作
  3. 由于缺乏安全边界,存在导致 Linux 内核损坏的风险

方式3: 使用 eBPF

借助 eBPF,如今有了一种新选择:无需修改内核源代码或加载内核模块,就能对 Linux 内核的行为进行重新编程。从诸多方面来看,这与 JavaScript 及其他脚本语言推动那些 "修改难度大、成本高" 的系统实现演进的方式,极为相似。

8. 开发工具链

目前存在多款开发工具链,可协助进行 eBPF 程序的开发与管理。这些工具链各自满足用户的不同需求:

8.1 bcc

BCC(BPF Compiler Collection,BPF 编译器集合)是一个框架,能让用户编写内嵌 eBPF 程序的 Python 程序。该框架的目标场景主要集中在应用程序与系统的性能分析 / 跟踪领域:在这类场景中,eBPF 程序负责收集统计数据或生成事件,而用户空间中对应的程序则负责收集这些数据,并以人类可读取的形式展示出来。运行该 Python 程序时,会自动生成 eBPF 字节码,并将其加载到 Linux 内核中。

8.2 bpftrace

bpftrace 是一种用于 Linux eBPF 的高级跟踪语言,可在较新的 Linux 内核(4.x 版本系列)中使用。bpftrace 以 LLVM 作为后端,将脚本编译为 eBPF 字节码;同时,它借助 BCC(BPF 编译器集合)与 Linux eBPF 子系统交互,并利用 Linux 现有的跟踪能力 ------ 包括内核动态跟踪(kprobes)、用户态动态跟踪(uprobes)以及跟踪点(tracepoints)。bpftrace 语言的设计灵感来源于 awk、C 语言,以及此前的跟踪工具(如 DTrace 和 SystemTap)。

8.3 eBPF Go Library

eBPF Go 库是一个通用的 eBPF 库,它将 "生成 eBPF 字节码的过程" 与 "eBPF 程序的加载和管理过程" 进行了解耦。通常情况下,创建 eBPF 程序的流程是:先使用某种高级语言编写代码,然后借助 clang/LLVM 编译器将其编译为 eBPF 字节码。

8.4 libbpf C/C++ Library

libbpf 库是一个基于 C/C++ 的通用 eBPF 库,其主要作用包括:将经 clang/LLVM 编译器生成的 eBPF 目标文件加载到内核中;同时,它还通过为应用程序提供易于使用的库 API,对(应用程序与)BPF 系统调用的交互过程进行了整体抽象。

9. 参考

  1. ebpf.io/what-is-ebp...
相关推荐
Livingbody12 小时前
《LIC·2025语言与智能技术竞赛——智源研究院赛道二》数据处理分析
后端
南囝coding12 小时前
2025 最新!独立开发者穷鬼套餐
前端·后端
JohnYan12 小时前
工作笔记-微信消息接收机制与实现
javascript·后端·微信
程序员鱼皮12 小时前
在国企干了 5 年 Java,居然不知道 RPC?这正常吗?
后端·rpc·程序员
bobz96512 小时前
windows 内 virtio 驱动版本和 qemu virtio 后端不兼容导致 qemu 内存泄漏被 cgroup oom
后端
苏三说技术12 小时前
Group By很慢,如何优化性能?
后端
微信bysj79812 小时前
基于深度学习的车牌识别系统(源码+文档
java·人工智能·spring boot·后端·深度学习·微信小程序·小程序
java干货12 小时前
Spring Boot 全局字段处理最佳实践
java·spring boot·后端
火柴就是我12 小时前
每日见闻之缓存击穿跟缓存穿透的理解
后端