---
title: eBPF 基础
url: https://doc.liz6.com/linux-kernel/08-ebpf-and-observability/01-ebpf-basics
locale: zh
area: linux-kernel
tags:
- linux-kernel
- eBPF与可观测性
date: 2026-06-30
modified: 2026-07-16
description: eBPF 让用户态把沙箱程序注入内核运行——verifier 静态证明安全性,JIT 编译到原生指令,maps 与用户态共享数据,CO-RE 一次编译到处运行。它之于内核,近似 JavaScript 之于浏览器。
---

# eBPF 基础

> eBPF 让用户态把沙箱程序注入内核运行——verifier 静态证明安全性,JIT 编译到原生指令,maps 与用户态共享数据,CO-RE 一次编译到处运行。它之于内核,近似 JavaScript 之于浏览器。

## 概述

想在内核里加一段自己的逻辑，传统上只有两条路：改内核源码重编（周期以月计），或写内核模块（一个空指针就 panic 整机）。eBPF (extended Berkeley Packet Filter) 给了第三条路：**把程序作为数据提交给内核，内核先用静态验证器证明它无害，再 JIT 成原生代码挂到钩子上运行**。就像浏览器敢跑任意网页的 JavaScript 一样，内核从此敢跑用户提交的代码——这是它从最初的包过滤器 (cBPF) 演变成通用内核可编程平台的原因。

版本覆盖 3.18 ~ 6.x。今天它撑起了四大领域：

- **可观测性**: bpftrace, bcc, Cilium Hubble
- **网络**: XDP, TC BPF, Cilium 容器网络
- **安全**: BPF LSM, seccomp
- **性能**: 热路径上的自定义逻辑，无需重编内核

## 生命周期

```mermaid
flowchart LR
    subgraph user["👩‍💻 用户态"]
        C["C 代码"] -->|"clang -target bpf -O2"| O["eBPF ELF (.o)"]
        O -->|"libbpf + CO-RE relocation"| L["bpf(BPF_PROG_LOAD)"]
        R["读取 maps / ringbuf"]
    end
    subgraph kernel["🛡️ 内核态"]
        L --> V{"<b>Verifier</b><br><small>静态证明所有路径安全</small>"}
        V ==>|"通过"| J["<b>JIT 编译</b><br><small>bytecode → 原生指令</small>"]
        V -.->|"拒绝 -EACCES<br>+ verifier log"| C
        J --> A["attach 到 hook<br><small>kprobe / tracepoint / XDP…</small>"]
        A -->|"事件触发执行"| M[("BPF maps")]
    end
    M --> R
    classDef gate fill:#d2992226,stroke:#d29922,stroke-width:2.5px
    classDef fast fill:#4493f81f,stroke:#4493f8,stroke-width:2px
    classDef store fill:#3fb9501f,stroke:#3fb950,stroke-width:2px
    class V gate
    class J,A fast
    class M store
```

关键在于顺序：**先验证、后编译、再挂载**。程序到不了 hook 上，除非 verifier 已经证明它在所有执行路径上都安全。

## Verifier: 安全性的基石

`kernel/bpf/verifier.c`，约 1.6 万行，内核中最复杂的验证器。它对程序做符号执行，穷举所有可能路径，检查三类性质：

| 类别 | 具体约束 |
|---|---|
| 控制流 | 无不可达指令；无死循环（5.3+ 允许可证明终止的 bounded loop）；无越界跳转；调用深度 ≤ 32 |
| 数据流 | 追踪每个寄存器的类型（`PTR_TO_CTX` / `PTR_TO_MAP_VALUE` / `SCALAR_VALUE`…）；追踪每个标量的取值范围；强制空指针检查；map 访问的 key/value 尺寸匹配 |
| 资源 | 指令数上限 100 万（5.1+）；栈深度 512 字节 |

验证失败时 `BPF_PROG_LOAD` 返回 `-EACCES`，并附带一份逐指令的 verifier log——读懂这份 log 是 eBPF 开发的日常。

### 边界追踪 (Bound Tracking)

verifier 最精妙的机制：对每个寄存器维护 `[smin, smax, umin, umax]` 四个边界，**沿着分支把条件"学"进去**：

```c
if (x < 10) {
    // verifier 在这个分支内推导出 x ∈ [0, 9]
    bpf_map_lookup_elem(&map, &x);   // OK: 索引已被证明在界内
}
// else 路径上则推导出 x ∈ [10, MAX]
```

数组索引不出界、map key 合法、指针算术不越界——都靠这套区间推导在加载时静态证明，运行时零开销。

## JIT 编译

验证通过后，BPF bytecode 被逐指令翻译成原生机器码（x86 侧在 `arch/x86/net/bpf_jit_comp.c`），每条 BPF 指令对应 1~3 条 x86 指令：

```
BPF_ADD  reg, imm  →  add $imm, %reg
BPF_LDX  reg, src  →  mov (%src), %reg
BPF_CALL fn        →  call fn        (tail call 优化成 jmp)
BPF_EXIT           →  ret
```

所以 eBPF 不是"内核里的解释器"——JIT 后的执行性能接近手写内核代码，这是它敢放在 XDP 这种每包必经的热路径上的底气。

