【EBPF.0x00】eBPF 入门指北(一):简介

本文最后更新于:2023年8月13日 晚上

BEE BEE I’M A SHEEP

0x00.一切开始之前

因为最近毕设搞得有点头大(←因为这个人各种摸鱼导致进度差的太多然后最后几天疯狂各种赶工),所以看点和毕设无关的东西(这样毕设不是更加做不完了嘛(悲)

笔者一直想着有机会深入学习一下 eBPF 相关的东西,可惜总是习惯性一头扎进源码各种繁杂的细节当中迷失自己然后放弃(悲)

于是趁现在有时间(?) 虽然现在没什么时间,笔者还是想给这玩意先开个头写第一篇博客,毕竟只要开了一个头之后后面的事情也就会简单得多了吧(大嘘)

What is eBPF?

伯克利包过滤器(Berkeley Packet Filter)是一个 Linux kernel 中用以对来自于链路层的数据包进行过滤的架构,其位于内核中的架构如下图所示:

image.png

相比起传统的数据包过滤器而言,BPF 在内核中实现了一个新的虚拟机设计,通过即时编译(Just-In-Time compilation)技术将 BPF 指令翻译为 BPF 虚拟机的字节码,可以高效地工作在基于寄存器结构的 CPU 上

Linux kernel 自 3.18 版本起提供了扩展伯克利包过滤器extended BPF,即 eBPF),其应用范围更广,能够被应用于更多的场景,原来的 BPF 被称为 classic BPF(cBPF),且目前基本上已经被废弃,Linux 会将 cBPF 字节码转化为 eBPF 字节码再执行

作为一个位于内核层面的虚拟机,eBPF 无疑为攻击者提供了一个相当大的新攻击面,因此也成为近几年内核利用中的“大热门”,本篇博客中笔者将简述 eBPF 的基本原理

本篇文章中涉及到的 Linux kernel 源码来自版本 6.3.2

0x01.eBPF 的基本架构

一、eBPF 的运行过程

Linux 下 eBPF 的整体架构如下图所示:

image.png

  • 用户进程首先在用户空间编写相应的 BPF 字节码程序,传入内核
  • 内核通过 verifier 对字节码程序进行安全性检查,通过检查后便通过 JIT 编译运行,eBPF 程序主要分为如下类型:
    • kprobes :内核中的动态跟踪,可以跟踪至内核中的函数入口或返回点
    • uprobes :用户空间中的动态跟踪,与 kprobes 不同的是跟踪的函数位于用户程序中
    • tracepoints :内核中的静态跟踪
    • perf_events :定时采样与 PMC
  • 映射(map)作为用以保存数据的通用结构,可以在不同的 eBPF 程序之间或是用户进程与内核间共享数据

不同版本的 eBPF 所支持的功能是不同的,参见这↑里↓

version 功能
4.1 kprobe support
4.4 Perf events
4.7 Tracepoints support
4.8 XDP core
4.10 cgroups support

一个 eBPF 程序可以被挂载到多个事件上,不同的 eBPF 程序之间可以共享同一个映射

1
2
3
4
5
6
7
8
9
tracing     tracing    tracing    packet      packet     packet
event A event B event C on eth0 on eth1 on eth2
| | | | | ^
| | | | v |
--> tracing <-- tracing socket tc ingress tc egress
prog_1 prog_2 prog_3 classifier action
| | | | prog_4 prog_5
|--- -----| |------| map_3 | |
map_1 map_2 --| map_4 |--

二、eBPF verifier

在 eBPF 字节码被传入到内核空间后,其首先需要经过 verifier 的安全检查,之后才能进行 JIT 编译,verifier 主要检查以下几点:

  • 没有回向边(back edge)、环路(loop)、不可达(unreachable)指令
  • 不能在指针之间进行比较,指针只能与标量进行加减(eBPF 中的标量值为不从指针派生的值),verifier 会追踪哪些寄存器包含指针、哪些寄存器包含标量值
  • 指针运算不能离开一个 map 的“安全”边界,这意味着程序不能访问预定义的 map 外的内存,verifier 通过追踪每个寄存器值的上界与下界
  • 不能将指针存储在 map 中或作为返回值,以避免将内核地址泄露到用户空间

kernel/bpf/verifier.c 开头注释阐述如下:

这里为了方便阅读,有的保留原文没有翻译

