eBPF 源码专题【左扬精讲】—— eBPF 概述:是什么、如何运行与发展历史

eBPF 源码专题【左扬精讲】—— eBPF 概述:是什么、如何运行与发展历史

本文讨论 eBPF(extended Berkeley Packet Filter)的基本概念、发展脉络、应用场景和运行流程。

eBPFBPFLinuxXDP可观测性网络安全

一、eBPF 是什么

eBPF 是 Linux 内核中的一种可编程机制:用户态程序准备 eBPF 指令,通过 bpf() 系统调用请求内核加载。

内核使用 verifier 对程序进行安全检查,验证通过后可以解释执行或经过 JIT(Just-In-Time)编译,再将程序挂载到特定的内核 hook 上。

“extended” 来自它相对于经典 BPF(cBPF)的能力扩展。经典 BPF 最初用于高效过滤网络数据包,eBPF 则逐渐支持网络、跟踪、性能分析、安全等非包过滤任务。

今天的 Linux 内核仍然保留 BPF 这一名称,但 eBPF 已经不应只理解为 packet filter。

核心心智模型
eBPF = 字节码 + verifier 安全检查 + 可选 JIT + hook 挂载点 + helper 与 map。它不是让用户态任意代码直接运行在内核中,而是让受约束的 eBPF 程序在内核提供的执行模型中工作。

二、发展历史:按用户指定年份梳理

年份可核实的代表性节点说明
1992 经典 BPF 提出 Steven McCanne 与 Van Jacobson 发表 Berkeley Packet Filter 相关论文,目标是让网络抓包工具在内核侧先过滤数据包,减少无关数据复制到用户态。
1997 BPF 进入 Linux 公开资料和相关演讲资料记载,BPF 在 Linux 2.1 系列进入内核,最初主要作为 socket filter 服务于 tcpdump/libpcap 等网络抓包场景。
2011 经典 BPF 的内核 JIT Linux 合入经典 BPF 的 in-kernel JIT 编译器,用本地机器码执行过滤程序,以改善性能。此时仍是经典 BPF,不应把它直接表述为 eBPF 的诞生。
2014 eBPF 架构进入 Linux 3.18 Linux 接受 BPF 解释器重构,内核内部引入 eBPF 指令表示,并开始把经典 BPF 转换到 eBPF 表示。eBPF 从网络过滤器向通用内核虚拟机演进。
2015 跟踪与网络挂载能力扩展;LLVM 后端合入 eBPF 支持 kprobe 等跟踪入口,tc 网络路径也开始成为重要挂载位置;LLVM 3.7 发布了 BPF 编译器后端。不同事件的具体合入时间应以对应 Linux/LLVM 提交记录为准。
2016 XDP 与 Cilium 项目出现 eBPF 可挂到网络驱动接收路径,形成后来称为 XDP(eXpress Data Path)的快速数据路径;Cilium 在 LinuxCon 期间公开,探索使用 eBPF/XDP 实现容器网络。
2017 成为独立的内核子系统 公开历史资料记载,eBPF 形成独立内核子系统,以应对不断增长的补丁、维护者和功能规模。
2018 本文未单独确认一个统一的里程碑 eBPF 生态持续演进,但本文不把某个项目发布、某个 helper 或某个特性未经一手资料核实后归为“2018 年官方节点”。
2019 BPF 书籍与生态持续成熟 Brendan Gregg 的《BPF Performance Tools》于 2019 年出版,推动性能工具知识传播。公开资料还记载 GCC 在 2019 年跟进 BPF 后端。
2020 BPF LSM 与 CO-RE 工具链继续成熟 公开资料记载,BPF LSM 支持在 Linux 内核中持续推进;同时 BTF、CO-RE 与 libbpf 逐渐成为跨内核版本开发的重要基础。本文将其表述为生态和内核能力的演进,不把单一项目的发布时间当作 eBPF 的唯一里程碑。
2021 eBPF Foundation 成立 eBPF Foundation 在 2021 年成立,目标是促进 eBPF 相关开源项目、社区协作和生态发展。这是社区组织层面的节点,不是 Linux 内核版本发布节点。
2022 libbpf 1.0 与 eBPF for Windows 公开资料记载,libbpf 在 2022 年达到 1.0 版本里程碑;微软也在 2022 年公开 eBPF for Windows 项目。后者是 Windows 平台的独立实现与项目生态,不能直接等同于 Linux 内核中的 eBPF 实现。
2023 开发体验与可移植性成为重点 公开技术资料将 BTF、CO-RE、libbpf 和 BPF skeleton 作为 eBPF 应用开发的重要基础。eBPF 的工程重点从“能否运行”进一步扩展到加载流程、跨内核版本适配和生产部署体验。
2024 eBPF 指令集规范化讨论推进 公开资料记载,eBPF Instruction Set Architecture 相关规范在 IETF 体系中推进并形成 RFC 文档。该类规范化工作描述的是指令集与生态互操作性,不代表所有 Linux 内核特性都已经由 IETF 定义。
2025 本文不指定未经一手资料确认的单一里程碑 eBPF 继续用于 Linux 网络、可观测性和安全生态;但本文当前资料没有足够可靠的一手来源,确认一个应归属于 2025 年且具有统一共识的单一历史事件,因此不强行编造具体版本、项目采用数量或性能数据。
2026 项目仍在持续演进 截至 2026 年本文生成时,libbpf 官方镜像资料显示项目仍有持续发布和维护活动,例如页面列出了 2026 年的版本发布记录。本文不据此推断整个 eBPF 生态的“最终状态”,也不把尚未核实的项目宣传内容写成事实。
历史边界
“2014 年 eBPF 进入 Linux 3.18”与“2011 年经典 BPF JIT”是两个不同节点;“1992 年 BPF 提出”也不等于 eBPF 在 1992 年已经存在。将这些年份混成一条“eBPF 从 1992 年开始”的表述,会掩盖经典 BPF 到 eBPF 的技术断层。

