eBPF 的边界:内核可编程化走到哪一步了

2022 年,Brendan Gregg 在一次演讲里说了一句话:”eBPF 对 Linux 的意义,就像 JavaScript 对浏览器的意义。”

这个类比被反复引用,但也经常被误解。它的意思不是”eBPF 让内核编程变得更容易”——内核编程从来不容易,eBPF 也没有改变这一点。它的意思是:eBPF 给了内核一个安全的运行时,让外部代码可以在内核上下文里执行,而不需要编写内核模块


内核模块的代价

在 eBPF 之前,想要深入内核做事——拦截系统调用、监控网络包、追踪函数执行——只有两条路。

一是内核模块。直接在内核地址空间运行,没有隔离,一个野指针就能把整个系统带走。内核模块的代码需要随内核版本维护,ABI 不稳定,商业 Linux 发行版对加载未签名模块有严格限制。

二是修改内核源码并重新编译。对于需要追踪线上系统的场景,这几乎不可行。

eBPF 提供了第三条路:把用户编写的程序编译成 eBPF 字节码,在内核里运行一个受限的虚拟机执行它。”受限”是这里的关键词。


验证器:那把真正的钥匙

eBPF 安全性的核心是验证器(Verifier),不是沙箱隔离。

每个 eBPF 程序在加载进内核之前,验证器会对它做静态分析:

  • 没有无限循环(有界循环,内核 5.3 之后支持)
  • 所有内存访问必须经过边界检查
  • 不能调用任意内核函数,只能调用白名单里的 BPF helper
  • 程序必须在有限步骤内终止

验证器通过对程序的所有执行路径做模拟,确保这些属性成立。验证失败,程序加载被拒绝,返回错误。

这个设计的代价是:验证器非常保守。即使一段逻辑上安全的代码,验证器也可能拒绝,因为静态分析无法证明它的安全性。早期的 eBPF 程序规模限制在 4096 条指令,现在已经放开到 100 万条,但验证复杂度随程序复杂度急剧增加,写出”验证器能接受的代码”本身是一门独立的技艺。

如果你用 libbpf 写过复杂的 BPF 程序,大概有过和验证器博弈的经历:明明逻辑正确,却因为某个路径上指针类型不明确而被拒绝;加了一个看似无害的函数调用,验证器状态数量爆炸。这种体验是 eBPF 学习曲线陡峭的主要原因,而不是概念本身的复杂度。


主要挂载点:不只是追踪

eBPF 程序的挂载点(Attach Point)决定了它能做什么。几个最重要的类型:

kprobes / kretprobes:动态挂载到任意内核函数的入口和返回点。这是 eBPF 追踪能力的基础,Brendan Gregg 的大量性能分析工具都基于此。缺点是依赖内核符号名,不同内核版本的函数名可能不同。

tracepoints:内核代码里显式插入的稳定追踪点。比 kprobes 更稳定,但覆盖范围有限,只覆盖内核开发者主动标注的位置。

XDP(eXpress Data Path):在网卡驱动层处理网络包,在内核网络栈之前运行。延迟极低,Cloudflare 用它做 DDoS 防护,Meta 用它加速内部负载均衡。XDP 可以在收包时就做 DROP、PASS、REDIRECT 决策,绕过整个 TCP/IP 栈。

TC(Traffic Control):挂载到内核的流量控制层,可以读写包内容,双向(ingress/egress)生效。Cilium 的 Pod 间网络策略执行主要在这一层。

LSM(Linux Security Modules):从内核 5.7 开始,eBPF 可以挂载到 LSM hook,实现细粒度的访问控制策略,无需修改内核或重新配置 SELinux/AppArmor 规则。Tetragon 用这个做运行时安全策略。

uprobes:挂载到用户态进程的函数。可以追踪任何语言的函数调用,不需要修改应用代码。Go、Python、Java 的语言级追踪工具很多基于 uprobes,但运行时差异带来了额外的复杂性,尤其是有 JIT 的语言。


落地的三个方向

从工程实践来看,eBPF 目前最有价值的落地场景集中在三个方向。

可观测性是最早成熟的方向。Pixie、Parca、Tetragon、bpftrace——不需要修改应用代码,就能获得系统调用级别的追踪数据。在 Kubernetes 环境里,这意味着不需要 sidecar 就能做服务级别的延迟、错误率分析。这对 sidecar 模式的 service mesh 是一个结构性替代,eBPF-based 的可观测性把侵入性降到了几乎为零。

网络里 Cilium 是最成熟的案例。用 eBPF 替换 kube-proxy 实现 Service 负载均衡,替换 iptables 实现网络策略,提供 Hubble 做网络可观测性。在大规模 Kubernetes 集群里,iptables 的 O(n) 规则匹配是真实的性能瓶颈——规则数量随 Service 数量线性增长,在几千个 Service 的规模下,iptables 的遍历本身就是显著的延迟来源。Cilium 的 eBPF 实现用哈希表把这个降到 O(1)。

