假设你已经阅读过 cBPF 相关文档,并对 Linux 网络栈、kprobe、netfilter 等机制有一定了解。
eBPF 的核心目标是:在不修改内核源码、无需重启系统的前提下,将用户自定义的逻辑安全地注入到内核的关键执行路径中运行。
典型应用场景包括:
可以将 eBPF 理解为一套完整的内核级可编程机制,具备受限沙箱环境以及完善的工具链支持(如 clang、libbpf、bpftool 等),形成一个强大的生态系统。
相较于传统的 cBPF,eBPF 在多个维度实现了显著增强:
cBPF 仅提供两个 32 位寄存器 A 和 X,外加一个临时存储区域(scratch space)用于辅助计算。
eBPF 则引入了 11 个 64 位通用寄存器:
R0–R10,具体用途如下:
此外,每个程序拥有最多 512 字节的栈空间,可通过 R10 加偏移的方式访问(例如 r10 - 16)。
M[16]
eBPF 支持调用预定义的 helper 函数,并能通过 map 结构实现数据持久化和用户态通信。常见的交互方式包括哈希表、数组、ringbuf 等结构。
bpf_map_update_elembpf_get_prandom_u32bpf_redirect
eBPF 可以附加在多种内核事件点上,包括但不限于:
所有 eBPF 程序在加载至内核前必须经过 Verifier 的严格校验,确保其安全性。主要检查内容包括:
若程序未能通过验证,则拒绝加载。因此,可以形象地理解为:cBPF 是简单的过滤脚本,而 eBPF 提供了一个“迷你 CPU + 安全沙箱”的完整执行环境。
| 寄存器 | 作用说明 |
|---|---|
| R0 | 用于存放函数返回值 |
| R1–R5 | 作为参数传递给 helper 函数 |
| R6–R9 | 由被调用方负责保存,适合用作局部状态存储 |
| R10 | 帧指针,始终指向栈顶 |
每个 eBPF 程序最多可使用 512 字节的栈空间,访问方式限定为基于 R10 的负偏移形式,即:R10 - offset(offset 为正整数)。
示例代码:
// 假设要在栈上创建一个本地结构体 foo
struct foo *ptr = (void *)(long)ctx;
// 编译后实际生成类似指令:*(struct foo *)(r10 - 16)
Verifier 会强制检查所有栈访问是否落在 [-512, -1] 范围内,超出则加载失败。
struct bpf_insn
XDP_PASSXDP_DROPXDP_TXXDP_REDIRECTTC_ACT_OKTC_ACT_SHOTTC_ACT_REDIRECTeBPF 指令在内核中的表示结构如下:
struct bpf_insn {
__u8 code; // 操作码
__u8 dst_reg:4;
__u8 src_reg:4;
__s16 off;
__s32 imm;
};
直接手写此类指令较为少见,开发者通常采用 C 语言编写程序,再由编译器自动生成对应字节码。
例如:
SEC("xdp")
int xdp_drop_udp_53(struct xdp_md *ctx) {
// C 源码逻辑
}
该代码会被
clang -target bpf 工具链编译为标准的 eBPF 字节码 .o,最终加载进内核执行。bpf_insn
CALL:用于调用 helper 函数或子程序;EXIT:表示函数返回,结束执行。Verifier 是保障 eBPF 安全性的核心组件,任何程序在加载进入内核前都必须通过其静态分析验证。
只允许访问以下几类已知安全的内存区域:
ctx)中明确暴露的字段;所有指针偏移必须经过显式的边界检查,禁止任意类型转换成内核地址进行非法读写。
程序不得包含无限循环。现代内核虽支持有界循环,但要求 Verifier 能够静态推导出最大迭代次数。
每条路径上的寄存器和栈变量在使用前必须已被赋值;
所有可能的执行分支最终返回的类型需保持一致。
单个 eBPF 程序的指令数量存在上限(通常为 4096 条),防止过长程序影响系统稳定性。
eBPF 程序受到最大指令数量的限制,该限制可在内核中进行配置,通常范围在几千到十几万条之间。当超出此限制时,系统会触发常见错误提示:
R# unbounded memory access
invalid mem access
R# min value is negative, must be >= 0
invalid BPF_LD/BPF_ST instruction
编写 eBPF 程序的一个核心技巧是“取悦 Verifier”,即确保程序能通过内核校验器的检查。为此,需遵循以下实践:
bpf_xdp_adjust_head
bpf_map_lookup_elem
以下列出的是常用的 eBPF 程序类别,每种可视为一个“Hook 家族”:
XDP(eXpress Data Path)
BPF_PROG_TYPE_XDP
struct xdp_md *ctx
XDP_DROP / XDP_PASS / XDP_TX / XDP_REDIRECT
tc(Traffic Control)
BPF_PROG_TYPE_SCHED_CLS
SCHED_ACT
struct __sk_buff *
TC_ACT_OK / TC_ACT_SHOT / TC_ACT_REDIRECT
kprobe / kretprobe
BPF_PROG_TYPE_KPROBE
struct pt_regs *
tracepoint / raw_tracepoint
perf event
BPF_PROG_TYPE_PERF_EVENT
cgroup 程序族
cgroup_skb
cgroup_sock
cgroup_sock_addr
socket 程序
SK_SKB
SK_MSG
SOCKET_FILTER
SOCK_OPS
LSM / LSM_CGROUP(安全相关)
典型的编译命令如下:
clang -O2 -g -target bpf \
-D__TARGET_ARCH_x86 \
-c xdp_prog.c -o xdp_prog.o
注意事项:
-target bpf
-D__TARGET_ARCH_x86 / arm / arm64 / riscv
-g
现代主流推荐方式为使用 libbpf + CO-RE(一次编译,到处运行):
CONFIG_DEBUG_INFO_BTF=y
bpftool gen skeleton
.o
#include "xdp_prog.skel.h"
struct xdp_prog_bpf *skel;
skel = xdp_prog_bpf__open_and_load();
if (!skel) { ... }
xdp_prog_bpf__attach(skel); // 自动 attach 到配置好的网卡
高层级辅助工具介绍:
bpftool prog load/attach
bpftool map dump/update
bpftool gen skeleton
eBPF Map 是一种内核级的键值存储结构,可供 eBPF 程序与用户态进程共享数据。
常见 Map 类型包括:
BPF_MAP_TYPE_HASH:通用哈希表;BPF_MAP_TYPE_HASH
BPF_MAP_TYPE_ARRAY:固定大小数组;BPF_MAP_TYPE_ARRAY
*_PERCPU
LRU_HASH
BPF_MAP_TYPE_RINGBUF:高效的单生产者多消费者环形缓冲区,依赖 Linux 5.7+ 内核版本;BPF_MAP_TYPE_RINGBUF
BPF_MAP_TYPE_PERF_EVENT_ARRAY:传统的 perf buffer 实现;BPF_MAP_TYPE_PERF_EVENT_ARRAY
BPF_MAP_TYPE_LPM_TRIE:前缀树结构,广泛用于 IP 前缀匹配场景。BPF_MAP_TYPE_LPM_TRIE
在 eBPF 程序中通过 helper 函数访问 Map,示例定义(CO-RE 风格):
// map 定义(CO-RE 风格)
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, __u32); // 举例:IP
// 定义一个用于统计的 map,存储 IP 对应的计数值
__type(value, __u64);
__uint(max_entries, 1024);
} ip_cnt SEC(".maps");
// 在 eBPF 程序中对源 IP 进行计数处理
__u32 key = src_ip;
__u64 init_val = 1;
__u64 *val;
// 查找当前 IP 是否已有记录
val = bpf_map_lookup_elem(&ip_cnt, &key);
if (!val) {
// 若未存在,则初始化为 1
bpf_map_update_elem(&ip_cnt, &key, &init_val, BPF_ANY);
} else {
// 已存在则原子增加计数
__sync_fetch_and_add(val, 1); // 或等价于 (*val)++
}
// 用户态通过 libbpf 获取 map 数据
int map_fd = bpf_map__fd(skel->maps.ip_cnt);
__u32 key = ...; // 设置要查询的键
__u64 value;
// 从 map 中读取对应值
bpf_map_lookup_elem(map_fd, &key, &value);
ctx->data/ctx->data_end
7. 初探 XDP 丢包实例
本示例展示如何在 XDP 层面拦截并丢弃目标端口为 53 的 UDP 数据包。
7.1 内核态 eBPF XDP 程序实现
以下代码实现了基于 XDP 的数据包过滤逻辑:
// xdp_drop_udp_53.c
#include "vmlinux.h" // 支持 CO-RE 的内核结构定义(可由 bpftool 自动生成)
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
// 许可证声明,必须存在以加载程序
char LICENSE[] SEC("license") = "GPL";
// 将函数绑定到 XDP 执行段
SEC("xdp")
int xdp_drop_udp_53(struct xdp_md *ctx)
{
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
__u16 h_proto;
__u64 nh_off;
// 第一步:确保有足够的空间容纳以太网头部
nh_off = sizeof(*eth);
if (data + nh_off > data_end)
return XDP_PASS;
h_proto = bpf_ntohs(eth->h_proto);
// 第二步:仅处理 IPv4 数据包
if (h_proto != ETH_P_IP)
return XDP_PASS;
struct iphdr *iph = data + nh_off;
if ((void *)(iph + 1) > data_end)
return XDP_PASS;
// 第三步:只关注 UDP 协议
if (iph->protocol != IPPROTO_UDP)
return XDP_PASS;
// 第四步:根据 IP 头长度计算 UDP 头位置
__u32 ip_hdr_len = iph->ihl * 4;
struct udphdr *udph = (void *)iph + ip_hdr_len;
if ((void *)(udph + 1) > data_end)
return XDP_PASS;
// 第五步:检查目的端口是否为 53(DNS)
if (bpf_ntohs(udph->dest) == 53) {
// 可在此处更新 map 实现丢包统计功能
return XDP_DROP;
}
return XDP_PASS;
}
关键说明:
指针 + sizeof(...) <= data_end
指向当前数据包的起始地址;
每次访问 packet 数据前都必须校验指针有效性;
bpf_ntohs()
用于网络字节序与主机字节序之间的转换;
返回
XDP_DROP
表示在网卡驱动层级直接丢弃该包;
此操作使数据包不会进入内核协议栈的后续处理流程,如
ip_rcv
或
udp_rcv
7.2 用户态加载逻辑(基于 libbpf 的 skeleton 模式)
以下是简化的用户态程序框架,展示如何加载和挂载 XDP 程序:
#include "xdp_drop_udp_53.skel.h"
int main(int argc, char **argv)
{
const char *ifname = "eth0";
__u32 ifindex = if_nametoindex(ifname);
struct xdp_drop_udp_53_bpf *skel;
int err;
// 打开并加载编译好的 BPF skeleton
skel = xdp_drop_udp_53_bpf__open_and_load();
if (!skel)
return 1;
// 将 XDP 程序附加到指定网络接口
// XDP 程序附加逻辑
err = xdp_drop_udp_53_bpf__attach(skel);
if (err) {
return 1;
}
printf("XDP program attached on %s\n", ifname);
// 可在此处进行阻塞处理或实现其他控制流程
// detach 与 destroy 操作将在程序退出时统一执行
xdp_drop_udp_53_bpf__destroy(skel);
return 0;
网卡 -> 驱动 -> netif_receive_skb -> ip_rcv -> udp_rcv -> 找 socket
↓
在这里跑 cBPF filter(per-socket)
eBPF XDP 程序在数据包进入时更早阶段生效:
网卡 -> 驱动
↓
XDP prog (DROP/PASS/REDIRECT)
↓
netif_receive_skb -> ip_rcv -> ...
eBPF tc ingress 则作用于网络栈稍后层级:
网卡 -> 驱动 -> netif_receive_skb
↓
tc ingress BPF (DROP/REDIRECT)
↓
ip_rcv
sock_filter
其资源受限,仅提供有限寄存器,不支持 helper 函数和 map 结构,无法实现复杂状态管理。
相比之下,eBPF 提供了现代化的开发体验:
- 使用 C 语言(甚至 Rust、Go 等高级语言)编写,通过 clang 编译;
- 支持完整的寄存器集、局部栈空间、辅助函数(helper)以及 map 数据结构;
- 更适合实现复杂的业务逻辑,如状态机维护、流量统计与行为分析。
可以将 cBPF 视作一种轻量级、为兼容旧系统而保留的技术方案,而 eBPF 才是当前及未来的核心发展方向。
samples/bpf/
重点参考以下目录中的实例:
xdp1_kern.c /
xdp2_kern.c /
xdp_tx_iptunnel_kern.c
同时结合内核源码中提供的文档辅助理解:
tools/lib/bpf 、
tools/bpf/bpftool
实践建议分阶段进行:
第一阶段:按程序类型完成两个实战项目
- XDP 类型:实现一个简易防火墙,支持基于 IP 地址和端口号对 UDP 53 流量进行丢弃或重定向,并使用 map 记录统计数据;
- kprobe/tracepoint 类型:构建一个内核函数延迟监控工具,例如追踪特定函数调用耗时:
tcp_v4_connect
第二阶段:引入 CO-RE 与 BTF 技术
采用 libbpf + skeleton 架构开发:
vmlinux.h
体验“一次编译,多版本内核运行”的优势,理解如何利用 BTF 实现跨内核版本兼容性。
第三阶段:拓展至 cgroup 与 socket 类型程序
尝试将 BPF 程序挂载到 cgroup 上,用于限制特定服务组的网络访问行为或带宽使用,深入掌握 eBPF 在安全与资源控制方面的应用场景。
扫码加好友,拉您进群



收藏
