《The eBPF Runtime in the Linux Kernel》论文学习笔记

写在前面

论文地址:《The eBPF Runtime in the Linux Kernel》https://arxiv.org/abs/2410.00026

第一次接触 eBPF 时,很多人都会把它理解成一个"更现代的 BPF",或者干脆把它归到网络性能优化、可观测性工具链那一类技术里。这样的理解当然不算错,但如果只停留在这个层面,就很容易低估 eBPF 在 Linux 内核中的真正定位。

我最近读的这篇论文,《The eBPF Runtime in the Linux Kernel》,做了一件很有价值的事:它没有停留在"eBPF 能做什么"的经验层面,而是试图系统解释,Linux 内核中的 eBPF 到底是什么,它为什么能在内核里运行用户定义的逻辑,又为什么在这样高风险的场景下,依然能尽量维持安全、性能和可维护性之间的平衡。

这篇论文的一个重要特点是,它不是入门教程,也不是单纯的功能综述,而是一篇偏"运行时机制解剖"的文章。作者试图完整梳理 Linux 6.7 时代的 eBPF runtime,包括它的对象模型、加载路径、验证器、JIT 编译流程,以及现实中的主要应用和挑战。如果说很多 eBPF 资料是在教人"怎么写",那么这篇论文更像是在回答另一个更基础的问题:为什么 eBPF 可以这样写,而且能被内核接受。

这份笔记会按论文原本的章节顺序整理。

因为在这里,在这人生有无价值,是得救或是沉沦的关头,起决定作用的不是哲学的僵硬概念,而是人自己最内在的本质;即柏拉图所说的神明,指导着人但不曾选定人,而是人自己所选定的 "神明";又即康德所说的 "悟知性格"。德性和天才一样,都不是可以教得会的。概念对于德性是不生发的,只能作工具用;概念对于艺术也是如此。----《作为意志和表象的世界》第四篇《世界作为意志再论》 §53

一、Introduction:eBPF 解决的,不只是"扩展内核"这么简单

论文开篇先从一个非常经典、但也非常现实的问题切入:Linux 这样的单体内核,本质上是为通用场景设计的。它要兼顾各种硬件、各种工作负载、各种部署模型,因此它提供的是一个足够稳定、足够普适的抽象层,而不是对每一种 workload 都做到极致优化。

这就带来了一个长期存在的矛盾:

  • 一方面,大规模生产环境的用户越来越希望针对自身 workload 去定制内核行为;
  • 另一方面,直接修改内核的成本又极其高昂。

如果开发者真的想改变 Linux 内核中的某一段策略,传统上通常只有两条路。

第一条路,是直接修改内核源码,或者编写内核模块。这条路理论上最彻底,因为你可以真正改动内核行为本身。但问题也恰恰在这里:Linux 内核代码复杂、接口不稳定、调试成本高,而且一旦有 bug,很可能不是应用崩溃,而是整个系统崩溃。更糟糕的是,这类改动还会带来漫长的部署链路:你不仅要编译和测试一个新内核,还要在大规模机器上逐步 rollout,并承担重启和业务中断的代价。

第二条路,是干脆绕开内核,或者用更激进的方式替代内核的一部分能力。比如 kernel bypass(内核旁路,跳过内核态,用户态直接操作硬件)、library OS(库操作系统,编译期按需裁剪操作系统组件)一类方案,往往能在特定场景中获得非常可观的性能收益。但它们的问题在于,通常会削弱操作系统原本提供的资源复用、隔离、共享和生态兼容能力。很多时候,它们不仅要求开发者重写应用逻辑,还会破坏不同 workload 共存时原本依赖的系统语义。

论文对 eBPF 的定位,正是在这两条路线之间。它既不要求开发者维护一套特化内核,也不要求他们彻底抛弃 Linux 现有的抽象和运行时能力,而是在现有内核之中引入一个安全、受约束、可动态装载的虚拟机运行时 。借助这个运行时,用户可以把自己定义的逻辑在加载时送入内核,并挂到特定的 hook 上执行,从而改变、增强或者补充内核在某些路径上的行为。

这个转变其实很关键。它意味着 eBPF 的意义并不只是"让内核支持插件",而是让 Linux 从一个相对固定策略的通用内核,逐渐变成一个可以在运行时安全重编程的系统平台

这也是作者在引言中想强调的核心观点:eBPF 与其说是在扩展 Linux,不如说是在改变我们理解操作系统可扩展性的方式。

二、Background and Design Principles:为什么内核定制这么难,eBPF 又想怎样把这件事变得可行

在正式进入 eBPF 机制之前,论文第二章花了不少篇幅去回答一个前置问题:为什么"定制内核"这件事在现实里如此困难?

1. Linux 的配置能力,并不等于 Linux 的可编程能力

Linux 一直以来都提供大量配置项,无论是编译期的 Kconfig,还是运行时的 sysctl,它们都允许用户对既有内核策略做参数级调整。但论文指出,这种"可配置"并不等于"可编程"。

换句话说,配置项解决的是"在内核已经设计好的策略空间里怎么调",而不是"我能不能引入一种新的策略"。如果用户只是想把某个阈值改大一点、把某个缓冲区调小一点,那配置项当然很好用;但如果用户真正想定义一套新的调度策略、一种新的网络处理逻辑,或者一种新的监控与安全判定路径,配置参数就无能为力了。

于是,开发者最终还是会落回到修改内核代码这条老路上。

2. 直接改内核的代价,不只是"写起来难"

论文对"改内核为什么难"总结得比较全面,而这些难点其实都很工程化。

首先是开发门槛高。Linux 内核本身就是一个规模巨大且高度演化的代码库,想在里面做出正确修改,需要对具体子系统和调用路径有非常深入的理解。很多改动不是"逻辑对了就行",而是必须同时满足上下文约束、锁语义、生命周期管理和性能要求。

其次是维护成本高。即使你写出的补丁在当前版本工作正常,只要这些修改没有进入主线,后续内核升级就意味着你需要持续 forward-port。由于 Linux 内核内部 API 本来就不承诺稳定,这种维护很容易演化成长期负担。

再次是部署代价高。论文强调了一点非常现实:哪怕只是一个很小的内核改动,一旦要部署到大规模机器集群里,它都会变成一个复杂、缓慢且昂贵的过程。你需要替换正在运行的内核,停止 workload,重启机器,恢复服务,然后再重新做验证。对于冷启动昂贵、恢复周期长的业务,这几乎是不可忽略的损耗。

所以,问题从来不只是"改内核难写",而是"改内核难写、难测、难发、难长期维护"。

3. 绕开内核的方案,性能很好,但往往失去操作系统最重要的部分

第二章还花了一节讨论另一类替代路线,比如 kernel bypass 和 library OS。论文并不否认这类方案在某些场景下可以提供极佳性能,但它们的代价也很明显。

  • 一类问题是资源共享能力下降。很多绕过内核的方案需要更直接地掌控硬件,甚至默认自己独占某些资源,这会让多个 workload 共存变得困难。
  • 另一类问题是系统语义割裂。操作系统原本提供的安全模型、隔离边界、观测能力、统一管理接口,都会被削弱甚至绕开。

因此,对很多生产环境用户来说,他们并不想"抛弃 Linux",他们真正想要的是:继续使用成熟、稳定、可管理的 Linux 内核,同时又能在某些关键路径上做 workload-specific 的定制。

这也就是 eBPF 想满足的需求。

三、Design Principles:eBPF 背后的三个设计哲学

这一章最值得反复看的,其实是作者总结的几个设计原则。它们决定了 eBPF 后续很多机制为什么会长成现在这个样子。

1. 安全的动态内核定制

如果说传统内核模块是"把自定义代码直接塞进内核",那么 eBPF 想做的是:在内核里托管一个受约束的、安全可验证的虚拟机,让用户在这个边界内动态定义行为。

