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)

2 BPF Instruction Set Architecture (ISA)

3 eBPF汇编指令你还不知道?看这一篇文就够了

4 eBPF 基础知识-BPF 指令集 (Instruction Set)

5 ebpf工作原理介绍------ebpf指令集及虚拟机

6 Linux内核eBPF虚拟机源码分析------verifier与jit

相关推荐
A小辣椒2 天前
TShark:Wireshark CLI 功能
linux
A小辣椒2 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334663 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪3 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩4 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言