比如说直接说 map element key 你肯定知道是什么东西,但是我要是说 映射元素键 那你肯定得楞一会….

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/* bpf_check() 是一个静态代码分析器,其逐条遍历 eBPF 程序中的指令,
* 并更新寄存器/堆栈的状态。
* 条件分支的所有路径都会被分析,直到 'bpf_exit' 指令。
*
* 首先通过深度优先搜索检查程序是否为有向无环图(Directed Acyclic Graph)
* 其拒绝以下程序:
* - 指令数大于 BPF_MAXINSNS
* - 出现了循环 (通过后向边检测)
* - 存在不可达指令 (不应当是一个森林. 程序 = 一个函数)
* - 越界或畸形跳转
* 接着是第一条指令展开的所有可能路径。
* 由于其分析程序中所有的路径,分析的长度被限制为 64k 指令,
* 即使总的指令数仅有 4k 可也能达到,但这有太多的会改变栈/寄存器的分支。
* “被分析的分支”的数量被限制在 1k
*
* 在每条指令的入口,每个寄存器都有一个类型,该指令根据指令语义改变寄存器的类型。
* 若指令为 BPF_MOV64_REG(BPF_REG_1, BPF_REG_5), 则 R5 的类型会被复制给 R1
*
* 所有的寄存器都是 64 位的
* R0 - 返回寄存器
* R1-R5 传参寄存器
* R6-R9 callee 保存的寄存器
* R10 - 只读帧指针
*
* 在 BPF 程序起始, R1 寄存器包含一个指向 bpf_context 的指针,
* 其类型为 PTR_TO_CTX.
*
* Verifier 跟踪指针上的运算,以免:
* BPF_MOV64_REG(BPF_REG_1, BPF_REG_10),
* BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, -20),
* 第一条指令将 R10 (FRAME_PTR) 的类型拷贝给 R1,
* 第二条算术指令通过模式匹配以识别其想要构造一个指向栈内元素的指针。
* 因此在第二条指令后,寄存器 R1 的类型为 PTR_TO_STACK
* (以及常量 -20 被存储用作未来的栈边界检查).
* 这意味着该寄存器为一个指向[栈 + 已知立即数常量]的指针
*
* 大部分情况下寄存器都有着 SCALAR_VALUE 类型,
* 这意味着寄存器存储着一些值,但并非一个可用的指针.
* (例如指针加上指针会变为 SCALAR_VALUE 类型)
*
* 当 verifier 遇到 load 或 store 指令时基寄存器(base register)的类型可以为:
* PTR_TO_MAP_VALUE, PTR_TO_CTX, PTR_TO_STACK, PTR_TO_SOCKET.
* 这些是 4 种被 check_mem_access() 函数所识别的指针类型.
*
* PTR_TO_MAP_VALUE 意为该寄存器指向 'map element value'
* 可访问的范围为 [ptr, ptr + map's value_size).
*
*
* 用以在函数调用时传值的寄存器被根据函数参数约束进行检查
*
* ARG_PTR_TO_MAP_KEY 便是其中一个这样的参数约束.
* 其意为传递给该函数的寄存器类型必须为 PTR_TO_STACK
* 且其在函数内将被作为 'pointer to map element key' 使用
*
* 例如,这些是 bpf_map_lookup_elem() 的参数约定:
* .ret_type = RET_PTR_TO_MAP_VALUE_OR_NULL,
* .arg1_type = ARG_CONST_MAP_PTR,
* .arg2_type = ARG_PTR_TO_MAP_KEY,
*
* ret_type 表示该函数返回 'pointer to map elem value or null'
* 函数希望第一个参数为一个指向 'struct bpf_map' 的常量指针,
* 第二个参数则应为指向栈的指针,其会在 helper 函数内用作
* 指向[map element key]的指针
*
* 内核侧的 helper 函数有如下形式:
* u64 bpf_map_lookup_elem(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)
* {
* struct bpf_map *map = (struct bpf_map *) (unsigned long) r1;
* void *key = (void *) (unsigned long) r2;
* void *value;
*
* 这里内核可以安全地访问 'key' 与 'map' 指针, 知晓
* [key, key + map->key_size) 字节为可用的且被
* 初始化在 eBPF 程序的栈上.
* }
*
* 相应的 eBPF 程序或许形如:
* BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), // 这条指令后 R2 的类型为 FRAME_PTR
* BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), // 这条指令后 R2 的类型为 PTR_TO_STACK
* BPF_LD_MAP_FD(BPF_REG_1, map_fd), // 这条指令后 R1 的类型为 CONST_PTR_TO_MAP
* BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
* 这里 verifier 关注 map_lookup_elem() 的原型,会看到:
* .arg1_type == ARG_CONST_MAP_PTR 以及 R1->type == CONST_PTR_TO_MAP, 这是 🆗 的,
* 现在 verifier 知道该 map 有一个 R1->map_ptr->key_size 字节的 key
*
* 然后, .arg2_type == ARG_PTR_TO_MAP_KEY and R2->type == PTR_TO_STACK, 到现在还🆗,
* 现在 verifier 检查 [R2, R2 + map's key_size) 在栈的限制内,
* 且在该调用之前被初始化.
* 若🆗, verifier 接下来允许该 BPF_CALL 指令并关注
* .ret_type (为 RET_PTR_TO_MAP_VALUE_OR_NULL), 故让
* R0->type = PTR_TO_MAP_VALUE_OR_NULL ,这意味着 bpf_map_lookup_elem() 函数
* 返回指向 map value 的指针或 NULL.
*
* 当类型 PTR_TO_MAP_VALUE_OR_NULL 通过 'if (reg != 0) goto +off' 指令,
* 在 true 分支中持有指针的寄存器将状态改变为 PTR_TO_MAP_VALUE,
* 在 false 分支中同样的寄存器将状态改变为 CONST_IMM 。
* 参见 check_cond_jmp_op().
*
* 在调用后 R0 被设为函数返回值,寄存器 R1-R5 被设为 NOT_INIT
* 以表示其不再可读.
*
* 以下引用类型表示一个对内核资源的潜在引用,
* 在其第一次被分配后, BPF 程序必须检查并释放该资源:
* - PTR_TO_SOCKET_OR_NULL, PTR_TO_SOCKET
*
* 当 verifier 遇到一个 helper 调用返回一个引用类型,
* 其为该引用分配一个指针 id 并将他储存在当前的函数状态中.
* 类似于将 PTR_TO_MAP_VALUE_OR_NULL 转化为 PTR_TO_MAP_VALUE 的方式,
* 当类型通过一个 NULL-check 条件, PTR_TO_SOCKET_OR_NULL 变为 PTR_TO_SOCKET。
* 对于状态变为 CONST_IMM 的分支,verifier会释放引用
*
* 对每个会分配一个引用的 helper 函数,例如 bpf_sk_lookup_tcp(),
* 都有一个对应的释放函数,例如bpf_sk_release()。
* 当一个引用类型传入释放函数时,verifier 同样释放引用。
* 若在程序末尾仍保留有任何未检查或未释放的引用,verifier 会拒绝他
*/

ALU Sanitation

ALU Sanitation 是 eBPF 中一个代码加固与运行时动态检测的框架,通过对程序正在处理的实际值进行运行时检查以弥补 verifier 静态分析的不足,这项技术通过调用 fixup_bpf_calls() 为 eBPF 程序中的每一条指令的前面都添加上额外的辅助指令、替换部分指令等方式来实现

三、eBPF 虚拟机

eBPF 虚拟机本质上是 RISC 架构,一共有 11 个 64 位寄存器,一个程序计数器(PC)与一个固定大小的堆栈(通常为 512KB),在 x86 架构下的对应关系如下:

eBPF 寄存器 映射 x86_64 寄存器 用途
R0 rax 函数返回值
R1 rdi argv1
R2 rsi argv2
R3 rdx argv3
R4 rcx argv4
R5 r8 argv5
R6 rbx callee 保存
R7 r13 callee 保存
R8 r14 callee 保存
R9 r15 callee 保存
R10(只读) rbp 堆栈指针寄存器

r1 ~ r5 这五个寄存器用作 eBPF 中的函数调用传参,且只能保存常数或是指向堆栈的指针,因此所有的内存访问都需要先把数据加载到 eBPF 堆栈中才能使用,这种限制简化了 eBPF 的内存模型,也更方便 verifier 进行检查

eBPF.png

bpf_reg_state - eBPF 寄存器状态