这种设计的价值在于,它降低了"改内核"的门槛。开发者不需要掌握整个内核子系统的全部细节,也不需要承担直接写内核 C 代码带来的全部风险,而是通过一套更小、更受限的执行模型来表达自己的逻辑。

当然,这里的关键不是"能动态加载",而是"能在动态加载前完成安全证明"。也正因为如此,论文之后才会把大量篇幅都放在 verifier 身上:因为 verifier 才是这个设计哲学真正成立的基础。

2. 快速部署与快速迭代

与传统内核补丁相比,eBPF 的另一个核心优势是部署速度。程序可以在运行时加载、卸载和更新,而不需要替换整个内核映像,更不需要重启机器。

这带来的不只是"方便",而是开发和运维反馈回路的根本变化。以前修改内核逻辑可能需要几周甚至更长的交付周期;而借助 eBPF,很多实验、监控、策略调整甚至修复,都可以在更短时间内完成验证和发布。

从系统工程角度看,这意味着 eBPF 不是单纯提供了一种新技术,而是提供了一种更高迭代速度的系统变更机制。

3. 与内核协同,而不是取代内核

eBPF 并不追求把 Linux 变成一个"完全由用户逻辑重写"的平台。相反,它的模型是增量式的、协同式的。

程序被挂到 hook 上之后,可以在某些时刻接管决策,也可以在不需要干预时退回到原有内核路径。用户并不需要在 eBPF 程序里重新实现一个完整网络栈、一个完整调度器或者一整套安全框架;他们可以利用既有内核状态、对象和基础能力,只在真正需要定制的点上做变化。

这个设计思想特别重要,因为它解释了为什么 eBPF 能在现实里快速普及:它不是要推翻 Linux,而是在 Linux 的基础上增加一层运行时可编程能力。

四、Overview:从"一个特性"到"一整套运行时"

第三章开始,论文正式进入 eBPF 的整体架构。读到这里时,一个最重要的认知变化是:eBPF 并不是单独的一段内核机制,而是一个由多种组件共同构成的运行时系统。

1. eBPF 是一个抽象虚拟机

论文首先从抽象层定义 eBPF:它是一套虚拟机模型,拥有固定的寄存器、栈和指令集。程序以 eBPF bytecode 的形式存在,由这个虚拟机模型来执行。

这件事的意义在于,eBPF 程序并不是"任意的内核代码片段",而是必须先被收敛到一个受控的中间表示里。只有这样,内核才有可能在加载前做系统性的静态分析与安全检查。

换句话说,eBPF 的第一道安全边界 ,其实不是 verifier,而是它本身已经把可执行程序限制在一套定义明确、规模较小、便于分析的 ISA 之内。(ISA 是CPU 硬件和上层软件(操作系统、应用程序)之间的接口规范,规定了 CPU 能识别哪些机器指令、寄存器格式、内存寻址方式、异常中断规则等。)

2. eBPF runtime 的核心组成部分

论文把 eBPF 运行时拆成几个关键组件,这个拆分很有助于建立整体图景。

首先是 bytecodeeBPF 程序最终提交给内核时,不是源码,而是字节码指令序列。程序由一个或多个 subprogram 组成,执行时从主入口 subprog 开始。

其次是 userspace loader。用户态加载器负责把编译好的 eBPF 对象文件解析出来,调用 bpf() 系统调用把程序、mapBTF 等对象送进内核。libbpf、BCC、bpftrace 都属于这个层面的工具或库。论文在叙述中主要以 LLVM + libbpf 这一现代主流工具链为参照。

然后是 verifier。这是整套系统里最关键的一环。它会在程序进入内核前,从字节码层面对程序做静态分析,并据此决定程序是否安全。只有通过 verifier 的程序,才有机会真正执行。

再往后是 JITinterpreter。程序通过验证之后,内核会优先将其 JIT 编译为原生机器码;如果平台不支持 JIT,或者 JIT 被关闭,则回退到解释执行。

此外,还有两个不可忽视的概念:hookshelpershook 决定程序能挂在哪些事件点上执行,比如系统调用、tracepoint、网络路径、kprobe、uprobe 等;helper 则是内核提供给 eBPF 程序的受控能力接口,程序不能随意访问内核内部对象,而必须通过 helper 在内核允许的边界内完成操作。

最后,mapseBPF 编程模型中非常重要的数据组件。它承担用户态和内核态之间的数据交换,也承担程序自身跨调用、跨事件持久化状态的作用。某种意义上说,没有 map,很多 eBPF 程序就只能是一次性、无状态的 hook 逻辑。

如果把这些东西放在一起看,就会发现:eBPF 绝不是"内核里多了个脚本功能",而是一套包含对象模型、数据通道、受控 API、静态分析和执行引擎的完整运行时。

五、Objects and Lifecycle:理解 eBPF,必须理解对象生命周期

第三章里我觉得最容易被忽略、但其实非常关键的一节,是 eBPF 对象生命周期

论文指出,eBPF 的很多实体,比如 programmaplinkBTF,在内核里都不是"无名资源",而是各自有明确的内核侧表示,并且在用户态通过文件描述符暴露出来。这意味着它们的生命周期默认是和 fd 绑定的:当最后一个引用它的 fd 被关闭,对应对象在内核里的状态也会被释放。

这是一个非常 Unix 风格的设计,但它对 eBPF 的资源管理非常重要。因为 eBPF 程序不是传统意义上"启动一个进程然后运行",而是一个对象化、fd 化的资源模型。

为了让这些对象在加载进程退出之后仍然存在,内核还提供了 bpffs 这样的伪文件系统来做 pinning。只要把相关对象 pin 到 bpffs,它们就可以脱离单个用户进程的生命周期,继续留在系统里。

这其中 link 的引入尤其值得注意。早期很多 eBPF attach 方式是"程序直接挂上去",其生命周期管理并不优雅。link 把"程序与 hook 的绑定关系"也变成了一个独立对象,使 attach、detach、update 都能通过 fd 生命周期来管理。这不仅让资源清理更自然,也大幅降低了"加载程序的进程退出后,挂载关系谁来维护"的复杂度。

如果要用一句话总结这一节,那就是:eBPF 在内核里并不是靠隐式状态拼出来的,而是通过 programmaplinkBTF 等对象化抽象来组织自身运行时。

六、Instruction Set 与 BTF:为什么 eBPF 能既高效又可移植

第三章后半部分开始讲 eBPF 指令集与 BTF。这两节看似偏基础,实际上分别对应了 eBPF 的两个核心能力:性能和可移植性。

1. 指令集为什么要尽量靠近硬件 ISA

论文对 eBPF instruction set 的描述相对简洁,但有一个设计原则非常重要:eBPF 的 ISA 被刻意设计得尽量接近真实硬件指令集。

这样做的直接好处,是让 JIT 编译器更容易做近乎一对一的翻译。换句话说,eBPF 指令不是那种语义非常复杂、必须经过大规模 lowering 才能变成机器码的抽象机器语言;它本身就足够贴近底层,因此在通过 verifier 之后,转成原生代码的过程可以很直接。

这也是 eBPF 能在很多性能敏感场景里使用的重要原因。程序虽然从用户态提交进来,但最终执行的常常已经是内核态原生机器码,而不是某种高成本解释器逻辑。

2. BTF 不只是 debug 信息,而是现代 eBPF 的基础设施

如果说 ISA 决定了执行效率,那么 BTF 决定的则是现代 eBPF 的开发体验和分析能力。

BTF,即 BPF Type Format,是一种为 eBPF 场景专门设计的紧凑类型信息格式。相比 DWARF,它更轻量,更适合内核环境,也更适合作为 verifier 和工具链分析时的输入。

BTF 的价值远不止"让调试输出更好看"。它为内核和 eBPF 程序提供了结构体、函数原型、注解以及源码位置信息,使得 verifier 可以在更高语义层面上理解程序正在访问什么对象、使用什么类型、是否遵守了某些上下文约束。

