- 云原生
- 虚拟化
- 存储
- 后端
- 高可用
【免费下载链接】harvester
Open source hyperconverged infrastructure (HCI) software
本篇技术指南以 vendor/github.com/cilium/ebpf/ARCHITECTURE.md 为核心骨架,结合该 vendored 库的源码实现,完整讲解 cilium/ebpf——一个纯 Go 实现的 eBPF 工具库——的内部架构与数据流。它被 Harvester 作为间接依赖随仓库发布(见 go.mod 中的 replace 指令与 vendor/modules.txt),理解其架构有助于你读懂任何依赖该库的 Go 网络、可观测性组件,并为在自己的 Go 项目中加载、编译与调试 eBPF 程序打下坚实基础。读完后你将掌握该库从 ELF 字节码解析到内核对象装载、再到 hook 挂载的完整链路,以及CollectionSpec、ProgramSpec、MapSpec、Program、Map、Link这些核心抽象之间的关系。
总体架构:一条单向数据管线
库的架构被作者概括为一条四阶段流水线:
ELF -> Specifications -> Objects -> Links- ELF:由 Clang 编译 BPF C 程序得到的 ELF 文件,包含程序字节码与 map 元数据;
- Specifications:
CollectionSpec、ProgramSpec、MapSpec等"蓝图",描述内核对象的全部信息; - Objects:
Program、Map,即真正加载进内核、持有文件描述符的对象; - Links:
Link,把已加载程序附着到内核各类 hook 上的句柄。
下文按这四个阶段逐一展开,并穿插源码级证据。
第一阶段:ELF——从 Clang 产物到解析
BPF 程序通常由 Clang 编译 C 语言的子集产生。Clang 输出的 ELF 文件不仅包含程序字节码(BPF),还携带程序所用 map 的元数据。这些元数据遵循内核自带的 libbpf 约定:ELF 中的特定 section 具有特殊含义,其中包含由 libbpf 定义的结构;而新版本 Clang 还会额外输出BPF Type Format(BTF)元数据。
库的设计目标是"与 libbpf 兼容",从而让从 C 工具链迁移到 Go 工具链的摩擦尽可能小。为此,ELF 读取器 以 Linux selftests 为回归测试基准,尽量避免引入自定义行为。ELF 读取器的输出是一个CollectionSpec——它以 Go 中易于操作的形式编码了 ELF 携带的全部信息。
源码视角:section 如何被识别
在 elf_reader.go 中,入口函数LoadCollectionSpec(以及面向io.ReaderAt的LoadCollectionSpecFromReader)负责把 ELF 文件解析为CollectionSpec。解析过程中按 section 名称与属性分类(见 elf_reader.go):
| ELF section / 特征 | 含义 |
|---|---|
license前缀 | BPF 程序许可证 |
version前缀 | BPF 程序版本 |
maps前缀 | 传统 libbpf map 定义 |
.maps | 基于 BTF 的 map 定义 |
.bss、.data、.rodata前缀 | 全局数据段(数据区 section) |
SHT_REL类型 | 重定位节,按目标 section 索引归类,稍后解析 |
SHT_PROGBITS且带SHF_EXECINSTR且大小非零 | 程序(指令)section |
解析完成后,许可证(license)、版本(version)、BTF 规范(btf.LoadSpecFromReader)以及符号表(f.Symbols())被逐一加载并绑定到对应 section 上。可以看到,架构文档所说的"某些 ELF section 具有特殊含义"在实现层面就是上表中的分类逻辑。
BTF:超越类型的类型格式
BPF Type Format(BTF)描述的远不止 BPF 程序使用的类型,它还包括调试辅助信息:比如哪条源码行对应哪条指令、使用了哪些全局变量等。
internal/btf 包与 btf.Type
BTF 解析 被放在独立的 internal 子包中,原因有二:对外暴露它意味着额外的维护负担;且该 API 仍存在尖锐的边角问题。这个包最重要的概念是btf.Type接口——见 internal/btf/types.go,它定义了ID()、String()、copy()与walk()四个方法。
值得注意的是,btf.Type也描述那些"并非真正类型"的东西,例如.rodata或.bss这样的数据段 section。在实现中,它们对应Datasec(数据段)类型,其成员是Var(全局变量)——types.go 的接口断言清单即包含(*Datasec)(nil)与(*Var)(nil)。
btf.Type之间可以形成循环引用图,如果不小心,很容易陷入无限循环。作者坦言,希望随着代码量的增长,能沉淀出一套安全处理btf.Type图的通用模式——这既是架构文档的原话,也是该包仍处于 internal 状态的原因之一。
第二阶段:Specifications——内核对象的蓝图
CollectionSpec、ProgramSpec与MapSpec是内核对象的"蓝图"(blueprint),包含执行相关bpf(2)系统调用所需的全部信息。
- collection.go 中的
CollectionSpec持有Maps map[string]*MapSpec、Programs map[string]*ProgramSpec以及ByteOrder(ELF 编译目标的字节序)。 - map.go 中的
MapSpec定义了Name、Type(map 类型)、KeySize、ValueSize、MaxEntries、Flags、Pinning、NumaNode、初始内容Contents、是否冻结Freeze、内层 map 模板InnerMap以及关联的BTF等字段。 - prog.go 中的
ProgramSpec定义了Name、Type(决定程序在内核哪个 hook 运行)、AttachType、AttachTo、AttachTarget、Instructions asm.Instructions、Flags与License(部分 helper 仅在许可证与 GPL 兼容时才可用)。
因为 ELF 读取器输出的是CollectionSpec,所以可以在加载前修改 Clang 编译好的 BPF 代码,例如重写常量:
CollectionSpec.RewriteConstants(见 collection.go)用于替换多个常量的值。C 侧需按volatile const type foobar;方式声明,替换值长度必须与sizeof(type)一致;从 Linux 5.5 起,verifier 会利用这些常量消除死代码。CollectionSpec.RewriteMaps(见 collection.go)可用预先存在的 map 替换新建 map,替换后对应符号会从CollectionSpec.Maps中移除。
与此同时,asm 包提供了 eBPF 字节码汇编器(见 asm/doc.go),可在运行时直接生成ProgramSpec,无需依赖 C 工具链。
创建 spec 这一过程不应需要任何特权,也不应受任何限制(例如只允许原生字节序的程序)。这一设计约束保证了库的灵活性:spec 阶段是纯用户态操作,权限检查统一推迟到对象加载阶段。
此外,CollectionSpec还提供便捷方法Assign与LoadAndAssign(见 collection.go):通过 struct 字段上的ebpf:"程序名/映射名"标签,把 spec 中的对象批量赋值到 Go 结构体;LoadAndAssign还会在赋值前把依赖资源懒加载进内核。
第三阶段:Objects——把蓝图加载进内核
Program与Map是把 spec 加载进内核后的结果对象。但加载可能失败,因为内核太老、或某特性未启用。库用两种机制应对:
回退(Fallback):老内核不允许为程序与 map 命名。库会自动探测内核是否支持命名,必要时在加载时省略名称。这之所以可行,是因为名字主要只是调试辅助手段。
哨兵错误(Sentinel error):有时可以预先判定某特性不可用,此时库返回一个包装了ErrNotSupported的错误(见 prog.go 中对internal.ErrNotSupported的再导出)。这同样可用于跳过无法在当前内核上运行的测试。
底层 API 与安全封装
一旦Program与Map对象加载完成,它们便暴露内核的低层 API,例如Map.NextKey。这类 API 在 Go 中往往难以使用,因此库在其之上提供了更安全的封装,如MapIterator。低层 API 的价值在于:当高层 API 不支持某个特定用例时,开发者仍然可以直接操作。
在 collection.go 中,Collection即Programs与Maps的集合,NewCollection/NewCollectionWithOptions负责按依赖顺序(先 map 后 program,因为 map 的 fd 需要被嵌入程序)把整个 spec 实例化为内核对象。
第四阶段:Links——把程序挂到内核 hook 上
BPF 可以附着到内核中许多不同的位置。较新的 BPF hook 倾向于使用bpf_link完成附着;较老的 hook 则使用系统调用、netlink 消息等的组合。架构文档明确划定了边界:为新增 link 类型提供支持时,不应引入 netlink 这类重量级依赖,因此XDP 程序或 tracepoint 不在本库当前支持范围之内(这类附着需要 netlink 交互)。
link 子包与 Link 接口
link 子包封装了各种 hook 的附着逻辑,从该包的文件清单可以看出覆盖的 attach 类型:
- kprobe.go 与 uprobe.go:内核/用户态动态探针;
- tracepoint.go 与 raw_tracepoint.go:跟踪点;
- cgroup.go、netns.go、freplace.go:cgroup、网络命名空间、函数替换(fentry/fexit 类)等;
- perf_event.go:perf 事件。
Link接口(见 link/link.go)定义了一组生命周期操作:
Update(*ebpf.Program):用新程序替换当前程序(传入 nil 会报错);Pin(string):把 link 固定(pin)进 bpffs 以持久化;Unpin():撤销之前的 Pin;Close():释放资源。注意,除非 link 已被成功 pin,否则 Close 会断开附着;反过来,若不调用 Close,link 在进程退出后仍可能继续存活。
RawLinkOptions(同文件 link/link.go)则直接暴露了底层bpf_link创建所需的Target(附着目标 fd)、Program、Attach与BTF参数。
在本仓库中的位置与版本
cilium/ebpf 以 vendored 依赖的形式随 Harvester 仓库分发,位于 vendor/github.com/cilium/ebpf(顶层含collection.go、elf_reader.go、map.go、prog.go、asm/、link/、internal/btf/等文件)。版本约束见 go.mod 第 6 行的 replace 指令github.com/cilium/ebpf => github.com/cilium/ebpf v0.7.0,以及 vendor/modules.txt 中列出的asm、internal、internal/btf、internal/unix、link等子包清单。从仓库源码结构看,Harvester 自身业务代码并未直接 import 该库,它作为间接依赖被引入(go.mod 中标注为// indirect),但其四阶段架构与兼容 libbpf 的设计,对任何上游依赖方(包括网络与可观测性组件)的二次开发都具有直接参考价值。
小结
cilium/ebpf 以"ELF → Specifications → Objects → Links"四条阶段串起 eBPF 的完整生命周期:ELF 阶段借助 libbpf 约定与 BTF 完成格式解析;Specifications 阶段以纯用户态、无特权的方式提供可修改、可生成的蓝图;Objects 阶段通过回退与ErrNotSupported哨兵错误优雅应对内核差异;Links 阶段则用统一的Link接口管理附着生命周期。理解这条管线,是深入使用该库(无论是直接加载 Clang 产物,还是用asm包在 Go 中手写字节码)的起点。
- 云原生
- 虚拟化
- 存储
- 后端
- 高可用
【免费下载链接】harvester
Open source hyperconverged infrastructure (HCI) software
相关推荐
逆向 Agent 工具 REA:一句话查清应用的登录、存储与更新
逆向 Agent 工具 REA:一句话查清应用的登录、存储与更新 没拿到源码的应用,想弄清三件事:登录请求怎么走、用户数据落在哪、两个版本之间改了什么。传统做法
逆向工程MCP 服务AI 技能Valhalla 路由引擎开发者指南:从行星级 Tile 构建到路由流水线的完整技术地图
Valhalla 路由引擎开发者指南:从行星级 Tile 构建到路由流水线的完整技术地图 导读 本文是一份面向 Valhalla 项目开发者(尤其是 AI 编程
后端Cilium `cilium-dbg bpf sha` 命令详解:管理编译后的 BPF 模板对象(Datapath SHA)
Cilium cilium dbg bpf sha 命令详解:管理编译后的 BPF 模板对象(Datapath SHA) 导读 cilium dbg bpf s
云原生网络服务网格可观测性网络安全eBPF
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考