Skip to content

代码加载

整体流程(按 bpftrace 架构对应)

代码从写到内核跑起来,拆成十步。每步都有"谁在做"和"用的什么机制":

 ① 源码 ──→ ② Parser ──→ ③ 类型解析 ──→ ④ 语义分析 ──→ ⑤ 代码生成
                                                         │
                                                         ▼
⑩ 输出 ◀── ⑨ map ◀── ⑧ machine code ◀── ⑦ BPF ◀── ⑥ BPF bytecode
   perf buffer     perf buffer   JIT 后的      Verifier +    LLVM/Clang
   / ringbuf       Maps          原生码        JIT           后端

两条主流路径,区别只在 ①–⑥ 发生的位置:

路径 ①–⑥ 发生位置 工具链
预编译 构建期 clang + libbpf / aya / cilium-ebpf
运行时编译 进程启动时 BCC / bpftrace

① 源码

写 BPF 程序。语法视路径不同:

  • 预编译路径:标准 C(libbpf)/ Rust(aya)
  • 运行时路径:bpftrace DSL 或 BCC Python 包裹的 C 片段
/* C, 走 libbpf */
SEC("xdp")
int drop_ssh(struct xdp_md *ctx) { ... }
/* Rust, 走 aya */
#[xdp]
pub fn drop_ssh(ctx: XdpContext) -> u32 { ... }
# bpftrace
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'

② Parser → AST

clang frontend(libbpf / aya)或 bpftrace 自带的 flex/bison(parser.yy + lexer.l)把源码切成抽象语法树。语法错误在这一步报。

预编译路径下,这一步发生在 clang -target bpf 阶段。


③ 类型解析(BTF + clang AST)

解析出来的 AST 还不知道内核结构体的真实布局(struct task_struct 哪个字段在哪、偏移多少)。这一步:

  • 预编译路径:clang AST 把所有用到的类型都展开成带 BTF tag 的重定位指令,留到加载期再修
  • 运行时路径:bpftrace 直接读 /sys/kernel/btf/vmlinux,拿到真实类型信息,立刻可以填进 IR
/* 这种访问 task_struct->pid 的代码,clang 不会真去算偏移 */
pid_t p = bpf_get_current_task()->pid;

④ 语义分析 / 绑定 hook

校验 map 定义、补全 probe 句柄、确定挂载目标。这一步输出两类东西:

  • Maps:调用 bpf_create_map()(bcc 库)建好用户态 fd
  • Probes:把所有 SEC("tracepoint/...") 之类的 hook 名解析成 attach point,等会儿去 attach
/* aya 自动干这事;libbpf 在 skel->open() 时干;BCC 在 BPF(text=...) 时干 */
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 1024);
} config SEC(".maps");

⑤ 代码生成(AST → LLVM IR)

clang 把 AST 转成 LLVM IR;bpftrace 的 ast/codegen_llvm.cc 干同样的事。这一步把 printf("...") 转成对 BPF helper 的调用、把 map 操作转成 IR 指令。


⑥ 后端编译(IR → BPF 字节码)

LLVM BPF target(llvm/lib/Target/BPF/)把 IR 编译成 eBPF 字节码,写到 ELF 文件 .bpf.o。预编译路径下这一步产物就是 .bpf.o;运行时路径下产物是临时 ELF(在内存里)。

# 预编译路径的产物
clang -target bpf -O2 -g -c app.bpf.c -o app.bpf.o
bpftool gen skeleton app.bpf.o > app.skel.h

⑦ 加载入内核(bpf() 系统调用链)

libbpf / aya / cilium-ebpf / bcc 在背后发起的就是这套 syscall:

1. BPF_BTF_LOAD         上传 BTF 段,CO-RE 修偏移就在这里做
2. BPF_MAP_CREATE ×N    把 .maps 段里的 map 全建出来
3. BPF_PROG_LOAD        上传 program
   ├─ verifier 静态分析
   │    ├─ 控制流必须能证明终止(无死循环 / 无不可达)
   │    ├─ 所有内存访问越界检查
   │    ├─ 栈 ≤ 512 bytes,指令 ≤ 100 万
   │    └─ helper 函数白名单
   └─ 通过 → JIT 到原生码

任何一步失败 fd 都拿不到,整个 .bpf.o 都不进内核。


⑧ 执行(machine code → CPU)

JIT 把字节码编译成 x86_64 / arm64 原生指令,挂到对应 hook 上后:

硬件事件 / 内核路径 / 用户函数
   ▼
hook 触发 → 跳到 BPF machine code
   ▼
BPF 跑完后 → 返回原路径继续(XDP_PASS)或改写结果(XDP_DROP / TX)
  • 数据面:XDP / TC / sock_ops 每包触发一次,路径极短
  • 事件面:tracepoint / kprobe / uprobe 触发频率看事件量,单次允许稍重