在 eBPF 中,一个寄存器的状态信息使用 bpf_reg_state 进行表示:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
struct bpf_reg_state {
/* 各字段的顺序是重要的. 参见 states_equal() */
enum bpf_reg_type type;
/* 指针偏移的固定部分, 仅指针类型 */
s32 off;
union {
/* 当 type == PTR_TO_PACKET 时可用 */
int range;

/* 当 type == CONST_PTR_TO_MAP | PTR_TO_MAP_VALUE |
* PTR_TO_MAP_VALUE_OR_NULL 时可用
*/
struct {
struct bpf_map *map_ptr;
/* 为了从外部映射中区分映射查找
* map_uid 对于指向内部映射的寄存器为非 0 值
*/
u32 map_uid;
};

/* for PTR_TO_BTF_ID */
struct {
struct btf *btf;
u32 btf_id;
};

struct { /* for PTR_TO_MEM | PTR_TO_MEM_OR_NULL */
u32 mem_size;
u32 dynptr_id; /* for dynptr slices */
};

/* For dynptr stack slots */
struct {
enum bpf_dynptr_type type;
/* 一个 dynptr 为 16 字节, 故其占用 2 个 stack slots.
* 我们需要追踪哪一个 slot 为第一个防止用户可能尝试传入一个从
* dynptr 的第二个 slot 开始的地址的情况的 slot.
*/
bool first_slot;
} dynptr;

/* 以上任意一个的最大尺寸. */
struct {
unsigned long raw1;
unsigned long raw2;
} raw;

u32 subprogno; /* for PTR_TO_FUNC */
};
/* 对于标量类型 (SCALAR_VALUE), 其表示我们对实际值的了解.
* 对于指针类型, 其表示从被指向对象的偏移的可变部分,
* 且同与我们有相同 id 的所有 bpf_reg_states 共享.
*/
struct tnum var_off;
/* 被用于确定任何使用该寄存器的内存访问是否将导致一个坏的访问.
* These refer to the same value as var_off, not necessarily the actual
* contents of the register.
*/
s64 smin_value; /* 最小可能值 (s64) */
s64 smax_value; /* 最大可能值 (s64) */
u64 umin_value; /* 最小可能值 (u64) */
u64 umax_value; /* 最大可能值 (u64) */
s32 s32_min_value; /* 最小可能值 (s32) */
s32 s32_max_value; /* 最大可能值 (s32) */
u32 u32_min_value; /* 最小可能值 (u32) */
u32 u32_max_value; /* 最大可能值 (u32) */
/* 对于 PTR_TO_PACKET, 用以找到有着相同变量偏移的其他指针,
* 由此他们可以共享范围信息.
* 对于 PTR_TO_MAP_VALUE_OR_NULL 其被用于共享我们来自哪一个映射值
* 当其一被测试于 != NULL.
* 对于 PTR_TO_MEM_OR_NULL 其被用于辨识内存分配以追踪其释放.
* 对于 PTR_TO_SOCKET 其被用于共享哪一个指针保留了对 socket 的相同引用,
* 以确定合适的引用释放.
* 对于作为 dynptrs 的 stack slots, 其被用于追踪对 dynptr的引用
* 以确定合适的引用释放.
*/
u32 id;
/* PTR_TO_SOCKET 与 PTR_TO_TCP_SOCK 可以为一个返回自一个 pointer-cast helper
* bpf_sk_fullsock() 与 bpf_tcp_sock() 的指针 .
*
* 考虑如下情况, "sk" 为一个返回自 "sk = bpf_sk_lookup_tcp();" 的引用计数指针:
*
* 1: sk = bpf_sk_lookup_tcp();
* 2: if (!sk) { return 0; }
* 3: fullsock = bpf_sk_fullsock(sk);
* 4: if (!fullsock) { bpf_sk_release(sk); return 0; }
* 5: tp = bpf_tcp_sock(fullsock);
* 6: if (!tp) { bpf_sk_release(sk); return 0; }
* 7: bpf_sk_release(sk);
* 8: snd_cwnd = tp->snd_cwnd; // verifier 将抗议
*
* 在第 7 行的 bpf_sk_release(sk) 之后, "fullsock" 指针与
* "tp" 指针都应当被无效化. 为了这么做, 保存 "fullsock" 与 "sk"
* 的寄存器需要记住在 ref_obj_id 中的原始引用计数指针 id(即, sk_reg->id)
* 这样 verifier 便能重置所有 ref_obj_id 匹配 sk_reg->id 的寄存器
*
* sk_reg->ref_obj_id 在第 1 行被设为 sk_reg->id.
* sk_reg->id 将仅作为 NULL-marking 的目的保持.
* 在 NULL-marking 完成后, sk_reg->id 可以被重置为 0.
*
* 在第 3 行的 "fullsock = bpf_sk_fullsock(sk);" 之后,
* fullsock_reg->ref_obj_id 被设为 sk_reg->ref_obj_id.
*
* 在第 5 行的 "tp = bpf_tcp_sock(fullsock);" 之后,
* tp_reg->ref_obj_id 被设为 fullsock_reg->ref_obj_id
* 与 sk_reg->ref_obj_id 一致.
*
* 从 verifier 的角度而言, 若 sk, fullsock 与 tp 都非 NULL,
* 他们为有着不同 reg->type 的相同指针.
* 特别地, bpf_sk_release(tp) 也被允许且有着与 bpf_sk_release(sk)
* 相同的影响.
*/
u32 ref_obj_id;
/* 用于存活检查的亲子链 */
struct bpf_reg_state *parent;
/* 在被调用方中两个寄存器可以同时为 PTR_TO_STACK 如同 R1=fp-8 与 R2=fp-8,
* 但其一指向该函数栈而另一指向调用方的栈. 为了区分他们 'frameno' 被使用,
* 其为一个指向 bpf_func_state 的 bpf_verifier_state->frame[] 数组中的下标.
*/
u32 frameno;
/* 追踪子寄存器(subreg)定义. 保存的值为写入 insn 的 insn_idx.
* 这是安全的因为 subreg_def 在任何仅在主校验结束后发生的 insn 修补前被使用.
*/
s32 subreg_def;
enum bpf_reg_liveness live;
/* if (!precise && SCALAR_VALUE) min/max/tnum don't affect safety */
bool precise;
};

寄存器运行时值与边界范围校验

eBPF 程序的安全主要是由 verifier 保证的,verifier 会模拟执行每一条指令并验证寄存器的值是否合法,主要关注这几个字段:

  • smin_valuesmax_value: 64 位有符号的值的可能取值边界
  • umin_valueumax_value:64 位无符号的值的可能取值边界
  • s32_min_values32_max_value:32 位有符号的值的可能取值边界
  • u32_min_valueu32_max_value:32 位无符号的值的可能取值边界

而寄存器中可以确定的值实际上通过 var_off 字段进行表示,该值用一个 tnum 结构体表示,mask 中为 0 对应的 value 位为已知位

1
2
3
4
struct tnum {
u64 value;
u64 mask;
};

一个 verifier 完全未知的寄存器如下:

1
const struct tnum tnum_unknown = { .value = 0, .mask = -1 };

需要注意的是寄存器边界值是 verifier 通过模拟执行推测出来的,运行时的寄存器值不一定与 verifier 所推测的一致,这也曾是很多 eBPF 漏洞产生的原因

寄存器类型

寄存器在程序运行的不同阶段可能存放着不同类型的值,verifier 通过跟踪寄存器值的类型来防止越界访问的发生,主要有三类:

  • 未初始化(not init):寄存器的初始状态,尚未经过任何赋值操作,此类寄存器不能参与运算
  • 标量值(scalar):该寄存器被赋予了整型值,此类寄存器不能被作为指针进行内存访问
  • 指针类型(pointer):该寄存器为一个指针,verifier 会检查内存访问是否超出指针允许的范围
    • 实际上 eBPF 按照用途的不同划分多个不同的指针类型,例如指向栈的指针为 PTR_TO_STACK 类型
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
/* types of values stored in eBPF registers */
/* Pointer types represent:
* pointer
* pointer + imm
* pointer + (u16) var
* pointer + (u16) var + imm
* if (range > 0) then [ptr, ptr + range - off) is safe to access
* if (id > 0) means that some 'var' was added
* if (off > 0) means that 'imm' was added
*/
enum bpf_reg_type {
NOT_INIT = 0, /* nothing was written into register */
SCALAR_VALUE, /* reg doesn't contain a valid pointer */
PTR_TO_CTX, /* reg points to bpf_context */
CONST_PTR_TO_MAP, /* reg points to struct bpf_map */
PTR_TO_MAP_VALUE, /* reg points to map element value */
PTR_TO_MAP_VALUE_OR_NULL,/* points to map elem value or NULL */
PTR_TO_STACK, /* reg == frame_pointer + offset */
PTR_TO_PACKET_META, /* skb->data - meta_len */
PTR_TO_PACKET, /* reg points to skb->data */
PTR_TO_PACKET_END, /* skb->data + headlen */
PTR_TO_FLOW_KEYS, /* reg points to bpf_flow_keys */
PTR_TO_SOCKET, /* reg points to struct bpf_sock */
PTR_TO_SOCKET_OR_NULL, /* reg points to struct bpf_sock or NULL */
PTR_TO_SOCK_COMMON, /* reg points to sock_common */
PTR_TO_SOCK_COMMON_OR_NULL, /* reg points to sock_common or NULL */
PTR_TO_TCP_SOCK, /* reg points to struct tcp_sock */
PTR_TO_TCP_SOCK_OR_NULL, /* reg points to struct tcp_sock or NULL */
PTR_TO_TP_BUFFER, /* reg points to a writable raw tp's buffer */
PTR_TO_XDP_SOCK, /* reg points to struct xdp_sock */
/* PTR_TO_BTF_ID points to a kernel struct that does not need
* to be null checked by the BPF program. This does not imply the
* pointer is _not_ null and in practice this can easily be a null
* pointer when reading pointer chains. The assumption is program
* context will handle null pointer dereference typically via fault
* handling. The verifier must keep this in mind and can make no
* assumptions about null or non-null when doing branch analysis.
* Further, when passed into helpers the helpers can not, without
* additional context, assume the value is non-null.
*/
PTR_TO_BTF_ID,
/* PTR_TO_BTF_ID_OR_NULL points to a kernel struct that has not
* been checked for null. Used primarily to inform the verifier
* an explicit null check is required for this struct.
*/
PTR_TO_BTF_ID_OR_NULL,
PTR_TO_MEM, /* reg points to valid memory region */
PTR_TO_MEM_OR_NULL, /* reg points to valid memory region or NULL */
PTR_TO_RDONLY_BUF, /* reg points to a readonly buffer */
PTR_TO_RDONLY_BUF_OR_NULL, /* reg points to a readonly buffer or NULL */
PTR_TO_RDWR_BUF, /* reg points to a read/write buffer */
PTR_TO_RDWR_BUF_OR_NULL, /* reg points to a read/write buffer or NULL */
PTR_TO_PERCPU_BTF_ID, /* reg points to a percpu kernel variable */
};

