文章目录
- [1. 前言](#1. 前言)
- [2. eBPF 虚拟机基本框架](#2. eBPF 虚拟机基本框架)
-
- [2.1 寄存器](#2.1 寄存器)
- [2.2 栈](#2.2 栈)
- [2.3 指令执行](#2.3 指令执行)
-
- [2.3.1 解释执行](#2.3.1 解释执行)
- [2.3.2 编译后执行](#2.3.2 编译后执行)
- [3. 用户接口](#3. 用户接口)
- [4. 参考资料](#4. 参考资料)
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. eBPF 虚拟机基本框架
eBPF 虚拟机可以认为由以下三部分构成:
- 寄存器
- 栈
- 指令集
下面基于 Linux 4.14.x 内核源码,分别对这些部分一一进行简单分析。
2.1 寄存器
eBPF 虚拟机一共有 11 个 64-bit 寄存器,如下:
c
/* kernel/core/bpf.c */
/* Registers */
#define BPF_R0 regs[BPF_REG_0]
#define BPF_R1 regs[BPF_REG_1]
#define BPF_R2 regs[BPF_REG_2]
#define BPF_R3 regs[BPF_REG_3]
#define BPF_R4 regs[BPF_REG_4]
#define BPF_R5 regs[BPF_REG_5]
#define BPF_R6 regs[BPF_REG_6]
#define BPF_R7 regs[BPF_REG_7]
#define BPF_R8 regs[BPF_REG_8]
#define BPF_R9 regs[BPF_REG_9]
#define BPF_R10 regs[BPF_REG_10]
这些寄存器的作用如下表:
| 寄存器 | 用途 |
|---|---|
| R0 | BPF 辅助函数调用返回值,以及 BPF 程序退出值。 |
| R1~R5 | 用来传递 BPF 调用参数,BPF 程序调用参数个数限制为最多 5 个。其中 R1 传递上下文参数,如 skb 指针等。 |
| R6~R9 | 通用寄存器。 |
| R10/FP | BPF 程序运行栈空间地址,进入 BPF 程序前设置好,BPF 程序运行期间不会修改它。 |
注:后面行文中,不区分寄存器的大小写,即 r0~r10 等同于 R0~R10。
2.2 栈
eBPF 虚拟机目前支持最大 512 字节的栈空间,依据 eBFP 程序调用的解释器入口函数不同,栈空间大小也不同。由下面几个宏定义了 eBPF 程序解释器入口:
c
/* kernel/core/bpf.c */
EVAL6(DEFINE_BPF_PROG_RUN, 32, 64, 96, 128, 160, 192);
EVAL6(DEFINE_BPF_PROG_RUN, 224, 256, 288, 320, 352, 384);
EVAL4(DEFINE_BPF_PROG_RUN, 416, 448, 480, 512);
#define PROG_NAME_LIST(stack_size) PROG_NAME(stack_size),
static unsigned int (*interpreters[])(const void *ctx,
const struct bpf_insn *insn) = {
EVAL6(PROG_NAME_LIST, 32, 64, 96, 128, 160, 192)
EVAL6(PROG_NAME_LIST, 224, 256, 288, 320, 352, 384)
EVAL4(PROG_NAME_LIST, 416, 448, 480, 512)
};
上面的宏展开后,定义了如下 eBPF 程序解释器入口:
c
__bpf_prog_run32()
__bpf_prog_run64()
__bpf_prog_run96()
__bpf_prog_run128()
__bpf_prog_run160()
__bpf_prog_run192()
__bpf_prog_run224()
__bpf_prog_run256()
__bpf_prog_run288()
__bpf_prog_run320()
__bpf_prog_run352()
__bpf_prog_run384()
__bpf_prog_run416()
__bpf_prog_run448()
__bpf_prog_run480()
__bpf_prog_run512()
每个解释器后面的数字,决定了栈空间大小,如 __bpf_prog_run32() 其定义的栈空间大小为:
c
u64 stack[32 / sizeof(u64)];
32 字节,而 __bpf_prog_run512() 其定义的栈空间大小为:
c
u64 stack[512 / sizeof(u64)];
512 字节。
既然栈空间在不同的解释器入口有不同的大小,那是如何选择的呢?选择了解释器入口,也就选择了栈空间大小:
c
sys_bpf(BPF_PROG_LOAD, ...)
bpf_prog_load(&attr)
bpf_prog_select_runtime(prog, &err)
struct bpf_prog *bpf_prog_select_runtime(struct bpf_prog *fp, int *err)
{
#ifndef CONFIG_BPF_JIT_ALWAYS_ON
u32 stack_depth = max_t(u32, fp->aux->stack_depth, 1);
fp->bpf_func = interpreters[(round_up(stack_depth, 32) / 32) - 1];
#else
...
#endif
...
}
从 bpf_prog_select_runtime() 可见,eBPF 程序的栈深 fp->aux->stack_depth 决定了解释器入口,从而决定了栈大小。fp->aux->stack_depth 默认为 0,向上对齐到 32。当然其值也可以通过不同子系统 eBPF 相关接口进行配置,如 socket 的 eBPF 钩子的配置如下:
c
/* net/core/filter.c */
static int bpf_convert_filter(struct sock_filter *prog, int len,
struct bpf_prog *new_prog, int *new_len)
{
...
for (i = 0; i < len; fp++, i++) {
...
switch (fp->code) {
...
/* Store to stack. */
case BPF_ST:
case BPF_STX:
stack_off = fp->k * 4 + 4;
*insn = BPF_STX_MEM(BPF_W, BPF_REG_FP, BPF_CLASS(fp->code) ==
BPF_ST ? BPF_REG_A : BPF_REG_X,
-stack_off);
/* check_load_and_stores() verifies that classic BPF can
* load from stack only after write, so tracking
* stack_depth for ST|STX insns is enough
*/
if (new_prog && new_prog->aux->stack_depth < stack_off)
new_prog->aux->stack_depth = stack_off;
break;
...
}
...
}
...
}
2.3 指令执行
2.3.1 解释执行
eBPF 指令有 64-bit 和 128-bit 两种格式。对 eBPF 指令集的所有细节,本文不做展开,读者可参考文档 BPF Instruction Set Architecture (ISA) 了解详情。
Linux 内核用 struct bpf_insn 定义 eBPF 指令:
c
/* include/uapi/linux */
struct bpf_insn {
__u8 code; /* opcode */
__u8 dst_reg:4; /* dest register */
__u8 src_reg:4; /* source register */ /* 值 BPF_PSEUDO_MAP_FD 表示 bpf_map 访问指令 */
__s16 off; /* signed offset */
__s32 imm; /* signed immediate constant */
};
struct bpf_insn 内容一目了然,就不多做解释了。
现在来简要了解下 eBPF 解释器对指令的执行过程,以栈深 32 字节的 eBPF 程序为例,eBPF 程序执行从解释器入口 __bpf_prog_run32() 开始:
c
/* kernel/bpf/core.c */
/* 这是宏 EVAL6(DEFINE_BPF_PROG_RUN, 32, 64, 96, 128, 160, 192); 展开后的代码 */
static unsigned int __bpf_prog_run32(const void *ctx, const struct bpf_insn *insn)
{
u64 stack[32 / sizeof(u64)]; /* 定义栈空间 */
u64 regs[__MAX_BPF_REG]; /* 定义寄存器空间 */
regs[BPF_REG_10] = (u64) (unsigned long) &stack[(sizeof(stack) / sizeof((stack)[0]) +
(sizeof(struct {
int:(-!!(__builtin_types_compatible_p(typeof((stack)), typeof(&(stack)[0]))));
}))
)];
regs[BPF_REG_1] = (u64) (unsigned long) ctx;
return ___bpf_prog_run(regs, insn, stack);
}
不管哪个解释器入口,最终都调用解释器公共接口 ___bpf_prog_run():
c
static unsigned int ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn,
u64 *stack)
{
u64 tmp;
static const void *jumptable[256] = {
[0 ... 255] = &&default_label,
/* Now overwrite non-defaults ... */
/* 32 bit ALU operations */
[BPF_ALU | BPF_ADD | BPF_X] = &&ALU_ADD_X,
[BPF_ALU | BPF_ADD | BPF_K] = &&ALU_ADD_K,
[BPF_ALU | BPF_SUB | BPF_X] = &&ALU_SUB_X,
[BPF_ALU | BPF_SUB | BPF_K] = &&ALU_SUB_K,
[BPF_ALU | BPF_AND | BPF_X] = &&ALU_AND_X,
[BPF_ALU | BPF_AND | BPF_K] = &&ALU_AND_K,
[BPF_ALU | BPF_OR | BPF_X] = &&ALU_OR_X,
[BPF_ALU | BPF_OR | BPF_K] = &&ALU_OR_K,
[BPF_ALU | BPF_LSH | BPF_X] = &&ALU_LSH_X,
[BPF_ALU | BPF_LSH | BPF_K] = &&ALU_LSH_K,
[BPF_ALU | BPF_RSH | BPF_X] = &&ALU_RSH_X,
[BPF_ALU | BPF_RSH | BPF_K] = &&ALU_RSH_K,
[BPF_ALU | BPF_XOR | BPF_X] = &&ALU_XOR_X,
[BPF_ALU | BPF_XOR | BPF_K] = &&ALU_XOR_K,
[BPF_ALU | BPF_MUL | BPF_X] = &&ALU_MUL_X,
[BPF_ALU | BPF_MUL | BPF_K] = &&ALU_MUL_K,
[BPF_ALU | BPF_MOV | BPF_X] = &&ALU_MOV_X,
[BPF_ALU | BPF_MOV | BPF_K] = &&ALU_MOV_K,
[BPF_ALU | BPF_DIV | BPF_X] = &&ALU_DIV_X,
[BPF_ALU | BPF_DIV | BPF_K] = &&ALU_DIV_K,
[BPF_ALU | BPF_MOD | BPF_X] = &&ALU_MOD_X,
[BPF_ALU | BPF_MOD | BPF_K] = &&ALU_MOD_K,
[BPF_ALU | BPF_NEG] = &&ALU_NEG,
[BPF_ALU | BPF_END | BPF_TO_BE] = &&ALU_END_TO_BE,
[BPF_ALU | BPF_END | BPF_TO_LE] = &&ALU_END_TO_LE,
/* 64 bit ALU operations */
[BPF_ALU64 | BPF_ADD | BPF_X] = &&ALU64_ADD_X,
[BPF_ALU64 | BPF_ADD | BPF_K] = &&ALU64_ADD_K,
[BPF_ALU64 | BPF_SUB | BPF_X] = &&ALU64_SUB_X,
[BPF_ALU64 | BPF_SUB | BPF_K] = &&ALU64_SUB_K,
[BPF_ALU64 | BPF_AND | BPF_X] = &&ALU64_AND_X,
[BPF_ALU64 | BPF_AND | BPF_K] = &&ALU64_AND_K,
[BPF_ALU64 | BPF_OR | BPF_X] = &&ALU64_OR_X,
[BPF_ALU64 | BPF_OR | BPF_K] = &&ALU64_OR_K,
[BPF_ALU64 | BPF_LSH | BPF_X] = &&ALU64_LSH_X,
[BPF_ALU64 | BPF_LSH | BPF_K] = &&ALU64_LSH_K,
[BPF_ALU64 | BPF_RSH | BPF_X] = &&ALU64_RSH_X,
[BPF_ALU64 | BPF_RSH | BPF_K] = &&ALU64_RSH_K,
[BPF_ALU64 | BPF_XOR | BPF_X] = &&ALU64_XOR_X,
[BPF_ALU64 | BPF_XOR | BPF_K] = &&ALU64_XOR_K,
[BPF_ALU64 | BPF_MUL | BPF_X] = &&ALU64_MUL_X,
[BPF_ALU64 | BPF_MUL | BPF_K] = &&ALU64_MUL_K,
[BPF_ALU64 | BPF_MOV | BPF_X] = &&ALU64_MOV_X,
[BPF_ALU64 | BPF_MOV | BPF_K] = &&ALU64_MOV_K,
[BPF_ALU64 | BPF_ARSH | BPF_X] = &&ALU64_ARSH_X,
[BPF_ALU64 | BPF_ARSH | BPF_K] = &&ALU64_ARSH_K,
[BPF_ALU64 | BPF_DIV | BPF_X] = &&ALU64_DIV_X,
[BPF_ALU64 | BPF_DIV | BPF_K] = &&ALU64_DIV_K,
[BPF_ALU64 | BPF_MOD | BPF_X] = &&ALU64_MOD_X,
[BPF_ALU64 | BPF_MOD | BPF_K] = &&ALU64_MOD_K,
[BPF_ALU64 | BPF_NEG] = &&ALU64_NEG,
/* Call instruction */
/* 如 samples/bpf/sockex1_kern.c: bpf_map_lookup_elem(&my_map, &index); */
[BPF_JMP | BPF_CALL] = &&JMP_CALL,
[BPF_JMP | BPF_TAIL_CALL] = &&JMP_TAIL_CALL,
/* Jumps */
[BPF_JMP | BPF_JA] = &&JMP_JA,
[BPF_JMP | BPF_JEQ | BPF_X] = &&JMP_JEQ_X,
[BPF_JMP | BPF_JEQ | BPF_K] = &&JMP_JEQ_K,
[BPF_JMP | BPF_JNE | BPF_X] = &&JMP_JNE_X,
[BPF_JMP | BPF_JNE | BPF_K] = &&JMP_JNE_K,
[BPF_JMP | BPF_JGT | BPF_X] = &&JMP_JGT_X,
[BPF_JMP | BPF_JGT | BPF_K] = &&JMP_JGT_K,
[BPF_JMP | BPF_JLT | BPF_X] = &&JMP_JLT_X,
[BPF_JMP | BPF_JLT | BPF_K] = &&JMP_JLT_K,
[BPF_JMP | BPF_JGE | BPF_X] = &&JMP_JGE_X,
[BPF_JMP | BPF_JGE | BPF_K] = &&JMP_JGE_K,
[BPF_JMP | BPF_JLE | BPF_X] = &&JMP_JLE_X,
[BPF_JMP | BPF_JLE | BPF_K] = &&JMP_JLE_K,
[BPF_JMP | BPF_JSGT | BPF_X] = &&JMP_JSGT_X,
[BPF_JMP | BPF_JSGT | BPF_K] = &&JMP_JSGT_K,
[BPF_JMP | BPF_JSLT | BPF_X] = &&JMP_JSLT_X,
[BPF_JMP | BPF_JSLT | BPF_K] = &&JMP_JSLT_K,
[BPF_JMP | BPF_JSGE | BPF_X] = &&JMP_JSGE_X,
[BPF_JMP | BPF_JSGE | BPF_K] = &&JMP_JSGE_K,
[BPF_JMP | BPF_JSLE | BPF_X] = &&JMP_JSLE_X,
[BPF_JMP | BPF_JSLE | BPF_K] = &&JMP_JSLE_K,
[BPF_JMP | BPF_JSET | BPF_X] = &&JMP_JSET_X,
[BPF_JMP | BPF_JSET | BPF_K] = &&JMP_JSET_K,
/* Program return */
[BPF_JMP | BPF_EXIT] = &&JMP_EXIT,
/* Store instructions */
[BPF_STX | BPF_MEM | BPF_B] = &&STX_MEM_B,
[BPF_STX | BPF_MEM | BPF_H] = &&STX_MEM_H,
[BPF_STX | BPF_MEM | BPF_W] = &&STX_MEM_W,
[BPF_STX | BPF_MEM | BPF_DW] = &&STX_MEM_DW,
[BPF_STX | BPF_XADD | BPF_W] = &&STX_XADD_W,
[BPF_STX | BPF_XADD | BPF_DW] = &&STX_XADD_DW,
[BPF_ST | BPF_MEM | BPF_B] = &&ST_MEM_B,
[BPF_ST | BPF_MEM | BPF_H] = &&ST_MEM_H,
[BPF_ST | BPF_MEM | BPF_W] = &&ST_MEM_W,
[BPF_ST | BPF_MEM | BPF_DW] = &&ST_MEM_DW,
/* Load instructions */
[BPF_LDX | BPF_MEM | BPF_B] = &&LDX_MEM_B,
[BPF_LDX | BPF_MEM | BPF_H] = &&LDX_MEM_H,
[BPF_LDX | BPF_MEM | BPF_W] = &&LDX_MEM_W,
[BPF_LDX | BPF_MEM | BPF_DW] = &&LDX_MEM_DW,
[BPF_LD | BPF_ABS | BPF_W] = &&LD_ABS_W,
[BPF_LD | BPF_ABS | BPF_H] = &&LD_ABS_H,
[BPF_LD | BPF_ABS | BPF_B] = &&LD_ABS_B,
[BPF_LD | BPF_IND | BPF_W] = &&LD_IND_W,
[BPF_LD | BPF_IND | BPF_H] = &&LD_IND_H,
[BPF_LD | BPF_IND | BPF_B] = &&LD_IND_B,
[BPF_LD | BPF_IMM | BPF_DW] = &&LD_IMM_DW,
};
u32 tail_call_cnt = 0;
void *ptr;
int off;
#define CONT ({ insn++; goto select_insn; }) /* 移动到下一条指令 */
#define CONT_JMP ({ insn++; goto select_insn; })
select_insn:
goto *jumptable[insn->code]; /* 执行指令 */
/* 解释执行每一条指令,这里随便挑选几条来展开下 */
...
/* 展开 LDST(W, u32) */
#if 0
STX_MEM_W:
*(u32 *)(unsigned long) (regs[insn->dst_reg] + insn->off) = regs[insn->src_reg];
({
insn++;
goto select_insn;
});
ST_MEM_W:
*(u32 *)(unsigned long) (regs[insn->dst_reg] + insn->off) = insn->imm;
({
insn++;
goto select_insn;
});
LDX_MEM_W:
regs[insn->dst_reg] = *(u32 *)(unsigned long) (regs[insn->src_reg] + insn->off);
({
insn++;
goto select_insn;
});
#endif
LDST(W, u32)
...
}
LDST(W, u32) 定义了 3 条 eBPF 指令的解释执行逻辑:
-
STX_MEM_W地址
32-bit写入操作。目的寄存器 dst_reg 包含了一个基地址,然后加上一个insn->off偏移,得到目的地址,然后将源寄存器insn->src_reg中的值写入该目的地址。 -
ST_MEM_W地址
32-bit写入操作。目的寄存器dst_reg包含了一个基地址,然后加上一个insn->off偏移,得到目的地址,然后将一个常量立即数insn->imm的值写入到该目的地址。 -
LDX_MEM_W地址
32-bit读操作。源寄存器insn->src_reg中含了一个基地址,然后加上insn->off一个偏移,得到目的地址,然后读取该目的地址的值到目的寄存器insn->dst_reg。
对于解释器的执行,就聊到这里,对于其它指令的解释执行过程,本文就不做展开了,感兴趣的读者可自行参照进行研究。
2.3.2 编译后执行
解释器的逻辑浅显易懂,但边解释边执行,运行效率上不高。另外一种方式是,将 eBPF 虚拟机的字节码,编译成硬件架构本地指令,然后再执行,这样执行效率上会有很大的提高。Linux 内核 eBPF JIT 编译器通过配置项 CONFIG_BPF_JIT=y 开启。另外,Linux 内核开启 CONFIG_BPF_JIT_ALWAYS_ON=y 的情形下,eBPF 虚拟机的字节码程序,总是被特定于硬件架构的 JIT(Just-In-Time) 编译器编译后执行,简单看下细节。
c
sys_bpf(BPF_PROG_LOAD, ...)
bpf_prog_load(&attr)
bpf_prog_select_runtime(prog, &err)
c
static unsigned int __bpf_prog_ret0(const void *ctx,
const struct bpf_insn *insn)
{
return 0;
}
struct bpf_prog *bpf_prog_select_runtime(struct bpf_prog *fp, int *err)
{
#ifndef CONFIG_BPF_JIT_ALWAYS_ON
...
#else
fp->bpf_func = __bpf_prog_ret0;
#endif
/*
* 调用硬件架构图的 eBPF JIT(Just-In-Time) 编译器, 将 eBPF 字节码编译为本地指令:
* 新编译的程序将替换原有的字节码
*/
fp = bpf_int_jit_compile(fp); /* 如 arch/arm/net/bpf_jit_32.c: bpf_int_jit_compile() */
...
return fp;
}
因为是将 eBPF 虚拟机字节码翻译为本地指令,所以 JIT 编译过程是硬件架构强相关,这里以 ARMv7 为例来加以分析:
c
/* arch/arm/net/bpf_jit_32.c */
struct bpf_prog *bpf_int_jit_compile(struct bpf_prog *prog)
{
struct bpf_prog *tmp, *orig_prog = prog;
struct bpf_binary_header *header;
bool tmp_blinded = false;
struct jit_ctx ctx;
unsigned int tmp_idx;
unsigned int image_size;
u8 *image_ptr; /* JIT 编译的 native 指令存放地址 */
...
/* Now we know the size of the structure to make */
/* 分配用来放置 JIT 编译后 native 指令的空间 (@image_ptr) */
header = bpf_jit_binary_alloc(image_size, &image_ptr,
sizeof(u32), jit_fill_hole);
...
/* 2.) Actual pass to generate final JIT code */
ctx.target = (u32 *) image_ptr;
ctx.idx = 0;
/* JIT 编译 eBPF 字节码为 native 指令, 存放到 bpf_jit_binary_alloc() 分配的 ctx.target 空间 */
build_prologue(&ctx);
...
flush_icache_range((u32)header, (u32)(ctx.target + ctx.idx));
set_memory_ro((unsigned long)header, header->pages);
prog->bpf_func = (void *)ctx.target; /* 执行 JIT 编译后指令序列 */
prog->jited = 1; /* 标记 eBPF 程序经过了 JIT 编译 */
prog->jited_len = image_size;
...
}
那么 JIT 编译后的程序,要如何执行呢?还是一样,通过 struct bpf_prog::bpf_func 执行。如:
c
#define BPF_PROG_RUN(filter, ctx) (*filter->bpf_func)(ctx, filter->insnsi)
从前面分析知道,struct bpf_prog::bpf_func 现在指向存储 JIT 编译后的 native 指令序列,而不是 eBPF 解释器入口。
3. 用户接口
Linux 内核提供 sys_bpf() 系统调用供用户空间进行 eBPF 编程:
c
/* kernel/bpf/syscall.c */
SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size)
{
...
}
cmd 参数决定了 sys_bpf() 的行为,其中最常见的命令是以下几个:
- BPF_MAP_CREATE:用于创建 bpf_map 数据,用于内核和用户空间交互。
- BPF_MAP_LOOKUP_ELEM:用于查找指定的 bpf_map。
- BPF_PROG_LOAD:用于将 eBPF 字节码程序注入内核。
- BPF_PROG_ATTACH (需开启 CONFIG_CGROUPS && CONFIG_CGROUP_BPF):用于将 eBPF 字节码程序挂接到指定观察对象。
看一个例子,Linux 内核自带的 sockex1_kern.c,这是 eBPF 程序(内核)部分:
c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <linux/ip.h>
#include "bpf_helpers.h"
typedef unsigned int u32;
struct bpf_map_def SEC("maps") my_map = {
.type = BPF_MAP_TYPE_ARRAY,
.key_size = sizeof(u32),
.value_size = sizeof(long long),
.max_entries = 256,
};
SEC("socket1")
int bpf_prog1(struct __sk_buff *skb)
{
int index = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol));
long long *value;
if (skb->pkt_type != PACKET_OUTGOING)
return 0;
value = bpf_map_lookup_elem(&my_map, &index); /* 返回 bpf_map 中 index 指向数据的地址指针 */
if (value)
__sync_fetch_and_add(value, skb->len); /* 累加写 skb 包长度到 bpf_map,供用户空间读取 */
return 0;
}
char _license[] SEC("license") = "GPL";
编译后反汇编的 eBPF 字节码程序如下:
c
bpf/sockex1/kern/sockex1_kern.o: file format ELF64-BPF
Disassembly of section socket1:
0000000000000000 bpf_prog1:
0: bf 16 00 00 00 00 00 00 r6 = r1 // r1 = skb, r6 = skb
1: 30 00 00 00 17 00 00 00 r0 = *(u8 *)skb[23] // load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol))
2: 63 0a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r0 // index = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol));
3: 61 61 04 00 00 00 00 00 r1 = *(u32 *)(r6 + 4) // r1 = skb->pkt_type
4: 55 01 08 00 04 00 00 00 if r1 != 4 goto +8 <LBB0_3> // if (skb->pkt_type != PACKET_OUTGOING) return 0;
5: bf a2 00 00 00 00 00 00 r2 = r10 // r2 = stack[]
6: 07 02 00 00 fc ff ff ff r2 += -4 // 调用 bpf_map_lookup_elem() 传递参数 &index
7: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll
9: 85 00 00 00 01 00 00 00 call 1 // bpf_map_lookup_elem(&my_map, &index);
10: 15 00 02 00 00 00 00 00 if r0 == 0 goto +2 <LBB0_3> // if (!value) return 0;
11: 61 61 00 00 00 00 00 00 r1 = *(u32 *)(r6 + 0) // r1 = skb->len
12: db 10 00 00 00 00 00 00 lock *(u64 *)(r0 + 0) += r1 // __sync_fetch_and_add(value, skb->len);
0000000000000068 LBB0_3:
13: b7 00 00 00 00 00 00 00 r0 = 0
14: 95 00 00 00 00 00 00 00 exit
然后是 eBPF 用户空间部分 sockex1_kern.c,节选主干部分:
c
/* 1. 创建 bpf_map 数据,返回 fd 指代这些 bpf_map 数据 */
union bpf_attr attr;
int map_fd;
memset(&attr, '\0', sizeof(attr));
attr.map_type = map_type;
attr.key_size = key_size;
attr.value_size = value_size;
attr.max_entries = max_entries;
attr.map_flags = map_flags;
map_fd = sys_bpf(BPF_MAP_CREATE, &attr, sizeof(attr));
/* 2. 加载 eBPF 程序到内核空间,返回 fd 指代该程序 */
int fd;
union bpf_attr attr;
bzero(&attr, sizeof(attr));
attr.prog_type = type;
attr.insn_cnt = (__u32)insns_cnt;
attr.insns = ptr_to_u64(insns);
attr.license = ptr_to_u64(license);
attr.log_buf = ptr_to_u64(NULL);
attr.log_size = 0;
attr.log_level = 0;
attr.kern_version = kern_version;
fd = sys_bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
/* 3. 挂接 eBPF 到 socket */
assert(setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, prog_fd, sizeof(prog_fd[0])) == 0);
/* 4. 从 bpf_map 读取 eBPF 程序(sockex1_kern.c) 写入 skb 长度累加数据 */
long long tcp_cnt, udp_cnt, icmp_cnt;
/* 读取数据 */
key = IPPROTO_TCP;
assert(bpf_map_lookup_elem(map_fd[0], &key, &tcp_cnt) == 0);
key = IPPROTO_UDP;
assert(bpf_map_lookup_elem(map_fd[0], &key, &udp_cnt) == 0);
key = IPPROTO_ICMP;
assert(bpf_map_lookup_elem(map_fd[0], &key, &icmp_cnt) == 0);
小结一个 sockex1_kern.c(sockex1_kern.o) 和 sockex1_user.c 的工作流程:
- bpf_map 创建
用户空间程序调用
map_fd = syscall(__NR_bpf, BPF_MAP_CREATE, attr, size);
创建 bpf_map, 返回到 map_fd, 然后用户空间程序修正 sock_kern.o 对 my_map 的访问为 bpf_map 的 map_fd。 - 加载 sock_kern.o 到内核
- 修正 sockex1_kern.o 对 bpf_map 的 fd 为 bpf_map 指针
- 修正 sockex1_kern.o 函数调用 bpf_map_lookup_elem() 为正确地址
- 挂接 bpf_prog sock_kern.o 到 socket
- 然后在收包时触发 bpf_prog sockex1_kern.o 运行,并将数据写入到 bpf_map
- 用户空间程序通过 bpf_map_lookup_elem() 读取 bpf_map 数据。
用户空间的 bpf_map_lookup_elem() 实现为 syscall(__NR_bpf, BPF_MAP_LOOKUP_ELEM,...)。
- 用户空间程序通过 bpf_map_lookup_elem() 读取 bpf_map 数据。
注意,内核 eBPF 程序 sockex1_kern.c 中的 bpf_map_lookup_elem() 返回指定元素的数据指针;而用户空间程序 sockex1_user.c 中的 bpf_map_lookup_elem(map_fd[0], &key, &X),首先找到指定元素的数据指针,然后从地址读取数据并返回用户空间。这两个 bpf_map_lookup_elem() 是不同的,后者在前者的基础上更进一步。
4. 参考资料
1\] [Linux Socket Filtering aka Berkeley Packet Filter (BPF)](https://www.kernel.org/doc/html/latest/networking/filter.html#ebpf-opcode-encoding "Linux Socket Filtering aka Berkeley Packet Filter (BPF)") \[2\] [BPF Instruction Set Architecture (ISA)](https://docs.kernel.org/bpf/standardization/instruction-set.html "BPF Instruction Set Architecture (ISA)") \[3\] [eBPF汇编指令你还不知道?看这一篇文就够了](https://zhuanlan.zhihu.com/p/487995137 "eBPF汇编指令你还不知道?看这一篇文就够了") \[4\] [eBPF 基础知识-BPF 指令集 (Instruction Set)](https://juejin.cn/post/7520911407015510058 "eBPF 基础知识-BPF 指令集 (Instruction Set)") \[5\] [ebpf工作原理介绍------ebpf指令集及虚拟机](https://mp.weixin.qq.com/s/EODZfKRn15HVXZTKh6Vg3Q "ebpf工作原理介绍——ebpf指令集及虚拟机") \[6\] [Linux内核eBPF虚拟机源码分析------verifier与jit](https://bbs.kanxue.com/thread-267956-1.htm "Linux内核eBPF虚拟机源码分析——verifier与jit")