Linux eBPF 虚拟机简析

文章目录

  • [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 虚拟机一共有 1164-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-bit128-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) 定义了 3eBPF 指令的解释执行逻辑:

  • 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 的工作流程:

  1. 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。
  2. 加载 sock_kern.o 到内核
    • 修正 sockex1_kern.o 对 bpf_map 的 fd 为 bpf_map 指针
    • 修正 sockex1_kern.o 函数调用 bpf_map_lookup_elem() 为正确地址
    • 挂接 bpf_prog sock_kern.o 到 socket
  3. 然后在收包时触发 bpf_prog sockex1_kern.o 运行,并将数据写入到 bpf_map
    • 用户空间程序通过 bpf_map_lookup_elem() 读取 bpf_map 数据。
      用户空间的 bpf_map_lookup_elem() 实现为 syscall(__NR_bpf, BPF_MAP_LOOKUP_ELEM,...)。

注意,内核 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")

相关推荐
EndingCoder8 小时前
接口基础:定义对象形状
linux·运维·前端·javascript·typescript
遇见火星9 小时前
Linux 运维:删除大日志文件时避免磁盘 IO 飙升,echo 空文件 vs truncate 命令对比实操
linux·运维·服务器
食咗未9 小时前
Linux SPI接口显示屏调试过程记录
linux
A-花开堪折9 小时前
Qemu-NUC980(十一):SPI Controller
linux·arm开发·驱动开发·嵌入式硬件
RisunJan9 小时前
Linux命令-ipcrm命令(删除Linux系统中的进程间通信(IPC)资源)
linux·运维·服务器
Joren的学习记录9 小时前
【Linux运维大神系列】Kubernetes详解2(kubeadm部署k8s1.27单节点集群)
linux·运维·kubernetes
lbb 小魔仙9 小时前
【Linux】K8s 集群搭建避坑指南:基于 Linux 内核参数调优的生产级部署方案
linux·运维·kubernetes
老兵发新帖9 小时前
ubuntu服务器配置私钥登录
linux·服务器·ubuntu
vortex59 小时前
Linux 用户组查询命令详解
linux·运维·服务器