四、eBPF 指令与 eBPF 程序

eBPF 为 RISC 指令集,单条 eBPF 指令在内核中定义为一个 bpf_insn 结构体:

1
2
3
4
5
6
7
struct bpf_insn {
__u8 code; /* opcode */
__u8 dst_reg:4; /* dest register */
__u8 src_reg:4; /* source register */
__s16 off; /* signed offset */
__s32 imm; /* signed immediate constant */
};

相应地,一个最简单的 eBPF 程序便是一个 bpf_insn 结构体数组,我们可以直接在用户态下编写形如这样的结构体数组来描述一个 eBPF 程序,并作为 eBPF 程序字节码传入内核:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define BPF_RAW_INSN(CODE, DST, SRC, OFF, IMM)          \
((struct bpf_insn) { \
.code = CODE, \
.dst_reg = DST, \
.src_reg = SRC, \
.off = OFF, \
.imm = IMM \
})

struct bpf_insn test_bpf_prog[] = {
BPF_RAW_INSN(BPF_ALU64 | BPF_MOV | BPF_K, BPF_REG_0, 0, 0, 0x114514),
BPF_RAW_INSN(BPF_JMP | BPF_EXIT, 0, 0, 0, 0),
};

载入到内核中后,内核最终会使用一个 bpf_prog 结构体来表示一个 eBPF 程序:

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
struct bpf_prog {
u16 pages; /* 分配的页面数量 */
u16 jited:1, /* 我们的 filter 是否是即时编译的? */
jit_requested:1,/* 架构需要即时编译程序 */
gpl_compatible:1, /* filter 是否兼容 GPL? */
cb_access:1, /* 控制块被访问了吗? */
dst_needed:1, /* 我们是否需要 dst 入口? */
blinding_requested:1, /* needs constant blinding *///译注:不知道咋翻
blinded:1, /* Was blinded *///译注:瞎了?
is_func:1, /* 程序为一个 bpf 函数 */
kprobe_override:1, /* 我们是否在一个 kprobe 之上? */
has_callchain_buf:1, /* callchain buffer 分配了吗? */
enforce_expected_attach_type:1, /* 在 attach 时强制执行 expected_attach_type 检查 */
call_get_stack:1, /* 我们是否调用 bpf_get_stack() 或 bpf_get_stackid() */
call_get_func_ip:1, /* 我们是否调用 get_func_ip() */
tstamp_type_access:1; /* 被访问的 __sk_buff->tstamp_type */
enum bpf_prog_type type; /* BPF 程序类型 */
enum bpf_attach_type expected_attach_type; /* 用于一些程序类型 */
u32 len; /* filter 块的数量 */
u32 jited_len; /* 按字节计的被即时编译的指令大小 */
u8 tag[BPF_TAG_SIZE];
struct bpf_prog_stats __percpu *stats;
int __percpu *active;
unsigned int (*bpf_func)(const void *ctx,
const struct bpf_insn *insn);
struct bpf_prog_aux *aux; /* 辅助域 */
struct sock_fprog_kern *orig_prog; /* 原始 BPF 程序 */
/* 翻译器的指令 */
union {
DECLARE_FLEX_ARRAY(struct sock_filter, insns);
DECLARE_FLEX_ARRAY(struct bpf_insn, insnsi);
};
};

其中 bpf_func 函数指针便指向 BPF 字节码经过 JIT 编译生成的汇编代码入口点

五、eBPF map

bpf map 是一个通用的用以储存不同种类数据的结构,用以在用户进程与 eBPF 程序、eBPF 程序与 eBPF 程序之间进行数据共享,这些数据以二进制形式储存,因此用户在创建时只需要指定 key 与 value 的 size

bpf map 主要有以下五个基本属性:

  • type:map 的数据结构类型
  • key_size:以字节为单位的用以索引一个元素的 key 的 size(在数组映射中使用)
  • value_size:以字节为单位的每个元素的 size
  • max_entries:map 中 entries 的最大数量
  • map_flags:描述 map 的独特特征,例如是否整个 map 的内存应被预先分配等

在内核当中使用一个 bpf_map 结构体表示:

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
struct bpf_map {
/* 前两条缓存行带有以读取为主的成员,
* 其中一些也在快速路径中被访问 (e.g. ops, max_entries).
*/
const struct bpf_map_ops *ops ____cacheline_aligned;
struct bpf_map *inner_map_meta;
#ifdef CONFIG_SECURITY
void *security;
#endif
enum bpf_map_type map_type;
u32 key_size;
u32 value_size;
u32 max_entries;
u64 map_extra; /* any per-map-type extra fields */
u32 map_flags;
u32 id;
struct btf_record *record;
int numa_node;
u32 btf_key_type_id;
u32 btf_value_type_id;
u32 btf_vmlinux_value_type_id;
struct btf *btf;
#ifdef CONFIG_MEMCG_KMEM
struct obj_cgroup *objcg;
#endif
char name[BPF_OBJ_NAME_LEN];
struct btf_field_offs *field_offs;
/* The 3rd and 4th cacheline with misc members to avoid false sharing
* particularly with refcounting.
*/
atomic64_t refcnt ____cacheline_aligned;
atomic64_t usercnt;
struct work_struct work;
struct mutex freeze_mutex;
atomic64_t writecnt;
/* 'Ownership' of program-containing map is claimed by the first program
* that is going to use this map or by the first program which FD is
* stored in the map to make sure that all callers and callees have the
* same prog type, JITed flag and xdp_has_frags flag.
*/
struct {
spinlock_t lock;
enum bpf_prog_type type;
bool jited;
bool xdp_has_frags;
} owner;
bool bypass_spec_v1;
bool frozen; /* write-once; write-protected by freeze_mutex */
};

map 类型

可选 map 类型如下:

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
enum bpf_map_type {
BPF_MAP_TYPE_UNSPEC,
BPF_MAP_TYPE_HASH,
BPF_MAP_TYPE_ARRAY,
BPF_MAP_TYPE_PROG_ARRAY,
BPF_MAP_TYPE_PERF_EVENT_ARRAY,
BPF_MAP_TYPE_PERCPU_HASH,
BPF_MAP_TYPE_PERCPU_ARRAY,
BPF_MAP_TYPE_STACK_TRACE,
BPF_MAP_TYPE_CGROUP_ARRAY,
BPF_MAP_TYPE_LRU_HASH,
BPF_MAP_TYPE_LRU_PERCPU_HASH,
BPF_MAP_TYPE_LPM_TRIE,
BPF_MAP_TYPE_ARRAY_OF_MAPS,
BPF_MAP_TYPE_HASH_OF_MAPS,
BPF_MAP_TYPE_DEVMAP,
BPF_MAP_TYPE_SOCKMAP,
BPF_MAP_TYPE_CPUMAP,
BPF_MAP_TYPE_XSKMAP,
BPF_MAP_TYPE_SOCKHASH,
BPF_MAP_TYPE_CGROUP_STORAGE_DEPRECATED,
/* BPF_MAP_TYPE_CGROUP_STORAGE is available to bpf programs attaching
* to a cgroup. The newer BPF_MAP_TYPE_CGRP_STORAGE is available to
* both cgroup-attached and other progs and supports all functionality
* provided by BPF_MAP_TYPE_CGROUP_STORAGE. So mark
* BPF_MAP_TYPE_CGROUP_STORAGE deprecated.
*/
BPF_MAP_TYPE_CGROUP_STORAGE = BPF_MAP_TYPE_CGROUP_STORAGE_DEPRECATED,
BPF_MAP_TYPE_REUSEPORT_SOCKARRAY,
BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE,
BPF_MAP_TYPE_QUEUE,
BPF_MAP_TYPE_STACK,
BPF_MAP_TYPE_SK_STORAGE,
BPF_MAP_TYPE_DEVMAP_HASH,
BPF_MAP_TYPE_STRUCT_OPS,
BPF_MAP_TYPE_RINGBUF,
BPF_MAP_TYPE_INODE_STORAGE,
BPF_MAP_TYPE_TASK_STORAGE,
BPF_MAP_TYPE_BLOOM_FILTER,
BPF_MAP_TYPE_USER_RINGBUF,
BPF_MAP_TYPE_CGRP_STORAGE,
};

