Skip to content

eBPF kprobe:load / attach / 命中

Ubuntu HWE Linux 5.15(与本站多数文章的 6.8 基准不同,文中路径以 5.15 为准)· kernel/bpf + kernel/kprobes + kernel/trace
Linux 内核 · BPF / kprobe
以 usbtrace 的 SEC("kprobe/…") 为锚点,拆解 load → 插桩 → attach → 命中执行;续篇见 kprobe-on-ftrace 插桩实测

配套实验模块:code/kprobe-bytes-demo(站内说明见续篇)。


目录

  1. 总览:load ≠ attach ≠ 执行
  2. 用户态到底传了什么
  3. bpf_prog_load():装进内核
  4. 验证:bpf_check()
  5. JIT 与解释器
  6. prog 存在哪:prog_idr
  7. 真正挂钩:两步
  8. 命中之后:谁跑 eBPF
  9. 同一函数挂多个 eBPF
  10. 一句话记忆

本文以 usbtrace 的 kprobe 模块(如 powerSEC("kprobe/usb_autosuspend_device"))为锚点,顺着 Ubuntu HWE 5.15 内核源码,把「用户态一次 sudo ./usbtrace power」拆成内核里真实发生的 load → 插桩 → attach → 命中执行 四段。

对照树路径(本机常见布局):

text
linux-hwe-5.15-5.15.0/
  kernel/bpf/syscall.c      # bpf():PROG_LOAD / LINK_CREATE
  kernel/bpf/verifier.c     # bpf_check()
  kernel/bpf/core.c         # JIT / 解释器选择
  kernel/trace/bpf_trace.c  # perf_event_attach_bpf_prog / trace_call_bpf
  kernel/trace/trace_kprobe.c
  kernel/trace/trace_event_perf.c
  kernel/kprobes.c          # register_kprobe / arm_kprobe
  kernel/events/core.c      # perf_event_set_bpf_prog
  include/uapi/linux/bpf.h  # bpf_attr / bpf_cmd

1. 总览:load ≠ attach ≠ 执行

text
.bpf.c  ──clang──►  .bpf.o (ELF)

                   libbpf 拆 ELF

     ┌────────────────┼────────────────┐
     ▼                ▼                ▼
 bpf(MAP_CREATE)  bpf(BTF_LOAD)  bpf(PROG_LOAD)     ← 装进内核、验证、JIT

                                      ▼  prog fd
                         perf_event_open(kprobe)    ← 在目标函数入口插桩

                                      ▼  perf fd
                    bpf(BPF_LINK_CREATE)            ← 把 prog 绑到该探针
                      或 ioctl(SET_BPF)


                    内核调用目标函数 → kprobe 命中
                      → kprobe_dispatcher
                      → kprobe_perf_func
                      → trace_call_bpf → 跑 eBPF
                      → ringbuf → 用户态 poll
阶段关键调用内核入口
加载程序bpf(BPF_PROG_LOAD)bpf_prog_load()
函数入口插桩perf_event_openperf_kprobe_initregister_kprobe
绑定 eBPFbpf(BPF_LINK_CREATE)link_createbpf_perf_link_attach
命中执行(被动)kprobe_dispatchertrace_call_bpf

2. 用户态到底传了什么

用户态 不是.bpf.c 或整份 ELF 塞进内核,而是:

c
bpf(BPF_PROG_LOAD, &attr, sizeof(attr));

attrunion bpf_attrBPF_PROG_LOAD 那一组字段 (include/uapi/linux/bpf.h):

字段含义
prog_typeBPF_PROG_TYPE_KPROBE
insn_cnt / insns指令条数 + 指针struct bpf_insn[]
license指针"GPL" 等字符串
prog_name程序名
prog_flags对齐、SLEEPABLE
log_*verifier 日志缓冲(-v 时 libbpf 会开)
prog_btf_fd程序 BTF(CO-RE)
func_info / line_info调试信息
attach_btf_idfentry 等场景;经典 kprobe 常不用

usbtrace 侧对应关系:

text
power.bpf.c
  → clang -target bpf → power.bpf.o
  → libbpf 解析 ELF、先建 map/BTF
  → bpf(BPF_PROG_LOAD):insns = 抽出的字节码,license = "GPL"

3. bpf_prog_load():装进内核

文件:kernel/bpf/syscall.c,由 bpf()case BPF_PROG_LOAD 调用。

3.1 函数开头的三个局部变量

c
struct bpf_prog *prog, *dst_prog = NULL;
struct btf *attach_btf = NULL;
变量作用usbtrace 经典 kprobe
prog正在加载的这份程序(主角)
dst_prog挂到另一份已加载 BPFattach_prog_fd通常 NULL
attach_btf用 BTF 描述挂接点(fentry 等)通常 NULL

