”
系列目录
1. 疑惑
2. vfsstat_bpf__open
3. bpf_object__load_skeleton加载bpf
4. bpf_object__attach_skeleton附着bpf程序
5. 触发bpf程序
6 .总结
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了)
1、先介绍一下bpf的寄存器,目前bpf支持r0-r15,一共16个寄存器:
1) r0是返回值的寄存器
2) r1-r5是ebpf程序的参数
3) r6-r9保留的寄存器,用于调用方保存寄存器
4) r10只读的帧栈寄存器,用于访问堆栈
5) fp寄存器(r10)后面的3个寄存器,r12, r13, r15寄存器都用于bpf的jit即时编译
6) r14寄存器用于尾部调用
7) BPF_REG_AX = r11寄存器用于致盲的临时寄存器
2、build_prologue保存原有堆栈信息
操作如下:
1) 保存A64_FP(栈顶指针),A64_LR(连接寄存器)
2) 更改栈顶寄存器A64_FP = A64_SP(如现在是在程序current A64_SP的地方)
3) 保存调用bfp之前的堆栈寄存器, r6/r7/r8/r9/fp/tcc,同时更新fp = A64_SP(现在在BPF fp register这个位置)
4) 接下去stack_depth的深度是bpf程序的内容、stack_size是当前SP指针的地方
(SP也叫堆栈寄存器(也称为栈底指针),用于存放要执行的数据)
=> 大致的结构如下:
3、emit函数,每次执行都会将指令存储在ctx->image中
4、即时编译bpf程序的指令
遍历bpf程序(函数如kprobe_vfs_read)指令集合prog->insnsi[],调用build_insn进行指令的jit编译
5、还原堆栈build_epilogue
1) 当前指针减去stack_size,将是BPF fp register的地方(前面build_prologue我们报错寄存器的尾部)
2) 依次弹出刚才我们保存的寄存器,tcc、r9/r8/r7/r6、FP/LR
6、关于bpf_jit_binary_alloc分配bpf_binary_header内存的函数也介绍一下:
由于分配地址是起码会多128,于是存储bpf即时编译后的指令集合起始地址image_ptr,是随机的
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) perf_event_open_probe找到kprobe内核函数func_name = "vfs_read",并创建对应的perf_event,注册register_kprobe
2) bpf_program__attach_perf_event_opts上面我们已经找到了"vfs_read"内核对应的函数地址,通过perf_event的fd关联起来(如此处的pfd)
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。
接着调用perf_try_init_event
2、perf_try_init_event调用的event_init就是perf_kprobe_event_init
3、perf_kprobe_event_init
根据attr.config是否为1,来确定是否ret probe(章节4.3中设置)
调用perf_kprobe_init
4、perf_kprobe_init
1) 通过config1(kprobe_func)获取函数名字func("vfs_read")
2) create_local_trace_kprobe找到对应函数的内核地址,并且注册kprobe. (单步调试的中断指令BRK64_OPCODE_KPROBES_SS在注册时插入)
3) perf_trace_event_init使能kprobe,向对应位置插入BRK64_OPCODE_KPROBES指令
4.4.3 create_local_trace_kprobe
1) alloc_trace_kprobe分配trace kprobe,设置kprobe中断触发的时候处理的函数,包括pre_handler(进入kprobe函数时处理)、handler(ret probe处理)
2) init_trace_event_call设置kprobe的注册函数call->class->reg
3) __register_trace_kprobe注册kprobe函数
1、alloc_trace_kprobe分配trace kprobe
1) 设置kprobe的函数符号名字symbol_name(此处是vfs_read)
2) 设置kprobe的中断处理函数pre_handler = kprobe_dispatcher,返回处理函数handler = kretprobe_dispatcher
3) 初始化tp->event->call
2、init_trace_event_call
1) 设置kprobe的注册函数kprobe_register
2) 标记call->flags是TRACE_EVENT_FL_KPROBE(这个event是一个kprobe类型的)
3、__register_trace_kprobe
1) 设置kprobe的flags,默认是KPROBE_FLAG_DISABLED
2) 注册kprobe,register_kprobe
4、register_kprobe
1) 通过kprobe_addr去遍历/proc/kallsyms找到函数("vfs_read")的符号地址
2) 如果该函数允许单步调试,则会插入单步中断指令BRK64_OPCODE_KPROBES_SS,触发单步异常的时候会进入异常处理函数
(prepare_kprobe->arch_prepare_kprobe->arch_prepare_ss_slot)
3) 由于kprobe此时还是disabled的,所以不会调用arm_kprobe
4.4.4 perf_trace_event_init
这里主要是trace event的注册perf_trace_event_reg
1、perf_trace_event_reg
在章节4.4.2的第2小点init_trace_event_call有设置kprobe的reg函数为kprobe_register
此处tp_event->class->reg调用的是kprobe_register
2、kprobe_register
type传递的是TRACE_REG_PERF_REGISTER,于是运行的是enable_trace_kprobe
3、enable_trace_kprobe
1) 设置TP_FLAG_PROFILE的flag
2) 调用__enable_trace_kprobe使能enable kprobe
4、__enable_trace_kprobe
判断kprobe是否已经注册了,是否设置了KPROBE_FLAG_GONE(移除kprobe的flag)
5、enable_kprobe
使能kprobe,如果kprobe被disable了,则去掉disable(KPROBE_FLAG_DISABLED)标签,其中被移除(KPROBE_FLAG_GONE)的kprobe将不能再enable
6、arm_kprobe
加锁text_mutex调用__arm_kprobe
7、__arm_kprobe/arch_arm_kprobe
在register_kprobe中已经找到函数"vfs_read"对应的符号地址p->addr,
arch_arm_kprobe会在该地址处插入BRK64_OPCODE_KPROBES指令,
这样在触发blk中断异常后,首先进入的是中断异常处理函数。
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
我们主要关注bpf_link_create,看一下bpf程序是怎么注入到内核函数中的
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
ps:
kprobe处理函数kprobe_breakpoint_handler插入到kernel_break_hook链表的流程如下=>
early_initcall(kernel\kprobes.c) -> init_kprobes -> arch_init_kprobes(probes\kprobes.c)
-> register_kernel_break_hook(&kprobes_break_hook)(debug-monitors.c) -> 插入到kernel_break_hook链表中
break_hook中的指令是KPROBES_BRK_IMM,处理函数是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程序
参考链接
https://www.kernel.org/doc/html/latest/bpf/instruction-set.html
https://github.com/iovisor/bcc/tree/master/libbpf-tools
https://github.com/libbpf/libbpf
往
期
推
荐
长按关注内核工匠微信
Linux内核黑科技| 技术文章| 精选教程