楼主: LMR1234
769 0

[其他] Linux基础 -- eBPF 简介笔记 [推广有奖]

  • 0关注
  • 0粉丝

等待验证会员

学前班

40%

还不是VIP/贵宾

-

威望
0
论坛币
0 个
通用积分
0.0107
学术水平
0 点
热心指数
0 点
信用等级
0 点
经验
20 点
帖子
1
精华
0
在线时间
0 小时
注册时间
2018-9-1
最后登录
2018-9-1

楼主
LMR1234 发表于 2025-12-5 20:56:10 |AI写论文

+2 论坛币
k人 参与回答

经管之家送您一份

应届毕业生专属福利!

求职就业群
赵安豆老师微信:zhaoandou666

经管之家联合CDA

送您一个全额奖学金名额~ !

感谢您参与论坛问题回答

经管之家送您两个论坛币!

+2 论坛币

eBPF 学习笔记(面向内核 / 网络开发工程师)

假设你已经阅读过 cBPF 相关文档,并对 Linux 网络栈、kprobe、netfilter 等机制有一定了解。

背景介绍

eBPF 的核心目标是:在不修改内核源码、无需重启系统的前提下,将用户自定义的逻辑安全地注入到内核的关键执行路径中运行。

典型应用场景包括:

  • 在最早的数据包接收阶段(XDP 层)实现丢包控制、流量重定向或负载均衡;
  • 在 tc 的 ingress 或 egress 阶段进行 QoS 控制和流量整形;
  • 通过 kprobe 或 tracepoint 实现细粒度性能分析与在线故障排查;
  • 结合 socket 和 cgroup 实现访问控制、速率限制等安全策略。

可以将 eBPF 理解为一套完整的内核级可编程机制,具备受限沙箱环境以及完善的工具链支持(如 clang、libbpf、bpftool 等),形成一个强大的生态系统。

1. eBPF 与 cBPF 的主要区别:升级点解析

相较于传统的 cBPF,eBPF 在多个维度实现了显著增强:

更强大的指令集与寄存器架构

cBPF 仅提供两个 32 位寄存器 A 和 X,外加一个临时存储区域(scratch space)用于辅助计算。

eBPF 则引入了 11 个 64 位通用寄存器:
R0–R10,具体用途如下:

  • R0:函数返回值寄存器;
  • R1–R5:传递函数参数;
  • R6–R9:被调用者保存的寄存器,可用于长期存放局部变量;
  • R10:帧指针,指向当前栈顶位置。

此外,每个程序拥有最多 512 字节的栈空间,可通过 R10 加偏移的方式访问(例如 r10 - 16)。

M[16]

eBPF 支持调用预定义的 helper 函数,并能通过 map 结构实现数据持久化和用户态通信。常见的交互方式包括哈希表、数组、ringbuf 等结构。

bpf_map_update_elem
bpf_get_prandom_u32
bpf_redirect

丰富的挂载点与程序类型

eBPF 可以附加在多种内核事件点上,包括但不限于:

  • XDP
  • tc ingress / egress
  • kprobe / tracepoint
  • socket 操作
  • cgroup 子系统
  • LSM(Linux Security Module)
  • perf 事件监控

静态验证机制(Verifier)

所有 eBPF 程序在加载至内核前必须经过 Verifier 的严格校验,确保其安全性。主要检查内容包括:

  • 内存访问无越界;
  • 指针操作合法且受控;
  • 循环结构具有静态可确定的上界,防止无限循环;
  • 所有执行路径均正确初始化寄存器与栈变量。

若程序未能通过验证,则拒绝加载。因此,可以形象地理解为:cBPF 是简单的过滤脚本,而 eBPF 提供了一个“迷你 CPU + 安全沙箱”的完整执行环境。

2. eBPF 执行模型详解

2.1 寄存器与调用约定