更进一步,BTF 还是 CO-RE 的基础。所谓 Compile Once, Run Everywhere,本质上是在编译阶段保留符号化的结构信息,把不同内核版本之间的字段偏移、枚举值、配置差异推迟到加载阶段再解析。这样一来,同一份编译产物就不必因为目标机器 kernel 小版本不同而频繁重编。

eBPF 生态真正走向大规模工程化之后,BTFCO-RE 的重要性已经不亚于 verifier 本身。它们让 eBPF 从"高手工具"逐渐变成一种更现实可维护的生产技术。

七、Workflow:一段 eBPF 程序是怎么从源码走到内核执行的

第四章是整篇论文里的重点,因为它用一个具体示例,把 eBPF 程序从编写到执行的全过程串了起来。

论文选的例子是一个 XDP 程序,逻辑很简单:检查收到的数据包,如果是 IPv4 UDP,就直接丢弃,否则放行。

这个例子本身并不复杂,但它很好地展示了 eBPF 的整个工作流

第一步,开发者先用高级语言写程序,通常是 C。程序会接收特定 hook 对应的上下文对象,比如 XDP 中的 struct xdp_md。开发者需要在程序中显式做边界检查,例如检查 datadata_end 之间是否有足够空间容纳要读取的头部。这里已经能看到 eBPF 编程的一种独特风格:很多"普通内核 C 程序里理所当然的内存访问",在 eBPF 里都必须写成 verifier 能理解和证明安全的形式。

第二步,程序通过 clang/LLVM 编译成以 bpf 为目标架构的 object file。这个阶段生成的是 eBPF bytecode,而不是本机机器码。

第三步,用户态 loader 读取 object file,解析程序、mapBTF 等元数据,然后通过 bpf() 系统调用把 program 提交给内核。论文中使用的是 bpftool + libbpf 这条常见路径。

第四步,内核中的 verifier 开始工作。它会对程序做控制流校验、符号执行 、安全分析和必要的转换。如果 verifier 认为程序存在不安全之处,就会直接拒绝加载;如果分析通过,则程序继续进入 JIT 编译流程。

第五步,JIT 编译完成后,内核会返回对应 program 的文件描述符。用户态接下来可以通过 BPF_LINK_CREATE 等方式,把这个程序 attach 到具体 hook 上。论文例子里 attach 的对象是网卡 eth0XDP hook

第六步,一旦 attach 成功,这段程序就真正进入系统执行路径。以后每次网络设备收到包时,内核都会在相应时刻调用这段已经 JIT 化的 eBPF 逻辑。

最后,当用户态关闭 link fd 时,程序会从 hook 上 detach;关闭 program fd 后,若没有其他引用,程序对象及其相关资源最终会被释放。

这套流程之所以重要,是因为它能帮助我们建立一个非常清晰的认识:eBPF 的核心不是"写一段运行在内核里的代码",而是"围绕 program object、verifier、attach point 和 fd lifecycle 所组织的一整条受控执行链路"。

也就是说,eBPF 并不是一个"运行命令"的模型,而是一个"编译、加载、验证、挂接、触发、销毁"的运行时模型。

复制代码
  +------------------------------------------------------+
  | 1. 开发者编写 eBPF 程序                              |
  |    - 使用 C 等高级语言                               |
  |    - XDP 上下文: struct xdp_md                       |
  |    - 显式做 data / data_end 边界检查                |
  +------------------------------------------------------+
                            |
                            v
  +------------------------------------------------------+
  | 2. clang / LLVM 编译                                 |
  |    - 目标架构: bpf                                   |
  |    - 生成 object file                                |
  |    - 输出 eBPF bytecode                              |
  +------------------------------------------------------+
                            |
                            v
  +------------------------------------------------------+
  | 3. 用户态 loader 读取 object file                    |
  |    - 解析 program / map / BTF 等元数据              |
  |    - 通过 bpf() 系统调用提交给内核                  |
  +------------------------------------------------------+
                            |
                            v
  +------------------------------------------------------+
  | 4. 内核 verifier 校验                                |
  |    - 控制流检查                                      |
  |    - 符号执行                                        |
  |    - 安全分析                                        |
  |    - 必要转换                                        |
  +------------------------------------------------------+
                   |                        |
            不通过 |                        | 通过
                   v                        v
          +------------------+    +--------------------------+
          | 拒绝加载         |    | 5. JIT 编译              |
          | program 无法进入 |    |    转为本机机器码        |
          | 内核执行路径     |    +--------------------------+
          +------------------+                 |
                                               v
                                  +------------------------------+
                                  | 6. 内核返回 program fd       |
                                  +------------------------------+
                                               |
                                               v
                                  +------------------------------+
                                  | 7. 用户态 attach 程序        |
                                  |    - BPF_LINK_CREATE 等      |
                                  |    - 挂到 eth0 的 XDP hook   |
                                  +------------------------------+
                                               |
                                               v
                                  +------------------------------+
                                  | 8. 网络设备收到数据包        |
                                  +------------------------------+
                                               |
                                               v
                                  +------------------------------+
                                  | 9. 内核触发 eBPF 程序执行    |
                                  +------------------------------+
                                               |
                                               v
                                  +------------------------------+
                                  | 10. 判断: 是否为 IPv4 UDP?   |
                                  +------------------------------+
                                        |                 |
                                     是 |                 | 否
                                        v                 v
                             +----------------+   +----------------+
                             | 丢弃数据包     |   | 放行数据包     |
                             +----------------+   +----------------+

  生命周期结束时:
      close(link fd)  -->  detach from hook
      close(program fd) --> 若无其他引用, 释放 program object 和相关资源

八、前面部分小结

如果只看论文前四章,其实已经可以建立起一个相当完整的高层心智模型。

eBPF 之所以重要,不是因为它提供了一种新的 hook 机制,而是因为它试图在 Linux 内核内部提供一个既动态、又受约束,既高效、又尽可能安全可编程运行时 。为了实现这个目标,它需要一整套协同机制:受限的字节码 ISA、用户态加载器、对象化的 program/map/link 模型、BTF 类型信息、受控 helper 接口,以及最关键的 verifier

理解到这里之后,后续再去看 verifier符号执行 、路径裁剪、资源追踪和 JIT 编译,就不会把它们误解成"某种实现细节"。恰恰相反,它们才是 eBPF 这套设计真正成立的技术支点。


verifier 才是 eBPF 真正的灵魂

如果说前四章帮助我们建立了一个"整体地图",那么从第五章开始,这篇论文才真正进入它最有价值、也最难的部分:eBPF 到底是怎样被证明"可以安全进入内核"的。

很多人在学习 eBPF 时,都会很自然地记住一句话:eBPF 很安全,因为它有 verifier

但这句话其实太粗了。只要稍微往下追问一步,就会冒出一串更本质的问题:

  • verifier 说的"安全"到底是什么意思?
  • 它到底是在验证程序的什么性质?
  • 它为什么要做控制流检查、符号执行和状态裁剪?
  • 为什么同样逻辑上看起来差不多的程序,verifier 会接受其中一段、却拒绝另一段?

论文第五章到第九章,几乎就是围绕这些问题展开的。

如果要先给一个总的结论,那我会这么概括:

eBPF 的安全,不是靠运行时"出问题了再拦",而是靠程序在进入内核之前,先被静态分析到足够可证明安全。

这也是 verifier 会成为整个 eBPF runtime 中最关键、最复杂、也最容易成为瓶颈的组件的原因。

九、Safety of eBPF Program:eBPF 所谓"安全",到底在说什么

第五章一上来做的第一件事,其实特别重要:不是直接讲 verifier 算法,而是先定义"安全"本身。

