从零构建 eBPF Go 应用:基于 cilium/ebpf 的 XDP 包计数实战指南
系统底层网络可观测性【免费下载链接】ebpfebpf-go is a pure-Go library to read, modify and load eBPF programs and attach them to various hooks in the Linux kernel.项目地址https://gitcode.com/gh_mirrors/eb/ebpf点击查看免费下载本指南以 cilium/ebpf 官方入门文档为核心带你从零搭建一个完整的 eBPF 驱动 Go 应用编写一段最小的 eBPF C 程序挂在 XDP 钩子上统计物理网卡收到的报文数用 bpf2go 把它编译成 ELF 并自动生成 Go 脚手架再编写 Go 主程序将 eBPF 程序加载进内核、挂接到网络接口并周期性地读取计数器。读完本文你将掌握 eBPF 在 Go 中的完整开发闭环C 程序编写 → bpf2go 代码生成 → 内核加载/挂接 → 用户态交互并理解 Map、Program、Link 等核心抽象与内核资源管理的细节。本文对应的完整示例代码位于 docs/examples/getting_startedcounter.c、gen.go、main.go以及 bpf2go 生成的文件你可以对照阅读。前置依赖搭建 eBPF 编译与运行环境本示例对内核与工具链有一定要求官方文档列出的依赖如下依赖版本/安装要求Linux 内核5.7 及以上需要 bpf_link 支持LLVM11 及以上需要clang与llvm-striplibbpf 头文件Debian/Ubuntu 安装libbpf-devFedora 安装libbpf-develLinux 内核头文件AMD64 Debian/Ubuntu 安装linux-headers-amd64Fedora 安装kernel-develGo 编译器与当前仓库 go.mod 所声明版本兼容的版本几个容易踩坑的细节用clang --version可以确认 LLVM 版本。不同发行版对 LLVM 的打包方式差异很大有些发行版把clang和llvm-strip拆成两个独立软件包需要分别安装。在 Debian 上如果示例期望找到asm/types.h而系统缺少该路径可能需要手动建立软链接ln -sf /usr/include/asm-generic/ /usr/include/asm。内核版本要求 5.7是因为bpf_link这一内核特性从 5.7 开始引入而本示例在 Go 侧正是通过link.AttachXDP来建立程序-接口的持久关联。编写 eBPF C 程序 counter.ceBPF C 程序的结构将直接决定 bpf2go 生成的 Go 脚手架形态因此我们先从 C 代码入手。创建一个空目录将下面的代码保存为counter.c//go:build ignore #include linux/bpf.h #include bpf/bpf_helpers.h struct { __uint(type, BPF_MAP_TYPE_ARRAY); __type(key, __u32); __type(value, __u64); __uint(max_entries, 1); } pkt_count SEC(.maps); // count_packets atomically increases a packet counter on every invocation. SEC(xdp) int count_packets() { __u32 key 0; __u64 *count bpf_map_lookup_elem(pkt_count, key); if (count) { __sync_fetch_and_add(count, 1); } return XDP_PASS; } char __license[] SEC(license) Dual MIT/GPL;对照仓库中的 docs/examples/getting_started/counter.c 逐段拆解1. 首行的//go:build ignore是必须的。当 C 文件与 Go 文件放在同一目录时必须通过 build tag 排除它否则go build会报错C source files not allowed when not using cgo or SWIG。这个 tag 让 Go 工具链安全地忽略 eBPF C 文件。2. 头文件提供了全部 C 宏。__u64、BPF_MAP_TYPE_ARRAY这类标识符由 Linux 内核头文件提供而__uint、__type、SEC以及 BPF helper 的声明则由 libbpf 提供。3. 声明一个名为pkt_count的 BPF Map。它是 Array 类型的 Map只保存一个u64值。用man bpf或在线 bpf man 手册可以查看内核支持的全部 Map 类型。这里选 Array 是因为它是大家最熟悉的数据结构在 BPF 中数组是预分配且清零的无需任何初始化即可安全使用。4. Map 定义放在.mapsELF section 中。这正是 ebpf 库期望发现 Map 的位置bpf2go 生成的代码会基于该 section 解析出 MapSpec。5.SEC(xdp)声明了程序类型。并非所有 BPF 程序都等价有的处理原始报文有的在内核或用户态函数上下文中执行有的期望操作__sk_buff。这些差异编码在 Program Type 里libbpf 约定了一系列 ELF section 与程序类型的对应关系。这里选择xdp因为后续要把程序挂到 XDP 钩子上。6.max_entries为 1意味着pkt_count只有一个元素。因此我们总是访问第 0 个元素。7.bpf_map_lookup_elem向 BPF 运行时请求第 0 个元素的指针。这是 BPF helper——内核提供的、让 BPF 程序能与上下文或其他内核部分交互的小段逻辑。用man bpf-helpers或在线 bpf-helpers man 手册可以查看内核支持的所有 helper。8. 所有 Map 查找都可能失败。如果 Map 中不存在请求的 keycount将是空指针。BPF verifier 对潜在空指针的访问检查极其严格任何对count的进一步访问都必须用空指针检查把关。9.__sync_fetch_and_add原子地给count指向的值加 1。注意在启用 SMP 的系统如今绝大多数系统上同一个 BPF 程序可能被并发执行。即使我们只加载了一份 Program 副本、配了一个pkt_countMap内核也可能并行处理多个接收队列上的报文导致程序多实例并发运行pkt_count实际成为一块共享内存。使用原子操作可以避免脏读写。10.XDP_PASS是 XDP 的处置结果之一。XDP 能在报文被交给内核网络协议栈路由、iptables/nftables 防火墙、TCP 和 socket 等之前就提前丢弃它。这里我们发出XDP_PASS避免干扰内核网络栈。11. 许可证声明。部分 BPF helper 允许调用以 GPLv2 许可的内核代码因此使用这些 helper 的 BPF 程序需要声明自己至少部分按 GPL 许可。这里选择双许可Dual MIT/GPL与仓库整体的 MIT 许可保持一致。用 bpf2go 编译并生成 Go 脚手架有了counter.c再创建gen.go在其中放入//go:generate指令。在项目目录执行go generate时这条指令会调用 bpf2go//go:build linux package main //go:generate go tool bpf2go -tags linux counter counter.c对照 docs/examples/getting_started/gen.go把//go:generate语句放进独立文件是个好习惯可以把代码生成逻辑与应用逻辑分离在示例的这一步我们还没有main.go如果你愿意也可以把它放在已有的 Go 源文件中。bpf2go 除了编译 eBPF C 程序还会生成一系列脚手架代码用于把 eBPF 程序加载进内核并与各组件交互能大幅减少起步代码量。在开始使用 Go 工具链前需要先初始化 Go module% go mod init ebpf-test go: creating new go.mod: module ebpf-test go: to add module requirements and sums: go mod tidy % go mod tidy接着把 bpf2go 作为工具依赖加入 module确保 Go 工具链使用的 bpf2go 版本与库的版本始终一致% go get -tool github.com/cilium/ebpf/cmd/bpf2go然后执行go generate% go generate Compiled /home/timo/getting_started/counter_bpfel.o Stripped /home/timo/getting_started/counter_bpfel.o Wrote /home/timo/getting_started/counter_bpfel.go Compiled /home/timo/getting_started/counter_bpfeb.o Stripped /home/timo/getting_started/counter_bpfeb.o Wrote /home/timo/getting_started/counter_bpfeb.gobpf2go 在幕后用clang把counter.c构建成counter_bpf*.o它基于对象文件内容生成两个目标架构的对象文件和两个对应的 Go 源文件。这些生成文件一个都不能删后面加载程序时会用到。从 cmd/bpf2go/main.go 的源码可以看到 bpf2go 的常用参数与默认值-cc用于把 C 编译成 BPF 的编译器默认取环境变量BPF2GO_CC缺省为clang-strip用于剥离已编译 BPF 中 DWARF 信息的工具默认取BPF2GO_STRIP对应输出日志中的Stripped一行-no-strip禁用 DWARF 剥离disable stripping of DWARF-cflags传给编译器的 C 编译参数可含引号包裹的参数默认取BPF2GO_CFLAGS-tags要写入生成文件的 Go build tags 列表逗号分隔示例中正是用它传入linux-target编译的目标 clang target默认bpfel,bpfeb两个都编-makebase输出 make 风格依赖文件.d 文件时的基准目录-type为指定 C 类型生成 Go 声明可重复传入-no-global-types跳过为 Map key/value 等生成类型-output-stem生成文件名的替代主干默认取 ident-output-dir生成文件的目标目录默认当前目录-go-package输出 Go 文件的包名默认取环境变量GOPACKAGE-verbose开启详细日志。值得一提的是-tags的实现cmd/bpf2go/flags.go 中定义了buildTags类型它按 Go 1.17 之前的语法// build形式解析逗号分隔的 tag 列表再组合成constraint.Expr写进生成文件兼顾了兼容性。剖析 bpf2go 生成的 Go 脚手架看一下生成的其中一个 Go 文件docs/examples/getting_started/counter_bpfel.go能发现 bpf2go 自动生成了一套与count_packets程序交互的脚手架// Code generated by bpf2go; DO NOT EDIT. //go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 || wasm) linux package main import ( bytes _ embed fmt io github.com/cilium/ebpf ) // Names of all BPF objects in the ELF. // // Used for safe lookups in a Collection or CollectionSpec. const ( counterMapPktCount pkt_count counterProgCountPackets count_packets ) // loadCounter returns the embedded CollectionSpec for counter. func loadCounter() (*ebpf.CollectionSpec, error) { reader : bytes.NewReader(_CounterBytes) spec, err : ebpf.LoadCollectionSpecFromReader(reader) if err ! nil { return nil, fmt.Errorf(cant load counter: %w, err) } return spec, err } // loadCounterObjects loads counter and converts it into a struct. func loadCounterObjects(obj any, opts *ebpf.CollectionOptions) error { spec, err : loadCounter() if err ! nil { return err } return spec.LoadAndAssign(obj, opts) } type counterObjects struct { counterPrograms counterMaps counterVariables } func (o *counterObjects) Close() error { return _CounterClose(o.counterPrograms, o.counterMaps) } type counterMaps struct { PktCount *ebpf.Map ebpf:pkt_count } type counterPrograms struct { CountPackets *ebpf.Program ebpf:count_packets } // Do not access this directly. // //go:embed counter_bpfel.o var _CounterBytes []byte关键机制值得展开//go:embed内嵌 ELF生成文件末尾通过//go:embed counter_bpfel.o把编译产物直接嵌入 Go 二进制loadCounter()用ebpf.LoadCollectionSpecFromReader把它解析成CollectionSpec。这意味着发布时只需分发单一 Go 可执行文件无需携带 .o 文件。结构体 tag 驱动的加载counterObjects、counterPrograms、counterMaps等结构体上的ebpf:pkt_count/ebpf:count_packetstag让LoadAndAssign能把 Collection 中的对象按名字填充到对应字段。双端生成counter_bpfel.go小端架构386、amd64、arm64 等与counter_bpfeb.go大端架构mips、ppc64、s390x 等分别内嵌各自的 .o 文件配合文件头 build tagGo 工具链会自动选择与运行平台匹配的版本。类型安全如果 Map 或 Program 不存在于 ELF 中它就不会以结构体字段的形式出现应用直接编译失败从根上消除了一整类运行时错误。编写 Go 主程序 main.goC 代码编译完成、Go 脚手架生成之后剩下的就是编写负责加载并挂接程序的 Go 代码。把下面的内容保存为main.go与counter.c、gen.go放在同一目录//go:build linux package main import ( log net os os/signal time github.com/cilium/ebpf/link github.com/cilium/ebpf/rlimit ) func main() { // Remove resource limits for kernels 5.11. if err : rlimit.RemoveMemlock(); err ! nil { log.Fatal(Removing memlock:, err) } // Load the compiled eBPF ELF and load it into the kernel. var objs counterObjects if err : loadCounterObjects(objs, nil); err ! nil { log.Fatal(Loading eBPF objects:, err) } defer objs.Close() ifname : eth0 // Change this to an interface on your machine. iface, err : net.InterfaceByName(ifname) if err ! nil { log.Fatalf(Getting interface %s: %s, ifname, err) } // Attach count_packets to the network interface. link, err : link.AttachXDP(link.XDPOptions{ Program: objs.CountPackets, Interface: iface.Index, }) if err ! nil { log.Fatal(Attaching XDP:, err) } defer link.Close() log.Printf(Counting incoming packets on %s.., ifname) // Periodically fetch the packet counter from PktCount, // exit the program when interrupted. tick : time.Tick(time.Second) stop : make(chan os.Signal, 5) signal.Notify(stop, os.Interrupt) for { select { case -tick: var count uint64 err : objs.PktCount.Lookup(uint32(0), count) if err ! nil { log.Fatal(Map lookup:, err) } log.Printf(Received %d packets, count) case -stop: log.Print(Received signal, exiting..) return } } }对照 docs/examples/getting_started/main.go 中的注释逐点说明1.rlimit.RemoveMemlock()处理 5.11 之前内核的内存限制。Linux 5.11 之前的版本用RLIMIT_MEMLOCK控制一个进程能分配多少 eBPF 内核内存默认值相对较低5.11 起内核改用 memory cgroupmemcg记账。rlimit包的RemoveMemlock()会先探测内核是否支持 memcg 记账如果支持则不做任何事no-op否则把RLIMIT_MEMLOCK提升到无限。细节见 Resource Limits该包在init()阶段会先把当前进程的 rlimit 降到 0、尝试创建一次 BPF Map 来探测内核能力、再恢复原值若你的程序有多个入口或 CLI 子命令可多次调用RemoveMemlock()rlimit 操作只会执行一次。如果不想引入该包也可以用 Docker 的--ulimit memlock-1或 systemd 的LimitMEMLOCKinfinity等方式提升限制。2.counterObjects与loadCounterObjects生成代码的装配入口。counterObjects是包含 Map 和 Program 空指针的结构体loadCounterObjects依据结构体上的 tag 从 ELF 解析出的 CollectionSpec 加载并填充这些字段内部走spec.LoadAndAssign。这个机制省去了按字符串在 Collection 里逐个查找 Map/Program 的大量重复代码并将查找变成编译期类型安全的操作。3.defer objs.Close()退出前关闭所有文件描述符。更深入的生命周期管理见 Object Lifecycle。4.link.AttachXDP把count_packets脚手架中风格化为CountPackets与eth0关联。它返回一个link.Link抽象。注意ifname硬编码为eth0请改成你机器上实际存在的网卡名示例代码中的注释也明确提示了这一点。5.defer link.Close()关闭程序-接口关联的文件描述符。注意如果 Link 没有被Pin到 bpf 文件系统关闭它就会让程序停止在入站报文上执行。6.objs.PktCount.Lookup(uint32(0), count)从pkt_countMap 读取索引 0 处的 u64。对应counter.c中的计数逻辑Map 在脚手架中风格化为PktCount。构建并运行 Go 应用main.go就位后编译并运行% go build sudo ./ebpf-test 2023/09/20 17:18:43 Counting incoming packets on eth0.. 2023/09/20 17:18:47 Received 0 packets 2023/09/20 17:18:48 Received 4 packets 2023/09/20 17:18:49 Received 11 packets 2023/09/20 17:18:50 Received 15 packets运行需要 root 权限加载 eBPF 程序需要CAP_BPF等相关能力。程序每秒打印一次收到的累计报文数——注意运行需要 sudo因为 XDP 挂接需要特权。此时在 eth0 上产生一些流量例如ping或发起 HTTP 请求就能看到计数器不断增长。迭代工作流当迭代 C 代码时务必保持生成文件是最新的。如果不重新运行 bpf2goeBPF C 不会被重新编译C 程序结构上的任何改动都不会反映到 Go 脚手架中。推荐的迭代命令链% go generate go build sudo ./ebpf-testgo generate会重新调用 bpf2go 编译 C 并重新生成 Go 文件go build重新编译应用然后运行。下一步继续 eBPF 之旅到这里你已经完成了第一个大概率是eBPF 驱动的 Go 应用。通过 XDP 我们只触及了 eBPF 众多应用场景的表面。继续进阶可以关注以下方向更多开箱示例仓库 examples 目录展示了更多用例包括跟踪用户态程序kprobe/uretprobe 等、从内核提取信息、把 eBPF 程序挂接到网络 socket、tcx/ringbuffer/struct_ops 等不同挂接与通信方式深入概念Resource Limits 详解内核版本与内存记账机制的差异Object Lifecycle 讲透 Map、Program、Link 的文件描述符生命周期portable-ebpf 与 windows-support 则面向把应用交付给更广泛用户的场景掌握构建机制bpf2go 的完整参数与依赖输出能力见 cmd/bpf2go如-makebase可生成 make 风格依赖文件便于集成进大型构建系统。至此你已经掌握了从编写 eBPF C 程序、经 bpf2go 生成脚手架、到在 Go 中加载挂接并读取数据的完整链路可以着手把自己的 eBPF 想法变成可运行的 Go 程序了。赞分享系统底层网络可观测性【免费下载链接】ebpfebpf-go is a pure-Go library to read, modify and load eBPF programs and attach them to various hooks in the Linux kernel.项目地址https://gitcode.com/gh_mirrors/eb/ebpf点击查看免费下载相关推荐基于 Docker 的可复现 eBPF 构建环境cilium/ebpf LLVM Builder 镜像实战指南基于 Docker 的可复现 eBPF 构建环境cilium/ebpf LLVM Builder 镜像实战指南 导读 本指南围绕仓库 testdata/doc系统底层网络可观测性使用 ebpf-gocilium/ebpf从零构建 eBPF Go 应用安装、bpf2go 工作流与内核对象生命周期使用 ebpf gocilium/ebpf从零构建 eBPF Go 应用安装、bpf2go 工作流与内核对象生命周期 导读 ebpf go https:/系统底层网络可观测性ebpf-go 的 Windows 支持指南基于 eBPF for Windows 构建跨平台 Go 应用ebpf go 的 Windows 支持指南基于 eBPF for Windows 构建跨平台 Go 应用 本指南围绕 ebpf go 库对 eBPF for系统底层网络可观测性创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