## BPF Maps: 程序间数据共享

BPF 程序本身是无状态的、事件驱动的；状态放在 **maps** 里——内核态的键值存储，BPF 程序之间、BPF 与用户态之间靠它共享数据。

```c
// include/uapi/linux/bpf.h — 常用类型节选
enum bpf_map_type {
    BPF_MAP_TYPE_HASH,          // 通用 hash table (最常用)
    BPF_MAP_TYPE_ARRAY,         // 固定大小数组 (O(1) 查找, 不能删除)
    BPF_MAP_TYPE_PERCPU_HASH,   // per-CPU 各存一份, 无锁
    BPF_MAP_TYPE_LRU_HASH,      // 满了按 LRU 淘汰
    BPF_MAP_TYPE_RINGBUF,       // 环形缓冲, 事件流到用户态
    BPF_MAP_TYPE_STACK_TRACE,   // 内核栈追踪存储
    BPF_MAP_TYPE_BLOOM_FILTER,  // 快速"可能在集合中"检查
};
```

BPF 侧通过 helper 操作：`bpf_map_lookup_elem` / `bpf_map_update_elem` / `bpf_map_delete_elem`；用户态通过 `bpf()` 系统调用或 libbpf 的封装读写同一个 map。

### Ring Buffer vs Perf Buffer

把内核事件源源不断送到用户态，是可观测性场景的主干道。5.8 之前用 perf buffer，之后应一律用 ringbuf：

| | Perf Buffer（旧） | Ring Buffer (5.8+) |
|---|---|---|
| 缓冲结构 | 每 CPU 一个 | 全 CPU 共享一个 |
| 顺序 | 跨 CPU 乱序 | 全局有序 |
| 内存效率 | 按最忙 CPU 预留 | 共享,省内存 |
| 拷贝 | 两次 | 预留-提交 (reserve/commit),mmap 零拷贝 |
| 记录长度 | 固定 | 可变 |

## CO-RE: Compile Once, Run Everywhere

**问题**：BPF 程序编译时引用内核结构体，字段偏移被固化进 bytecode；下一个内核版本字段挪了位置，程序就读错内存。早年的 bcc 因此要在目标机上现场编译（拖一整套 LLVM + kernel headers）。

**解法**分两半：

1. **BTF (BPF Type Format)**——内核自带的类型描述，嵌在 `/sys/kernel/btf/vmlinux` 里，相当于压缩到几 MB 的"内核 debug info"。
2. **libbpf 的 CO-RE 重定位**——加载 `.o` 时读取当前内核的 BTF，把 bytecode 里所有字段偏移**现场修正**成本机的真实偏移。

于是同一个 `.o` 文件可以不加修改地跑在 5.10 和 6.5 上：

```c
#include <bpf/bpf_core_read.h>
x = BPF_CORE_READ(task, pid);   // 偏移在加载时按本机内核解析
```

## bpftool: 管理 BPF 对象

```bash
bpftool prog list                          # 列出已加载的程序
bpftool map dump id <map_id>               # 导出 map 内容
bpftool prog dump xlated id <prog_id>      # 查看 BPF bytecode
bpftool prog dump jited  id <prog_id>      # 查看 JIT 后的 x86 汇编
bpftool prog load myprog.o /sys/fs/bpf/myprog   # 加载并 pin 到 bpffs
```

## 安全模型

eBPF 程序被约束到"不可能伤害内核"的子集里，它**不能**：

- 无限运行（指令数上限 + 终止性验证）
- 做没有边界检查的指针访问
- 直接读任意内核内存——只能经 helper（`bpf_probe_read_kernel`）
- 直接写任意内核内存——只能写自己的 maps
- 睡眠或调用 `schedule`（sleepable BPF 是受限的例外）
- 调用白名单（helper/kfunc）之外的内核函数

特权层级：

| 能力 | 可以做什么 |
|---|---|
| `CAP_BPF` | 加载大多数 BPF 程序类型 |
| `CAP_SYS_ADMIN` | 所有类型，包括 tracing |
| 无特权 | 仅当 `kernel.unprivileged_bpf_disabled=0`，且只有 socket filter 等有限类型 |

程序能挂到哪些钩子、各类型能拿到什么上下文，见 [eBPF 程序类型](/linux-kernel/08-ebpf-and-observability/02-ebpf-program-types.md);实战追踪工具链见[追踪调试](/linux-kernel/08-ebpf-and-observability/03-tracing-and-debugging.md)与[ftrace 与 bpftrace 用户态](/systems-programming/08-performance-and-debugging/03-ftrace-and-bpftrace-user-space-tracing.md)。

## 参考

- **源码**: `kernel/bpf/` (verifier, syscall, helpers), `arch/x86/net/bpf_jit_comp.c` (JIT), `tools/lib/bpf/` (libbpf), `tools/bpf/bpftool/`
- **内核文档**: `Documentation/bpf/` (极好的文档)
- **书籍**: "BPF Performance Tools" (Brendan Gregg)
- **LWN**: "A thorough introduction to eBPF" (lwn.net/Articles/740157/)

*关键词: eBPF, verifier, JIT, BPF maps, BTF, CO-RE, libbpf, bpftool, ring buffer, CAP_BPF*
