eBPF Program Injection into the Kernel (Part 1)
The article walks through the complete eBPF injection workflow—opening a skeleton, loading and JIT‑compiling the program with bpf_object__load_skeleton, attaching it to a kernel kprobe via perf_event_open_probe and bpf_link_create, and finally triggering the probe so the JIT‑compiled eBPF code runs inside the kernel.
系列目录
1. 疑惑
2. vfsstat_bpf__open
3. bpf_object__load_skeleton加载bpf
4. bpf_object__attach_skeleton附着bpf程序
5. 触发bpf程序
6 .总结
eBPF程序注入到内核中的流程,现在就带你研究(上)
3. bpf_object__load_skeleton加载bpf
vfsstat_bpf__load调用的就是bpf_object__load_skeleton,
bpf_object__load_skeleton会进行bfp加载bpf_object__load、初始化map的mmaped 3.1 bpf_object__load 加载bpf程序和maps,这里我们只讲一条线bpf_object__load_progs加载bpf程序 3.2 bpf_object__load_progs加载bpf程序 1) 检查一下是否需要跳过,本例子中针对不支持的函数,如fentry_vfs_write,会使用bpf_program__set_autoload跳过,
设置的就是load标签(可以看到设置不加载某个bpf函数,需要在open之后,load之前设置)
2) 加载bpf程序bpf_object_load_prog,并放入instances.fds中(bpf-prog的fd) 3.3 bpf_object_load_prog加载单个bpf函数 这个的bpf程序是指单个bpf函数
1) 新建prog->instances.fds数组
2) 加载bpf程序bpf_object_load_prog_instance,成功将返回bpf-prog的fd 3.4 bpf_object_load_prog_instance加载程序实例 1) 初始化load_attr(bpf_prog_load_opts)加载bpf程序的参数结构体
2) 调用bpf_prog_load函数进行bpf程序加载
=>具体流程external/libbpf/src/bpf.c => bpf_prog_load_v0_6_0 -> sys_bpf_prog_load -> BPF_PROG_LOAD -> syscall(__NR_bpf
实际调用的内核函数是bpf_prog_load(kernel_platform/common/kernel/bpf/syscall.c) 3.5 bpf_prog_load系统调用 1) 进来会先检查各类权限,如CAP_BPF、CAP_SYS_ADMIN、CAP_PERFMON、CAP_NET_ADMIN的权限检查。
2) bpf_prog_alloc新建bfp程序,同时会设置bpf_prog的jit_requested = 1
3) bpf的校验检查bpf_check
4) bpf_prog_select_runtime会将bfp程序prog即时(jit)编译,同时修改栈指针,保存进入之前的指针、寄存器等
5) 新建bpf_ksym并添加到bpf_kallsyms中
6) 创建bpf-prog的fd信息(bpf_prog_new_fd) 3.6 bpf_prog_select_runtime将bfp程序prog即时编译 主要调用的是bpf_int_jit_compile函数 3.7 bpf_int_jit_compile即时编译 1) 调用bpf_jit_blind_insn致盲eBPF指令中的立即数
2) 第一次执行时,校验build_prologue(堆栈保存)、build_body(填充bpf指令)、build_epilogue(堆栈还原)是否可以正常执行,
由于ctx.image = NULL, emit提交的指令都不会设置到ctx.image,只是指令的ctx->idx++;
3) 根据上面的ctx.idx(指令个数)和extable_size额外的大小申请bpf_binary_header *hdr的内存,并将存储位置设置成ctx.image
4) 指令ctx->idx清零,重新调用build_prologue(堆栈保存)、build_body(填充bpf指令)、build_epilogue(堆栈还原),此处报错的是bpf函数指令
5) 调用flush_icache_range,将bpf_binary_header *header到ctx.image + ctx.idx地址范围内的指令缓存刷新(旧的数据刷新掉)
6) jit编译之后prog->bpf_func就有设置了,而且设置jited = 1(代表即时编译了该函数)
7) tmp_blinded = true代表致盲指令成功,然后此处会将原来的bpf程序orig_prog给释放掉(已经有新的bpf程序prog了) 3.8 bpf_prog_new_fd返回给应用bpf-prog的fd信息 创建名字叫"bpf-prog"的fd给回应用
3.9 load总结
load主要是加载maps和即时编译bpf函数(和内核有交互),返回给应用的是bpf-prog的fd
4. bpf_object__attach_skeleton附着bpf程序
调用的是bpf_program__attach
这个运行的是attach_fn,前面介绍了通过sec_name = 'kprobe/vfs_read'去找的时候可以找到attach_kprobe(如章节2.8) 4.1 attach_kprobe 1) 确定是kprobe(进入探针)还是kretprobe(返回探针)
2) 调用bpf_program__attach_kprobe_opts 4.2 bpf_program__attach_kprobe_opts 1) 找到通过start_kernel->perf_event_init->perf_tp_register->perf_pmu_register(&perf_kprobe, "kprobe", -1);
注册的pmu_idr(type = 6, 这个是由于perf_pmu_register传入的type = -1,于是会从PERF_TYPE_MAX = 6开始找一个没有使用过的id),
也就是pmu = perf_kprobe。
2) 调用perf_event_open_probe找到kprobe内核函数func_name = "vfs_read",并创建对应的perf_event,注册register_kprobe 4.3 perf_event_open_probe 1. 构建perf_event_attr结构体,用于向内核传递数据。如ret probe会额外设置attr.config |= 1、
attr.type传递是kprobe类型(6)、attr.config1传递的是attach的函数名字"vfs_read"
2. 然后调用内核函数sys_perf_event_open 4.4 sys_perf_event_open 1. perf_event_alloc会找到对应的内核函数,如本例中的vfs_read,并且插入@BRK64_OPCODE_KPROBES指令(异常中断),然后enable该kprobe。
这里只是插入中断,还未添加执行函数(在libbpf的bpf_program__attach_perf_event_opts才会将bpf程序的二进制指令放入中断执行函数中)
2. 根据perf_event event创建event_file,将perf_event和fd("[perf_event]")关联起来 4.4.1 perf_event_alloc 新建和初始化perf_event *event,然后调用perf_init_event 4.4.2 perf_init_event 1、perf_init_event
找到通过start_kernel->perf_event_init->perf_tp_register->perf_pmu_register(&perf_kprobe, "kprobe", -1);
注册的pmu_idr(type = 6, 这个是由于perf_pmu_register传入的type = -1,于是会从PERF_TYPE_MAX = 6开始找一个没有使用过的id),
也就是pmu = perf_kprobe。
2、调用perf_try_init_event找到对应的内核函数,如本例中的vfs_read,并且插入brk64_opcodes(异常中断指令),然后register_kprobe 4.4.3 create_local_trace_kprobe 1) 分配trace kprobe,设置kprobe中断触发的时候处理的函数,包括pre_handler(进入kprobe函数时处理)、handler(ret probe处理)
2) init_trace_event_call设置kprobe的注册函数call->class->reg 4.4.4 perf_trace_event_init 这里主要是trace event的注册perf_trace_event_reg 4.5 bpf_program__attach_perf_event_opts bpf_program__attach_kprobe_opts调用perf_event_open_probe进行
perf_event_open的操作后(找到对应的内核函数,创建使能kprobe并且插入blk异常中断),接下去就是调用bpf_program__attach_perf_event_opts将bpf程序注入到kprobe的异常处理函数中。
1) 找到bpf_prog_load得到的bpf-prog fd(里面包含了bpf程序的内核进行jit编译后的指令集)
2) 初始化bpf_link_perf(bpf程序和bpf performance event之间的桥梁)
3) bpf_link_create将bpf程序注入到kprobe/uprobe异常处理函数会执行的prog_array中
4) 使能perf_event_open_probe得到的performance event
4.5.1 bpf_link_create
用联合体bpf_attr中的link_create传递参数,调用bpf的系统调用sys_bpf_fd(BPF_LINK_CREATE
(bpf的系统调用通过kernel_platform/common/kernel/bpf/syscall.c的
__sys_bpf进行,此处调用的是link_create
)) 4.5.2 link_create 系统调用link_create 4.5.3 bpf_perf_link_attach 1) 通过anon_inode:[perf_event]的fd找到对应的perf_event
2) 创建anon_inode:bpf_link的fd(bpf_perf_link的perf_file是perf_event,
bpf_perf_link->link->prog是bpf程序,bpf_perf_link->link本身关联着anon_inode:bpf_link)
3) perf_event_set_bpf_prog将perf_event *event跟prog关联起来(bpf程序) 4.5.4 perf_event_set_bpf_prog kprobe和uprobe都走的这里,最后调用的是
perf_event_attach_bpf_prog将bpf程序attach到perf_event中 4.5.5 perf_event_attach_bpf_prog 这里除了将bpf程序放入(perf_event event)->prog中,
还将bpf程序放入event->tp_event->prog_array(这个是blk中断会执行的指令集) 4.6 attach总结 attach包括的2个冠军流程
1、bpf_program__attach_kprobe_opts调用perf_event_open_probe进行perf_event_open:找到对应的内核函数,创建使能kprobe并且插入blk异常中断。
2、bpf_program__attach_perf_event_opts将bpf程序注入到kprobe的异常处理函数中。
5. 触发bpf程序
5.1 brk_handler
blk中断异常处理函数,call_break_hook调用hook函数 5.2 call_break_hook 找到注册到kernel_break_hook链表的kprobe处理函数kprobe_breakpoint_handler 5.3 kprobe_breakpoint_handler/kprobe_handler 找到对应的kprobe,还有kprobe的blk中断处理函数pre_handler
(在章节4.4.3中的alloc_trace_kprobe,设置了tk->rp.kp.pre_handler = kprobe_dispatcher) 5.4 kprobe_dispatcher 在章节4.4.4中的enable_trace_kprobe设置了TP_FLAG_PROFILE,于是运行的是kprobe_perf_func 5.5 kprobe_perf_func 1) 检查bpf程序的指令集合call->prog_array是否有效
2) trace_call_bpf运行bpf程序 5.6 trace_call_bpf bpf_prog_run运行bpf程序call->prog_array(章节4.5.5的perf_event_attach_bpf_prog设置了bpf程序集合call->prog_array),
于是此处就开始运行我们注入的bpf程序了 5.7 bpf_prog_run 最后运行的是prog->bpf_func(ctx, insnsi),
bpf_func是load的时候针对注入函数jit编译后的指令集(加入堆栈保存,执行bpf程序,堆栈还原的操作),
insnsi是bpf程序本身的指令
6 总结
总结一下bpf程序整个注入和执行过程:
1、bpf_object__open_skeleton读取和初始化bpf程序、bpf maps(通过libbpf/libelf)
2、bpf_object__load_skeleton即时编译bpf函数(和内核有交互),返回给应用的是bpf-prog的fd
3、bpf_object__attach_skeleton设置对应内核函数的kprobe blk异常中断和处理函数,注入bpf程序到call->prog_array中,同时返回perf_event和fd(probe函数相关)、bpf_link的fd(关联bpf程序和perf_event的fd)
4、blk异常中断触发,执行bpf程序
参考链接
1、https://www.kernel.org/doc/html/latest/bpf/instruction-set.html
2、https://github.com/iovisor/bcc/tree/master/libbpf-tools
3、https://github.com/libbpf/libbpf
往期推荐
eBPF程序注入到内核中的流程,现在就带你研究(上)
一文了解Vulkan在移动端渲染中的带宽与同步
AMD高保真超分算法1.0解密
长按关注内核工匠微信
Linux内核黑科技| 技术文章| 精选教程
OPPO Kernel Craftsman
Sharing Linux kernel-related cutting-edge technology, technical articles, technical news, and curated tutorials
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