常用的主要是以下几种类型:

  • BPF_MAP_TYPE_HASH:以哈希表形式存储键值对,比较常规
  • BPF_MAP_TYPE_ARRAY:以数组形式存储键值对,key 即为数组下标,对应的 value 皆初始化为 0
  • BPF_MAP_TYPE_PROG_ARRAY:特殊的数组映射,value 为其他 eBPF 程序的文件描述符
  • BPF_MAP_TYPE_STACK:以栈形式存储数据

map wrapper

0x02.bpf 系统调用

我们对 eBPF 所有的操作其实都是通过 bpf 系统调用来完成的,其原型如下:

1
int bpf(int cmd, union bpf_attr *attr, unsigned int size);

一、bpf_attr 结构体

bpf 系统调用中的第二个参数是指向联合体 bpf_attr 的指针,定义于 kernel/bpf/syscall.c 中如下,对于不同的 cmd 而言其含义不同,因此这里是一个由多个结构体构成的联合体:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
union bpf_attr {
struct { /* BPF_MAP_CREATE 命令所使用的匿名结构体 */
__u32 map_type; /* one of enum bpf_map_type */
__u32 key_size; /* key 按字节计的大小 */
__u32 value_size; /* value 按字节计的大小 */
__u32 max_entries; /* map 中最大的 entries 数量 */
__u32 map_flags; /* BPF_MAP_CREATE 相关的
* 在上面定义的 flags.
*/
__u32 inner_map_fd; /* 指向内部 map 的 fd */
__u32 numa_node; /* numa node (仅当设置了
* BPF_F_NUMA_NODE 时有效).
*/
char map_name[BPF_OBJ_NAME_LEN];
__u32 map_ifindex; /* ifindex of netdev to create on */
__u32 btf_fd; /* 指向一个 BTF 类型数据的 fd */
__u32 btf_key_type_id; /* BTF type_id of the key */
__u32 btf_value_type_id; /* BTF type_id of the value */
__u32 btf_vmlinux_value_type_id;/* BTF type_id of a kernel-
* struct stored as the
* map value
*/
/* Any per-map-type extra fields
*
* BPF_MAP_TYPE_BLOOM_FILTER - 最低 4 位指示了
* 哈希函数的数量(若为 0, bloom filter 将默认
* 使用 5 个哈希函数).
*/
__u64 map_extra;
};

struct { /* BPF_MAP_*_ELEM 命令所使用的匿名结构体 */
__u32 map_fd;
__aligned_u64 key;
union {
__aligned_u64 value;
__aligned_u64 next_key;
};
__u64 flags;
};

struct { /* BPF_MAP_*_BATCH 命令所使用的匿名结构体 */
__aligned_u64 in_batch; /* start batch,
* NULL to start from beginning
*/
__aligned_u64 out_batch; /* output: next start batch */
__aligned_u64 keys;
__aligned_u64 values;
__u32 count; /* input/output:
* input: # of key/value
* elements
* output: # of filled elements
*/
__u32 map_fd;
__u64 elem_flags;
__u64 flags;
} batch;

struct { /* BPF_PROG_LOAD 命令所使用的匿名结构体 */
__u32 prog_type; /* one of enum bpf_prog_type */
__u32 insn_cnt;
__aligned_u64 insns;
__aligned_u64 license;
__u32 log_level; /* verbosity level of verifier */
__u32 log_size; /* size of user buffer */
__aligned_u64 log_buf; /* user supplied buffer */
__u32 kern_version; /* not used */
__u32 prog_flags;
char prog_name[BPF_OBJ_NAME_LEN];
__u32 prog_ifindex; /* ifindex of netdev to prep for */
/* For some prog types expected attach type must be known at
* load time to verify attach type specific parts of prog
* (context accesses, allowed helpers, etc).
*/
__u32 expected_attach_type;
__u32 prog_btf_fd; /* fd pointing to BTF type data */
__u32 func_info_rec_size; /* userspace bpf_func_info size */
__aligned_u64 func_info; /* func info */
__u32 func_info_cnt; /* number of bpf_func_info records */
__u32 line_info_rec_size; /* userspace bpf_line_info size */
__aligned_u64 line_info; /* line info */
__u32 line_info_cnt; /* number of bpf_line_info records */
__u32 attach_btf_id; /* in-kernel BTF type id to attach to */
union {
/* valid prog_fd to attach to bpf prog */
__u32 attach_prog_fd;
/* or valid module BTF object fd or 0 to attach to vmlinux */
__u32 attach_btf_obj_fd;
};
__u32 core_relo_cnt; /* number of bpf_core_relo */
__aligned_u64 fd_array; /* array of FDs */
__aligned_u64 core_relos;
__u32 core_relo_rec_size; /* sizeof(struct bpf_core_relo) */
};

struct { /* BPF_OBJ_* 命令所使用的匿名结构体 */
__aligned_u64 pathname;
__u32 bpf_fd;
__u32 file_flags;
};

struct { /* BPF_PROG_ATTACH/DETACH 命令所使用的匿名结构体 */
__u32 target_fd; /* container object to attach to */
__u32 attach_bpf_fd; /* eBPF program to attach */
__u32 attach_type;
__u32 attach_flags;
__u32 replace_bpf_fd; /* previously attached eBPF
* program to replace if
* BPF_F_REPLACE is used
*/
};

struct { /* BPF_PROG_TEST_RUN 命令所使用的匿名结构体 */
__u32 prog_fd;
__u32 retval;
__u32 data_size_in; /* input: len of data_in */
__u32 data_size_out; /* input/output: len of data_out
* returns ENOSPC if data_out
* is too small.
*/
__aligned_u64 data_in;
__aligned_u64 data_out;
__u32 repeat;
__u32 duration;
__u32 ctx_size_in; /* input: len of ctx_in */
__u32 ctx_size_out; /* input/output: len of ctx_out
* returns ENOSPC if ctx_out
* is too small.
*/
__aligned_u64 ctx_in;
__aligned_u64 ctx_out;
__u32 flags;
__u32 cpu;
__u32 batch_size;
} test;

struct { /* BPF_*_GET_*_ID 命令所使用的匿名结构体 */
union {
__u32 start_id;
__u32 prog_id;
__u32 map_id;
__u32 btf_id;
__u32 link_id;
};
__u32 next_id;
__u32 open_flags;
};

struct { /* BPF_OBJ_GET_INFO_BY_FD 命令所使用的匿名结构体 */
__u32 bpf_fd;
__u32 info_len;
__aligned_u64 info;
} info;

struct { /* BPF_PROG_QUERY 命令所使用的匿名结构体 */
__u32 target_fd; /* container object to query */
__u32 attach_type;
__u32 query_flags;
__u32 attach_flags;
__aligned_u64 prog_ids;
__u32 prog_cnt;
/* output: per-program attach_flags.
* not allowed to be set during effective query.
*/
__aligned_u64 prog_attach_flags;
} query;

struct { /* anonymous struct used by BPF_RAW_TRACEPOINT_OPEN command */
__u64 name;
__u32 prog_fd;
} raw_tracepoint;

struct { /* anonymous struct for BPF_BTF_LOAD */
__aligned_u64 btf;
__aligned_u64 btf_log_buf;
__u32 btf_size;
__u32 btf_log_size;
__u32 btf_log_level;
};

struct {
__u32 pid; /* input: pid */
__u32 fd; /* input: fd */
__u32 flags; /* input: flags */
__u32 buf_len; /* input/output: buf len */
__aligned_u64 buf; /* input/output:
* tp_name for tracepoint
* symbol for kprobe
* filename for uprobe
*/
__u32 prog_id; /* output: prod_id */
__u32 fd_type; /* output: BPF_FD_TYPE_* */
__u64 probe_offset; /* output: probe_offset */
__u64 probe_addr; /* output: probe_addr */
} task_fd_query;

struct { /* struct used by BPF_LINK_CREATE command */
__u32 prog_fd; /* eBPF program to attach */
union {
__u32 target_fd; /* object to attach to */
__u32 target_ifindex; /* target ifindex */
};
__u32 attach_type; /* attach type */
__u32 flags; /* extra flags */
union {
__u32 target_btf_id; /* btf_id of target to attach to */
struct {
__aligned_u64 iter_info; /* extra bpf_iter_link_info */
__u32 iter_info_len; /* iter_info length */
};
struct {
/* black box user-provided value passed through
* to BPF program at the execution time and
* accessible through bpf_get_attach_cookie() BPF helper
*/
__u64 bpf_cookie;
} perf_event;
struct {
__u32 flags;
__u32 cnt;
__aligned_u64 syms;
__aligned_u64 addrs;
__aligned_u64 cookies;
} kprobe_multi;
struct {
/* this is overlaid with the target_btf_id above. */
__u32 target_btf_id;
/* black box user-provided value passed through
* to BPF program at the execution time and
* accessible through bpf_get_attach_cookie() BPF helper
*/
__u64 cookie;
} tracing;
};
} link_create;

struct { /* struct used by BPF_LINK_UPDATE command */
__u32 link_fd; /* link fd */
/* new program fd to update link with */
__u32 new_prog_fd;
__u32 flags; /* extra flags */
/* expected link's program fd; is specified only if
* BPF_F_REPLACE flag is set in flags */
__u32 old_prog_fd;
} link_update;

struct {
__u32 link_fd;
} link_detach;

struct { /* struct used by BPF_ENABLE_STATS command */
__u32 type;
} enable_stats;

struct { /* struct used by BPF_ITER_CREATE command */
__u32 link_fd;
__u32 flags;
} iter_create;

struct { /* struct used by BPF_PROG_BIND_MAP command */
__u32 prog_fd;
__u32 map_fd;
__u32 flags; /* extra flags */
} prog_bind_map;

} __attribute__((aligned(8)));