⑨ map 数据通道

BPF 程序跑出来的数据靠两类东西送回用户态:

map(K/V 状态、配置、双向通信)

/* 用户态写 */
__u32 key = 0, val = 1;
bpf_map__update_elem(skel->maps.config, &key, sizeof(key), &val, sizeof(val), BPF_ANY);

/* BPF 读 */
SEC("xdp")
int prog(struct xdp_md *ctx) {
    __u32 *v = bpf_map_lookup_elem(&config, &(int){0});
    if (v && *v) return XDP_DROP;
    return XDP_PASS;
}

ringbuf(程序 → 用户态事件流)

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} events SEC(".maps");

SEC("tracepoint/syscalls/sys_enter_openat")
int trace_openat(struct trace_event_raw_sys_enter *ctx) {
    struct event_t *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e) return 0;
    e->pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(e->comm, sizeof(e->comm));
    bpf_ringbuf_submit(e, 0);
    return 0;
}

ringbuf 是 BPF_MAP_TYPE_RINGBUF 的内核 ring buffer(共享内存),新代码统一用这个,老的 perf_event_array 已被替代。


⑩ 用户态输出

数据进 ringbuf 后,用户态三类消费方式:

ringbuf / perf_event poll(程序化消费)

struct ring_buffer *rb = ring_buffer__new(bpf_map__fd(skel->maps.events),
                                          handle_event, NULL, NULL);
while (!exiting) ring_buffer__poll(rb, 100);

printf(bpftrace / BCC 风格,文本输出)

bpftrace / BCC 在用户态 perf_event_printer() 把 perf buffer 里的事件反序列化,调用对应 AsyncAction::print_* 或直接 printf()。这是 ⑩ 里最常见的路径。

async action(统计聚合、histogram)

bpftrace 的 @count[tid]++ / @hist.comm = lhist(...) 之类的聚合,每秒或事件触发时通过 async 输出汇总结果。


Attach 路径(第 ⑦ 步的细分)

第 ⑦ 步的 BPF_PROG_LOAD 拿到 fd 之后,要真正挂到 hook 上才生效,按 hook 类型走不同接口:

Hook Attach 机制 用户态 API
XDP setsockopt(SO_ATTACH_XDP) 或 netlink link add xdp bpf_xdp_attach / bpf_link_create
TC ingress/egress netlink TC_FILTER bpf_tc_attach
kprobe perf_event_open + ioctl(PERF_EVENT_IOC_SET_BPF) libbpf auto / bpf_program__attach_kprobe
tracepoint perf_event_open(type=PERF_TYPE_TRACEPOINT) bpf_program__attach_tracepoint
uprobes perf_event_open(type=PERF_TYPE_UPROBE) bpf_program__attach_uprobe
LSM BPF_PROG_ATTACH bpf_program__attach_lsm
cgroup BPF_PROG_ATTACH + cgroup fd bpf_program__attach_cgroup
socket BPF_PROG_ATTACH + sock_ops / sk_msg bpf_program__attach_socket_ops
fentry/fexit bpf_link_create(BPF_LINK_TYPE_TRACING) libbpf attach()

生命周期与热更新

第 ⑦–⑩ 步建立起来后,进程还要管:

  • pin 到 bpffs(/sys/fs/bpf/xxx):跨进程共享,进程退出也活着;要手动 unpin
  • bpf link:内核态持引用计数,fd 关就自动 detach,推荐做法
  • 热更新:bpf_link_update(new_prog_fd) 原子替换,避免 destroy + load 窗口期丢事件
# 看当前所有挂载
bpftool prog show
bpftool map show
bpftool prog show pinned /sys/fs/bpf/my_prog
bpftool prog profile id 42            # 性能统计

权限

第 ⑦ 步需要的能力:

  • CAP_BPF(Linux 5.8+)+ CAP_PERFMON(5.8+):加载 BPF 程序
  • CAP_NET_ADMIN:attach XDP / TC
  • CAP_SYS_ADMIN(5.8 之前):万能钥匙
  • kernel.unprivileged_bpf_disabled=1(默认)禁止非 root 加载
  • systemd 服务必须 LimitMEMLOCK=infinity,否则 map 创建 ENOMEM

容器内:默认缺 CAP_BPF + 没有 /sys/fs/bpf 挂载,要跑 eBPF 必须 privileged 或加 capability + mount bpffs。


一句话

① 写代码 → ② 解析 → ③ 类型(BTF)→ ④ 绑 hook / 建 map → ⑤ 生成 IR → ⑥ 编 BPF 字节码 → ⑦ bpf() 加载 + verifier + JIT → ⑧ CPU 执行 → ⑨ map 通信 → ⑩ 用户态输出。libbpf/aya 在 ⑦ 之前是构建期做的,bpftrace/BCC 全程运行时做。