libbpf-go是当前最稳妥选择,因其为官方推荐、支持BTF及最新内核特性,而gobpf已停止维护且不支持现代eBPF程序类型。

为什么直接用 libbpf-go 而不是 gobpf
因为 gobpf 已停止维护,且不支持现代 eBPF 程序类型(如 BPF_PROG_TYPE_SK_MSG 或带 BTF 的 map),而 libbpf-go 是 libbpf 官方绑定,与内核行为一致,能正确处理 map 自动加载、BTF 类型解析和 perf event 读取。如果你用 gobpf 加载一个带 struct bpf_map_def 的旧式 map,运行时大概率遇到 invalid argument 错误,且无法 debug map key/value 布局。
实操建议:
- 用
go get github.com/cilium/ebpf(注意:这是当前主流的libbpf-go封装,非 cilium 自研框架) - 确保内核头文件已安装(
linux-headers-$(uname -r)),否则ebpf.LoadCollection会因找不到bpf.h失败 - 编译 eBPF C 代码时必须用
clang -O2 -target bpf -c,不能用gcc;且需加-D__BPF_TRACING才能用 tracepoint
如何定义并访问 per-CPU hash map 统计源 IP
eBPF 中统计高频连接(如每秒数千个 SYN)必须用 per-CPU map,否则多核写入竞争会导致丢数或性能骤降。普通 BPF_MAP_TYPE_HASH 在高并发下锁开销大,而 BPF_MAP_TYPE_PERCPU_HASH 每个 CPU 拥有独立 slot,bpf_map_lookup_elem 返回的是指向本 CPU 内存的指针,可直接原子累加。
示例 C 片段(count_by_src.c):
立即学习“go语言免费学习笔记(深入)”;
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_HASH);
__uint(max_entries, 65536);
__type(key, __be32);
__type(value, __u64);
} ip_count SEC(".maps");
Go 端读取时不能直接 Map.Lookup,必须用 Map.LookupAndDelete 或 Map.Get + 手动聚合各 CPU 值:
-
ipCountMap.Get(&srcIP, &percpuVal)返回的是[]byte,长度为numCPUs * 8(value 是__u64) - 需用
binary.LittleEndian.Uint64()对每 8 字节解包,再求和 - 若 map key 是 IPv6,key 类型必须是
[16]byte,且 Go 中传入的 key 必须按网络字节序填充,不能直接用net.IP.To16()(它返回大端,但 eBPF 运行在小端机器上,需 byte-reverse)
怎么从 socket 上下文安全提取 src IP(避免 verifier 拒绝)
eBPF verifier 对网络数据包字段访问极其严格。想在 sk_msg 或 socket hook 中取 src IP,不能直接解包 IP header —— verifier 会报 invalid access to packet,因为没有做 skb->data_end 边界检查。
正确做法分两步:
- 用
bpf_skb_load_bytes(对 skb)或bpf_sk_fullsock+bpf_probe_read_kernel(对 socket)获取地址结构体 - 对 TCP/UDP 场景,优先用
struct sock的sk->__sk_common.skc_daddr和sk->__sk_common.skc_rcv_saddr字段,它们是内核已解析好的网络字节序地址,无需校验 offset - 若 hook 点是
tracepoint:syscalls:sys_enter_connect,可用args->args[0](struct sockaddr *)+bpf_probe_read_user,但必须先if (args->args[1] == AF_INET) { ... },否则 verifier 无法推导指针有效性
常见错误:在 kprobe:tcp_v4_connect 中直接 read(&sk->sk_rcv_saddr),verifier 因无法确认 sk 非空而拒绝加载。
perf event 输出到用户态时为什么只收到部分事件
perf buffer(BPF_MAP_TYPE_PERF_EVENT_ARRAY)默认大小极小(通常单页 4KB),且无自动丢弃策略。当 Go 程序读取速度跟不上内核写入速度,就会触发 PERF_LOST 事件,但 github.com/cilium/ebpf/perf 默认不暴露该信号,导致你以为“没数据”,其实是被静默丢弃了。
解决方法:
- 创建 perf reader 时显式设置
perf.NewReader(..., 4*os.Getpagesize()),至少 16KB - 用
reader.Lost() <- chan uint64监听丢包数,若 > 0,说明需要加大 buffer 或优化 Go 端处理逻辑(比如批量读、减少 fmt.Sprintf) - 不要在
reader.Read()循环里做阻塞操作(如 HTTP 请求、数据库写入),否则必然丢事件
另一个坑:eBPF 程序中调用 bpf_perf_event_output 时,第 3 个参数(data size)必须精确等于你 Go 端 struct 的 unsafe.Sizeof,多 1 字节都会导致 verifier 拒绝或用户态读出乱码。


