二、__sys_bpf():bpf 系统调用的核心函数

bpf 系统调用定义于 kernel/bpf/syscall.c 中,最终调用到 __sys_bpf() ,其核心主要是一个巨大的 switch,根据 cmd 的不同进行不同的操作:

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
static int __sys_bpf(int cmd, bpfptr_t uattr, unsigned int size)
{
union bpf_attr attr;
bool capable;
int err;

capable = bpf_capable() || !sysctl_unprivileged_bpf_disabled;

/* Intent here is for unprivileged_bpf_disabled to block key object
* creation commands for unprivileged users; other actions depend
* of fd availability and access to bpffs, so are dependent on
* object creation success. Capabilities are later verified for
* operations such as load and map create, so even with unprivileged
* BPF disabled, capability checks are still carried out for these
* and other operations.
*/
if (!capable &&
(cmd == BPF_MAP_CREATE || cmd == BPF_PROG_LOAD))
return -EPERM;

err = bpf_check_uarg_tail_zero(uattr, sizeof(attr), size);
if (err)
return err;
size = min_t(u32, size, sizeof(attr));

/* copy attributes from user space, may be less than sizeof(bpf_attr) */
memset(&attr, 0, sizeof(attr));
if (copy_from_bpfptr(&attr, uattr, size) != 0)
return -EFAULT;

err = security_bpf(cmd, &attr, size);
if (err < 0)
return err;

switch (cmd) {
case BPF_MAP_CREATE:
err = map_create(&attr);
break;
//...
default:
err = -EINVAL;
break;
}

return err;
}

SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size)
{
return __sys_bpf(cmd, USER_BPFPTR(uattr), size);
}

0x03. raw eBPF 程序编写入门

由于 eBPF 相关的各种操作实际上都是通过 bpf() 系统调用完成的,因此我们可以通过直接调用 bpf() 系统调用来感受 eBPF 的魅力:)

注:在 eBPF 的实际应用中很少会直接写 raw BPF 指令,而是会借助诸如 bcc 这样的各类工具,不过那不是这一篇博客的重点:)

有时间的话再在后面的博客中简单讲讲(

注2:内核在 /samples/bpf 目录下提供了很多帮助我们快速编写 eBPF 程序的工具与一些示例,其中 /samples/bpf/bpf_insn.h 文件提供了封装好的各类指令模板 :)

一、eBPF 指令格式

eBPF 为优雅的 RISC 指令集(这就要说到 Intel CISC 的含屎量了),单条指令长度为 8 字节,定义如下:

1
2
3
4
5
6
7
struct bpf_insn {
__u8 code; /* 操作码 */
__u8 dst_reg:4; /* 目的寄存器 */
__u8 src_reg:4; /* 源寄存器 */
__s16 off; /* 有符号偏移 */
__s32 imm; /* 有符号立即数 */
};

而 eBPF 实际上有两种编码模式:

  • 基础编码,单条指令为 64 bit
  • 宽指令编码, 在基础编码后添加一个 64bit 的立即数 ,单条指令为 128 bit

基础编码的指令格式如下:

长度 8 bits 4 bits 4 bits 16 bits 32 bits
含义 opcode(操作码) dst_reg(目的寄存器) src_reg(源寄存器) off(有符号偏移) imm(有符号32位立即数)

eBPF 指令中的 opcode 域长度为 8 bit,其中低 3 位固定表示指令类型,剩下的高 5 位根据类型不同用途也不同

指令类型如下表所示:

类型 描述
BPF_LD 0x00 只能用于宽指令,从 imm64 中加载数据到寄存器
BPF_LDX 0x01 从内存中加载数据到 dst_reg
BPF_ST 0x02 imm32 数据保存到内存中
BPF_STX 0x03 src_reg 寄存器数据保存到内存
BPF_ALU 0x04 32bit 算术运算
BPF_JMP 0x05 64bit 跳转操作
BPF_JMP32 0x06 32bit 跳转操作
BPF_ALU64 0x07 64bit 算术运算

注:在 classic BPF 中 0x06 为函数返回指令 BPF_RET0x07 为寄存器交换指令 BPF_MISC (cBPF 只有 AX 两个寄存器)

我超,__!.png

算术 & 跳转指令

对于算术 & 跳转指令而言由高位到低位分为三个部分:

4 bit 1 bit 3 bit
operation code (操作代码) source(源) instruction class (指令类型)

① 操作代码

opcode 的最高 4 bit 用来保存操作代码,对于算术指令而言有如下类型:

指令类型 操作代码 描述
BPF_ALU / BPF_ALU64 BPF_ADD 0x00 dst += src
BPF_ALU / BPF_ALU64 BPF_SUB 0x10 dst -= src
BPF_ALU / BPF_ALU64 BPF_MUL 0x20 dst *= src
BPF_ALU / BPF_ALU64 BPF_DIV 0x30 dst /= src
BPF_ALU / BPF_ALU64 BPF_OR 0x40 dst |= src
BPF_ALU / BPF_ALU64 BPF_AND 0x50 dst &= src
BPF_ALU / BPF_ALU64 BPF_LSH 0x60 dst <<= src
BPF_ALU / BPF_ALU64 BPF_RSH 0x70 dst >>= src
BPF_ALU / BPF_ALU64 BPF_NEG 0x80 dst = ~src
BPF_ALU / BPF_ALU64 BPF_MOD 0x90 dst %= src
BPF_ALU / BPF_ALU64 BPF_XOR 0xA0 dst ^= src
BPF_ALU / BPF_ALU64 BPF_MOV 0xB0 dst = src
BPF_ALU / BPF_ALU64 BPF_ARSH 0xC0 算术右移操作(正数补 0 负数补 1 )
BPF_ALU / BPF_ALU64 BPF_END 0xD0 字节序转换

对于跳转指令而言有如下类型:

指令类型 操作代码 描述 备注
BPF_JMP BPF_JA 0x00 PC += off 仅用于 BPF_JMP
BPF_JMP / BPF_JMP64 BPF_JEQ 0x10 PC += off if dst == src
BPF_JMP / BPF_JMP64 BPF_JGT 0x20 PC += off if dst > src
BPF_JMP / BPF_JMP64 BPF_JGE 0x30 PC += off if dst >= src
BPF_JMP / BPF_JMP64 BPF_JSET 0x40 PC += off if dst & src
BPF_JMP / BPF_JMP64 BPF_JNE 0x50 PC += off if dst != src 仅 eBPF:不等时跳转
BPF_JMP / BPF_JMP64 BPF_JSGT 0x60 PC += off if dst > src 仅 eBPF:有符号 ‘>’
BPF_JMP / BPF_JMP64 BPF_JSGE 0x70 PC += off if dst >= src 仅 eBPF:有符号 ‘>=’
BPF_JMP / BPF_JMP64 BPF_CALL 0x80 函数调用 仅 eBPF:函数调用
BPF_JMP / BPF_JMP64 BPF_EXIT 0x90 函数或者程序返回 仅 eBPF:函数返回
BPF_JMP / BPF_JMP64 BPF_JLT 0xA0 PC += off if dst < src 仅 eBPF:无符号 ‘<’
BPF_JMP / BPF_JMP64 BPF_JLE 0xB0 PC += off if dst <= src 仅 eBPF:无符号 ‘<=’
BPF_JMP / BPF_JMP64 BPF_JSLT 0xC0 PC += off if dst < src 仅 eBPF:有符号 ‘<’
BPF_JMP / BPF_JMP64 BPF_JSLE 0xD0 PC += off if dst <= src 仅 eBPF:有符号 ‘<=’

② 源

opcode 中间的一个 bit 用来表示 ,对于普通的跳转与算术指令而言含义如下表:

指令类型 描述
BPF_ALU / BPF_ALU64 / BPF_JMP / BPF_JMP64 BPF_K 0x00 使用32-bit imm32 作为源操作数
BPF_ALU / BPF_ALU64 / BPF_JMP / BPF_JMP64 BPF_X 0x08 使用源寄存器 (src_reg) 作为源操作数

对于 BPF_END 操作码而言含义如下:

指令类型 操作代码 描述
BPF_ALU / BPF_ALU64 BPF_END BPF_TO_LE 0x00 转为小端序
BPF_ALU / BPF_ALU64 BPF_END BPF_TO_BE 0x08 转为大端序

Load & Store 指令

对于 Load & Store 指令而言,opcode 由高到低分为如下三部分:

3 bits 2 bit 3 bits
mode(模式) size(大小) instruction class (指令类型)

① 大小

Load & Store 指令的 size 域用来表示操作的字节数

不知道为啥排序设为 4 2 1 8 :(

大小 描述
BPF_W 0x00 单字(4 字节)
BPF_H 0x08 半字(2字节)
BPF_B 0x10 单字节(1字节)
BPF_DW 0x18 双字(8字节)

② 模式

Load & Store 指令的 mode 域用来表示操作的模式,也就是如何去操作指定大小的数据:

模式 描述 备注
BPF_IMM 0x00 64 位立即数 eBPF 为64 位立即数,cBPF 中为 32 位
BPF_ABS 0x20 数据包直接访问 兼容自 cBPF 指令。R6 作为隐式输入,存放 struct *sk_buff ;R0 作为隐式输出,存放包中读出数据;R1 ~ R5 作为 scratch registers,在每次调用后会被清空
BPF_IND 0x40 数据包间接访问 同 BPF_ABS
BPF_MEM 0x60 赋值给 *(size *)(dst_reg + off) 标准 load & store 操作
BPF_LEN 0x80 保留指令 仅用于 cBPF
BPF_MSH 0xA0 保留指令 仅用于 cBPF
BPF_XADD 0xC0 原子操作,*(无符号类型 *)(dst_reg + off16) 运算= src_reg 仅用于 eBPF,不支持 1 / 2 字节曹祖

对于 BPF_XADDimm32 域被用来表示原子操作的运算类型:

imm32 描述
BPF_ADD 0x00 原子加
BPF_OR 0x40 原子或
BPF_AND 0x50 原子与
BPF_XOR 0xa0 原子异或

反正👴看得是有点头大的

二、raw eBPF 程序编写

一个最简单的 eBPF 程序便是一个 bpf_insn 结构体数组,我们可以直接在用户态下编写一个 bpf_insn 结构体数组并直接调用 bpf() 系统调用完成 eBPF 程序的创建与挂载:)

最简单的方法便是直接按如下形式定义一条基本的 eBPF 指令:

注:这里可以直接使用内核源码目录下提供的 /samples/bpf/bpf_insn.h :)

1
2
3
4
5
6
7
8
#define BPF_RAW_INSN(CODE, DST, SRC, OFF, IMM)        \
((struct bpf_insn) { \
.code = CODE, \
.dst_reg = DST, \
.src_reg = SRC, \
.off = OFF, \
.imm = IMM \
})

之后直接开写就行,需要注意的是我们应当以一条 跳转结束指令 (opcode 为 BPF_JMP | BPF_EXIT )作为结尾,下面是一个🌰:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <stdint.h>
#include <sys/syscall.h>
#include <linux/bpf.h>

void err_exit(const char *msg)
{
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
exit(EXIT_FAILURE);
}

#define BPF_RAW_INSN(CODE, DST, SRC, OFF, IMM) \
((struct bpf_insn) { \
.code = CODE, \
.dst_reg = DST, \
.src_reg = SRC, \
.off = OFF, \
.imm = IMM \
})

struct bpf_insn test_bpf_prog[] = {
BPF_RAW_INSN(BPF_ALU64 | BPF_MOV | BPF_K, BPF_REG_0, 0, 0, 0x114514),
BPF_RAW_INSN(BPF_JMP | BPF_EXIT, 0, 0, 0, 0),
};

#define TEST_BPF_LOG_SZ 0x10000
char test_bpf_log_buf[TEST_BPF_LOG_SZ] = { '\0' };

union bpf_attr test_bpf_attr = {
.prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
.insns = (uint64_t) &test_bpf_prog,
.insn_cnt = sizeof(test_bpf_prog) / sizeof(test_bpf_prog[0]),
.license = (uint64_t) "GPL",
.log_level = 2,
.log_buf = (uint64_t) test_bpf_log_buf,
.log_size = TEST_BPF_LOG_SZ,
};

static inline int bpf(int cmd, union bpf_attr *attr)
{
return syscall(__NR_bpf, cmd, attr, sizeof(*attr));
}