安全里 Falco 和 Tetragon 是代表。传统安全工具基于 audit 子系统,开销大且延迟高;eBPF 实现可以在事件发生时直接在内核里做过滤和决策,大幅降低用户态的处理压力。更重要的是,基于 LSM 的 eBPF 安全策略可以做到真正的”阻断”而不只是”告警”,这是检测和防护之间的本质差异。


CO-RE:可移植性问题的工程解法

eBPF 程序的历史痛点是内核版本兼容性。内核内部结构体的布局随版本变化,一个在 5.10 上编译的 eBPF 程序,在 5.15 上运行时访问某个结构体字段,可能读到错误的偏移量。

CO-RE(Compile Once, Run Everywhere)是解决这个问题的工程方案,核心是两个机制:

BTF(BPF Type Format):内核把自己的类型信息嵌入到内核二进制里(/sys/kernel/btf/vmlinux)。eBPF 程序加载时可以读取当前内核的 BTF 信息,知道结构体字段的实际偏移量。

重定位:libbpf 在加载时根据 BTF 信息对程序做字段访问的重定位,把编译时的偏移量替换成运行时内核实际的偏移量。

结果是:用支持 CO-RE 的工具链(clang + libbpf)编译一次,同一个 eBPF 程序可以在不同版本的内核上运行,只要目标内核支持 BTF——从内核 5.4 起,主流发行版已经默认开启。

这个机制把 eBPF 工具的分发从”针对每个内核版本编译”变成了”编译一次分发”,是 eBPF 生态走向成熟的关键基础设施转变。


边界在哪里

eBPF 不是万能的。几个真实的限制值得明确。

栈空间严格受限:BPF 程序的栈只有 512 字节。需要更多临时空间,必须用 BPF map。这个限制影响了程序结构,写复杂逻辑时需要反复拆函数、用 map 传数据,代码组织方式和普通用户态程序有显著差异。

无法做阻塞操作:BPF 程序在内核上下文执行,不能睡眠、等待锁、或者做任何可能阻塞的事情。需要和用户态通信时,只能通过 perf event 或 ring buffer 异步传数据,架构上天然是事件驱动的。

调试体验差:BPF 程序没有传统意义上的调试器。出错了,你看到的是验证器拒绝加载,或者程序加载成功但没有预期行为。bpf_printk 是唯一的”printf 调试”,输出需要从 /sys/kernel/debug/tracing/trace_pipe 读,格式受限,高频调用有性能影响。

验证器复杂度上限:程序越复杂,验证时间越长,验证失败的可能性越高。有时候需要刻意简化代码结构——人为增加冗余的边界检查,或者拆分逻辑——只是为了让验证器能够在合理时间内完成分析。这种”为验证器写代码”的体验是 eBPF 开发里最反直觉的部分。

这些不是设计缺陷,是有意识的权衡。eBPF 的安全性保证和这些限制是同一枚硬币的两面。


几个值得关注的方向

BPF token 和权限细化:目前加载 eBPF 程序需要 CAP_BPF 或 CAP_SYS_ADMIN,权限粒度较粗。BPF token 机制允许把特定的 eBPF 能力授权给容器内的程序,不需要给整个 CAP_SYS_ADMIN。这对云原生场景的安全边界设计很重要,是让 eBPF 工具在 unprivileged 容器里安全运行的前提。

eBPF for Windows:Microsoft 在维护一个 eBPF for Windows 项目,把 eBPF 的编程模型和工具链移植到 Windows。技术上用的是 PREVAIL 验证器和用户态执行引擎,不是直接在 Windows 内核里跑。成熟度还早,但方向说明了 eBPF 作为可观测性和安全编程模型的通用价值已经被认可,不再只是 Linux 社区的事。

用 Rust 写 BPF 程序:aya 是一个纯 Rust 的 eBPF 开发框架,程序端和用户态都用 Rust 编写,编译到 BPF 字节码。对于已经用 Rust 的团队,这比 C + libbpf 的工具链更整洁,内存安全的保证也减少了写 BPF 程序时的一类低级错误。Cloudflare 的部分 eBPF 工具已经迁移到 aya。


eBPF 的意义不是”内核编程变简单了”。内核编程本来就不简单,eBPF 把复杂度换了一种形式——从”可能宕机的内核模块”换成了”和验证器博弈的 BPF 程序”。

这个换法是值得的。它给了工程师一条在不重启、不修改内核、不影响生产稳定性的前提下,深入内核行为的通道。可观测性、网络性能、运行时安全——这三个领域在过去五年里有大量创新,eBPF 是共同的技术底座。

下一次你在 Kubernetes 集群里用 Cilium 做网络策略,或者用 Pixie 看服务延迟分布,背后运行的 BPF 程序是在内核里执行的代码,只是先经过了验证器的审查。


eBPF 的边界:内核可编程化走到哪一步了
https://www.krli.org/2026/05/06/eBPF-内核可编程化走到哪一步了/
作者
李科燃
发布于
2026年5月6日
许可协议