这一步之所以关键,是因为如果不先把安全目标定义清楚,那么后面看到的一切验证逻辑、类型跟踪、状态裁剪和资源管理,都会显得像是一堆零散技巧。但只要把这些安全属性 先放在脑子里,后面再看 verifier 的各种设计,就会清楚很多:它们其实都在服务这些目标。

1. Memory Safety:eBPF 的第一层底线

第一类安全属性 当然是内存安全 。这几乎是 eBPF 整个模型成立的前提。

在论文的表述里,verifier 要尽力保证:

  • 程序不会发生越界访问
  • 程序不会随意解引用无效指针
  • 程序不能把任意数值当成指针使用
  • 已经释放的对象不会再被访问

这听起来很像大家熟悉的"安全语言"或者"静态分析器"目标,但在 eBPF 的语境里,它的分量更重。因为出问题的地方不是普通应用进程,而是 Linux 内核自己。

也就是说,用户写的一段程序一旦进入内核,它面对的就不是"我这次请求返回错了",而是"我可能直接把系统搞挂"。在这种情况下,内存安全不是一条 nice-to-have 的额外约束,而是最低底线。

这也是为什么 eBPF 编程里你会反复看到一类写法:

  • 先取 data
  • 再取 data_end
  • 每访问一层头部之前,先比较边界

从程序员视角看,这些写法有时会显得啰嗦;但从 verifier 视角看,这正是它理解和证明内存安全的支点。

2. Type Safety:知道"这是什么",和知道"这能不能访问",同样重要

很多人把 verifier 理解成"边界检查器",但论文特别强调,eBPF 的安全并不只是边界问题,它还有很强的类型约束。

verifier 需要知道每个寄存器、每段栈内容到底是什么:

  • 是普通标量值
  • 还是指向上下文对象的指针
  • 还是 map value 指针
  • 还是某类特定内核对象的引用

而且这种"知道"不是泛泛的知道,而是精确到后续行为能不能做:

  • 这个寄存器是不是允许参与某种算术
  • 这个指针是不是允许被解引用
  • 当前偏移是不是还落在合法对象边界内
  • 这块栈数据是不是已经初始化

换句话说,verifier 并不是只想防止程序"踩坏内存",它还想防止程序通过类型混淆把不该做的事做出来。

这一点其实非常接近现代内核安全中的一个共识:很多内核安全问题,本质上不是"访问错了地址",而是"用错了对象"。

3. Resource Safety:程序退出时,不能留下一地没收拾的状态

论文还把资源安全单独提了出来。这部分很有内核工程的味道。

在用户态应用里,如果你忘记释放一块内存、少解一个锁、或者某个引用计数路径漏掉了,最常见的后果是:

  • 内存泄漏
  • 性能下降
  • 程序行为异常

但在内核里,这类问题往往意味着:

  • 资源泄漏积累成系统性问题
  • 锁状态紊乱影响其他路径
  • 内核对象生命周期被破坏

所以 verifier 要检查的不只是"会不会崩",还包括:

  • 有没有未释放的内存
  • 有没有没还回去的引用
  • 有没有忘记 unlock 的锁

这一点非常能说明 eBPF 的特殊性。它不是把一段逻辑塞进内核"试试看",而是要求这段逻辑在离开时也必须保持内核状态整洁。

4. Information Leak Safety:不能把内核的秘密带出去

这一类安全属性,很多纯功能导向的介绍里都会略过,但论文把它明确列出来了,我觉得非常好。

verifier 还要阻止程序把内核里的敏感信息泄漏到用户可见区域,比如:

  • 泄漏内核地址
  • 读取未初始化栈内容
  • 让本应只存在于内核的指针或对象信息逃逸出去

这件事的意义不只在"信息保密"层面,更在于现代漏洞利用中,信息泄漏经常是第一步。很多攻击并不是直接从"任意写"开始,而是先通过信息泄漏建立稳定利用条件。

所以,从 verifier 的角度看,阻止这类泄漏,其实是在守住内核攻击面的另一条边界。

5. Termination:程序必须证明自己会结束

eBPF 之所以能挂到内核热路径上,一个很关键的前提是:它不能无休止地跑。

否则,一段挂在网络路径、tracepoint 或 LSM hook 上的程序,如果无法保证终止,等于随时可能把系统拖死在关键路径上。

因此 verifier 还需要保证程序满足终止性。用更现实的话说,它必须相信:

  • 程序不会形成无法证明结束的无限循环
  • 不会在复杂路径中不断自我膨胀直到不可控

这也就是为什么后面讲循环时,论文会花很大篇幅。

6. Deadlock Freedom:靠强约束换取可分析性

论文还明确提到了死锁相关约束。一个很典型的做法是:

同一时刻不允许持有多个 eBPF spin lock。

从表达力角度看,这当然很保守。你可能会觉得:"这不是太严格了吗?" 但从 verifier 设计者角度看,这种限制带来的收益很明显:

  • 不用做复杂的多锁顺序分析
  • 不用建模各种锁交错关系
  • 可以直接从模型层面极大降低死锁风险

这恰好体现了 eBPF 很典型的设计哲学:宁可保守,也先把事情做成。

7. Upholding Execution Context Invariants:程序不能破坏所在内核上下文

最后还有一类很容易被忽略,但非常重要的安全属性:程序必须遵守它所在执行上下文的基本规则。

因为不同 hook 下,程序拿到的上下文对象完全不同:

  • XDP 拿到的是网络设备驱动层的包上下文
  • tracing 程序面对的是函数或 tracepoint 上下文
  • LSM 程序面对的是安全相关对象上下文

这些上下文本身都带着内核对子系统的假设。如果程序破坏了这些不变量,即便它没有"越界",也依然会把内核运行语义搞坏。

所以 eBPF 的安全,本质上不仅仅是"别乱写内存",还包括:

  • 别误用对象
  • 别泄漏内核信息
  • 别留下资源脏状态
  • 别破坏所处内核路径的上下文规则

把这一整套安全属性 放在一起之后,你再去看 verifier,才会意识到:它并不是一个"简单 loader 检查器",而是一个带着很强内核语义的静态程序分析器。

十、The eBPF Verifier:它为什么是整套体系的中心

在明确了安全目标之后,再看 verifier 的角色,就会变得非常清楚。

论文对 verifier 的定义很直接:

它是一个在程序加载时运行的静态分析器。

注意这里最关键的两个词:

  • 加载时
  • 静态分析

这说明 eBPF 的安全模型不是"先让程序跑,再在运行时限制它的危险行为",而是:

  • 程序进入内核之前,先尽可能证明它是安全的
  • 如果证明不了,即使程序逻辑可能本来没问题,也宁可拒绝

这其实也是 eBPF 和很多用户态沙箱模型的根本差别之一。

1. verifier 不是"简单规则检查",而是一条分析流水线

很多人第一次听说 verifier,容易把它想成"若干 if/else 规则":

  • 指针不能这样用
  • 调用 helper 前得满足什么条件
  • 某些操作不允许

当然,这些规则都存在,但这不是 verifier 的全貌。论文把 verifier 讲得非常清楚:它不是一个简单过滤器,而是一条完整的分析管线。

作者把它概括成四个主要 pass:

  1. 控制流图验证
  2. 符号执行
  3. 验证后的优化与改写
  4. 提交给 JIT

这个拆分特别重要。因为它说明 verifier 不只是"检查程序像不像样",而是在做下面这些事:

  • 建立程序结构图
  • 跟踪程序状态
  • 判断所有可行路径上的安全性
  • 在必要时对程序做优化和重写

从这个角度看,verifier 已经不只是检查组件,它还是 runtime 的一部分。

2. verifier 同时定义了安全边界能力边界

从论文读下来,我觉得 verifier 的地位有一种很有意思的"双重性"。

  • 一方面,它是安全边界 。没有它,eBPF 几乎等于"用户提供代码进内核执行",风险高得不可接受。
  • 另一方面,它也是能力边界 。很多 eBPF 程序之所以"写不出来",不是因为 eBPF 指令集不能表达,而是因为 verifier 很难在现实成本内证明它安全。

