深入 Cilium 的 BPF 与 XDP 参考指南:从 eBPF 架构、程序类型到调试实践
【免费下载链接】ciliumeBPF-based Networking, Security, and Observability项目地址: https://gitcode.com/GitHub_Trending/ci/cilium
本篇技术文章基于 Cilium 仓库中的 BPF 参考指南(Documentation/reference-guides/bpf/index.rst及其子文档)编写,面向希望深入理解 BPF/XDP 的开发者与用户。读完本文后,你将掌握 eBPF 指令集与验证器机制、Maps/Tail Calls/JIT 等核心基础设施、XDP 与 tc 两类网络程序类型的差异与挂载方式,以及使用 clang、iproute2、bpftool 编译、加载、调试 BPF 程序的完整工作流——这正是 Cilium 数据路径(data path)的底层原理。
1. BPF 与 eBPF:Cilium 数据路径的核心
BPF(Berkeley Packet Filter)是 Linux 内核中一种高度灵活、类虚拟机的结构,它允许在内核的各个钩子点(hook point)以安全的方式执行字节码,被广泛用于网络、追踪与安全(如沙箱)等子系统。参考指南开篇特别指出:虽然深入阅读这份指南有助于理解 Cilium,但它不是使用 Cilium 的前置要求,初学者应先看入门指南;本篇的价值在于把 Cilium 数据路径中"看不见"的内核侧机制讲透。
几个关键历史与概念事实(来自 BPF 参考指南索引页):
- BPF 诞生于 1992 年,但该指南覆盖的是扩展版eBPF(首次出现于 Linux 内核 3.18),它已基本取代了"经典"BPF(cBPF,即 tcpdump 所用的包过滤语言)。如今内核只运行 eBPF,加载的 cBPF 字节码会在执行前被透明地翻译为 eBPF 表示。
- 尽管名字叫 "Packet Filter",eBPF 的指令集已足够通用,远不止网络用途。
- Cilium 在数据路径中大量使用 BPF:策略执行、负载均衡、监控等核心功能都由内核中的 BPF 程序完成。参考指南的目标正是帮助读者理解 BPF 本身,包括用
tc(traffic control)和XDP(eXpress Data Path)加载 BPF 程序的网络场景,以及如何开发 Cilium 的 BPF 模板。
在 Cilium 仓库中,这套体系对应着bpf/目录:各数据路径程序入口如 bpf_host.c(主机网络接口)、bpf_lxc.c(容器端点)、bpf_sock.c(socket 层)、bpf_xdp.c(XDP)等;大量可复用的 BPF 逻辑放在 bpf/lib/ 下的头文件中(如conntrack.h、drop_reasons.h、classifier.h等);单元测试与自检测试则位于 bpf/tests/(如bpf_ct_tests.c、bpf_nat_tests.c)。
2. BPF 架构:指令集、Helpers、Maps 与 JIT
本节内容对应参考指南的 BPF Architecture 子文档。BPF 不只是指令集,它还提供 Maps(内核键值存储)、helper 函数、tail call、安全加固原语、用于固定(pinning)对象的伪文件系统,以及向网卡 offload 的基础设施。
2.1 指令集与寄存器模型
BPF 是一个通用 RISC 指令集,设计目标是让 C 程序子集能通过编译器后端(如 LLVM)编译为 BPF 指令,再由内核中的 JIT 编译为原生机器码。将程序推进内核的好处包括:无需跨越内核/用户空间边界即可实现容器策略、负载均衡等;程序可为特定用例裁剪(例如端点不需要 IPv4 时就只处理 IPv6,节省快速路径资源);网络程序可原子更新而不中断流量,程序状态可通过 Maps 在更新中保持;BPF 提供与用户空间的稳定 ABI,不需要第三方内核模块,且可跨架构移植。
BPF 程序执行是事件驱动的:网卡 ingress 上收到包触发程序,kprobe 地址被执行触发程序。硬件与执行模型:
- 11 个 64 位寄存器
r0–r10(各含 32 位子寄存器)、程序计数器、512 字节 BPF 栈; r10为只读帧指针(访问栈),r0–r9为通用读写寄存器;- helper 调用约定:
r0返回,r1–r5传参,r6–r9为跨调用保存的 callee-saved 寄存器;该约定可直接映射到x86_64/arm64等 ABI,JIT 只需发出 call 指令; - 程序入口处
r1初始指向上下文(context,如网络程序的skb表示),一个程序只操作一个上下文; - 单程序指令上限为 4096 条(内核 5.1 之后提升为 100 万条);验证器(verifier)会禁止循环以保证程序终止;tail call 嵌套上限 33 次;
- 指令为双操作数格式、固定 64 位编码(
op:8, dst_reg:4, src_reg:4, off:16, imm:32),当前实现 87 条指令,编码定义在内核头linux/bpf.h中;指令类包括BPF_LD/LDX(加载)、BPF_ST/STX(存储,含原子加)、BPF_ALU/ALU64(算术,32/64 位)、BPF_JMP(跳转、exit、call、隐藏的 tail call)。
2.2 Helper 函数:BPF 与内核的接口
Helper 函数让 BPF 程序调用内核核心定义的函数集,可用集合因程序类型而异(例如 socket 程序可调用的 helper 是 tc 程序的子集;轻量隧道封装/解封装 helper 只在低层 tc 可用,事件输出 helper 则 tc 与 XDP 都可用)。每个 helper 的签名形如系统调用:
u64 fn(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)文档给出了bpf_map_update_elem的内核侧示例:通过BPF_CALL_4(...)宏实现,并附带struct bpf_func_proto(ret_type、arg1_type = ARG_CONST_MAP_PTR等),验证器据此对寄存器内容做类型检查(如缓冲区是否已初始化)。所有 helper 属于内核核心、不能用模块扩展;内核的struct bpf_verifier_ops通过get_func_proto回调把enum bpf_func_id映射到具体 helper。文档提到,写作时 tc BPF 程序可选 38 个 helper。
2.3 Maps:跨调用、跨进程的状态
Maps 是驻留在内核空间的高效键值存储:BPF 程序用它在多次调用间保持状态,用户空间也可通过文件描述符访问并与任意 BPF 程序共享(无需同类型)。单个 BPF 程序最多直接访问 64 个 Maps。
- 通用 Maps:
BPF_MAP_TYPE_HASH、ARRAY、PERCPU_HASH、PERCPU_ARRAY、LRU_HASH、LRU_PERCPU_HASH、LPM_TRIE,共享同一组 lookup/update/delete helper,但后端语义与性能不同; - 非通用 Maps:
PROG_ARRAY(装其他 BPF 程序)、PERF_EVENT_ARRAY、CGROUP_ARRAY、STACK_TRACE、ARRAY_OF_MAPS、HASH_OF_MAPS(后者两类可原子替换整个 Map 运行时状态)。
Cilium 的策略、身份(identity)、连接跟踪、IP cache 等状态本质上都是通过这些 Maps 在内核与cilium-agent用户空间守护进程之间同步的。
2.4 对象固定(Pinning)与 BPF 伪文件系统
Maps 与程序在内核中是以匿名 inode 为后端的文件描述符资源——fd 可被 Unix socket 传递,但受进程生命周期限制:例如 iproute2 用 tc/XDP 加载程序后退出,Map 就不再可被用户空间访问。为解决该问题,内核实现了 BPF 伪文件系统(BPFFS),bpf()系统调用扩展了BPF_OBJ_PIN(固定)与BPF_OBJ_GET(取回)两条命令;BPFFS 支持多挂载实例、硬/软链接。tc 正是借助它共享 ingress/egress 两端的 Map,第三方应用也能在程序运行时监视或更新 Map 内容。
2.5 Tail Calls 与 BPF-to-BPF 调用
Tail call允许一个 BPF 程序跳转到另一个程序而不返回,实现上是长跳转、复用同一栈帧,开销极小。约束:被调程序独立验证(状态传递需用 per-CPU Map 或 tc 的skb->cb[]);只能同类型调用且 JIT 方式要一致;机制由BPF_MAP_TYPE_PROG_ARRAY(用户空间写程序的 fd 值)+bpf_tail_call()helper 组成,内核将其内联为专门指令;Map 槽位不存在时"fall through"继续执行原程序。典型用法是把协议解析结构化为多个可用 tail call 原子替换的阶段。
BPF-to-BPF 调用是较新的核心特性:在 Linux 4.16 + LLVM 6.0 之前,可复用代码必须写成always_inline并全量内联,导致对象文件代码膨胀;之后 BPF 程序可自然调用普通静态函数(文档给出了always_inline前后两段xdp_drop对比示例)。约定与 helper 相同(r1–r5传参、r0返回、r6–r9保持),最大嵌套 8 层;调用者可向下传指针但不可反向。JIT 为每个函数体生成独立镜像并在最后修正调用地址。注意:直到内核 5.9,tail call 与 BPF 子程序互斥;5.10 起允许组合,但引入了栈限制——验证器检测到该组合时,每个子程序栈上限降到 256 字节,整条调用链最多 8KB(对比 512 字节栈 × 33 次 tail call = 16KB 会溢出),且该组合当时仅 x86-64 支持。
2.6 JIT、加固与 Offload
x86_64、arm64、ppc64、s390x、mips64、sparc64(64 位)及 32 位arm、x86_32均自带 eBPF JIT(功能等价,echo 1 > /proc/sys/net/core/bpf_jit_enable启用);没有 JIT 的架构回退到内核解释器。JIT 显著降低每条指令的执行成本、缩小镜像尺寸(CISC 架构如 x86 会优化输出最短操作码)。
加固(Hardening):BPF 在程序生命周期内把解释器镜像(struct bpf_prog)与 JIT 镜像(struct bpf_binary_header)锁定为只读,防止代码被静默篡改;bpf_jit_harden=1时为无特权用户做常量遮蔽(constant blinding),把立即数指令改写为"加载rnd ^ imm再异或rnd"两步,防御 JIT 喷射攻击(文档附了启用/禁用加固的两段反汇编对比);同时禁用 kallsyms 暴露。CONFIG_BPF_JIT_ALWAYS_ON可整体移除解释器(Spectre v2 缓解的一部分)。另一个重要 sysctl 是/proc/sys/kernel/unprivileged_bpf_disabled——这是一个一次性开关(置 1 后重启前不可复位),置位后仅初始命名空间中的CAP_SYS_ADMIN特权进程可使用bpf(2)。文档特别强调:Cilium 启动时也会把它置为 1,收紧系统攻击面。
Offload:tc/XDP 网络程序具备硬件 offload 接口,当前 Netronome 的nfp驱动通过 JIT 把 BPF 指令翻译为网卡指令集,连 Maps 也可 offload 到网卡(lookup/update/delete 都能在卡上完成)。
2.7 BPF sysctl 速查表
| Sysctl | 取值 | 含义 |
|---|---|---|
/proc/sys/net/core/bpf_jit_enable | 0 / 1 / 2 | 禁用 JIT 仅解释(默认)/ 启用 JIT / 启用 JIT 并向内核日志输出调试 trace(配合bpf_jit_disasm使用) |
/proc/sys/net/core/bpf_jit_harden | 0 / 1 / 2 | 禁用(默认)/ 仅对无特权用户加固 / 对所有用户加固 |
/proc/sys/net/core/bpf_jit_kallsyms | 0 / 1 | 禁用(默认)/ 仅特权用户可把 JIT 程序导出为bpf_prog_<tag>符号,供perf与栈展开使用;bpf_jit_harden开启时此功能被禁用 |
/proc/sys/kernel/unprivileged_bpf_disabled | 0 / 1 / 2 | 允许无特权使用bpf(2)(默认)/ 禁用(重启前不可逆)/ 禁用但运行时可改回(Linux 5.13+;内核启用BPF_UNPRIV_DEFAULT_OFF时默认为 2)。该开关不影响 seccomp、传统 socket filter 等 cBPF 程序 |
3. 程序类型:XDP 与 tc
参考指南的 Program Types 子文档在 18 种 BPF 程序类型中重点剖析 Cilium 使用的两种:XDP 与 tc。
3.1 XDP:最早期的可编程包处理
XDP(eXpress Data Path)在驱动收到包的瞬间运行 BPF 程序——此时驱动刚从接收环取出包,还未分配skb、未进入 GRO 引擎,是软件路径上最早的点。XDP 与内核协同而非绕过内核,优势包括:复用上游驱动与工具、复用路由表/socket 等内核设施、无需跨内核/用户空间边界(在 Meltdown/Spectre 时代尤其重要)、可平凡地 punt 给内核 TCP/IP 栈、程序运行时原子热替换、无需专用硬件/hugepages/第三方模块,且在 4.8+ 内核的主流发行版中开箱即用。
XDP 框架保证包在单个 DMA 页内线性排布、可读可写,并提供 256 字节 headroom(通过bpf_xdp_adjust_head()做封装/解封装,bpf_xdp_adjust_meta()在包前添加对内核协议栈不可见、但对 tc 程序可见的自定义元数据)。程序上下文为:
struct xdp_buff { void *data; void *data_end; void *data_meta; void *data_hard_start; struct xdp_rxq_info *rxq; };指针不变式为data_hard_start <= data_meta < data < data_end;rxq提供接收队列元数据(queue_index等,环配置时填充而非 XDP 运行时)。
返回码(linux/bpf.h的enum xdp_action):
enum xdp_action { XDP_ABORTED = 0, XDP_DROP, XDP_PASS, XDP_TX, XDP_REDIRECT, };XDP_DROP:驱动层直接丢弃,适合 DDoS 缓解与防火墙;XDP_PASS:交给内核协议栈(分配skb、进 GRO),等价于无 XDP 时的默认行为;XDP_TX:从同一网卡发回(hairpin 负载均衡器场景);XDP_REDIRECT:从另一块网卡发出,或重定向到 BPF cpumap(让 XDP 服务 CPU 不阻塞、把包推给远端 CPU 处理);XDP_ABORTED:异常态,行为同 DROP 但会经过trace_xdp_exceptiontracepoint,便于监控程序误行为。
典型用例:DDoS 缓解/防火墙(XDP_DROP极低每包成本,offloaded XDP 可做到线速);转发与负载均衡(XDP_TX/XDP_REDIRECT+ headroom 封装解封装);协议栈前过滤(在协议栈看到无关包之前丢弃,缩小攻击面;由于此时包还未分配skb,BPF 可自由改写包再"伪装"成网卡刚收到);流量采样与监控(通过 lockless per-CPU perf ring buffer 上送截断/完整包 + 自定义元数据)。
三种运行模式:Native XDP(默认,直接运行在驱动早收路径,主流 10G+ 网卡已支持);Offloaded XDP(SmartNIC 上的内核 JIT 将 BPF 翻译为网卡指令,部分 helper 不可用);Generic XDP(驱动无需改动,在协议栈更晚处运行,主要供开发测试,性能显著低于前两者)。文档附了完整的原生 XDP 驱动支持表(Intel i40e/ixgbe/ice、Mellanox mlx4/mlx5、Amazon ena、Netronome nfp、virtio_net、veth 等及各自的内核版本门槛),可用ethtool -i eth0查看接口驱动名。
3.2 tc(traffic control):基于sk_buff的双向钩子
相对 XDP,tc BPF 的三大差异:
- 输入上下文是
sk_buff而非xdp_buff:协议栈已分配缓冲并解析出元数据,tc ingress 程序可读写mark、pkt_type、protocol、priority、queue_mapping、napi_id、cb[]、hash、tc_classid/tc_index、VLAN 元数据及 XDP 传递的自定义元数据(struct __sk_buff各成员定义在linux/bpf.h)。代价是协议栈做这些工作本身有开销——这正是 XDP 与 tc 性能差异的主要来源。sk_buff元数据丰富但改协议更麻烦(栈基于元数据而非逐包内容工作),xdp_buff改写包简单但拿不到元数据——两者通过"XDP 传自定义元数据给 tc"互补组合。 - 可挂在 ingress 和 egress 两个方向(XDP 仅 ingress)。内核钩子
sch_handle_ingress()(来自__netif_receive_skb_core())与sch_handle_egress()(来自__dev_queue_xmit())是数据路径的主收发函数,除 XDP 外所有进出包都会经过,tc BPF 因此具备全量可见性。 - 不需要驱动改动:运行在通用层的钩子,可挂到任意网络设备(veth 等虚拟设备亦可)。代价是性能低于 native XDP,但仍处于 GRO 之后、任何协议处理与 iptables/nftables 钩子之前(ingress);egress 则在 iptables POSTROUTING 之后、交给驱动 GSO 引擎之前的最后一点。
tc 层的 BPF 运行于cls_bpf分类器。文档强调这个"分类器"称谓有误导性:cls_bpf实际是全可编程包处理器,可读写skb元数据与包数据并直接返回动作裁决,是"自包含实体"。Cilium 部署cls_bpf时,每个钩子只挂一个程序且使用direct-action模式——分类与动作融合为单一单元,避免传统"分类器 + 动作模块"线性遍历的扩展性问题。cls_bpf实例挂在伪 qdiscsch_clsact(ingressqdisc 的超集,可同时管理 ingress/egress 钩子)上;两个钩子都在 fast-path 中无锁执行(egress 不在 qdisc root lock 下,仅在 RCU 读侧、禁抢占),这使得sch_clsact的 egress 钩子可以先于sch_htb等 qdisc 完成重分类并设置skb->mark/priority,降低锁竞争。
返回码(linux/pkt_cls.h):TC_ACT_UNSPEC(-1)、TC_ACT_OK(0)、TC_ACT_SHOT(2)、TC_ACT_STOLEN(4)、TC_ACT_REDIRECT(7)等。语义要点:TC_ACT_UNSPEC≈TC_ACT_OK(区别是后者会按skb->tc_classid设置skb->tc_index);TC_ACT_SHOT通过kfree_skb()释放 skb 并返回NET_XMIT_DROP(perf的 drop monitor 可见),TC_ACT_STOLEN通过consume_skb()释放并向上层报告成功(drop monitor 不可见);TC_ACT_REDIRECT配合bpf_redirect()把 skb 注入任意设备的 ingress/egress 路径,目标设备无需另挂cls_bpf。
tc BPF FAQ(文档原样保留三个高频问题):act_bpf还有意义吗?——没有,cls_bpf是其超集,act_bpf需配合cls_matchall使用反而更慢,且无 offload 接口;cls_bpf不建议用非 direct-action 模式;offloadedcls_bpf与 offloaded XDP 性能无本质差异(同一内核 JIT 编译到同一目标指令集),选择取决于可用 helper 集合。
tc BPF 用例(与 Cilium 强相关):容器策略执行——容器网络命名空间通过 veth 对与宿主相连,宿主机侧 veth 的 tc ingress/egress 钩子成为所有容器流量的必经之路;veth 是纯skb世界,generic XDP 因克隆 skb 与线性化限制不适用,tc BPF 正是正确选择;转发与负载均衡——bpf_redirect()接管转发逻辑,桥接设备变得不必要;双向流监控——通过bpf_skb_event_output()上送 per-CPU perf ring buffer,文档特别指出Cilium 深度使用这种机制:对丢弃的包附带注解(endpoint 标签、策略违规原因等)以提供更丰富的上下文;包调度器预处理——sch_clsactegress 钩子在拿 qdisc root lock 之前完成重活。
4. 开发工具链:LLVM 编译、C 语言陷阱与 iproute2 加载
Development Tools 子文档覆盖了 BPF 生态的用户态工具、内省设施与内核开关。Cilium 的 BPF 程序正是面向iproute2 的 BPF 加载器实现的(仓库保留了兼容性以便用 iproute2 开发调试)。
4.1 环境准备与内核配置
开发环境依赖(Fedora/Ubuntu/openSUSE 分别给出 dnf/apt/zypper 安装列表,核心是 clang、llvm、libelf、libcap、graphviz 等);如需构建开发内核,net-next树是 BPF 新特性的家。内核.config中需要("这些项也是 Cilium 需要的"):
CONFIG_CGROUP_BPF=y CONFIG_BPF=y CONFIG_BPF_SYSCALL=y CONFIG_NET_SCH_INGRESS=m CONFIG_NET_CLS_BPF=m CONFIG_NET_CLS_ACT=y CONFIG_BPF_JIT=y CONFIG_LWTUNNEL_BPF=y CONFIG_HAVE_EBPF_JIT=y CONFIG_BPF_EVENTS=y CONFIG_TEST_BPF=mCONFIG_HAVE_EBPF_JIT由架构自动 select,无 JIT 的架构回退解释器(效率更低)。验证方式是在内核树tools/testing/selftests/bpf/下make后运行sudo ./test_verifier(verifier 测试会打印全部检查项并以Summary: N PASSED, ...汇总),sudo make run_tests跑完整自测。
4.2 LLVM/clang:最小 XDP 程序与完整编译流程
LLVM 是(文档写作时)唯一提供 BPF 后端的编译器套件。标准工作流:BPF 程序用 C 编写 → LLVM 编译为 ELF 对象 → 用户态 BPF ELF 加载器(iproute2 等)解析并经bpf()系统调用推入内核 → 内核验证 + JIT → 返回程序 fd → 挂载到子系统,必要时再 offload 到网卡。
最小可运行的 XDP drop 程序(文档xdp-example.c原文):
#include <linux/bpf.h> #ifndef __section # define __section(NAME) \ __attribute__((section(NAME), used)) #endif __section("prog") int xdp_drop(struct xdp_md *ctx) { return XDP_DROP; } char __license[] __section("license") = "GPL";编译与加载:
$ clang -O2 -Wall --target=bpf -c xdp-example.c -o xdp-example.o # ip link set dev em1 xdp obj xdp-example.o要点:
- 目标三元组:
bpf(跟随宿主字节序,推荐)、bpfel/bpfeb(交叉编译到不同字节序宿主);LLVM >= 3.9 使用官方 BPF 机器值EM_BPF(0xf7),file命令可见unknown arch 0xf7; - LLVM 6.0 起支持 BPF 汇编解析(
llvm-mc -triple bpf -filetype=obj),>= 4.0 可用-g生成 DWARF 调试信息;llvm-objdump -S --no-show-raw-insn可反汇编,其行号与内核验证器日志一一对应——程序被验证器拒绝时,这是把指令关联回 C 源码的关键手段; - BTF(BPF Type Format):由 DWARF 转换而来(需 elfutils >= 0.173,否则
llc加-mattr=dwarfris;pahole -J完成转换),随对象加载进内核后可为 Map 标注 key/value 类型,大幅改善内省与 pretty-print(详见第 5 节); -mcpu=probe是文档推荐的选项,且"也是 Cilium 内部使用的":LLVM BPF 后端会向内核探测指令集扩展可用性并在适当处使用,而默认generic(=v1基础指令集)保证 4.9+ 老内核可加载;--target=bpfvs 默认目标的行为差异(引用内核bpf_devel_QA.txt):头文件内联汇编、.eh_frame段、switch 跳转表(可用-fno-jump-tables关闭)、以及 32 位架构上指针/long 位宽——网络场景一律首选--target=bpf;- LLVM 7.0+ 的
-mattr=+alu32启用 32 位子寄存器(w寄存器)代码生成,可减少类型扩展指令序列并利于 32 位架构 JIT。
4.3 用 C 写 BPF 程序的 11 个陷阱
工具链文档列出的 C 语言编写注意事项是实战中最容易踩坑的部分,完整继承如下:
一切必须内联(旧内核/旧 LLVM 上无函数调用、无共享库):公共库代码放头文件(Cilium 正是重度使用者,见
bpf/lib/);库函数应标注always_inline,否则 LLVM 可能不内联并生成加载器无法解决的 relocation;一个 C 文件可含多个程序段:通过 section 注解组织,如
__section("ingress")、__section("egress")共享同一个maps段定义的acc_map与account_data()内联 helper。文档给出的完整tc-example.c演示了struct bpf_elf_map(iproute2 私有格式,Cilium 遵循该模型):type = BPF_MAP_TYPE_ARRAY、pinning = PIN_GLOBAL_NS、max_elem = 2,两个程序分别以dir = 0/1记账,lock_xadd映射为 BPF 原子加指令(BPF_STX | BPF_XADD | BPF_W)。pinning 语义:PIN_GLOBAL_NS固定到/sys/fs/bpf/tc/globals/<map>,跨对象文件共享;PIN_OBJECT_NS为对象本地目录;PIN_NONE不固定(tc 退出后用户空间不可见,且每个程序得到独立 Map 实例)。加载后 ingress 程序找不到已存在的同名片就创建并固定,egress 加载时发现已存在即复用(加载器还会校验同名片的 key/value 尺寸等属性一致):$ clang -O2 -Wall --target=bpf -c tc-example.c -o tc-example.o # tc qdisc add dev em1 clsact # tc filter add dev em1 ingress bpf da obj tc-example.o sec ingress # tc filter add dev em1 egress bpf da obj tc-example.o sec egresslicense段的 GPL 许可证决定 GPL-only helper(bpf_ktime_get_ns()、bpf_probe_read()等)是否可见;不允许全局变量:变通方案是用单槽
BPF_MAP_TYPE_PERCPU_ARRAY作 scratch 缓冲区(BPF 程序执行期间内核保证不被抢占,tail call 间也成立);跨调用持状态用普通 Map;不允许 const 字符串/数组(会生成加载器拒绝的 relocation):
trace_printk()需用"把 fmt 复制进栈上局部数组"的宏包装;但文档不推荐生产使用(字符串每次上栈、helper 最多 5 个参数只留 3 个变量位),推荐skb_event_output()/xdp_event_output()走 lockless per-CPU perf ring buffer——Cilium 的 monitor 正是用这些 helper 实现调试框架与策略违规通知;memset/memcpy/memmove 用 LLVM 内建(常量尺寸
n保证内联);memcmp内建有 corner case 暂不推荐;(当时)没有循环:验证器以深度优先搜索保证终止;常量上限循环可用
#pragma unroll(文档给出遍历 IPv6 扩展头的例子);或用 tail call 自调 + per-CPU scratch Map 实现动态循环(上限 34 次迭代:初始程序 + 33 次 tail call);用 tail call 切分程序:通过
BPF_MAP_TYPE_PROG_ARRAY+__section_tail(ID, KEY)段命名实现,iproute2 加载器按段名1/0匹配struct bpf_elf_map的id并把程序装入对应索引;运行时用tc exec bpf graft m:globals/jmp_map key 0 obj new.o sec foo原子替换某阶段的程序。文档举了 Cilium 的实例:包丢弃通知可运行时开关——skb_event_output()位于被 tail call 的程序里,不启用时走 fall-through 零成本,启用时由守护进程向对应索引写入程序;512 字节栈上限:超出的临时数据用单槽 per-CPU Map 扩容;
内联汇编:LLVM 6.0+ 支持,文档给了一个
lock *(u64 *)(%0+0) += %1的 64 位原子加玩具示例及其验证器输出;#pragma pack消除结构体 padding:现代编译器默认对齐会插入 padding,若结构体(含 padding)作 Map value 且整结构体入栈,验证器会报invalid indirect read from stack(文档附了struct called_info的 24 vs 20 字节布局图与完整验证器报错);解法:#pragma pack(n)(代价是非对齐访问可能降性能或被验证器拒绝),或更推荐在末尾显式添加u32 pad;失效引用问题:
bpf_skb_store_bytes等可能改变包大小的 helper 之后,之前取的包数据指针被验证器置为失效,必须重新从skb->data取引用再访问(文档给了ip4->protocol被拒与修复的两段代码及验证器日志R9 invalid mem access 'inv')。
4.4 iproute2 加载工作流:XDP 与 tc
XDP 对象加载:ip link set dev em1 xdp obj prog.o(默认段名prog,否则sec foobar;甚至可从.text段加载)。已有程序时默认报错,替换需-force(大多数 XDP 驱动支持无中断原子替换;XDP 驱动同一时刻只有一个程序,多级逻辑用 tail call 实现)。ip link | grep xdp找挂载了 XDP 的接口,ip -d link看详情,ip link set dev em1 xdp off卸载。三种模式:xdpdrv(native)、xdpgeneric(实验用途)、xdpoffload(SmartNIC;不支持的 Map 类型/helper 会被验证器拒绝并告知)。注意:ip link set ... xdp obj会先尝试 native、失败自动回退 generic;显式xdpdrv则失败即错、绝不回退;模式之间切换不原子(generic→offload 直接替换会报File exists),必须先off再进新模式。加载后可bpftool prog dump xlated id <N>/bpftool prog show id <N>检查(示例输出见第 5 节)。
tc 对象加载:
# tc qdisc add dev em1 clsact # tc filter add dev em1 ingress bpf da obj prog.o # ingress 钩子 # tc filter add dev em1 egress bpf da obj prog.o # egress 钩子clsact是仅容纳分类器与动作、不做实际排队的哑 qdisc,提供 ingress/egress 两个钩子;da(direct-action)模式文档明确要求"应该总是指定"——BPF 程序自己完成全部包操作并返回TC_ACT_*裁决,无需外部 action 模块。tc filter show输出中的prog.o:[ingress] direct-action id 1 tag c5f7825e5dac396f:id是系统唯一 BPF 程序号(供 bpftool 使用),tag是指令流的哈希(可关联对象文件或 perf 栈);pref/handle自动生成,但若计划原子替换,建议首次加载就显式指定以便后续tc filter replace dev em1 ingress pref 1 handle 1 bpf da obj prog.o sec foobar。tc filter del dev em1 ingress删除程序,tc qdisc del dev em1 clsact移除整个 qdisc。硬件 offload 需先ethtool -K em1 hw-tc-offload on再bpf skip_sw da,输出出现in_hw即已 offload;tc 与 XDP offload 不能同时加载。netdevsim驱动(modprobe netdevsim+echo "1 1" > /sys/bus/netdevsim/new_device)提供实现 XDP/tc offload 接口的假网卡,供测试内核改动或控制面程序。
进阶选项:verb在加载成功时也打印验证器日志;pinned /sys/fs/bpf/prog(或短格式m:prog)直接挂载 BPFFS 中已固定的程序;iproute2 自动探测已挂载的 BPFFS 实例(未找到则自动挂到/sys/fs/bpf/,或复如/var/run/bpf下的既有挂载),并建立ip/xdp→tc/符号链接使globals命名空间跨程序类型共享 Map。iproute2 安装时提供的<iproute2/bpf_elf.h>头文件即struct bpf_elf_map的稳定契约。加载器解析顺序:先取maps/license辅助段并做兼容性处理 → 创建(或复用固定)Map → 处理含 Map relocation 的程序段(把 map fd 编码进指令立即数)→ 通过bpf()系统调用创建程序 → 更新 tail call Map。
5. 调试与测试:bpftool、JIT 调试、tracepoint 与资源限制
Debugging and Testing 子文档给出完整的排障方法论。bpftool(位于内核树tools/bpf/bpftool/)是主内省工具:
总览:
bpftool prog列出全部已加载程序(含 tag、xlated/jited/memlock 尺寸、map_ids);bpftool map列出全部 Map(类型、key/value 尺寸、max_entries、memlock);任意命令可加--json --pretty;从 Cilium 实际部署出发定位程序——这是文档给出的实战链路:
# tc filter show dev cilium_host egress filter protocol all pref 1 bpf chain 0 filter protocol all pref 1 bpf chain 0 handle 0x1 bpf_host.o:[from-netdev] \ direct-action not_in_hw id 406 tag e0362f5bd9163a0a jited # bpftool prog show id 406 406: sched_cls tag e0362f5bd9163a0a loaded_at Apr 09/16:24 uid 0 xlated 11144B jited 7721B memlock 12288B map_ids 18,20,8,5,6,14即 Cilium 主机端口的 egress 程序来自
bpf_host.o的from-netdev段(对应仓库中 bpf_host.c),程序类型sched_cls(BPF_PROG_TYPE_SCHED_CLS),关联 6 个 Map id 可继续下钻;指令转储:
bpftool prog dump xlated id 406输出验证器之后的指令镜像(加载器提供的原始指令经验证器多种改写,例如 hash Map lookup helper 被内联重写),输出中map[id:18]、call bpf_skb_event_output#5656112等关联信息由 bpftool 通过 kallsyms 解析——因此需echo 0 > /proc/sys/kernel/kptr_restrict且echo 1 > /proc/sys/net/core/bpf_jit_kallsyms,否则调用显示为bpf_unspec#0;dump jited id 406(可加opcodes)反汇编 JIT 原生镜像;dump xlated id 406 visual生成 dot 文件,dot -Tpng转图(文档给出了bpf_host.o控制流图的截图示例);tail call 在 dump 中呈现为call bpf_tail_call#12形式以便调试;Map 转储与 BTF 增强:无 BTF 时
bpftool map dump id 5输出原始十六进制;带 BTF(配合 iproute2 的BPF_ANNOTATE_KV_PAIR(map, key_type, value_type)宏,编译器流程为clang -g ... | llc -mcpu=probe -mattr=dwarfris ... && pahole -J ...)时,dump 变为带结构体字段名的可读 JSON(文档以内核 selftesttest_xdp_noinline.o为例);程序加载成功且带 BTF 时prog show显示btf_id,可用bpftool btf show/btf dump id <N> format c查看类型信息。此外 bpftool 支持按 key 的 lookup/update/delete/get-next-key 以及把程序/Map pin 入 BPFFS;内核自测:
tools/testing/selftests/bpf/套件覆盖验证器、程序 tag、Map 接口与类型,以及针对 LLVM 后端/解释器/JIT 的运行时测试;JIT 调试:
echo 2 > /proc/sys/net/core/bpf_jit_enable后,每次编译把 JIT 镜像打到内核日志(flenBPF 指令数、proglen生成字节数、pass优化遍数、image地址、触发进程),内核树tools/bpf/下的bpf_jit_disasm(可加-o附带原始操作码)读取最近一次 dump 反汇编;新版 bpftool 已可直接按程序 id 做同样转储;perf 剖析 JIT 程序:前置
bpf_jit_kallsyms=1(切换无需重载程序),文档演示了perf record -a -g -e skb:kfree_skb捕获bpf_clone_redirect()内克隆 skb 释放失败的完整栈,栈帧中出现bpf_prog_<tag>符号,把内核事件与具体 BPF 程序、挂载点(设备 + ingress/egress)精确对应;tracepoint 内省:内核提供
bpf:bpf_map_create、bpf:bpf_prog_load、bpf:bpf_obj_pin_map等一组 tracepoint(perf record -a -e bpf:*),XDP 的xdp:xdp_exception在三种场景触发:返回非法 action 码、返回XDP_ABORTED、XDP_TX发送失败(端口未 up、发送环满、分配失败等);这些事件还可被挂在 tracepoint 上的 BPF 程序自身二次处理,经bpf_perf_event_output()上送用户空间;tracing pipe:
bpf_trace_printk()的输出经/sys/kernel/debug/tracing/trace_pipe消费(tail -f即可);资源限制:BPF 程序与 Map 的内存计入
RLIMIT_MEMLOCK(ulimit -l查看,与 perf 相同)。默认限额通常不足以加载复杂程序或大 Map,bpf()会以EPERM失败;该限制主要针对无特权用户,按需ulimit -l unlimited或设为足够大的值即可。
6. 配套资源与延伸阅读
BPF 参考指南章节(toctree)共 5 个子文档,可作为深入阅读的路径:
| 子文档 | 覆盖内容 |
|---|---|
| architecture.rst | 指令集、helper、Maps、pinning、tail call、bpf2bpf、JIT、加固、offload、sysctl |
| toolchain.rst | 开发环境、LLVM 编译、C 语言陷阱、iproute2 加载器 |
| debug_and_test.rst | bpftool、内核自测、JIT 调试、tracepoint、tracing pipe |
| progtypes.rst | XDP 与 tc 两种网络程序类型的架构、返回码、用例与驱动支持 |
| resources.rst | BPF 生态项目列表(即索引页引用的bpf_users锚点所在) |
Cilium 侧的数据路径总览见 eBPF 数据路径文档;结合本仓库源码阅读时,建议从 bpf/bpf_host.c 的from-netdev段入口出发,沿着 bpf/lib/ 中classifiers.h、drop_reasons.h等头文件理解策略与丢包注解的实现,并用bpf/tests/下的自测程序(如 bpf_ct_tests.c)验证连接跟踪等逻辑。掌握以上内容后,你就能完整读懂 Cilium 在 tc/XDP 钩子点加载的每一段 BPF 程序,并具备开发、加载、验证与调试自有 BPF 数据路径的能力。
【免费下载链接】ciliumeBPF-based Networking, Security, and Observability项目地址: https://gitcode.com/GitHub_Trending/ci/cilium
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考