三、eBPF 的应用场景

3.1 网络与容器网络

在 XDP、tc、socket 等网络 hook 上,eBPF 可用于数据包过滤、重定向、负载均衡、流量统计和容器网络策略。Cilium 是公开生态中使用 eBPF 构建容器网络与网络安全能力的代表项目。

3.2 可观测性与性能分析

eBPF 可以挂接 kprobe、tracepoint、uprobe、perf event 等入口,采集系统调用、内核函数、用户态函数、调度和 I/O 等事件。BCC、bpftrace、libbpf 和 bpftool 是常见工具或库;实际可观测内容受内核版本、程序类型和权限约束。

3.3 运行时安全

eBPF 可用于系统调用与进程行为观测、网络策略、LSM(Linux Security Module)相关安全检查和运行时检测。安全程序仍要遵守 verifier、helper 权限和 Linux 能力控制,不能把 eBPF 误解为绕过内核安全边界的工具。

3.4 数据路径与内核扩展

当需求需要修改或观测内核行为,而直接修改内核源码、编写内核模块成本较高时,eBPF 提供了更细粒度的扩展路径。但可挂载的 hook、上下文、返回值语义和 helper 集合均由具体 program type 决定。

四、eBPF 如何运行

用户态源码(C/Rust 等) ↓ 编译为 eBPF 字节码/ELF 加载器(libbpf 等)调用 bpf() 系统调用 ↓ 内核 verifier:控制流、寄存器类型、指针边界、helper 合法性等检查 ↓ 验证通过 解释器执行,或 JIT 编译为目标 CPU 的机器码 ↓ attach kprobe / tracepoint / XDP / tc / socket / LSM 等 hook 被触发 ↓ eBPF 程序通过 helper 访问受限内核能力,通过 map/ring buffer 与用户态交换数据

4.1 编译与加载

开发者通常使用 Clang/LLVM 等工具把源代码编译为 eBPF 目标文件,再由用户态加载器解析 ELF、创建 map、加载 program 并完成 attach。

CO-RE(Compile Once – Run Everywhere)依赖 BTF 等内核类型信息,目标是减少针对每个内核版本重新编译的需要.

它不是 “任何内核都无需检查即可运行”

4.2 verifier 是什么

verifier 是 Linux 内核 eBPF 子系统中的一个静态分析组件。

它位于 bpf() 系统调用加载 eBPF 程序的路径上,只在程序加载、attach 或被更新时运行,目标是在程序真正在内核事件路径上执行之前,判断该程序是否可被接受

verifier 要解决的核心问题是:eBPF 程序运行在内核上下文,能访问 helper、map、内核对象指针。如果它陷入死循环、越界访问任意内存、调用非法 helper 或绕过类型约束,就会让内核稳定性甚至内核安全面临风险。

verifier 就是用来在加载阶段把这类风险排除在外的关卡。

verifier 的核心思路
不依赖运行时观测,而是对每条指令模拟执行:遍历控制流图,记录每条指令处寄存器的状态范围、类型和边界前提,把所有可能执行路径都分析一遍。无法证明安全的路径会被拒绝,程序加载失败。

从结构上看,verifier 通常包含:

    • 指令解析与规范化:检查指令是否能被解码、是否超出 program type 限制;
    • 控制流图构建:将基本块、跳转、调用组织成图,识别不可终止的环路;
    • 抽象状态机:把寄存器、栈、helper 调用结果记录为一组带范围与类型的抽象状态;
    • helper 与 program type 合法性检查:确保被调用 helper 在当前 program type 下可用;
    • 资源上限检查:指令数、调用深度、栈深度、map 操作次数等;
    • 验证日志和拒绝原因:当验证失败时输出可供排查的日志。

需要注意的是 verifier 不是万能安全证明,它验证的是加载阶段能静态推导出的性质。运行时仍可能受并发、用户态对 map 的异常使用、内核自身 bug 等因素影响,这些都不在 verifier 的覆盖范围。

4.3 verifier 实际检查什么

