eBPF 核心机制与安全
eBPF 核心机制与安全
eBPF 是什么(一段简史)
| 阶段 | 名称 | 能力 |
|---|---|---|
| 1992 | BPF(cBPF) | 仅用于包过滤(tcpdump),基于很有限的虚拟机指令 |
| 2014 | eBPF(extended BPF) | 通用字节码引擎,不止网络;引入 map、helper、verifier |
| 2016+ | 成熟 | 挂载点扩展到 kprobe/tracepoint/XDP/cgroup/socket/LSM,成为可观测性与网络事实标准 |
今天说”BPF”和”eBPF”基本同义;“cBPF”才是老式包过滤器。
程序执行生命周期
graph TD
A[用户写 eBPF 程序 C/Python] --> B[LLVM/clang 编译为 eBPF 字节码]
B --> C["bpf() syscall 加载到内核"]
C --> D{Verifier 静态校验}
D -->|拒绝| Z[加载失败 报错]
D -->|通过| E[JIT 编译为本地机器码]
E --> F[attach 到 Hook 点]
F --> G[事件触发时在内核态执行]
G --> H[(读写 BPF Map)]
H --> I[用户态工具读结果/下发配置]
关键点:加载(bpf syscall)只有一次,运行时触发(hook 事件)是纯内核态执行,用户态不再参与每条事件的处理。
Verifier(校验器)—— 安全的根基
传统内核模块(LKM)有 bug 就 panic 整个内核。eBPF 靠 verifier 在加载前把危险程序挡在门外。它主要检查:
| 检查项 | 具体内容 |
|---|---|
| 指针校验 | 只允许访问被批准的上下文(ctx)和 map 内的内存;禁止任意解引用、禁止未初始化指针读 |
| 有界循环 | 不允许无限循环;循环必须有 verifier 可证明的退出上界(早期版本直接禁止循环,5.x 起允许有界循环) |
| 指令数上限 | 单程序指令数上限约 100 万(防止 DoS) |
| 栈大小限制 | 栈上限 512 字节(需更大用 map 存) |
| 可达性 | 不可达指令会被拒绝 |
| helper 白名单 | 只能调用当前上下文允许的 bpf_helper 函数(如 bpf_probe_read/bpf_trace_printk) |
| 能力检查 | 某些 helper(如修改 skb)需对应 CAP_* 能力 |
结论:eBPF 程序”要么被 verifier 拒绝,要么安全运行”,不存在”跑起来再崩内核”的中间态。
JIT 编译
verifier 通过后,字节码由内核 JIT 编译器(如 x86/arm64 后端)编译为本地机器码,直接执行,而非解释执行——性能接近原生 C 函数。可通过 bpftool prog show 看是否 jited。
Hook 点全景(深入)
eBPF 程序必须挂载到某个 hook 才能生效。不同 hook 对应不同程序类型与能力:
| Hook 点 | 挂载位置 | 触发时机 | 典型用途 | 程序类型 |
|---|---|---|---|---|
| kprobe | 任意内核函数入口 | 进入该函数时 | 函数级追踪、延迟统计 | BPF_PROG_TYPE_KPROBE |
| kretprobe | 任意内核函数返回 | 该函数返回时 | 函数耗时、返回值 | 同上 |
| uprobe | 用户态程序函数 | 进入用户态函数时 | 追踪 MySQL/Redis/Nginx 内部 | BPF_PROG_TYPE_KPROBE |
| tracepoint | 内核静态稳定插桩点 | 内核到达该埋点时 | 稳定的 syscall/调度事件 | BPF_PROG_TYPE_TRACEPOINT |
| perf_event | 性能计数器 | 采样/ PMC 溢出时 | CPU 火焰图、硬件计数 | BPF_PROG_TYPE_PERF_EVENT |
| XDP | 网卡驱动最早收包点 | 驱动刚拿到 skb(甚至前) | DDoS 防护、高性能 LB、包过滤 | BPF_PROG_TYPE_XDP |
| TC(clsact) | 网络栈 ingress/egress | 包进出协议栈时 | 容器网络策略、流量整形 | BPF_PROG_TYPE_SCHED_CLS |
| cgroup | cgroup v2 层级 | 该 cgroup 内流量/套接字事件 | 按 cgroup 的流量/套接字策略 | BPF_PROG_TYPE_CGROUP_SKB 等 |
| socket / sock_ops | socket 层 | 连接建立/数据收发时 | socket LB、无 sidecar 加速 | BPF_PROG_TYPE_SOCK_OPS |
| LSM | 内核安全钩子 | 安全决策点(open/exec 等) | 运行时安全策略 | BPF_PROG_TYPE_LSM |
应用型重点记三类:XDP/TC(网络)、kprobe/tracepoint(观测)、cgroup/socket(容器级策略)。Cilium 主要吃 XDP + TC + socket。
程序类型 vs attach 类型
- 程序类型(prog type):决定程序能用哪些 helper、能访问什么上下文(如
BPF_PROG_TYPE_XDP拿到xdp_md而非pt_regs)。 - attach 类型(attach type):决定程序挂到哪个具体 hook(如同一个
cgroup_skb程序可 attach 为ingress或egress)。 - 二者必须匹配,否则
bpf()加载报EINVAL。
BPF Map 深入(内核态↔用户态桥梁)
eBPF 程序本身无状态——不能声明持久全局变量。所有跨事件、跨内核/用户态的数据都走 BPF Map:
| Map 类型 | 用途 | 容量行为 |
|---|---|---|
hash / array | 通用 KV 统计(每 PID 的 syscall 计数) | 满则写入失败(-E2BIG) |
percpu_hash / percpu_array | 每 CPU 独立计数,避免多核抢占 | 高性能场景必备;满静默丢弃 |
lru_hash | 满时淘汰最久未用条目(缓存语义) | 不报错,自动淘汰 |
ringbuf / perf_event_array | 事件流(每条连接元数据推用户态) | ringbuf 满则事件丢失 |
lpm_trie | 最长前缀匹配 | Cilium 路由/LB 查找核心 |
sockmap / sockhash | socket 重定向 | socket LB、无 sidecar 加速 |
stack_trace | 栈追踪(火焰图) | — |
sock | 存 socket 引用 | — |
坑:
percpu_*/ringbuf满后静默丢弃不报错——观测数据”看起来变少”可能是 map 爆了(详见 eBPF 排障实战 生产反模式)。
Cilium 的 Service 负载均衡用 lpm_trie + sockmap 存 “VIP:Port → 后端 Endpoint” 映射,包进来直接查 map 改写,全程不过 iptables。
eBPF vs 传统内核模块(LKM)
| 维度 | LKM | eBPF |
|---|---|---|
| 安全性 | bug 可 panic 内核 | verifier 预校验,安全 |
| 加载 | insmod,需编译匹配内核 | LLVM 编译字节码,运行时加载 |
| 数据出口 | 自己实现(procfs/debugfs) | 标准 BPF Map |
| 热更新 | 需卸载/重装 | 可原子替换(bpftool) |
| 崩溃影响 | 可能整机挂 | 程序拒绝加载或异常被隔离 |
关联知识
- eBPF 知识总览 — 本主题索引
- eBPF 性能模型 — 为什么 eBPF 快
- eBPF 工具链实战 — bpftool/bpftrace 怎么用
- ../cgroup v2 详解 — cgroup 级 eBPF(BPF_CGROUP)与 v2 的 NIP 约束
- ../Linux 内核调优总览 — eBPF 在内核调优中的位置
学习时间
| 阶段 | 时间 | 备注 |
|---|---|---|
| 核心机制 | 2026-07-16 | verifier 检查清单、JIT、Hook 点触发时机、Map 全类型与容量行为、prog/attach 类型区分 |