int main(int argc , char **argv, char **envp)
{
int test_bpf_prog_fd;
char *err_msg;

/* load bpf prog into kernel */
test_bpf_prog_fd = bpf(BPF_PROG_LOAD, &test_bpf_attr);
if (test_bpf_prog_fd < 0) {
err_msg = "FAILED to load bpf program!";
goto err_bpf_load;
}

/* output the log */
puts(test_bpf_log_buf);

close(test_bpf_prog_fd);

return 0;

err_bpf_load:
puts(test_bpf_log_buf);
err_socket:
err_exit(err_msg);

return 0;
}

内核会将相关的运行日志写入到我们所指定的缓冲区当中,这里输出日志缓冲区可以看到内核成功地解析了我们的 eBPF 程序:

image.png

三、raw eBPF map 使用

eBPF map 为以 key→value 映射格式存储数据的通用的数据存储结构,用于在不同程序之间共享数据,本节主要介绍 eBPF map 的基本用法

创建 eBPF map

我们可以通过 BPF_MAP_CREATE 命令创建一个新的 eBPF map,其会返回一个文件描述符作为该 map 的引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
static __always_inline int
bpf_map_create(unsigned int map_type, unsigned int key_size,
unsigned int value_size, unsigned int max_entries)
{
union bpf_attr attr = {
.map_type = map_type,
.key_size = key_size,
.value_size = value_size,
.max_entries = max_entries,
};

return bpf(BPF_MAP_CREATE, &attr);
}

更新 eBPF map

我们可以通过 BPF_MAP_UPDATE 命令更新 map 中对应的 key→value 映射:

1
2
3
4
5
6
7
8
9
10
11
12
static __always_inline int
bpf_map_update_elem(int map_fd,const void *key,const void *value,uint64_t flags)
{
union bpf_attr attr = {
.map_fd = map_fd,
.key = (uint64_t) key,
.value = (uint64_t) value,
.flags = flags,
};

return bpf(BPF_MAP_UPDATE_ELEM, &attr);
}

flags 应当为如下之一:

flags 描述 备注
BPF_ANY 有则更新,无则新建
BPF_NOEXIST 仅在不存在时进行创建 若已有对应的 key 则返回 -EEXIST
BPF_EXIST 仅在存在时进行更新 若无对应的 key 则返回 -ENOENT

在创建新映射时若 map 中映射数量已经达到 max_entries 则会返回 E2BIG

在 eBPF map 中查找

我们可以通过 BPF_MAP_LOOKUP_ELEM 命令查找 map 中是否存在对应的 key,若是则内核会将 value 拷贝到用户空间指定的 value 缓冲区:

1
2
3
4
5
6
7
8
9
10
11
static __always_inline int
bpf_map_lookup_elem(int map_fd, const void *key, void *value)
{
union bpf_attr attr = {
.map_fd = map_fd,
.key = (uint64_t) key,
.value = (uint64_t) value,
};

return bpf(BPF_MAP_LOOKUP_ELEM, &attr);
}

遍历 eBPF map

BPF_MAP_GET_NEXT_KEY 是一个非常有意思的命令,其会在 map 中查找我们所传入的 key,并将该 key 的下一个 key 拷贝回用户空间,若不存在该 key 则会返回 0 并拷贝 map 中第一个 key 到用户空间,若该 key 为最后一个 key 则返回 -1

1
2
3
4
5
6
7
8
9
10
11
static __always_inline int
bpf_map_get_next_key(int map_fd, const void *key, void *value)
{
union bpf_attr attr = {
.map_fd = map_fd,
.key = (uint64_t) key,
.next_key = (uint64_t) value,
};

return bpf(BPF_MAP_GET_NEXT_KEY, &attr);
}

利用这个命令我们可以很方便地遍历一个 eBPF map:先传入一个不存在的 key 获取到 map 中的第一个 key,接下来再不断 BPF_MAP_GET_NEXT_KEY 直到返回 -1 即可

删除 eBPF map 数据

我们可以通过 BPF_MAP_DELETE_ELEM 命令删除 map 中已有的映射,若不存在则会返回 -EPERM

1
2
3
4
5
6
7
8
9
10
static __always_inline int
bpf_map_delete_elem(int map_fd, const void *key)
{
union bpf_attr attr = {
.map_fd = map_fd,
.key = (uint64_t) key,
};

return bpf(BPF_MAP_DELETE_ELEM, &attr);
}

销毁 eBPF map

在内核的 eBPF map 数据结构中会保存引用了该 map 的程序数量,若该 map 不再被任一程序引用则会自动释放,因此我们并不需要主动去销毁一个 eBPF map:)

一个🌰程序

下面是使用 eBPF map 的一个示🌰程序:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <net/if.h>
#include <linux/if_packet.h>
#include <linux/if_ether.h>
#include <arpa/inet.h>
#include <linux/bpf.h>

static __always_inline int bpf(int cmd, union bpf_attr *attr)
{
return syscall(__NR_bpf, cmd, attr, sizeof(*attr));
}

static __always_inline int
bpf_map_create(unsigned int map_type, unsigned int key_size,
unsigned int value_size, unsigned int max_entries)
{
union bpf_attr attr = {
.map_type = map_type,
.key_size = key_size,
.value_size = value_size,
.max_entries = max_entries,
};

return bpf(BPF_MAP_CREATE, &attr);
}

static __always_inline int
bpf_map_lookup_elem(int map_fd, const void *key, void *value)
{
union bpf_attr attr = {
.map_fd = map_fd,
.key = (uint64_t) key,
.value = (uint64_t) value,
};

return bpf(BPF_MAP_LOOKUP_ELEM, &attr);
}

static __always_inline int
bpf_map_update_elem(int map_fd,const void *key,const void *value,uint64_t flags)
{
union bpf_attr attr = {
.map_fd = map_fd,
.key = (uint64_t) key,
.value = (uint64_t) value,
.flags = flags,
};

return bpf(BPF_MAP_UPDATE_ELEM, &attr);
}

static __always_inline int
bpf_map_delete_elem(int map_fd, const void *key)
{
union bpf_attr attr = {
.map_fd = map_fd,
.key = (uint64_t) key,
};

return bpf(BPF_MAP_DELETE_ELEM, &attr);
}

void err_exit(const char *msg)
{
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
exit(EXIT_FAILURE);
}

char orig_value[0x100] = "1145141919810";

int main(int argc , char **argv, char **envp)
{
char value[0x100];
int map_fd;

puts("[*] Creating new eBPF map...");
map_fd = bpf_map_create(BPF_MAP_TYPE_HASH, 0x10, 0x100, 0x10);
if (map_fd < 0) {
err_exit("FAILED to create eBPF map!");
}

puts("[*] Adding new map of key->value...");
if (bpf_map_update_elem(map_fd, "arttnba3", orig_value, BPF_ANY) < 0) {
err_exit("FAILED to update eBPF map!");
}

puts("[*] Looking up element in map...");
if (bpf_map_lookup_elem(map_fd, "arttnba3", value) < 0) {
err_exit("FAILED to look up elem in eBPF map!");
}

printf("[+] Successfully get the elem of key %s: %s\n", "arttnba3", value);

close(map_fd);

return 0;
}

运行效果如下:

image.png

运行在内核中的 eBPF 程序也可以通过 eBPF map 的 fd 访问一个 eBPF map,下面是一个示🌰程序:

注:这里笔者将常用函数 & 指令封装在了 bpf_tools.h

1

0xFF.REFERENCE

[译] Linux Socket Filtering (LSF, aka BPF)(KernelDoc,2021)

HeapDump - eBPF指令集规范v1.0

BPF之路一bpf系统调用


【EBPF.0x00】eBPF 入门指北(一):简介
https://arttnba3.github.io/2023/05/31/EBPF_0X00/
作者
arttnba3
发布于
2023年5月31日
许可协议