开发网站和application新手入门:搞定备案与部署避坑指南

开发网站和application新手入门:搞定备案与部署避坑指南

开发网站和application新手入门:搞定备案与部署避坑指南 备案流程一头雾水,是不是让你对着“接入商”、“主体信息”这些词就头大?很多新手在开发网站和application的起步阶段,最头疼的不是代码写不出来,而是卡在合规上线这一步。…

2026/10/22 0:12:08 阅读更多 →
亲测3款wordpress简单的博客主题避坑指南

亲测3款wordpress简单的博客主题避坑指南

亲测3款wordpress简单的博客主题避坑指南 找过建站公司的朋友,心里都有一本难念的经。明明是个简单的个人博客或内容站,报价单递过来直接吓一跳,几万块起步还不敢还价。怕代码烂、怕维护难、怕后期被软件商“绑架”交高昂年费。这种对高价的恐惧…

2026/10/28 14:05:30 阅读更多 →
Pandas set_index 完全指南:从索引机制到实战性能优化

Pandas set_index 完全指南:从索引机制到实战性能优化

学 pandas 的人,早晚会撞上set_index这个名字。我第一次认真研究它,是在处理一份几十万行的订单表时,业务方要求按订单号秒级查询,一开始我只会df[df[order_id] xxx],每次都慢得受不了。后来把order_id设成索引&#…

2026/10/24 18:46:59 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/27 17:33:32 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/28 7:28:00 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/28 9:02:11 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/27 18:20:50 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/28 9:00:54 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/28 7:29:02 阅读更多 →

日新闻

周新闻

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/27 13:55:19 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/28 9:01:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/27 19:19:37 阅读更多 →