verifier 在程序执行前分析控制流和状态,检查内存访问是否可证明安全、helper 是否适用于当前 program type、寄存器类型是否满足约束,以及程序是否可能产生不可接受的执行行为。

Linux 5.17 起,满足边界条件的 bounded loop 获得支持,因此 “verifier 永远禁止循环” 是过时说法。

4.4 JIT、helper 与 map

JIT 是什么

JIT 是 Just-In-Time compilation(即时编译)的缩写。eBPF 程序最初是 eBPF 字节码,内核可以先用解释器逐条执行。

启用 JIT 后,内核会在加载阶段把这些字节码翻译为当前 CPU 架构的机器码,例如 x86-64 或 arm64,事件触发时直接执行机器码。

JIT 的作用主要是减少解释器逐条解析指令的开销,但它不改变 verifier 的安全检查顺序:程序仍然必须先通过 verifier,才可能进入后续执行阶段。JIT 也不等于“没有成本”,编译、hook 触发、数据读写和用户态消费仍然会产生开销。

helper 是什么

helper 是 Linux 内核为 eBPF 程序提供的受控函数接口。

eBPF 程序不能像普通内核代码那样任意调用内核函数,而是只能调用当前 program type 和上下文允许的 helper。helper 为 eBPF 程序提供读取当前进程信息、访问 map、获取时间、重定向数据包等能力。

例如,bpf_map_lookup_elem 用于从 map 查找元素,bpf_get_current_comm 可用于获取当前任务的名称。这里的例子只说明 helper 的角色,不代表这两个 helper 可在所有 program type 中使用。具体可用范围必须以对应内核版本的 helper 文档为准。

map 是什么

map 是由内核管理的键值数据对象,也是 eBPF 程序和用户态程序交换状态的主要机制。

eBPF 程序可以把统计结果写入 map,用户态程序再通过 map 文件描述符读取。多个 eBPF 程序也可以共享同一个 map。

map 不是单一的数据结构,Linux 内核提供了不同用途的 map 类型,例如 hash、array、per-CPU map 和 ring buffer。选择哪一种 map,取决于访问模式、并发方式、数据量和用户态消费方式。

三者如何配合
verifier 负责“能不能加载”,JIT 负责“怎样更快执行”,helper 负责“程序能通过哪些受控接口使用内核能力”,map 负责“程序运行时怎样保存和交换数据”。它们分别解决安全、执行、能力和状态问题。

4.5 attach 是什么:把程序挂到 hook 上

attach 的中文含义可以理解为“挂载”或“附加”。

eBPF 程序通过 verifier 只代表它已经被内核接受并加载,程序还需要 attach 到一个具体的 hook(挂钩点),内核才知道在什么事件发生时调用它

例如,

    • 把 eBPF 程序 attach 到 tracepoint,表示对应 tracepoint 事件发生时调用该程序。
    • attach 到 kprobe,表示目标内核函数被探测时调用。
    • attach 到 XDP,则表示网络数据包到达网络设备接收路径的相应阶段时调用。

不同 hook 会提供不同上下文,因此也会影响 verifier 允许的 helper 和程序返回值。

加载:用户态通过 bpf() 把程序交给内核 ↓ 验证:verifier 判断程序是否满足安全约束 ↓ 编译:内核解释执行,或使用 JIT 生成机器码 ↓ attach:把程序与指定 hook 建立关联 ↓ 触发:事件到达 hook,内核调用 eBPF 程序 ↓ 处理:程序返回动作码,或更新 map/ring buffer

因此,attach 不是重新编译程序,也不是让程序主动轮询内核。

它是建立“事件入口到 eBPF 程序”的内核关联。detach 则是解除这种关联,使后续事件不再调用该程序。

attach 后的事件路径

程序加载成功不代表它已经处理业务。

只有 attach 到 hook 后,事件到达该 hook,内核才会按对应上下文调用程序。

程序执行结果可能是返回一个动作码、更新 map、写入 ring buffer,或通过 perf event 等机制把事件送到用户态。

一段话总结
经典 BPF 解决了高效网络包过滤,BPF 在此基础上成为 Linux 内核中的受约束可编程执行平台。
它通过 verifier 保证可接受的安全边界,通过 JIT 提升执行效率,通过 hook 接入内核事件,通过 helper 和 map 完成受控交互,因此能同时服务于网络、可观测性和安全场景。

五、使用 eBPF 时需要注意什么

  • 不要用某个发行版的现象替代 Linux 内核通用事实progr。am type、helper、BTF 和权限都可能受版本影响。
  • 不要把“JIT 后接近本地代码”写成“零开销”。加载、验证、attach、数据采集和用户态消费都可能产生成本。
  • 不要把 verifier 描述成完整的安全证明;它是内核加载阶段的约束检查,仍需要权限控制、资源限制和程序审计。
  • 需要确认具体能力时,应查对应内核版本的 Documentation/bpf/man 7 bpf、libbpf 文档和项目官方资料。
posted @ 2026-08-11 22:02  左扬  阅读(10)  评论(0)    收藏  举报