3.2 执行流水线(按源码顺序)

  1. 门禁CHECK_ATTR、flags、从用户态拷 license、算 GPL 兼容、 insn_cnt 上限、bpf_capable / perfmon_capable 等。
  2. 可选 attach 元数据:解析 attach_prog_fd / attach_btf_id
  3. 分配 + 拷指令
    c
    prog = bpf_prog_alloc(...);
    copy_from_bpfptr(prog->insns, ...);
  4. 定类型find_prog_type(type, prog) —— 决定合法 helper、上下文形态。
  5. 验证(安全边界):
    c
    err = bpf_check(&prog, attr, uattr);   /* verifier.c */
  6. 选运行时
    c
    prog = bpf_prog_select_runtime(prog, &err);  /* core.c:JIT 或解释器 */
  7. 曝光 + 返回 fd
    c
    bpf_prog_alloc_id(prog);   /* 插入 prog_idr */
    bpf_prog_kallsyms_add(prog);
    err = bpf_prog_new_fd(prog);  /* 返回值 = prog fd */

此处只完成「验证并持有程序」;尚未挂到 usb_autosuspend_device


4. 验证:bpf_check()

调用点(syscall.c):

c
/* run eBPF verifier */
err = bpf_check(&prog, attr, uattr);

实现:kernel/bpf/verifier.cint bpf_check(struct bpf_prog **prog, ...)

Verifier 在 load 时 做完安全分析(指针、越界、helper 合法性、CO-RE 重定位等)。 之后的 JIT/解释器 不再做安全验证,只负责怎么执行已通过的字节码。


5. JIT 与解释器:bpf_prog_select_runtime()

文件:kernel/bpf/core.c

解释器JIT
实现___bpf_prog_run():按 insn jumptable 模拟bpf_int_jit_compile() → 本机机器码
跑什么prog->insns eBPF 字节码prog->bpf_func 原生函数
速度接近原生
失败时默认兜底CONFIG_BPF_JIT_ALWAYS_ON 时失败则 load 失败

桌面 Ubuntu 通常 bpf_jit_enable=1,usbtrace 加载成功后多数已 JIT。 命中时统一经 bpf_prog_run() → 调 prog->bpf_func


6. prog 存在哪:prog_idr

对象分配:bpf_prog_alloc()(堆上的 struct bpf_prog)。

全局索引bpf_prog_alloc_id()

c
static DEFINE_IDR(prog_idr);

id = idr_alloc_cyclic(&prog_idr, prog, 1, INT_MAX, GFP_ATOMIC);

prog_idrIDR(ID → pointer),底层是 radix treeinclude/linux/idr.hstruct idr),不是链表:

text
prog_id ──► struct bpf_prog *

bpftool prog listBPF_PROG_GET_FD_BY_ID 查的就是这张表。

用户态句柄bpf_prog_new_fd()

c
anon_inode_getfd("bpf-prog", &bpf_prog_fops, prog, O_RDWR | O_CLOEXEC);

file->private_data = prog,返回值是进程 fd 表里的 prog fd。

步骤函数存到哪
分配bpf_prog_allocstruct bpf_prog
全局注册bpf_prog_alloc_idprog_idr
交给用户态bpf_prog_new_fdfd → private_data

7. 真正挂钩:两步,不是一次 syscall

「把 eBPF 挂到目标函数入口」常被理解成一步,内核里拆成:

  1. 在函数入口插桩(改指令 / ftrace)—— perf_event_open
  2. 把这份 prog 绑到该探针 —— bpf(BPF_LINK_CREATE) 或旧 ioctl

7.1 插桩:perf_event_openregister_kprobe

text
perf_event_open
  → perf_kprobe_event_init          # kernel/events/core.c
    → perf_kprobe_init()            # kernel/trace/trace_event_perf.c
      → create_local_trace_kprobe() # kernel/trace/trace_kprobe.c
        → __register_trace_kprobe()
          → register_kprobe()       # kernel/kprobes.c  ★
            → arm_kprobe()
              → arch_arm_kprobe()   # 架构层改入口指令

分配 trace_kprobe 时会设置命中回调:

c
tk->rp.kp.pre_handler = kprobe_dispatcher;  /* 仅填函数指针 */

真正改入口发生在 register_kprobe()arm_kprobe()。 open 路径里随后还有 TRACE_REG_PERF_REGISTERenable_trace_kprobe,使能该探针。

因此:open 成功后,目标函数入口已被拦截;别人一调用就会进 kprobe_dispatcher。但此时 prog_array 往往还是空的,你的 eBPF 尚未绑定。

bpf() 分发:

c
case BPF_LINK_CREATE:
    err = link_create(&attr, uattr);

