代码加载
整体流程(按 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 / TCCAP_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 全程运行时做。