寄存器 作用说明
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 程序:返回动作码,决定如何处理数据包;
    XDP_PASS
    XDP_DROP
    XDP_TX
    XDP_REDIRECT
  • tc 程序:返回相应操作标识,控制后续转发行为;
    TC_ACT_OK
    TC_ACT_SHOT
    TC_ACT_REDIRECT
  • kprobe 程序:通常返回 0,也可配合 override_return 等 helper 修改原函数返回值;
  • tracepoint 程序:一般返回 0 表示正常执行;
  • socket filter 程序:返回允许拷贝到用户空间的字节数,语义与 cBPF 类似。

2.2 指令格式与编码结构

eBPF 指令在内核中的表示结构如下:

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

主要指令类别

  • ALU/ALU64:执行算术运算(加减乘除)和位操作;
  • JMP/JMP32:实现条件跳转、比较判断及函数调用;
  • LD/ST:完成寄存器与栈或 map 之间的数据加载与存储;
  • CALL
    :用于调用 helper 函数或子程序;
  • EXIT
    :表示函数返回,结束执行。

3. Verifier:eBPF 的安全守门人

Verifier 是保障 eBPF 安全性的核心组件,任何程序在加载进入内核前都必须通过其静态分析验证。

关键验证项

内存安全

只允许访问以下几类已知安全的内存区域:

  • 上下文结构体(如
    ctx
    )中明确暴露的字段;
  • 从 map 查询返回的有效指针;
  • 栈上已分配并初始化的对象。

所有指针偏移必须经过显式的边界检查,禁止任意类型转换成内核地址进行非法读写。

控制流终止性

程序不得包含无限循环。现代内核虽支持有界循环,但要求 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”,即确保程序能通过内核校验器的检查。为此,需遵循以下实践:

  • 在访问任何数组或指针前,必须先进行边界范围检查;
  • 在条件分支中,确保各个路径都对指针进行了合法性验证;
  • 优先使用由 helper 提供的 API 接口,例如:
bpf_xdp_adjust_head
bpf_map_lookup_elem

4. eBPF 程序类型与 Hook 点概览

以下列出的是常用的 eBPF 程序类别,每种可视为一个“Hook 家族”:

XDP(eXpress Data Path)

  • 挂载于网卡驱动最前端的数据包接收点;
  • 接收到的上下文包含 data / data_end 和 ingress 接口索引等信息;
  • 返回值控制数据包处理行为,如:
BPF_PROG_TYPE_XDP
struct xdp_md *ctx
XDP_DROP / XDP_PASS / XDP_TX / XDP_REDIRECT
  • 适用场景:抵御 DDoS 攻击、实现四层/七层负载均衡、早期丢包策略。

tc(Traffic Control)

  • 挂载于 ingress 或 egress 队列调度器(clsact)上;
  • 上下文结构通常为:
BPF_PROG_TYPE_SCHED_CLS
SCHED_ACT
struct __sk_buff *
  • 返回动作包括:
TC_ACT_OK / TC_ACT_SHOT / TC_ACT_REDIRECT
  • 典型用途:流量整形、QoS 控制、路由策略实施、透明代理功能。

kprobe / kretprobe

  • 用于挂接到任意内核函数的入口或返回点;
  • 上下文为:
BPF_PROG_TYPE_KPROBE
struct pt_regs *
  • 支持读取函数参数、返回值以及当前进程信息;
  • 常用于性能分析、延迟统计和系统调试。

tracepoint / raw_tracepoint

  • 绑定到内核预定义的 tracepoint 上,不依赖具体函数名,因此更加稳定;
  • 上下文为固定结构体,其定义位于内核对应的 tracepoint 头文件中;
  • 适用于需要长期运行的监控与统计任务。

perf event

  • 通过 perf 子系统的事件触发执行,如周期性采样或硬件事件;
  • 主要用于系统性能剖析(profiling)。
BPF_PROG_TYPE_PERF_EVENT

cgroup 程序族

  • 包括以下几种类型:
cgroup_skb
cgroup_sock
cgroup_sock_addr
  • 挂载至特定 cgroup,仅影响该组内进程的网络行为;
  • 适合实现基于 cgroup 的防火墙规则或带宽限速机制。