也就是说,在今天的 eBPF 世界里,很多能力边界其实不是"语义边界",而是"可证明性边界"。

这件事理解了,后面再看到路径爆炸 、循环难题、状态剪枝 时,就会自然很多。因为你会知道,大家不是在给 verifier 做奇怪优化,而是在努力让"足够强的可证明性"这件事在现实里还能跑得动。

十一、Pass 1:控制流图 验证,为什么 verifier 先关心"程序长什么样"

论文第六章开始讲 verifier 的第一个 major pass:控制流图验证。

乍一看,这一步似乎比较基础。很多读者第一次看到时,可能会觉得它就是:

  • 检查跳转是否合法
  • 看看有没有奇怪的控制流

但实际上,这一步的作用比"程序体检"更重要。它是在为后续最复杂的符号执行阶段搭建骨架。

1. verifier 在这一阶段会检查什么

论文列出的重点大致包括:

  • 是否存在不可达指令
  • 是否存在非法控制流
  • 子程序末尾是否正确结束
  • 是否有不符合要求的循环结构

这些检查本身很好理解。因为如果一个程序的基本结构都不干净,那么后面再细致地做类型跟踪和边界分析,意义就不大了。

2. 为什么"不可达代码"也会成为问题

从很多语言和编译器视角看,不可达代码最多是"多余代码",可能不优雅,但未必危险。

verifier 的出发点不一样。它需要面对的是一个要进入内核的程序对象,所以它会更保守地看待这类结构异常。不可达代码的存在,至少说明了:

  • 程序控制流结构并不干净
  • 可能存在分析上不必要的复杂性
  • 也可能意味着开发者对程序实际执行路径的理解存在偏差

所以 verifier 更倾向于在这一阶段就把这类问题处理掉。

3. 这一阶段还在为状态剪枝做准备

我觉得第六章里特别值得注意的一个点,是论文提到了 pruning points

这说明 CFG 阶段并不是只是"扫一遍程序结构",它还在做一件对后续极其关键的事情:

  • 决定哪些程序点适合保存和比较状态

原因很简单。等到符号执行 开始以后,路径数和状态数很快就会膨胀。如果没有提前设计好在哪里缓存状态、在哪里做等价判断,那么 verifier 很容易在现实中完全算不动。

所以这一阶段其实是在回答两个问题:

  • 这段程序在结构上合法吗?
  • 后面如果要对它做大规模路径分析,应该把哪些位置当成"状态锚点"?

这也是为什么我更愿意把第一个 pass 理解成: 既做结构校验,也为后续搜索策略做预布局。

十二、Pass 2:符号执行verifier 最核心也最难的部分

从论文结构上看,第七章几乎就是全文技术密度最高的部分。

如果说前面的 CFG 验证是在看"程序结构像不像一个可分析对象",那么符号执行阶段看的就是:

沿着程序所有可能路径走下去,它在每个点上的状态是否仍然满足安全要求。

1. verifier 跟踪的不是"真实值",而是"抽象状态"

这一点非常关键。程序被加载进来时,verifier 不会真的拿一个真实网络包或真实上下文跑程序。它要做的是:

  • 抽象地表示寄存器和栈当前"可能是什么状态"
  • 沿路径推进这些状态
  • 在每一步检查这种状态是否仍然安全

论文里把 verifier 状态描述成一个三元组,本质上可以理解为:

  • 当前探索过的指令序列
  • 当前符号存储
  • 当前路径附加状态

换句话说,verifier 真正处理的不是"程序具体跑了一次会怎样",而是"程序所有可行执行中,状态会不会进入危险区域"。

2. 符号存储里到底要保存什么

论文在这部分讲得很细。verifier 至少要跟踪这些东西:

  • 寄存器里是 scalar 还是 pointer
  • 如果是 scalar,它的取值范围可能是什么
  • 如果是 pointer,它指向什么对象
  • 当前偏移是多少
  • 哪段内存区域允许被访问
  • 栈上哪些字节已经初始化

从工程角度看,这已经非常接近一个专门为内核安全定制的抽象解释器了。

而且 verifier 保存的还不只是"值域",还包括很多和后续路径分析密切相关的附加状态,比如:

  • 哪些变量之后还会被用到
  • 哪些值需要保持更高精度
  • 哪些资源已经被获取
  • 当前是否持有锁

也正因为这套状态如此丰富,verifier 才能做到"不是只看一条路径的显式行为",而是对后续风险做静态判断。

3. verifier 的寄存器类型系统,决定了后续几乎所有判断

论文里提到 verifier 内部会给寄存器赋予一套抽象类型,例如:

  • NOT_INIT
  • SCALAR_VALUE
  • PTR_TO_CTX
  • PTR_TO_MAP_VALUE

这些类型标签非常关键。因为后续很多规则都依赖于它:

  • 哪个 helper 可以被调用
  • 哪类指针可以参与何种算术
  • 哪些内存区域允许解引用
  • 栈上的数据能否被安全读写

如果失去这层类型信息,verifier 就只能把所有寄存器都当成"一个 64 位整数"。那样一来,它不仅看不懂程序,也无法证明程序对内核上下文的访问是否合法。

所以从某种意义上说,寄存器类型系统是 verifier 的"内部语义语言"。

十三、数值抽象、tnum 与为什么 verifier 能证明边界

很多人第一次了解 verifier 时,心里都会冒出一个自然疑问:

它又没真的执行程序,怎么知道某个偏移一定不会越界?

答案就在论文提到的数值抽象域里。

1. 只做简单区间分析还不够

如果 verifier 只是维护"这个寄存器大概在某个最小值和最大值之间",那很多情况下信息会太粗。

比如:

  • 某些位其实是确定的
  • 某些位是不确定的
  • 某些值虽然落在大区间里,但在具体位模式上又受限

这时,如果只用简单上下界,verifier 很容易因为"不够确定"而变得过度保守。

2. tnum 的作用,是更精细地表达"不确定中的确定"

论文提到 verifier 使用 tnum 来描述值的一种抽象形式:有些 bit 已知,有些 bit 未知。

它的意义不在于"数学形式多优雅",而在于:

verifier 因此可以更精细地跟踪值的可能集合,而不是只拿一个粗糙区间糊住一切。

配合区间分析之后,verifier 就更有能力判断:

  • 当前偏移是否仍在对象边界内
  • 某次内存访问是否可能越界
  • 某个条件表达式是否可以在静态阶段被进一步收紧

3. 这也解释了为什么 eBPF 程序写法经常"很讲究"

同样逻辑含义的代码,不同写法对 verifier 的友好度可能差很多。不是因为 verifier 在故意刁难开发者,而是因为:

  • 某些写法更容易保留精度
  • 某些写法更容易让值域收敛
  • 某些写法会让指针偏移与边界关系更清晰

这也是为什么 eBPF 编程经常给人一种强烈感受:

你不是只在给编译器写代码,也是在给 verifier 写代码。

十四、条件跳转与状态分叉:路径爆炸从哪里开始

一旦 verifier 进入符号执行,它最怕遇到的事情之一就是条件分支。

1. 条件无法静态确定时,状态就必须分叉

verifier 遇到一个条件跳转时,它会先尝试判断:

  • 这个条件在当前状态下是否一定为真
  • 或者一定为假

如果可以判断,那最好,只需要沿一条路径继续走。

但如果无法确定,它就必须把当前状态分成两份:

  • 一份走 true 分支
  • 一份走 false 分支

从这一刻起,后面所有寄存器范围、指针偏移、资源状态,都可能沿着两条路径各自演化。

2. 分支一多,状态空间会迅速膨胀

这件事的可怕之处在于,它不是线性增长。

每多一个关键分支,后面可能就多一倍状态组合;如果这些分支又嵌套在循环体里,复杂度会非常快地失控。

