- Daily Report
- eBPF
- Userspace eBPF
- Runtime Systems
- bpftime
用户态 eBPF 要成为真正的运行时,还缺什么?
用户态 eBPF 已经能在内核外执行字节码,但真正可部署的运行时还需要明确的挂载、能力、安全、状态生命周期和资源归属契约。
假设运维人员想给一个数据库进程加动态观测,又不想修改应用,也不希望每次高频事件都绕进内核再回来。把 eBPF 放到用户态执行看起来很直接:准备一个虚拟机,把 BPF 字节码解释执行或 JIT 成本地代码,然后在目标进程里运行。
真正部署时,麻烦却发生在第一条 BPF 指令执行之前。程序到底挂到哪个函数、哪个模块版本、哪个进程实例?目标库被热更新之后,旧地址还能不能继续用?这个程序允许访问哪些内存、调用哪些 helper 或宿主函数?map 属于谁,detach 之后还在不在?如果注入只完成了一半,谁负责恢复?谁又有权在之后撤销这个扩展?
这些都不是指令集问题,而是运行时问题。
BPF 指令集本身已经走到了标准化阶段。IETF 的 RFC 9669 定义了 BPF ISA,使不同实现可以共享一套指令语义。但是 Linux eBPF 之所以能成为完整的平台,并不只是因为内核能执行这些指令。它同时提供程序和 map 对象、验证器、helper 和 kfunc、程序类型、attach 类型、BPF link、pinning、对象生命周期、权限检查以及用户态管理接口。
本文的判断是:用户态 eBPF 如果要从“能执行 BPF 的 VM”变成真正可部署、可复用的运行时,需要在 ISA 之上增加一层可移植的执行契约。 这层契约至少要说明五件事:挂载目标如何标识和重新验证、程序拥有哪些能力、状态由谁拥有、对象如何创建和销毁,以及一个扩展消耗了多少资源并产生了哪些影响。
这并不意味着所有后端必须实现成一样。相反,契约的价值在于把差异说清楚,让程序在运行之前就知道自己依赖哪些语义,让运行时可以明确回答“我支持其中哪些部分”,并用测试验证这个回答。
今天的实现已经说明这种区分是必要的。uBPF 是一个可嵌入的用户态 BPF VM,提供解释器、JIT、helper 注册以及新的安全执行配置。bpftime 则把范围扩大到 loader、map、helper、验证、多个 attach 后端和 bpf_link 风格对象,并尝试兼容现有 libbpf、bpftrace 工作流。eBPF for Windows 也复用了 BPF 指令格式和常见 libbpf API,但需要自己提供 Windows 上的 hook、helper、验证器路径和执行方式。它们共享的是程序语言,却并没有天然共享完整的运行时语义。
BPF VM 和 BPF 运行时不是一回事
判断一个系统是否“支持 eBPF”时,至少应该拆成三个不同问题。
第一个问题是:它能不能正确执行 BPF 指令? RFC 9669 解决的是这一层。后端可以解释执行,也可以把 BPF JIT 成 x86-64、Arm64 或其他本地代码。
第二个问题是:BPF 程序看到的执行环境是什么? 程序并不只依赖寄存器和 opcode。它还依赖 context 结构、helper、map、可访问内存、返回值约定,以及触发程序的事件。两个 VM 完全可以都正确实现同一套 ISA,却在这些地方毫不兼容。
第三个问题是:运维人员如何把它当成一个长期存在的系统对象管理? 这涉及 load、verify、attach、查询、更新、detach、状态所有权、权限、诊断和异常清理。一个 VM 能稳定运行函数,并不自动意味着它能在目标进程升级、崩溃、重启或权限变化时保持可理解的行为。
uBPF 很适合说明这个边界。它的 README 明确把项目定位为执行 eBPF 程序的库,提供 assembler、disassembler、interpreter 和 JIT。它的新安全模式增加了指针来源跟踪、越界检查、带类型的 helper 元数据和外部内存区域注册。这些都是有实际意义的执行安全语义。
但把 uBPF 嵌进一个程序之后,仍然要由宿主自己决定事件是什么、怎么找到目标、程序如何命名和撤销、多个 VM 是否共享状态,以及进程退出时如何清理。这个职责不应该强塞给一个轻量 VM。问题在于,如果生态只用“能跑同样的 BPF 字节码”判断兼容性,就会把这一大层运行时语义漏掉。
Linux eBPF 实际上已经有一套更完整的运行模型
Linux 上这些语义很容易被视为理所当然,因为它们已经由内核和 libbpf 提供。
内核的 libbpf 生命周期文档 把一个应用明确分为 open、load、attach 和 tear down。load 阶段会创建 map、处理 relocation、验证并加载程序,而且允许在程序真正开始执行前初始化状态。attach 阶段再把程序连接到具体 hook。tear down 则解除连接并释放资源。
bpf() 用户态 API 进一步把 map、program、link 等对象放进基于文件描述符和引用计数的生命周期中。需要长期存在的对象可以 pin 到 BPF 文件系统。BPF link 把“某个已加载程序挂在某个目标上”本身变成了可管理对象。较新的 BPF token 又把允许使用的 BPF 命令、map 类型、program 类型和 attach 类型变成可委托的能力集合,而不是只有“有权限”和“没权限”两种状态。
这些都不属于 BPF ISA,但真实应用每天都在依赖它们。
因此可以给“真正的运行时”一个更有用的定义:运维人员不仅能解释程序执行时的指令,还能解释它在执行前如何被授权和挂载、执行过程中拥有何种状态、失败后如何清理,以及最终如何被撤销。
把执行搬到用户态以后,attach 本身变复杂了
用户态 eBPF 的吸引力很明确。高频事件可以少一次内核往返;内核 BPF 不可用或权限受限时仍然能够扩展应用;程序还能更直接地访问应用内部函数和数据。代价是,过去由内核替我们定义的很多边界需要重新显式化。
以内核 uprobe 为例,Linux tracing 栈会参与管理进程或二进制位置上的挂载。用户态运行时如果使用动态二进制改写,则可能直接 patch 函数入口、系统调用路径、语言运行时、数据包处理循环,甚至 GPU 设备代码。目标可能用 symbol、build ID、模块偏移、JIT 方法名或者驱动事件 ID 表示,而且这些目标的生命周期和 BPF 程序本身并不一致。
因此,一个可移植的 attach 描述至少要能回答:
- 选中的到底是哪个进程、可执行文件、模块、函数或事件版本?
- 真正修改目标之前,哪些条件必须再次验证?
exec、fork、动态库 reload 或目标替换后,挂载关系如何变化?- 另一个主体能不能查询、更新或撤销这个挂载?
- 如果注入、patch 或回调注册只成功一部分,运行时保证清理到什么程度?
bpftime 的 attach 机制 已经需要面对这些问题,因为它不仅处理 uprobe 和 syscall,还尝试支持 XDP 风格路径、GPU instrumentation 以及可插拔事件后端。其 OSDI 2025 工作 Extending Applications Safely and Efficiently 提出了 Extension Interface Model(EIM),把扩展需要的能力看成资源,例如一段内存的访问权,或者调用应用函数的能力,再由扩展管理者明确授予。
EIM 的价值不只在隔离。它提示了一个更一般的方向:运行时应该先描述扩展“需要什么”,然后再选择具体后端去实现这些能力,而不是先暴露一堆实现细节再让程序猜环境。
“兼容 eBPF”应该拆成多个层级
现在一句“compatible with eBPF”可能表达完全不同的事情。
| 兼容层级 | 真正承诺的内容 | 仍然可能不同的部分 |
|---|---|---|
| ISA | 同样的 BPF 指令有一致执行语义 | context、helper、map、attach、生命周期 |
| 对象和工具链 | 能消费 clang 生成的 ELF/BTF 等产物 | relocation、CO-RE 数据、helper 可用性 |
| API | loader 可以调用熟悉的 libbpf 风格接口 | 支持的 API 子集和失败语义 |
| 程序环境 | 某类程序得到相近的 context 和 helper | 目标身份、权限、生命周期、性能 |
| 运维行为 | load、attach、查询、撤销和清理可预测 | 具体后端实现和成本 |
eBPF for Windows 是很直接的证据。项目刻意复用既有 eBPF toolchain 和 libbpf API,但 README 同时明确说明它需要 Windows 特有的 hosting layer、hook、helper、验证流程,并且可以选择 native、JIT 或 interpreter 等不同执行路径。源代码兼容当然有价值,但宿主操作系统能提供什么语义,不能被一个 API 名字抹平。
用户态 eBPF 也应该采用类似的分层表述。一个运行时完全可以声明:“支持 RFC 9669 ISA;支持这一组 ELF/BTF 和 libbpf loader 操作;uprobe attach 满足某个版本的目标身份和生命周期语义;共享 map 可以跨进程保留;helper 能力支持委托和撤销。”这种声明看起来不如一个“100% compatible”标签简洁,但它可以测试,也能帮助真正的部署决策。
一套可用的用户态运行时契约至少需要五部分
不必把所有后端实现细节都标准化。下面五部分已经覆盖大多数运维语义。
1. 挂载目标的身份和前置条件
挂载描述必须能在执行前重新验证,而不是只保存一个容易失效的地址。
对本地进程,身份可能由 PID namespace、可执行文件 build ID、模块 build ID、symbol/offset 和目标 generation 共同组成。对网络路径,可能是接口、queue 和驱动能力。对 GPU,则可能要绑定模块和 kernel identity。
一旦这些前置条件不再成立,运行时应该明确失败,而不是把程序悄悄挂到“当前碰巧占据同一个地址或名字”的对象上。
2. 程序能力和 context
程序需要声明自己依赖的 helper、宿主函数、context 字段、map、内存区域和副作用。Linux 通过 program type、验证器规则、helper/kfunc、capability 和 BPF token 表达其中很大一部分。uBPF 的安全模式把 helper 元数据和可访问外部内存显式化。EIM 则把应用扩展需要的资源显式化。
用户态契约不必强制所有后端用同一种隔离技术。更合理的方式是先声明能力表面,再让后端证明自己能够提供并约束这些能力。
3. 状态所有权和生命周期
map 和其他状态不能只依附一次 VM 调用。运行时应该说明状态是程序私有、多个程序共享、跨进程共享,还是在 detach 后继续存在;也要说明状态能否从另一种后端导入。
当验证失败、attach 只完成一半或目标退出时,状态又如何处理?如果这些语义完全隐含在实现中,后续就很难做可靠升级和恢复。
本文暂时不展开“多个 program、link、map 和用户态 control plane 如何事务化升级”。那是这个系列后续更值得单独分析的问题。当前最基本的要求只是:先让对象和状态有足够清晰的生命周期语义,事务机制才有落脚点。
4. 权限和撤销
谁可以把哪个程序挂到哪个目标,之后又由谁撤销?嵌入式库可能直接把宿主进程视为权限主体;系统级运行时则可能通过 daemon、容器边界、user namespace 或策略服务委托一部分操作。
关键是把权限变成挂载状态的一部分。只在 loader 启动时检查一次权限,对于一个会长期存在、目标和能力还可能变化的扩展并不够。
5. 资源和效果归属
用户态执行把很多成本一起搬进目标进程。JIT 代码、共享 map、插入的 trampoline、callback 和事件队列都会消耗 CPU、内存和带宽。普通进程级统计最终只会告诉你“数据库进程变慢了”,却很难回答是哪一个扩展导致的。
因此,一个真正的运行时还需要给每个扩展稳定身份,并把 attach 状态、CPU/JIT 时间、内存、helper 调用、事件量、drop 和异常关联起来。多个独立扩展共享一个进程时,这会从诊断便利变成基本的隔离和运营能力。
什么场景真的需要用户态 eBPF?
不是所有观测问题都值得引入这套机制。
事件频率不高、所需数据能低成本经过内核,而且 Linux 已经提供合适 hook 时,kernel uprobe 仍然是很强的默认选择。需要任意指令级改写、又不在乎 eBPF 工具链兼容时,传统 DBI 框架通常更灵活。如果所有目标都在 JVM 或其他托管运行时里,语言本身提供的语义 hook 往往比通用 eBPF 更丰富。
用户态 eBPF 更适合以下几类条件同时成立的情况:已经存在值得复用的 eBPF 程序和工具;事件足够高频,内核路径成本明显;目标的应用内部状态很重要;kernel eBPF 不可用或权限太强;或者同一套扩展模型要跨越进程、网络、GPU 等多个执行位置。
这也是反对过度抽象的最强理由。如果一个部署永远只使用一个 runtime、一个平台和固定 helper 集,那么实现私有 API 可能更简单。只有当程序、control plane 或策略确实需要跨后端迁移,或者运维人员需要一致的安全和生命周期保证时,可移植契约才值得这份复杂度。
现有研究还缺什么
ISA 一致性测试覆盖不到运行时语义
RFC 9669 给出了共同的指令语言,但它本来就没有试图定义完整宿主环境。因此,两个实现都可能是完全正确的 BPF engine,却在 context layout、helper、map、attach、失败清理和生命周期上产生不同结果。
最直接的实验是把同一批“契约测试 workload”分别跑在 Linux、uBPF embedding、bpftime 和 eBPF for Windows 上,把失败分成 ISA 错误和 host-contract mismatch。现在缺少的正是第二类广泛采用的 conformance suite。
各项目的兼容性声明很难横向比较
现有项目通常用“能加载已有 ELF”“现有 libbpf 工具无需修改”描述兼容性。它们对采用成本非常重要,却没有回答 target replacement、detach、helper error、状态共享和权限撤销是否一致。
真正缺的是一张由可执行测试生成的能力和生命周期矩阵。如果真实应用只要能 load,后面几乎就不会出现语义差异,那么本文提出的更高层契约确实没有必要。相反,如果问题集中发生在成功 load 之后,这一层缺口就很实际。
安全保证仍然绑定具体后端
Linux 组合使用 verifier、program type、helper restriction、capability、LSM 和 token;uBPF 的 safe profile 使用 pointer provenance 和带类型的 helper/region metadata;bpftime 使用验证、用户态隔离和 EIM;eBPF for Windows 则把 PREVAIL 和 Windows hosting environment 结合起来。
这些方案都可能在各自 threat model 内成立。缺少的是一种跨后端表述:程序或运维人员先声明自己需要什么权限,后端再明确报告自己到底执行了哪些约束。
用户态资源归属仍然是二等功能
把 probe 移到用户态有机会降低单次事件开销,但成本也更容易隐藏在宿主进程里。如果基准只报告平均 probe overhead,就回答不了共享运行时里的问题:哪个扩展在什么事件频率下制造了成本,运行时能否只限制它而不影响目标程序和其他扩展?
要判断这个缺口是否重要,需要多扩展 workload,而不是单 probe microbenchmark。指标也应该包括 attribution error、host tail latency、drop、资源上限触发时间和撤销之后的恢复情况。
兼具学术价值与生产价值的方向
机器可读的运行时契约与一致性测试套件
缺口。 ISA 和 loader 兼容没有覆盖 attach、能力、状态和生命周期。
机制。 定义一个小型 schema,声明程序要求的 context 版本、helper/宿主函数、map 和状态语义、挂载身份规则、生命周期以及可选权限约束。每个后端发布自己支持的子集。测试框架通过确定性的目标程序和故障注入验证这些语义,而不是只检查字节码能不能执行。
和已有工作的差异。 RFC 9669 标准化指令,libbpf 和各平台 API 描述单一宿主。这里既不增加新指令,也不规定一个万能 API,而是让宿主契约本身可以跨实现检查。
可交付成果。 开放 schema、validator,以及 Linux libbpf、bpftime、最小 uBPF embedding 和 eBPF for Windows 的 adapter。
评估。 选择观测、策略、packet processing 和应用扩展 workload;测量同一份契约无需修改即可运行的比例、语义 mismatch 检出率、adapter 复杂度和额外开销。主动注入目标替换、helper 缺失、模块过期、attach 失败和状态丢失。基线就是当前文档加每个项目自己的 integration test。
学术价值。 可以回答一个一般性的系统问题:执行环境语义能否从具体实现机制中抽出来,同时又不退化成没有实际能力的最小公分母。
生产价值。 工具作者和运维人员能在修改生产进程之前,机器可验证地回答“这个程序在这里到底能以什么保证运行”。
失败条件。 如果 schema 最终只是把每个后端的 feature list 换成另一种格式,或者大多数实际程序都无法使用共同部分,那么这层抽象不值得标准化。
把目标身份、能力和生命周期绑定在同一个 attach handle 上
缺口。 用户态 attach 面对的是会变化的进程状态,而目标身份检查、权限和清理经常彼此分离。
机制。 每次 attach 返回一个持久 handle,同时绑定精确目标 generation、程序身份、授予的能力集合和它拥有的状态对象。真正 patch 或注册 callback 前再次验证目标与权限。这个 handle 必须支持 query、revocation 和确定性 cleanup。Linux 后端可以映射到 BPF link,注入式用户态运行时可以维护自己的元数据对象。
和已有工作的差异。 BPF link 管对象生命周期,BPF token 约束可委托能力,EIM 描述扩展资源。这里把三类信息放到用户态动态挂载这个边界上,因为目标进程本身可以独立变化。
可交付成果。 一个 bpftime 原型、一套适合嵌入式运行时的轻量 host API,以及能够暴露类似元数据的 kernel BPF link adapter。
评估。 压测 PID reuse、exec、动态库 reload、并发 attach/detach、权限撤销、部分注入失败和目标 crash。对比 best-effort attach、只做版本检查、完整 capability-aware handle。指标包括避免的 stale-target 错误、清理完整度、attach 延迟、稳态开销和元数据大小。
学术价值。 它研究的是动态代码挂载中的一般问题:当目标身份、权限和生命周期分别变化时,怎样保证一次 attach 仍然指向原来被授权的对象。
生产价值。 长期运行的观测和策略系统可以安全更新或撤销扩展,而不必依赖进程重启或未经验证的清理假设。
失败条件。 如果正常的库更新和进程变化导致 handle 频繁失效,最终迫使运维关闭检查,那么设计要么过严,要么目标身份选错了。
给每个扩展单独做资源账本
缺口。 用户态 eBPF 的运行成本通常被记在宿主进程名下,多个扩展之间的干扰很难定位。
机制。 每个 attach handle 维护自己的资源账本,统计 CPU/JIT 时间、map 和代码内存、事件缓冲流量、helper/宿主函数调用、fault 和 drop,并允许在 dispatch 或 helper 边界设置预算。一次性的 attach/JIT 成本和稳态运行成本应分开记录。
和已有工作的差异。 普通进程 accounting 只能看到宿主,microbenchmark 又只给 aggregate probe overhead。资源账本把扩展本身变成 accounting principal。
可交付成果。 bpftime 中的 per-extension counter 和 budget hook、统一导出格式,以及一个同时运行多个不同事件频率扩展的 benchmark。
评估。 在数据库、Web server、syscall 和 packet-processing workload 上同时运行 1 到几十个扩展,对比进程级 accounting、sampling attribution 和运行时精确计数。测量 attribution error、预算执行延迟、运行时开销、宿主 tail latency,以及限制一个 noisy extension 时其他扩展是否保持正常。
学术价值。 更一般的问题是:动态注入、共享同一地址空间的扩展,能不能在资源上成为彼此独立且可追责的 tenant。
生产价值。 运维可以回答“哪个扩展导致了回归”,并只限制这个扩展,而不是关闭整套 instrumentation。
失败条件。 如果精确 attribution 的成本已经接近 probe 本身,那么采样或粗粒度进程 accounting 可能更划算。
哪些结果会改变这个判断?
本文有一个明确前提:用户态 eBPF 会逐渐成为共享、可迁移的运行时层,而不只是几个项目内部的嵌入式 VM。如果真实世界并没有朝这个方向走,那么很多契约都没有必要。
例如,如果部署研究发现绝大部分用户态 eBPF workload 永远只跑一个 backend,helper 集固定、不保留持久状态,也没有委托 attach 的需求,那么 ISA 加本地 API 已经足够。如果已经能通过 libbpf 成功 load 的程序,几乎从不因为生命周期或 capability 差异出错,那么跨运行时契约只解决了很小的问题。如果目标身份检查和 per-extension accounting 的开销足以抵消用户态执行的主要收益,它们也更适合作为可选诊断功能,而不是强制语义。
相反,几类证据会明显加强本文判断:程序成功 load 之后仍反复因为 attach 或状态语义失效;同一扩展在多个后端上的 detach、map lifetime 和权限行为不兼容;生产环境出现 stale attachment;多个扩展造成的干扰无法用进程级数据归属。
因此,近期最有价值的实验其实不复杂。找一组已经能在多个后端执行的 eBPF 程序,把今天写在 README、代码和运维经验里的隐含假设列出来,再把它们变成跨后端可执行测试。如果这些测试能稳定发现有实际后果的语义漂移,生态需要 ISA 之上的运行时契约;如果几乎发现不了,我们就应该保持接口更小,不要为了“统一”而增加一层空抽象。
参考资料
- IETF,RFC 9669: BPF Instruction Set Architecture,2024。
- Linux kernel documentation,libbpf Overview。
- Linux kernel documentation,eBPF Syscall。
- IO Visor,uBPF,用户态 BPF VM 及其安全执行配置。
- Eunomia,bpftime,用户态 eBPF runtime 与 extension framework。
- Zheng 等,Extending Applications Safely and Efficiently,OSDI 2025。
- Microsoft,eBPF for Windows,Windows 上的 eBPF hosting environment。
如果想先从现有实现入手,可以参考 用户态 eBPF 教程 和 bpftime 运行时文档。