从零起步探索 eBPF - 新手向¶
前阵子拜读了赖老师的惊世巨著 LEOCC (SIGCOMM'25). 实验非常硬核, 一眼望去有很多没用过/没听说过的高级开发工具, 比如eBPF, gRPC, ...
本文的目标就是: 记录从零入门eBPF的过程, 重点是了解它是什么?基础用法是什么?简单案例跑下试试?
References:
- eBPF explained
- eBPF - IBM
- eBPF技术介绍
- eBPF完全入门指南
- 聊聊最近很火的eBPF
- BPF and XDP Reference Guide
- The eXpress Data Path
在开始前,进行一个 whole view:
eBPF的入门有两条主线,
- 一条是: 基于C语言编写eBPF程序, 函数调用需要遵循特殊的 API, 这一部分涉及到一些 define, segment, maps 定义与使用
- 写好以后, llvm编译器会根据一些规范, 为我们生成 elf 格式(
.o) 的文件
- 写好以后, llvm编译器会根据一些规范, 为我们生成 elf 格式(
- 另一条是: iproute / bpftool 等加载器, 将eBPF程序加载进 kernel
- 加载工具会读取elf中的各种段信息,将其加载到内核中
- 这一步也会涉及到一些约定与规范
厘清这个“主干过程”, 就会自然地使用 kernel提供的辅助函数 和 不同的map 做一些特定功能的BPF程序开发了
iproute/bpftool/elf 叽里咕噜说什么呢
(1) Workflow:
- 用户写程序: 您在C语言中编写程序逻辑, 并使用
SEC("maps")等宏来组织程序和Map - 编译 (LLVM): LLVM (Clang) 将您的C代码编译成一个 ELF 文件 (
.o文件)- 这个ELF文件内部有清晰的“段”划分,比如 maps 段、xdp 段...
- 加载 (Loader):
- 如果写的程序是用于网络过滤 (TC/XDP), 可以使用 iproute2 这个“加载器”, 去解析这个ELF文件并附加程序
- 或者, 可以使用 bpftool (的 prog load 等命令) 去加载、附加和管理这个ELF文件中的程序和Map
- bpftool 更加通用, 可以处理所有类型的eBPF程序 (网络、跟踪、安全...)
(2) 名词解释:
- ELF (Executable and Linkable Format) - 程序的“集装箱”
- Linux系统上一种标准的二进制文件格式,用于存放可执行文件、目标代码、共享库等
- 是编译的产物! "加载器"通过解析这个文件, 就能知道我们的程序代码在哪里、我们定义了哪些Map...
- iproute2 - “网络专家”加载器
- 是Linux中功能强大的网络管理工具包:
ip addr/ip route... - 是一个网络专用的eBPF加载器
- 主要用它的 tc 命令, 将eBPF程序附加到网络数据包的处理路径上
- 是Linux中功能强大的网络管理工具包:
- bpftool - “eBPF万能瑞士军刀”
- 功能非常全面, 几乎涵盖了eBPF的所有管理任务
- 作用1: 加载程序
bpftool prog load - 作用2: 附加程序
bpftool net attach - 作用3: 查看
bpftool prog list/bpftool map list - 作用4: 调试
bpftool map dump - 是一个eBPF专用的加载和管理工具, 功能异常强大!
BPF 与 eBPF 是什么¶
BPF: Berkeley Packet Filter¶
BPF (Berkeley Packet Filter), 中文翻译为伯克利包过滤器, 是类 Unix 系统上数据链路层的一种原始接口,提供原始链路层封包的收发
1992年, Steven McCanne 和 Van Jacobson 写了一篇名为 The BSD Packet Filter: A New Architecture for User-level Packet Capture 的论文
在文中,作者描述了他们如何在 Unix 内核实现网络数据包过滤
由于这些巨大的改进,所有的 Unix 系统都选择采用 BPF 作为网络数据包过滤技术。直到今天,许多 Unix 内核的派生系统中(包括 Linux 内核)仍使用该实现
eBPF: extended Berkeley Packet Filter¶
2014 年初, Alexei Starovoitov 实现了 eBPF (extended Berkeley Packet Filter)
经过重新设计, eBPF 演进为一个通用执行引擎,可基于此开发性能分析工具、网络数据包过滤、系统调用过滤,系统观测和分析等诸多场景
eBPF 最早出现在 3.18 内核中,此后:
原来的 BPF 就被称为经典 BPF,缩写 cBPF(classic BPF),cBPF 现在已经基本废弃
现在, Linux 内核只运行 eBPF, 内核会将加载的 cBPF 字节码透明地转换成 eBPF 再执行
本文讲解 eBPF 的重心¶
本文侧重于讲解 eBPF 在网络层的使用, 本质可以简单理解成:
- linux kernel 网络协议栈有很多 "钩子" (Hook Function)
- 数据包, 在进入到网卡再到流出网卡的过程中, 会触发这些 HOOK 上注册的"回调函数"执行相关过滤动作
如图所示, netfilter [Layer3-网络层] 有5个HOOK: PREROUTING / INPUT / FORWARD / OUTPUT / POSTROUTING

每个数据包 dataflow 会按顺序顺次经过这5个HOOK, 并采取不同的处理方式
所以本质上 netfilter 的 HOOKs 更像是 "数据溪流中设置的5个网", 针对 IP 数据包进行过滤
其他的处理工具¶
这一部分本质上是快速回顾《linux 网络协议栈》
- XDP: eXpress Data Path
- 位于 Layer 2: 数据链路层 (网卡驱动的layer)
- 是整个协议栈中最早的处理点
- 可以在数据包进入主网络协议栈(IP层)之前,就将其丢弃(
XDP_DROP)或转发(XDP_REDIRECT/XDP_TX),从而绕过了后续所有高成本的协议栈处理
- TC: Traffic Control
- 比较特殊: 位于 Layer 2 和 Layer 3 之间
- TC传统上用于 流量整形和队列管理, 这发生在pkt准备从网卡发出的 Egress 路径
- 在Ingress 路径, TC提供了一个钩子: 它在XDP之后、但在Netfilter的
PREROUTING钩子之前执行
Linux网络协议栈: pkt进入ingress的步骤

- NIC: 数据包到达网卡
- XDP: 驱动程序最早被触发, 处理原始程序, 可以 redirect/drop
- Kernel 分配
sk_buff: 数据包被封装成 Kernel Network Stack 的标准结构 - TC Ingress: TC的HOOK被触发, 可以 redirect/drop
- Netfilter: 数据包正式进入IP协议栈, Netfilter 的 HOOKs 被顺次触发
- 后续 routing / IP processing: ...
因此:
-
xdp框架会调用ebpf程序, 对数据包进行上述处理, 因此我们可以在ebpf程序中实现诸如网络策略, 负载均衡等功能
-
tc是linux内核中用于数据包流量调度的功能组件, 可以拿到数据包的更多信息, 如ip地址... tc也会调用ebpf程序对数据包进行处理
这就是为什么我们要使用eBPF, 它的功能在xdp和tc的“嵌入支持”下, 格外强大!
eBPF 程序¶
要运行ebpf程序, 首先需要我们开发一个ebpf程序! 开发ebpf程序可以使用多种语言!
类比: 基于bcc开发python和lua的程序; 基于llvm开发c语言程序 ...
ebpf程序通过这些编译器, 编译成字节码后, 被加载到内核中运行
Whole View¶

- 用户编写eBPF程序
- 编译器编译成 program + maps
- 加载进 eBPF library
- Verifier: 验证有无问题, 会不会导致内核崩溃
- JIT Compiler: 将字节码转换成机器指令
- 执行ebpf call指令
Details¶
这一节建议过仔细一点, 理清整条workflow, 对理解eBPF和linux网络协议栈都特别有帮助:

Development¶
In User Space.
(1) 编写代码 (C source)
- 首先使用C语言编写eBPF程序(
eBPF Program) - 同时,用户会定义eBPF Maps(
Maps)- Maps是一种内核中的键值对存储,用于在
in-kernel eBPF programs之间、或在kernel - userspace program之间共享数据
- Maps是一种内核中的键值对存储,用于在
(2) 编译代码 (Clang)
clang编译器将C代码编译成 eBPF字节码 (eBPF bytecode)- 编译命令是
clang -target bpf ... - 编译的产物是一个ELF文件(
program.o), 这个文件里包含了编译好的 eBPF program [bytecode] 和 Maps Definition
Loading & Verification¶
连接用户空间和内核的桥梁! 也是内核保护自身安全的第一道防线!
(3) 加载程序 (Loader)
- 一个用户空间的“加载器”程序(Loader)负责将eBPF程序送入内核
- 这个加载器可以是上面提到的
bpftool、iproute2(tc)- 也可以是一个使用特定库(如第一张图中的
eBPF Go Library或Python的bcc)的自定义程序
- 也可以是一个使用特定库(如第一张图中的
(4) 系统调用 (Syscall)
- 加载器读取ELF文件中的字节码和Maps定义, 然后通过一个核心的
bpf()系统调用(Syscall), 将它们发送给Linux内核 - 创建Maps:
- 加载器首先请求内核创建 eBPF Maps
- 内核会返回一个文件描述符(fd)指向这个Map
- 加载字节码:加载器将eBPF字节码连同它需要访问的Maps的fd, 一起发送给内核
(5) 校验器 (Verifier)
- 这是eBPF最关键的安全机制!
- 在接受字节码后,内核的校验器(eBPF Verifier)会立即对代码进行静态分析
- 它会检查:
- 安全性:程序是否会访问非法的内存地址?是否会造成内核崩溃?
- 终止性:程序是否包含无限循环?(eBPF程序必须保证能在有限时间内执行完毕。
- 权限:程序是否只调用了允许的内核辅助函数(Helper Functions)?
- 如果校验失败,内核会拒绝加载该程序
Compilation & Attachment¶
校验通过后,内核会“认领”这个程序,并为它的运行做准备
(6) 即时编译 (JIT Compiler)
- 校验通过的字节码(
approved eBPF Program)会被交给 JIT编译器(Just-In-Time Compiler) - 如您文中所述,如果
bpf_jit_enable=1, JIT会将通用的 eBPF bytecode 翻译成 CPU原生机器指令(x86_64) - 好处:这使得eBPF程序的执行效率几乎与内核原生代码相同, 远快于“解释执行”
(7) 挂载 (Attachment)
- 程序此时已经在内核中,并且是编译好的状态,但它还不会运行
- 加载器还需要最后一步:将这个程序“挂载”(Attach)到一个特定的内核钩子(Hook Point)上
- 例如:
- 挂载到网络套接字(Sockets)
- 挂载到 TC(流量控制) / XDP(网卡驱动)
- 挂载到 Tracepoints(内核函数跟踪点)
Execution & Interaction¶
这是程序最终运行并发挥作用的阶段!
(8) 触发执行 (Trigger)
- 程序被挂载后,它就处于待命状态。当内核的执行流碰到这个钩子时,程序就会被触发
- 如图所示:
- 一个普通进程(
Process)调用了sendmsg()发送网络包 - 这个调用进入内核,流经网络协议栈 (
Sockets->TCP/IP) - 当它到达挂载了eBPF程序的那个钩子(
Sockets层)时, eBPF程序就会被立即执行
- 一个普通进程(
(9) 沙箱内执行 (Sandbox Execution)
- 即使被JIT编译成了原生指令, eBPF程序仍然在一个受限的“沙箱”中运行
- 它不能随意调用内核函数或访问内存
eBPF程序在沙箱中能做什么? 有三种调用方式:
- 辅助函数 (Helper Call):
- eBPF程序通过一个特殊的
ebpf call指令, 请求内核执行预先定义好的、安全的辅助函数 - e.g. 读写eBPF Maps、获取当前时间、获取进程ID
- eBPF程序通过一个特殊的
- 内部函数 (Internal Call):
- 可以调用自己在程序内部定义的其他函数,以便复用代码
- 尾调用 (Tail Call):
- 一个eBPF程序可以“跳转”到另一个eBPF程序, 且不再返回(类似
goto), 这用于实现复杂的逻辑链
- 一个eBPF程序可以“跳转”到另一个eBPF程序, 且不再返回(类似
eBPF程序在执行期间,最主要的“副作用”就是通过辅助函数读写eBPF Maps
例如: 一个网络eBPF程序可以统计收到的数据包, 并将计数器写入一个Map。然后, 在用户空间的加载器程序可以随时读取这个Map, 以向用户展示实时的统计数据
Case Study¶
目前我们只是看到了eBPF的来龙去脉, 但也只是冰山一角, 还有很多的问题值得去学习:
- eBPF Map
- BTF
- 库 libbpf/bpftool
- 语法/调用方式
但这篇文章的目的是理清eBPF的前世今生、整套workflow, 因此过于细节的知识点并不会出现在本文
我们将采用两个案例来让读者简单感受一下 eBPF 的使用方式:
- 直接函数调用
- 工具:
bpftrace. 一个eBPF的高级跟踪语言, 它把"编写C-编译-加载"的整个 Workflow 隐藏在一个简单的脚本语言背后 - 场景: 追踪系统上所有
execve系统调用(所有新执行的程序), 并打印出"PID"和"程序名"
- 工具:
- 自己写-编译-调用
- 复刻案例一的功能: 追踪 execve. 但这次, 要手动编写内核eBPF程序
直接函数调用¶
先安装 bpftrace:
| Bash | |
|---|---|
1 2 | |
执行脚本. 追踪所有新执行的程序:
| Bash | |
|---|---|
1 2 3 4 5 | |
执行效果:

自己写eBPF程序-编译-调用¶
第一步: 准备环境 + 基础文件
(1) 配置环境:
| Bash | |
|---|---|
1 2 | |
(2) 获取 BTF (内核类型信息):
| Bash | |
|---|---|
1 2 | |
(3) 编写eBPF内核程序 (命名 trace.bpf.c):
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | |
(4) 编写用户空间加载器 (trace.c):
| C | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | |
第二步: 编译
| Bash | |
|---|---|
1 2 3 4 5 6 7 | |
第三步: 测试结果
| Bash | |
|---|---|
1 2 | |
Over!