所以 verifier 的问题并不只是"程序长不长",而是"程序可行路径有多少"。一段指令数不算特别大的程序,只要路径结构复杂,verifier 一样会很痛苦。

3. 后面很多技巧,本质上都在对抗这个问题

等你看到 loops、state pruning、liveness、precision tracking 时,会发现它们其实都在围绕一件事打转:

如何在不漏掉危险路径的前提下,尽量少走冗余路径。

十五、Loops:为什么循环一直是 verifier 的难点

循环是 eBPF verifier 的经典难题,论文这一节讲得非常坦诚。

1. 有界循环相对容易,因为可以展开

如果 verifier 能明确知道循环上界,最自然的办法就是展开验证。这样做的好处是简单直接:展开之后,本质上又回到了普通路径分析问题。

但问题也很明显:

  • 上界可能不小
  • 每次展开都会复制循环体路径
  • 如果循环体里还有条件分支,状态数会继续膨胀

所以"有界"并不等于"容易",只是比完全无界稍微好处理一点。

2. 无界循环必须借助额外语义

论文提到,一些依赖特定 helper/iterator 模式的循环可以被接受。这里的关键不是 verifier 神奇地证明了任意循环都会结束,而是:

  • 某些 helper 本身携带已知的终止语义
  • verifier 可以借助这种额外语义,把问题缩小到"状态是否最终收敛"

也就是说,它靠的不只是程序文本本身,而是程序和受控运行时接口共同构成的语义。

3. 循环真正的难点,是它让状态更难收敛

一个没有复杂分支的短循环,很多时候并不可怕。真正麻烦的是:

  • 循环体里有条件跳转
  • 不同迭代之间寄存器范围不断变化
  • 指针偏移持续演化
  • 资源状态在不同路径上分裂

当这些东西叠加在一起时,verifier 很难判断什么时候"已经看够了",也很难保证复杂度不会爆掉。

这也是为什么循环不是一个单独问题,而是路径爆炸、状态等价和精度控制共同交汇的地方。

十六、State Pruning:verifier 怎样在现实里不把自己算死

面对路径爆炸verifier 当然不能老老实实把所有路径都完整跑完。论文因此引出了一个很关键的机制:state pruning

1. 它的核心思想,是"如果后续等价,就没必要重复探索"

在某些特定程序点,verifier 会把已经探索过的状态记录下来。之后如果又来到同一个程序点,它会尝试判断:

当前状态与先前某个状态,从后续程序安全性的角度看,是否已经足够等价。

如果是,那么后面的路径就可以不必再完全展开。

这听起来像一个优化技巧,但实际上它几乎是 verifier 能否在真实世界里工作下去的关键机制。

2. "等价"不是完全相同,而是"对后续安全判断已经够相同"

这里最容易误解的一点是:state pruning 要找的不是两个状态在每个细节上完全一致,而是:

  • 某些差异对后续判断已经无关紧要
  • 某些变量后面根本不会再用到
  • 某些值即便不完全一致,也已经不会影响后续安全属性

这就引出了后面提到的 liveness 跟踪与 precision 标记。

3. 这是一种非常典型的工程折中

我读到这里时一个很强烈的感受是:verifier 的很多设计都不是在追求"最优雅的理论形式",而是在追求"现实中足够安全、又能算得完"。

state pruning 就是这样:

  • 不剪枝,很多程序永远验证不完
  • 剪枝过头,又可能误把危险路径当作冗余路径跳过

所以它的难点不只是算法复杂,而是这个平衡本身就很难把握。

十七、Resource Management:verifier 还要替内核管资源出入账

如果说路径和类型分析体现了 verifier 的"程序分析器"属性,那么资源管理这部分则体现了它非常鲜明的"内核守门人"属性。

1. verifier 跟踪的不只是值,还跟踪资源责任

论文讲得很清楚,程序在运行中可能会通过 helper 获得不同形式的资源,例如:

  • 某个内核对象的引用
  • 一段被分配出来的内存
  • 某个锁的持有状态

这些资源如果不能在路径结束时被正确释放,就会直接影响内核的长期稳定性。

2. 为什么这件事必须在 verifier 里解决

因为 eBPF 程序进入的是内核路径,而不是用户态试验场。内核不能接受一种"先运行,资源慢慢回收看看会怎样"的模型。

它需要的是:

  • 每条路径都能被证明收支平衡
  • 不会遗留引用
  • 不会锁不释放
  • 不会对象生命周期乱掉

因此 verifier 要对 acquire/release 语义有精确跟踪。

3. 单锁限制虽然强硬,但很符合 eBPF 的哲学

论文提到的"同一时刻只允许持有一个 eBPF spin lock",其实非常能代表 eBPF 的整体思路。

从表达力角度看,这肯定牺牲了一部分能力;但从验证复杂度和系统风险角度看,它换来了非常大的确定性。

eBPF 很多地方都在做类似选择:

  • 限制更多一点
  • 换取更强可证明性
  • 先让体系稳固成立

这也是为什么我觉得 eBPF 的设计虽然常常显得保守,但这种保守是"内核级可编程能力"必须付出的代价。

十八、Pass 3:verifier 不只是保安,它还是程序进入内核前的最后优化器

很多人会把 verifier 想成一个"只会拒绝程序"的组件,但论文第八章给出了一个很有意思的事实: verifier 在验证通过后,还会做优化和改写。

1. Dead Code Elimination:有些死代码,verifier 比编译器更有机会删

原因在于,编译器在离线编译时并不知道目标内核的很多真实信息,而 verifier 在加载时已经知道更多上下文:

  • 当前目标内核环境
  • 某些对象的具体类型
  • 某类路径到底会不会发生

因此 verifier 可以更激进地消除那些在实际目标环境中根本不可能触发的分支。

这件事非常有意思,因为它意味着 verifier 不只是静态检查器,它还拥有一点"目标环境特化优化器"的味道。

2. helper 调用重写:从通用接口路径变成更直接的实现路径

对于一些 helper 调用,尤其和 map 访问相关的 helperverifier 在验证阶段已经知道足够多的对象与类型信息,于是可以把通用 helper 路径改写成更具体、更直接的底层实现调用。

这本质上是一种运行前 program rewriting。

3. 这进一步说明 verifier 本身就是 runtime 的一部分

如果一个组件不仅能判定"你能不能进",还会在"你能进之后"继续改写你、优化你,那它就已经不只是门卫,而是托管运行时的重要参与者了。

十九、Pass 4:Just-In-Time Compilation,eBPF 为什么最终能跑进热路径

第九章进入 JIT。很多人知道 eBPF 快,是因为它不是一直靠解释器,而是可以编译成原生机器码。论文在这里把这件事讲得非常系统。

1. JIT 的四个主要步骤

作者将 JIT 编译过程概括成四步:

  • 估算并分配 JIT image
  • 生成 prologue
  • 翻译主体指令
  • 生成 epilogue

这个结构和很多编译器后端思路有点像,但关键差异在于:eBPF JIT 面对的是一种已经被 verifier 保证满足一系列约束的受限程序表示。

2. eBPF ISA 的设计,让 JIT 更容易接近一对一翻译

这又回到了前面讲过的一个核心点:eBPF 指令集本身就尽量贴近真实硬件 ISA。

因此 JIT 在很多情况下不需要做过重的 lowering,而可以更直接地把 eBPF 指令映射成目标架构机器指令。

这带来的收益很直接:

  • 编译开销相对可控
  • 运行开销低
  • 程序有机会进入纳秒级或微秒级敏感路径

如果没有这层能力,eBPF 很可能只能停留在"偶尔做点控制逻辑"的层面,而很难真正参与 XDP 这类超热路径。

3. JIT 也不是只追求快,它同样要面对安全问题

论文提到了 constant blinding 等安全加固手段。这些机制的存在提醒我们:JIT 不只是一个性能组件,它也是安全攻击面的组成部分。