socket 程序

  • 相较于传统 socket filter,功能更强大;
  • 可在 socket 层实现数据包重写与拦截操作;
  • 支持多种类型,例如:
SK_SKB
SK_MSG
SOCKET_FILTER
SOCK_OPS

LSM / LSM_CGROUP(安全相关)

  • 利用 eBPF 实现 Linux 安全模块(LSM)hook,如 open、bind、exec 等系统调用的策略拦截;
  • 可用于构建细粒度的安全控制机制,作为 SELinux 之外的补充方案。

5. eBPF 工具链与开发流程

5.1 编译:clang + LLVM

典型的编译命令如下:

clang -O2 -g -target bpf \
-D__TARGET_ARCH_x86 \
-c xdp_prog.c -o xdp_prog.o

注意事项:

  • 必须指定目标架构为 BPF:
-target bpf
  • 根据实际运行环境添加相应的宏定义以适配架构:
-D__TARGET_ARCH_x86 / arm / arm64 / riscv
  • 建议启用调试信息选项:
-g
  • 以便后续结合 BTF 和 CO-RE 技术进行符号解析与调试。

5.2 加载与附加:libbpf + bpftool

现代主流推荐方式为使用 libbpf + CO-RE(一次编译,到处运行):

  • 要求内核开启 BTF 支持:
CONFIG_DEBUG_INFO_BTF=y
  • 使用工具生成 skeleton 头文件:
bpftool gen skeleton
  • 基于源码中的 BPF 程序定义:
.o
  • 用户态 C 程序可通过标准接口加载并运行:
#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:官方提供的命令行工具,可用于:
bpftool prog load/attach
bpftool map dump/update
bpftool gen skeleton
  • bcc:提供 Python 和 C++ 封装,适合快速编写调试脚本;
  • bpftrace:类 awk 的领域专用语言(DSL),适用于临时性的跟踪与诊断任务。

6. eBPF Map:状态存储与内核-用户态通信机制

eBPF Map 是一种内核级的键值存储结构,可供 eBPF 程序与用户态进程共享数据。

常见 Map 类型包括:

  • BPF_MAP_TYPE_HASH:通用哈希表;
BPF_MAP_TYPE_HASH
  • BPF_MAP_TYPE_ARRAY:固定大小数组;
BPF_MAP_TYPE_ARRAY
  • per-CPU 版本(如):
*_PERCPU
  • 每个 CPU 拥有独立副本,避免锁竞争;
  • LRU 版本(如 LRU hash):
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;

8. cBPF 与 eBPF 的关系及思维转换

8.1 数据路径中的位置对比

cBPF socket filter 所处的执行位置如下所示:
网卡 -> 驱动 -> 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

8.2 指令集与编程体验差异

cBPF 编程通常需要手动编写或通过 libpcap 生成字节码:
sock_filter
其资源受限,仅提供有限寄存器,不支持 helper 函数和 map 结构,无法实现复杂状态管理。 相比之下,eBPF 提供了现代化的开发体验: - 使用 C 语言(甚至 Rust、Go 等高级语言)编写,通过 clang 编译; - 支持完整的寄存器集、局部栈空间、辅助函数(helper)以及 map 数据结构; - 更适合实现复杂的业务逻辑,如状态机维护、流量统计与行为分析。 可以将 cBPF 视作一种轻量级、为兼容旧系统而保留的技术方案,而 eBPF 才是当前及未来的核心发展方向。

9. 推荐学习路线

从阅读 Linux 内核自带的 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 在安全与资源控制方面的应用场景。
二维码

扫码加我 拉你入群

请注明:姓名-公司-职位

以便审核进群资格,未注明则拒绝

关键词:Linux Lin instruction Profiling UNBOUNDED

您需要登录后才可以回帖 登录 | 我要注册

本版微信群
扫码
拉您进交流群
GMT+8, 2026-1-28 01:45