BPF_PROG_TYPE_KPROBE

c
ret = bpf_perf_link_attach(attr, prog);
/* → perf_event_set_bpf_prog()
   → perf_event_attach_bpf_prog()   # kernel/trace/bpf_trace.c */

perf_event_attach_bpf_progprog 放进该 kprobe 事件的 prog_array

c
event->prog = prog;
rcu_assign_pointer(event->tp_event->prog_array, new_array);

旧路径可用 ioctl(pfd, PERF_EVENT_IOC_SET_BPF, prog_fd),不经 LINK_CREATE。 libbpf 通常还会再 PERF_EVENT_IOC_ENABLE

attr->link_create.target_fd 是先前 perf_event_open 返回的 perf fd—— link 本身不解析函数名;函数名在 open 时已经用来建探针。


8. 命中之后:谁跑 eBPF

入口:kprobe_dispatcher()trace_kprobe.c):

c
if (trace_probe_test_flag(&tk->tp, TP_FLAG_TRACE))
    kprobe_trace_func(tk, regs);      /* ① ftrace 文本追踪 */
#ifdef CONFIG_PERF_EVENTS
if (trace_probe_test_flag(&tk->tp, TP_FLAG_PROFILE))
    ret = kprobe_perf_func(tk, regs); /* ② perf / eBPF 路径 */
#endif
分支标志做什么
TP_FLAG_TRACE写入 ftrace ring buffer(trace / trace_pipe),不是跑 BPF
TP_FLAG_PROFILEperf 路径;usbtrace / libbpf kprobe 走这里

kprobe_perf_func 内:

c
if (bpf_prog_array_valid(call)) {
    ret = trace_call_bpf(call, regs);  /* 真正执行 eBPF */
    ...
}

随后(若需要)还可向 perf 事件缓冲提交采样;usbtrace 侧数据多经 BPF ringbuf helper 回到用户态 ring_buffer__poll


9. 同一函数挂多个 eBPF:如何分发

libbpf / usbtrace 常见做法:每个 SEC("kprobe/foo") 各自 perf_event_open + register_kprobe + BPF_LINK_CREATE。 因此「两个 BPF 钩同一函数」在内核里首先是 同地址上的两个 kprobe, 不是一次把两个 prog 塞进同一个 prog_array

9.1 日常路径(两个 BPF = 两个 kprobe)

入口只插桩 一次(ftrace:NOP→CALL)。命中后:

text
foo 入口 CALL
  → ftrace trampoline → kprobe_ftrace_handler   // 只进一次
  → get_kprobe → 聚合探针(aggr)
  → aggr_pre_handler                            // ★ 多 kprobe 分发点
       ├─ kprobe_A → kprobe_dispatcher → prog_array_A → BPF₁
       └─ kprobe_B → kprobe_dispatcher → prog_array_B → BPF₂

aggr_pre_handlerkernel/kprobes.c)按聚合探针的 list 串行调用每个 pre_handler;每个探针的 prog_array 里通常只有各自那一个 prog。

结论:不是「一次 BPF_PROG_RUN_ARRAY 把两个 BPF 跑完」; 而是 kprobe 聚合 list 分两次,每次再跑对应的 eBPF。

9.2 对照:同一事件的 prog_array 里挂多个 prog

若把多个 prog 挂进同一个 perf / trace_eventprog_array (少见;需多次往同一事件 attach):

text
dispatcher → kprobe_perf_func → trace_call_bpf
  → BPF_PROG_RUN_ARRAY(prog_array)   // ★ BPF 层分发:顺序跑 M 个 prog
场景分发点扇出方式
多 BPF,各 attach 一次(常见)aggr_pre_handler + listN 个 kprobe,各跑自己的 array
多 BPF,同一事件BPF_PROG_RUN_ARRAY一个 dispatcher,数组里跑 M 个 prog

续篇(入口 NOP↔CALL、trampoline、handler 上下文):kprobe-on-ftrace 插桩实测


10. 一句话记忆

Load 把已验证的字节码变成内核里的 bpf_progprog_idr + fd);
Open 在目标函数入口插上 kprobe(register_kprobe;fentry 上常为 NOP→CALL trampoline);
Link 把 prog 放进该探针的 prog_array
命中ftrace_regs_callerkprobe_ftrace_handler →(perf 路径)kprobe_dispatcher / trace_call_bpf 执行 eBPF;
同函数多 BPF(各 attach) 时先经 aggr_pre_handler 按 kprobe list 扇出,再各自进 prog_array

安全边界在 bpf_check;插桩边界在 arm_kprobe / ftrace;绑定边界在 perf_event_attach_bpf_prog。三者不要混成一个「attach 函数」。

基于 VitePress 构建