一旦把受控字节码翻译成可执行机器码,就必须考虑:

  • 是否会被利用做 JIT spraying
  • 立即数模式是否可能帮助攻击构造
  • 生成后的代码是否可能被后续篡改

因此 JIT image 最终会被设为只读,某些立即数还会做混淆处理。

这说明 eBPF runtime 的设计思路始终一致:性能可以追求,但不能以放松内核 安全边界为代价。

二十、Part 2 小结

如果说 Part 1 讲的是 eBPF 为什么会出现、整体运行时长什么样,那么 Part 2 真正回答的是:

为什么 Linux 内核敢接受这样一种"动态加载程序"的机制。

答案并不神秘,但也绝不轻松。它依赖的是一整套非常重的加载前静态分析体系:

  • 先定义清楚安全属性
  • 再验证控制流图
  • 然后对程序做符号执行
  • 状态剪枝 对抗路径爆炸
  • 跟踪资源生命周期
  • 在验证后继续做优化和改写
  • 最后把结果交给 JIT,以原生机器码形式执行

也正因为 verifier 肩负了这么多职责,它才既是 eBPF 最核心的支点,也是今天 eBPF 最复杂、最难扩展、最容易成为能力瓶颈的部分。


eBPF 已经很强,但它还远没有"没有代价地强"

如果说 Part 1 帮我们建立了整体地图,Part 2 解释了这套机制为什么在技术上能够成立,那么从第十章开始,这篇论文就回到了一个更现实的问题:

eBPF 现在到底已经被用来做什么,它又卡在什么地方。

这也是我很喜欢这篇论文的一点。它没有把 eBPF 写成一种"功能无穷、边界完美、未来只会一路上扬"的技术神话,而是非常坦率地承认:eBPF 的确已经很强、很成熟、很有现实价值,但它的成本、约束和未解问题同样非常真实。

所以重点整理三件事:

  • eBPF 为什么会从网络一路扩展到 profiling、tracing、安全、调度甚至存储
  • 论文认为 eBPF 当前最大的现实挑战是什么
  • 读完之后,我们到底应该把 eBPF 放在 Linux 技术栈里的什么位置去理解

二十一、Use Cases:eBPF 为什么会迅速从网络走向更广阔的内核扩展层

很多人最早知道 eBPF,往往是从网络或者可观测性工具开始的。但论文第十章其实在传达一个更大的判断:

eBPF 已经不再只是"某类场景下很好用的增强技术",而是在逐渐演化成 Linux 的通用内核扩展层。

这件事不是口号,而是可以从它已经被用到的场景里看出来。

1. Networking:eBPF 最成熟、最成功、也最符合它气质的战场

网络几乎是 eBPF 最天然的主场。

一方面,网络路径本来就有大量事件驱动的 hook 点;另一方面,网络场景对:

  • 高性能
  • 动态插入逻辑
  • 低扰动灰度试验
  • 与现有内核栈协同

的需求都特别强。

论文列出的几个典型场景非常有代表性。

XDPTC:高性能数据路径上的直接可编程性

这是很多人最熟悉的 eBPF use case。XDP 允许程序在网络驱动层极早期看到包,并做:

  • 丢弃
  • 重定向
  • 重写
  • 负载均衡
  • DDoS 缓解

TC 则提供了在流量控制和队列路径上插入逻辑的能力。

这类能力之所以重要,不只是因为它们"很快",而是因为它们提供了一种此前非常昂贵的事情:在不打内核 patch 的前提下,对数据平面行为进行快速试验和迭代。

socket lookup / reuseport:更灵活的流量导向

论文还提到了 socket 选择相关的 hook。它们的价值在于:流量不必再完全按照传统内核默认逻辑去选择目标 socket,而是可以结合:

  • IP 与端口组合
  • NUMA 局部性
  • 连接迁移需求
  • 应用自定义策略

做更灵活的决策。

换句话说,eBPF 在这里提供的不是简单的"拦截",而是把一部分原本写死在内核里的分发策略变成了可编程策略。

cgroup hooks、ULP、拥塞控制:网络之外的"协同定制"

论文里提到的 cgroup 网络 hook、ULP(如 SK_MSG)、拥塞控制回调等场景特别能说明 eBPF 的一个核心优势:它不一定要完全替代原有栈,而是可以在保留原有栈大部分能力的前提下,对某个局部决策点做定制。

这也是为什么我一直觉得,eBPF 最厉害的地方不在"取代 Linux",而在"给 Linux 加一层高密度的可编程能力"。

2. Profiling:低开销持续观测,改变了很多团队的线上诊断方式

eBPF 在 profiling 领域的价值,过去几年已经越来越明显。

论文提到,把 eBPF 程序挂到 perf 事件上,可以相对低开销地收集:

  • 栈信息
  • 采样数据
  • 硬件性能计数器相关信息

这件事之所以重要,是因为线上 profiling 长期以来都在一个很尴尬的位置:

  • 太重的工具,不敢长期开
  • 太轻的工具,看不到足够多内部细节

eBPF 的介入,让"连续 profiling"这件事变得更现实。它不是完全零成本,但已经足够低到很多生产场景愿意接受。

从工程角度看,这种变化意义很大。因为它意味着运维和研发不再只能靠"出问题时临时抓一次",而是可以长期积累运行态剖面。

3. Tracing:eBPF 几乎重塑了现代 Linux 的动态追踪体验

如果说网络是 eBPF 的技术高地,那么 tracing 可能是它在实际普及层面最具决定性的领域。

论文提到,通过 kprobeuprobetracepointhookeBPF 程序可以在函数入口、函数出口或预定义内核事件上执行。

这件事的价值非常直接:

  • 不需要改内核源码
  • 不需要重启系统
  • 不需要大规模侵入原路径
  • 却能在运行时拿到很深入的内核和用户态行为数据

对很多团队来说,真正第一次体会到 eBPF 的威力,往往不是从 XDP 开始,而是从 tracing 工具开始。因为 tracing 直接改变了一个长期存在的痛点:线上定位问题时,过去很多几乎看不见的内核路径,现在可以被低扰动地看见了。

4. Security:eBPF 不再只是观察者,也开始成为策略执行者

这一点我觉得非常值得重视。

很多人把 eBPF 理解成一个"观测工具技术栈",也就是说它擅长的是:

  • 看日志之外的东西
  • 看性能计数器之外的东西
  • 看内核路径上的状态变化

但论文在安全场景里提到的 LSM BPF,说明 eBPF 已经开始进入另一个角色:不仅看,还参与裁决。

通过挂到 LSM hookeBPF 程序可以做:

  • 安全策略判定
  • 审计
  • 基于上下文的访问控制逻辑
  • 与 cgroup、对象本地存储等能力组合

这意味着 eBPF 的地位已经从"旁路观测层"向"核心策略层"移动。

当然,论文没有夸大这件事。它并没有说 eBPF 可以无痛取代传统安全模块,而是在说:eBPF 已经为 Linux 安全框架提供了一种新的、动态的、可编程的能力注入方式。

5. Emerging:驱动、调度、存储,说明 eBPF 正在向更核心区域扩张

论文列出的新方向包括:

  • HID-BPF
  • SCHED_EXT
  • 存储路径相关扩展

这些场景特别能说明一件事:eBPF 现在已经不满足于"在外围打补丁",而是在尝试进入一些过去几乎默认只能靠内核源码修改才能干预的区域。

例如:

  • 驱动路径意味着更靠近硬件
  • 调度意味着更靠近内核核心资源分配逻辑
  • 存储意味着更靠近系统 I/O 基础设施

从这个角度看,eBPF 的发展方向非常值得关注。它正在把"运行时可编程内核"这个概念,不断推进到更深的系统层次。

二十二、Challenges:论文最可贵的地方之一,是它讲清了 eBPF 的难点

