Operations 20 min read

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.

OPPO Kernel Craftsman
OPPO Kernel Craftsman
OPPO Kernel Craftsman
eBPF Program Injection into the Kernel (Part 1)

系列目录

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内核黑科技| 技术文章| 精选教程

KerneleBPFprofiling
OPPO Kernel Craftsman
Written by

OPPO Kernel Craftsman

Sharing Linux kernel-related cutting-edge technology, technical articles, technical news, and curated tutorials

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.