浏览器插件类比:秒懂 eBPF
Chrome 不装插件也能用,装了广告拦截就能屏蔽广告,装了翻译插件就能划词翻译------浏览器本身不用改,功能却能无限扩展。
eBPF 就是 Linux 内核的"浏览器插件系统":
-
内核 = 浏览器,功能强大但不一定满足你的特殊需求
-
eBPF 程序 = 插件,你写的小程序可以动态插入内核运行
-
Hook 点 = 插件安装位置,内核里预留了各种"插口"
-
验证器 = 插件审核机制,确保插件不会搞崩浏览器
传统方式?改源码重编译,或写内核模块冒崩溃风险。eBPF 让你像装插件一样,写个程序、加载进去、立即生效。
个人觉得最关键的一点:eBPF 兼具内核模块的灵活性和脚本的安全便捷性------验证器把关,程序过不了"安检"就根本不让跑,杜绝内核崩溃。
三方协作:eBPF 怎么工作
一个完整的 eBPF 应用由三部分协作完成,我把它理解成"指挥官-侦察兵-共享白板"模型:
┌─────────────────────────────────────┐
│ 用户空间 │
│ 用户态程序(Go/C) │
│ · 提交字节码到内核 │
│ · 从 Maps 读取结果 │
└──────────┬──────────────┬───────────┘
│ │
提交字节码 读写数据
▼ ▼
┌─────────────────────────────────────┐
│ 内核空间 │
│ ┌──────────┐ ┌───────────────┐ │
│ │ eBPF程序 │◄►│ BPF Maps │ │
│ │ 挂Hook执行│ │ 共享白板 │ │
│ └────┬─────┘ └───────────────┘ │
│ │ Hook 触发 │
│ ┌────▼─────┐ │
│ │ 内核事件源│ 系统调用/网络包/调度… │
│ └──────────┘ │
└─────────────────────────────────────┘
-
用户态程序(指挥官):把 eBPF 程序提交到内核、挂载到事件点、读取结果
-
内核态 eBPF 程序(侦察兵):在内核中执行观测逻辑,将数据写入 Maps
-
BPF Maps(共享白板):内核态和用户态之间的通信桥梁,双向传输,原生并发安全
三方各司其职,边界清晰。内核态程序有严格限制------不能随意调内核函数、不能无限循环、必须在有限时间内完成------这些限制恰恰是安全的保障。
五步流程:从写代码到内核执行
一个 eBPF 程序从编写到运行,我总结为五步:
编写代码 → 编译字节码 → 加载验证 → JIT 编译 → 挂载执行
-
编写代码:用受限 C 写内核态程序。受限是故意的------不能随便调函数、不能无限循环,保证不搞崩内核
-
编译字节码 :
clang -target bpf把 C 编译成 eBPF 字节码,类似 Java 字节码------跨平台、可验证 -
加载验证 :通过
bpf()系统调用提交到内核,验证器做安全检查。不通过就拒绝加载 -
JIT 编译:即时编译器把字节码翻译成原生机器码,像同声传译,性能接近原生
-
挂载执行:程序挂到 Hook 点,事件触发时自动执行,数据写入 Maps
五步走完,你的程序就在内核里跑起来了。
核心组件速览
不需要逐个展开,一张表搞定:
| 组件 | 类比 | 要点 |
|---|---|---|
| Hook | 监控摄像头安装点 | 选型原则:稳定选 Tracepoint,灵活选 Kprobe,网络性能选 XDP |
| 验证器 | 安检门 | 两阶段检查:结构检查(无环)+ 逐条校验(模拟执行),不通过就拒绝 |
| JIT | 同声传译 | 字节码→机器码,默认开启,还有常量盲化防注入攻击 |
| Maps | 共享白板 | 内核态/用户态通信桥梁,支持 Hash/Array/Ringbuf 等多种结构,原生并发安全 |
| 辅助函数 | 标准化接口 | eBPF 程序调用内核能力的唯一合法通道,不同程序类型可调用的集合不同 |
| CO-RE | Java 一次编译到处运行 | 基于 BTF 自动适配不同内核版本的差异,编译一次到处跑 |
| BTF | 数据字典 | 记录内核数据结构元信息,体积小(几十 KB),CO-RE 的基石 |
⚠️ 关于使用限制:eBPF 不是万能的------栈空间最多 512 字节、不能无限循环、只能调辅助函数、指令数有上限。这些限制恰恰是安全的代价,推荐内核 5.x 以上使用,4.x 限制较多。
实战:Hello World
环境准备(Ubuntu):
# 安装 Go(如果没有)
wget https://go.dev/dl/go1.21.0.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
# 安装 eBPF 编译工具链
sudo apt-get install clang llvm libbpf-dev linux-headers-$(uname -r)
# 安装 bpf2go(cilium/ebpf 的代码生成工具)
go install github.com/cilium/ebpf/cmd/bpf2go@latest
export PATH=$PATH:$(go env GOPATH)/bin
内核态程序(hello.bpf.c)
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
// SEC 宏指定程序类型和挂载点
SEC("kprobe/do_sys_openat2")
int hello_world(void *ctx)
{
bpf_trace_printk("Hello, World!\n"); // 向内核调试日志输出
return 0;
}
char LICENSE[] SEC("license") = "GPL";
用户态程序(hello.go)
package main
import (
"log"
"os"
"os/signal"
"syscall"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit"
)
//go:generate bpf2go -type Hello hello hello.bpf.c
func main() {
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal("解除内存锁失败:", err)
}
var objs helloObjects
if err := loadHelloObjects(&objs, nil); err != nil {
log.Fatal("加载eBPF程序失败:", err)
}
defer objs.Close()
kp, err := link.Kprobe("do_sys_openat2", objs.HelloWorld, nil)
if err != nil {
log.Fatal("挂载kprobe失败:", err)
}
defer kp.Close()
log.Println("正在追踪文件打开操作,按 Ctrl+C 退出...")
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
<-ch
log.Println("已退出")
}
运行
# 创建项目
mkdir hello-ebpf && cd hello-ebpf
go mod init hello-ebpf
go get github.com/cilium/ebpf
# 生成 Go 绑定代码并编译
go generate
# 运行
sudo go run hello.go
# 另开终端查看输出
sudo cat /sys/kernel/debug/tracing/trace_pipe
# <...>-1234 [001] d... 100.000: hello_world: Hello, World!
你用标准 BPF 头文件写了内核态函数,通过 cilium/ebpf 库加载并挂到 do_sys_openat2 入口------任何进程打开文件时,内核自动调用你的函数。bpf2go 自动生成 Go 绑定代码,让加载、挂载和交互都是类型安全的,整个流程清晰可控。
进阶方向:掌握 BPF Maps 是从"能跑"到"能用"的关键------Maps 让你采集聚合数据,而不只是打印字符串。生产环境也可以选择 libbpf-bootstrap(C 用户态脚手架),支持完整 CO-RE,部署无需目标机器装编译器。bpftrace 也值得了解------一行命令写追踪工具,临时排查利器。
常用命令速查
# 查询可用的插桩点
sudo bpftrace -l
# 模糊查找包含 "open" 的插桩点
sudo bpftrace -l '*open*'
# 查看已加载的 eBPF 程序和 Maps
sudo bpftool prog list
sudo bpftool map list
# 检查 BTF 是否可用(CO-RE 的前提)
ls /sys/kernel/btf/vmlinux
核心概念速记卡
| 概念 | 类比 | 一句话 |
|---|---|---|
| eBPF | 浏览器插件系统 | 给内核装插件,扩展功能不动内核 |
| 用户空间/内核空间 | 前台/后台 | eBPF 让你合法进入后台 |
| Hook | 监控摄像头安装点 | 指定"在哪里监控" |
| 验证器 | 安检门 | 不通过安检不让进 |
| JIT | 同声传译 | 字节码实时翻译成机器码 |
| Maps | 共享白板 | 内核态和用户态的通信桥梁 |
| 辅助函数 | 标准化接口 | 调用内核能力的唯一合法通道 |
| CO-RE | 一次编译到处运行 | 编译一次,不同内核版本都能跑 |
| BTF | 数据字典 | 记录数据结构元信息,支撑 CO-RE |
写在最后
eBPF 的学习曲线确实存在,但回报极高------掌握它,你就拥有了深入 Linux 内核的"超能力":不改内核代码,就能观测、追踪、控制内核行为。建议的学习路径:
-
一两天搞清概念:三方协作、五步流程、核心组件各干什么
-
搭环境跑通 Hello World:cilium/ebpf 是最快的起点,Go + bpf2go 就能加载内核程序
-
用 bpftrace 解决实际问题:一行命令追踪进程、文件操作、IO 延迟------先会用工具,再学写程序
-
进阶 libbpf-bootstrap:生产级部署,minimal 模板做实验,bootstrap 模板做项目