如果说 use case 展示的是 eBPF 的上限,那么挑战部分讲的就是它的代价和边界。

我特别喜欢第十一章,因为作者没有把 eBPF 写成一个"已经完美成熟"的体系。恰恰相反,他们很坦率地承认:eBPF 的确很强,但它当前的若干核心问题不仅没消失,反而会随着场景越来越多而变得更明显。

1. Usability:会用和用好之间,还有很高的门槛

今天的 eBPF 已经比早期好用很多了,但如果站在"普通系统开发者"视角,它依然不算一项轻松技术。

现实里的门槛包括:

  • hook 类型很多,选择并不直观
  • 不同 program type 的约束差异明显
  • verifier 报错常常不够友好
  • 同一段程序跨 kernel 版本时仍然需要理解不少兼容细节

也就是说,eBPF 今天虽然生态繁荣,但还没有进入"默认轻松好用"的阶段。它更像一种高能力专业工具,需要较强系统背景做支撑。

2. Scalability of the Verifier:很多表达力瓶颈,本质上是证明能力瓶颈

这一点我觉得是论文最值得记住的结论之一。

很多时候,eBPF 不是"语义上不能表达",而是:

verifier 很难在现实复杂度约束下证明它安全。

路径爆炸 、循环体复杂、状态分叉过多,这些问题在 Part 2 已经看到了。论文在挑战部分把它们重新放回更大的视角里,指出这已经成为 eBPF 继续扩展能力的重要瓶颈。

也就是说,今天 eBPF能力边界 很大一部分并不是 ISA 决定的,而是 verifier 当前静态分析能力决定的。

这点理解了,你就会知道为什么很多 eBPF 改进讨论,最终都会绕回 verifier

3. Correctness of the Verifier:最终仲裁者自己也必须足够可靠

verifier 在整个 eBPF 安全模型里几乎是"最后仲裁者"。这带来一个非常严肃的问题:

如果 verifier 自己有 bug,会怎样?

答案其实很直接:

  • 如果 verifier 太保守,大量本来安全的程序会被拒绝
  • 如果 verifier 太宽松,危险程序可能被放进内核

verifier 本身又是一个体量非常大、持续快速演化的代码库。每增加一种新 use case、一个新 helper、一个新 program type,都可能让 verifier 的分析逻辑进一步变复杂。

这意味着 verifier正确性 问题并不是"学术上的担忧",而是 eBPF 体系最现实的系统性风险之一。

4. Formal Verification:已经有研究进展,但完整形式化依然是开放问题

论文提到,一些工作已经开始针对 verifier 的局部部件做形式化研究,例如:

  • 数值抽象域的形式化
  • range analysis 的形式化验证
  • eBPF JIT 正确性的验证

这些工作都很重要,因为它们说明 eBPF 并不只是"工程上凑出来的安全",而是有人在努力把它的关键部分往可证明方向推进。

但作者同样强调,到论文写作时为止,还没有针对完整 verifier 的全面形式化验证

这意味着今天的 eBPF 更像一种:

  • 工程上已经非常有价值
  • 理论上仍在继续补强

的体系,而不是一套已经从理论到底层都完全封闭的模型。

5. Security:eBPF 越强,越需要谨慎的权限和攻击面管理

论文在安全挑战部分明确提到,eBPF 过去曾被当作提权或利用路径的一部分。这一点其实不难理解:只要一种机制允许用户提供程序进入内核,它天然就会成为高价值攻击面。

因此,内核社区对无特权 eBPF 的态度越来越谨慎,很多能力默认都只服务于受信任用户或需要特定 capability 的场景。

这背后的逻辑其实非常简单:

  • eBPF 很强
  • 它强,是因为它离内核太近
  • 离内核越近,攻击面价值越高
  • 所以权限模型必然不能太松

也就是说,eBPF 的强大能力和它的安全约束,是同一件事的两面。

6. Code Reuse:CO-RE 解决了跨版本,却没有彻底解决大型工程复用问题

论文最后提到的代码复用问题,我觉得特别工程化,也特别容易被忽视。

CO-RE 的确解决了很多跨 kernel 版本适配问题,但这并不等于 eBPF 工程已经像普通应用开发那样拥有成熟的复用体系。

在大型项目里,大家依然会面对:

  • 多个程序间共享逻辑如何组织
  • 通用组件如何复用
  • 如何在不破坏 verifier 可理解性的前提下提高抽象层次

这说明 eBPF 虽然已经具备很强工程能力,但从"像成熟应用生态那样开发"这个角度看,它仍有明显距离。

二十三、我读完之后的几个核心感受

到这里,论文的主要内容其实就已经差不多了。最后我想把我自己读完之后最强的几个感受整理一下。

1. eBPF 改变的,不只是"功能实现方式",而是 Linux 的扩展方式

过去如果你想让 Linux 更适配自己的 workload,通常只有两个极端选择:

  • 改 Linux
  • 绕开 Linux

eBPF 让中间地带第一次变得足够现实:

  • 保留 Linux
  • 在 Linux 内部插入自定义逻辑
  • 用强约束、强验证和受控接口来限制风险

这不只是一个技术特性升级,而是操作系统扩展方式的一次很重要的变化。

2. verifiereBPF 的灵魂,也是它最大的代价来源

没有 verifier,就没有今天的 eBPF

但与此同时,verifier 也带来了:

  • 学习门槛
  • 编程风格约束
  • 表达力瓶颈
  • 实现复杂度
  • 正确性压力

所以 verifier 既是 eBPF 最强的技术支点,也是它最脆弱、最昂贵的部分。

从某种意义上说,eBPF 的未来,很大程度上也取决于 verifier 能走多远。

3. BTF / CO-RE 是现代 eBPF 真正工程化的关键基础

如果只有 verifierJIT,而没有 BTF / CO-RE,那么 eBPF 很可能依然是一项"高手工具":

  • 很强
  • 很底层
  • 但大规模工程化维护非常痛苦

真正让它在现代工程里逐渐变得可维护、可移植、可组织的,是类型信息与重定位能力这套基础设施。

换句话说,eBPF 能走到今天,靠的不只是"安全"与"性能",也靠"工程可用性"的持续补强。

4. eBPF 已经很成熟,但还远没成熟到"谁都能放心乱用"

我觉得这是最需要保持清醒的一点。

它成熟在:

  • 已经有大量现实 use case
  • 工具链已经相当丰富
  • 核心安全与性能模型已经比较清晰

它不成熟在:

  • verifier 仍然很复杂
  • 可用性仍有明显门槛
  • 形式化验证仍不完整
  • 权限和攻击面问题一直存在

所以今天的 eBPF,更像一种强大、成熟、但需要敬畏的系统能力,而不是一个可以随意使用的"普通特性"。

简单总结

《The eBPF Runtime in the Linux Kernel》最重要的价值,不是教你如何写一个 eBPF demo,而是系统解释:

  • eBPF 为什么会出现
  • Linux 内核里的 eBPF runtime 由哪些核心组件构成
  • verifier 如何通过控制流验证、符号执行状态剪枝 和资源跟踪来建立安全边界
  • JIT 如何让这些程序最终以高性能形式进入内核热路径
  • eBPF 为什么会从网络扩展到 tracing、profiling、安全、调度、驱动与存储
  • verifier 的扩展性、正确性和形式化证明为什么仍是未来的核心难题

如果再压成一句话,那就是:

eBPF 是 Linux 内核中的安全可编程运行时,而 verifier 是这套运行时成立的核心前提。 eBPF 的重要性,不在于它让 Linux 多了几个可插拔钩子,而在于它正在把 Linux 变成一个可以在运行时被安全重编程的系统。

博文部分内容参考

© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知 😃


《The eBPF Runtime in the Linux Kernel》https://arxiv.org/abs/2410.00026


© 2018-至今 liruilonger@gmail.com, 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)