如果覺得文章對你有所啟發,可以考慮用 🌟 支持 Gthulhu 專案,短期目標是集齊 300 個 🌟 藉此被 CNCF Landscape 採納 [ref]。
在上一篇文章中,我們回顧了 eBPF 的誕生與演進史,了解了從 Classic BPF 到 eBPF 的技術發展脈絡。今天,我們將深入 eBPF 的核心架構,探討它的虛擬機器設計、指令集架構、Verifier 機制以及 JIT 編譯器的工作原理。
理解 eBPF 的架構設計對於後續的程式開發至關重要。只有掌握了這些底層機制,我們才能寫出高效、安全的 eBPF 程式。
eBPF 虛擬機器是一個 64 位的 RISC(精簡指令集)架構,專門設計用於在 Linux 內核中安全高效地執行使用者程式碼。
eBPF VM 包含 11 個 64 位暫存器:
// eBPF 暫存器定義
enum {
BPF_REG_0 = 0, // 返回值暫存器
BPF_REG_1, // 函數參數 1 / 程式 context
BPF_REG_2, // 函數參數 2
BPF_REG_3, // 函數參數 3
BPF_REG_4, // 函數參數 4
BPF_REG_5, // 函數參數 5
BPF_REG_6, // 被呼叫者保存暫存器
BPF_REG_7, // 被呼叫者保存暫存器
BPF_REG_8, // 被呼叫者保存暫存器
BPF_REG_9, // 被呼叫者保存暫存器
BPF_REG_10, // 堆疊指標(Read Only)
__MAX_BPF_REG,
};
每個暫存器都有特定的用途:
eBPF 程式的記憶體空間分為幾個部分:
每個 eBPF 指令都是 64 位(8 字節),格式如下:
struct bpf_insn {
__u8 code; // 操作碼
__u8 dst_reg:4; // 目標暫存器
__u8 src_reg:4; // 來源暫存器
__s16 off; // 偏移量
__s32 imm; // 立即數
};
eBPF 指令可以分為以下幾類:
// 64 位加法:dst_reg += src_reg
BPF_ALU64 | BPF_ADD | BPF_X
// 32 位加法:dst_reg += imm
BPF_ALU | BPF_ADD | BPF_K
// 示例:計算兩個數的和
BPF_MOV64_IMM(BPF_REG_1, 10), // r1 = 10
BPF_MOV64_IMM(BPF_REG_2, 20), // r2 = 20
BPF_ALU64_REG(BPF_ADD, BPF_REG_1, BPF_REG_2), // r1 += r2
// 無條件跳躍
BPF_JMP | BPF_JA
// 條件跳躍:if (dst_reg == src_reg) goto pc + off
BPF_JMP | BPF_JEQ | BPF_X
// 示例:簡單的條件判斷
BPF_MOV64_IMM(BPF_REG_1, 42), // r1 = 42
BPF_JMP_IMM(BPF_JEQ, BPF_REG_1, 42, 2), // if r1 == 42 goto +2
BPF_MOV64_IMM(BPF_REG_0, 0), // r0 = 0 (false case)
BPF_EXIT_INSN(), // exit
BPF_MOV64_IMM(BPF_REG_0, 1), // r0 = 1 (true case)
BPF_EXIT_INSN(), // exit
// 載入指令:dst_reg = *(size *)(src_reg + off)
BPF_MEM | BPF_LDX | BPF_W // 載入 32 位
BPF_MEM | BPF_LDX | BPF_DW // 載入 64 位
// 儲存指令:*(size *)(dst_reg + off) = src_reg
BPF_MEM | BPF_STX | BPF_W // 儲存 32 位
BPF_MEM | BPF_STX | BPF_DW // 儲存 64 位
// 示例:存取堆疊變數
BPF_MOV64_IMM(BPF_REG_1, 123), // r1 = 123
BPF_STX_MEM(BPF_DW, BPF_REG_10, BPF_REG_1, -8), // *(u64*)(r10-8) = r1
BPF_LDX_MEM(BPF_DW, BPF_REG_2, BPF_REG_10, -8), // r2 = *(u64*)(r10-8)
// 呼叫 helper 函數
BPF_JMP | BPF_CALL
// 示例:呼叫 bpf_printk
BPF_LD_MAP_FD(BPF_REG_1, fmt_map_fd), // r1 = format string
BPF_MOV64_IMM(BPF_REG_2, 42), // r2 = 42
BPF_CALL_FUNC(BPF_FUNC_trace_printk), // call bpf_trace_printk(r1, r2)
Verifier 是 eBPF 安全性的核心,它在程式載入時進行靜態分析,確保程式不會對系統造成危害。
// 簡化的 Verifier 檢查流程
int bpf_check(struct bpf_prog **prog, union bpf_attr *attr)
{
struct bpf_verifier_env *env;
int ret = -EINVAL;
// 1. 初始化驗證環境
env = kzalloc(sizeof(struct bpf_verifier_env), GFP_KERNEL);
// 2. 基本格式檢查
ret = check_cfg(env);
if (ret < 0)
goto err_free_env;
// 3. 指令層級檢查
ret = do_check(env);
if (ret < 0)
goto err_free_env;
// 4. 特權操作檢查
ret = check_map_access(env);
if (ret < 0)
goto err_free_env;
// 5. 最終最佳化
ret = convert_pseudo_ld_imm64(env);
err_free_env:
kfree(env);
return ret;
}
Verifier 會檢查程式的所有執行路徑:
// 檢查無限迴圈
static int check_cfg(struct bpf_verifier_env *env)
{
int insn_cnt = env->prog->len;
bool *visited;
int ret = 0;
int i;
visited = kcalloc(insn_cnt, sizeof(bool), GFP_KERNEL);
if (!visited)
return -ENOMEM;
// 深度優先搜尋,檢查所有路徑
ret = visit_insn(env, 0, visited);
// 檢查是否有未訪問的指令(dead code)
for (i = 0; i < insn_cnt; i++) {
if (!visited[i]) {
verbose(env, "unreachable insn %d\n", i);
ret = -EINVAL;
break;
}
}
kfree(visited);
return ret;
}
// 檢查記憶體存取的安全性
static int check_mem_access(struct bpf_verifier_env *env,
int regno, int off, int size,
enum bpf_access_type type)
{
struct bpf_reg_state *reg = &env->cur_state.regs[regno];
// 檢查暫存器類型
if (reg->type != PTR_TO_STACK &&
reg->type != PTR_TO_MAP_VALUE &&
reg->type != PTR_TO_CTX) {
verbose(env, "invalid read from register\n");
return -EACCES;
}
// 檢查存取邊界
if (off < reg->min_value || off + size > reg->max_value) {
verbose(env, "invalid memory access\n");
return -EACCES;
}
// 檢查對齊
if (off % size != 0) {
verbose(env, "misaligned memory access\n");
return -EACCES;
}
return 0;
}
Verifier 會追蹤每個暫存器在程式執行過程中的狀態:
struct bpf_reg_state {
enum bpf_reg_type type; // 暫存器類型
s32 min_value; // 最小可能值
u32 max_value; // 最大可能值
u32 id; // 暫存器 ID
u32 ref_obj_id; // 引用物件 ID
struct tnum var_off; // 變數偏移
};
// 更新暫存器狀態
static void mark_reg_known(struct bpf_reg_state *reg, u64 imm)
{
reg->min_value = imm;
reg->max_value = imm;
reg->var_off = tnum_const(imm);
reg->type = SCALAR_VALUE;
}
通過 Verifier 檢查的 eBPF 程式會被 JIT 編譯器編譯成原生機器碼。
// x86_64 JIT 編譯器的簡化流程
struct bpf_prog *bpf_int_jit_compile(struct bpf_prog *prog)
{
struct bpf_binary_header *header;
struct x64_jit_data *jit_data;
int proglen, oldproglen = 0;
u8 *image = NULL;
int pass;
// 多次遍歷最佳化
for (pass = 0; pass < 20; pass++) {
proglen = do_jit(prog, NULL, 0, &jit_data);
if (proglen <= 0) {
image = NULL;
goto out;
}
// 如果程式大小穩定,分配記憶體並生成程式碼
if (proglen == oldproglen) {
header = bpf_jit_binary_alloc(proglen, &image,
1, jit_fill_hole);
if (!header) {
prog = orig_prog;
goto out;
}
proglen = do_jit(prog, image, proglen, &jit_data);
break;
}
oldproglen = proglen;
}
out:
return prog;
}
讓我們看看一個簡單的 eBPF 指令如何被翻譯成 x86_64 機器碼:
// eBPF 指令:r1 += r2
// BPF_ALU64_REG(BPF_ADD, BPF_REG_1, BPF_REG_2)
static int emit_add(u8 **pprog, u32 dst_reg, u32 src_reg)
{
u8 *prog = *pprog;
// 生成 x86_64 ADD 指令
// 對應 "add %rsi, %rdi" (假設 r1->rdi, r2->rsi)
*prog++ = 0x48; // REX prefix for 64-bit
*prog++ = 0x01; // ADD opcode
*prog++ = 0xfe; // ModR/M byte: add %rsi, %rdi
*pprog = prog;
return 0;
}
eBPF 程式必須附加到特定的 Hook 點才能執行。不同的程式類型對應不同的 Hook 點和功能。
enum bpf_prog_type {
BPF_PROG_TYPE_UNSPEC,
BPF_PROG_TYPE_SOCKET_FILTER, // Socket 過濾器
BPF_PROG_TYPE_KPROBE, // 內核 probe
BPF_PROG_TYPE_SCHED_CLS, // 排程分類器
BPF_PROG_TYPE_SCHED_ACT, // 排程動作
BPF_PROG_TYPE_TRACEPOINT, // 追蹤點
BPF_PROG_TYPE_XDP, // eXpress Data Path
BPF_PROG_TYPE_PERF_EVENT, // perf 事件
BPF_PROG_TYPE_CGROUP_SKB, // cgroup skb
BPF_PROG_TYPE_CGROUP_SOCK, // cgroup socket
BPF_PROG_TYPE_CGROUP_DEVICE, // cgroup 裝置
BPF_PROG_TYPE_SK_MSG, // socket message
BPF_PROG_TYPE_RAW_TRACEPOINT, // 原始追蹤點
BPF_PROG_TYPE_SOCK_OPS, // socket 操作
BPF_PROG_TYPE_SK_SKB, // socket skb
BPF_PROG_TYPE_STRUCT_OPS, // 結構操作
BPF_PROG_TYPE_EXT, // 擴展程式
BPF_PROG_TYPE_LSM, // Linux Security Module
BPF_PROG_TYPE_SK_LOOKUP, // socket lookup
BPF_PROG_TYPE_SYSCALL, // 系統呼叫
};
每種程式類型都有對應的 Context 結構:
// XDP 程式的 Context
struct xdp_md {
__u32 data; // 封包資料開始
__u32 data_end; // 封包資料結束
__u32 data_meta; // metadata 區域
__u32 ingress_ifindex; // 輸入介面索引
__u32 rx_queue_index; // 接收佇列索引
};
// 追蹤程式的 Context
struct pt_regs {
unsigned long r15;
unsigned long r14;
unsigned long r13;
// ... 其他暫存器
unsigned long rip; // 指令指標
unsigned long cs;
unsigned long eflags;
unsigned long rsp; // 堆疊指標
unsigned long ss;
};
讓我們寫一個簡單的工具來解析 eBPF ByteCode:
// bytecode_analyzer.c
#include <stdio.h>
#include <stdint.h>
#include <linux/bpf.h>
// eBPF 指令操作碼定義
#define BPF_CLASS(code) ((code) & 0x07)
#define BPF_OP(code) ((code) & 0xf0)
#define BPF_SRC(code) ((code) & 0x08)
const char* get_opcode_name(uint8_t code)
{
uint8_t cls = BPF_CLASS(code);
uint8_t op = BPF_OP(code);
switch (cls) {
case BPF_ALU64:
switch (op) {
case BPF_ADD: return "add64";
case BPF_SUB: return "sub64";
case BPF_MUL: return "mul64";
case BPF_DIV: return "div64";
case BPF_MOV: return "mov64";
default: return "unknown_alu64";
}
case BPF_JMP:
switch (op) {
case BPF_JA: return "ja";
case BPF_JEQ: return "jeq";
case BPF_JNE: return "jne";
case BPF_JGT: return "jgt";
case BPF_EXIT: return "exit";
case BPF_CALL: return "call";
default: return "unknown_jmp";
}
case BPF_LD:
return "ld";
case BPF_LDX:
return "ldx";
case BPF_ST:
return "st";
case BPF_STX:
return "stx";
default:
return "unknown";
}
}
void disassemble_insn(struct bpf_insn *insn, int idx)
{
printf("%04d: %s", idx, get_opcode_name(insn->code));
// 根據指令類型顯示操作數
if (BPF_CLASS(insn->code) == BPF_ALU64 ||
BPF_CLASS(insn->code) == BPF_ALU) {
if (BPF_SRC(insn->code) == BPF_K) {
printf(" r%d, #%d", insn->dst_reg, insn->imm);
} else {
printf(" r%d, r%d", insn->dst_reg, insn->src_reg);
}
} else if (BPF_CLASS(insn->code) == BPF_JMP) {
if (insn->code == (BPF_JMP | BPF_CALL)) {
printf(" %d", insn->imm);
} else if (insn->code == (BPF_JMP | BPF_JA)) {
printf(" %+d", insn->off);
} else {
printf(" r%d, r%d, %+d", insn->dst_reg, insn->src_reg, insn->off);
}
}
printf("\n");
}
int main()
{
// 簡單的 eBPF 程式:return 42
struct bpf_insn program[] = {
BPF_MOV64_IMM(BPF_REG_0, 42), // r0 = 42
BPF_EXIT_INSN(), // exit
};
int insn_count = sizeof(program) / sizeof(program[0]);
printf("eBPF ByteCode 反組譯:\n");
printf("===================\n");
for (int i = 0; i < insn_count; i++) {
disassemble_insn(&program[i], i);
}
return 0;
}
編譯並執行:
# Makefile
CC = gcc
CFLAGS = -Wall -Wextra -std=c99
bytecode_analyzer: bytecode_analyzer.c
$(CC) $(CFLAGS) -o $@ $<
clean:
rm -f bytecode_analyzer
.PHONY: clean
make bytecode_analyzer
./bytecode_analyzer
預期輸出:
eBPF ByteCode 反組譯:
===================
0000: mov64 r0, #42
0001: exit
eBPF 在設計時面臨安全性和效能的權衡:
安全性措施:
效能最佳化:
eBPF 既要支援多種應用場景,又要在特定領域有出色表現:
通用性特點:
特化支援:
指令集設計的權衡:
簡潔性:
功能性:
今天我們深入探討了 eBPF 的核心架構,包括:
這些底層機制的理解對於編寫高效的 eBPF 程式至關重要。在下一篇文章中,我們將開始搭建 eBPF 開發環境,並實際編寫第一個 eBPF 程式。