eBPF程序注入到内核中的流程,现在就带你研究(下)

系列目录

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

d34111114977cc67ff3c697924f05582.png

3.1 bpf_object__load

加载bpf程序和maps,这里我们只讲一条线bpf_object__load_progs加载bpf程序

75b7136d1587e1bcd948e2ccee68e4c8.png

e317e9af3dc07dba66e12ddd42f835ef.png

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)

af596cec2696d4e8c4ac7197a9e02275.png

3.3 bpf_object_load_prog加载单个bpf函数

这个的bpf程序是指单个bpf函数

1) 新建prog->instances.fds数组

2) 加载bpf程序bpf_object_load_prog_instance,成功将返回bpf-prog的fd

6bfb298c429a22d26b20776f8f923038.png

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)

a3556878f39b932b9857d0546081bfb1.png

7972b54498386364959c20a407c952c5.png

b020c322fa10d303f859fc341509fa5a.png

e92c988ca3129298368053e3347a54e6.png

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)

87bf6c6472c05c536951f77c1a093df4.png

39ca41feac4f4dd4f5c877dd11d7447c.png

4936a88b47dd63dada7e9bf27572bf63.png

5267efb31f4fb0c77e5abbd147f68aef.png

26d4e83e353bf56921b3c6890db97dbd.png

c76f18a9e495f884bf123031e62ac1ec.png

3.6 bpf_prog_select_runtime将bfp程序prog即时编译

主要调用的是bpf_int_jit_compile函数

3bd6245095e559bb14b52aba4d92c718.png

63473c0bf2270491f2bc4e6cd9dcb81d.png

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了)

cec9bf294d1a1b41f9c3522da70d26cd.png

fadff52c5ce129842e215358420a162d.png

6ba810cf42fa17f028fe414070c21003.png

bd4c067dd410d4acc5a8551d4d368753.png

6936e445000bf5b1d36a0181ad7f8a95.png

b31b1e8fe251fb13aa0595a609797105.png

39da02c6fad627c5cc19bc3580d03715.png

7930c5552bebd302e6241ded6673a345.png

7e922b5c10930b26565b5d5f5b0540f9.png

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寄存器用于致盲的临时寄存器

2bf33455b5e8e63fbc5ea811fbba7d09.png

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也叫堆栈寄存器(也称为栈底指针),用于存放要执行的数据)

=> 大致的结构如下:

45337d73bef820430667a4e6240e2c9f.png

26d2c35bfad76ff715918764ea0136a2.png

48f10a3bcd3e27d1ee6193b06ec166e1.png

af0df729c3bd8a1aabe3de29c697eec9.png

3、emit函数,每次执行都会将指令存储在ctx->image中

4b00bdde0ceb4dda6d7a317e3741bc34.png

4、即时编译bpf程序的指令

遍历bpf程序(函数如kprobe_vfs_read)指令集合prog->insnsi[],调用build_insn进行指令的jit编译

5d9ef64cbd65b0c909992f6a37c07fa3.png

8bb81b319c9f1102315cdab119a15874.png

5、还原堆栈build_epilogue

1) 当前指针减去stack_size,将是BPF fp register的地方(前面build_prologue我们报错寄存器的尾部)

2) 依次弹出刚才我们保存的寄存器,tcc、r9/r8/r7/r6、FP/LR

3fad6e7557e8586819bf813644dae1c6.png

6、关于bpf_jit_binary_alloc分配bpf_binary_header内存的函数也介绍一下:

由于分配地址是起码会多128,于是存储bpf即时编译后的指令集合起始地址image_ptr,是随机的

0409e4f76828a7acafbdf2b84742e864.png

656b2ccf8ad2c2bd73e9f4bd782b26e6.png

3.8 bpf_prog_new_fd返回给应用bpf-prog的fd信息

创建名字叫"bpf-prog"的fd给回应用

b4f2443b4dd6f7c1faef1acb4a7099b6.png

3.9 load总结

load主要是加载maps和即时编译bpf函数(和内核有交互),返回给应用的是bpf-prog的fd

4. bpf_object__attach_skeleton附着bpf程序

调用的是bpf_program__attach

2af385adbb7d93b854d01b3b9e0b4005.png

这个运行的是attach_fn,前面介绍了通过sec_name = 'kprobe/vfs_read'去找的时候可以找到attach_kprobe(如章节2.8)

a5e49b07092678b116f8e75ed47a076b.png

4.1 attach_kprobe

1) 确定是kprobe(进入探针)还是kretprobe(返回探针)

2) 调用bpf_program__attach_kprobe_opts

acd5e069e8c1e659d6289e7ce1118463.png

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)

d4fa5f6fd4d8c3143aa211e9dcda75ee.png

fff7b219580a21888910a93448f191f0.png

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

e7d2ec38fc23e17737d43f104cd033c6.png

adbd5151460a290ad4f7fd405543c693.png

ebbd241bf5d817958eced8eb4cd2d69d.png

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]")关联起来

4215cae520d0133d6d976ce2753de0eb.png

87b1c1e3e409167d087477b4c1768a99.png

5706e25528df593f5c53870b14f6f3e7.png

50d917857e67d90dfc6ffcd36003a6b8.png

a5130d9e59c0c47d3888119da3697049.png

4.4.1 perf_event_alloc

新建和初始化perf_event *event,然后调用perf_init_event

142d0dd5e2af4513de9ede184d4b53f6.png

2e3b0f2a22237f955870935dfca953dc.png

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

40327a2a6ca33d03907258461c997f68.png

2、perf_try_init_event调用的event_init就是perf_kprobe_event_init

bc228d63ac1ff005f2ca534acfc7b6de.png

3、perf_kprobe_event_init

根据attr.config是否为1,来确定是否ret probe(章节4.3中设置)

调用perf_kprobe_init

d5e3ca9035c6bd74606a633b6ed3d769.png

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指令

dea72f90d016fea29218aca8f3f51421.png

d9df125927fbf9db51057daa46ef69e7.png

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函数

1840cfb20d7831fc82dc2eebfe9f87eb.png

273fab9c6d68bcd2918ab6e7dd938cb1.png

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

2710c78d486b2fd0dee5184f0e3aaacd.png

1638d319326f1e77e103c56dd98069ed.png

2、init_trace_event_call

1) 设置kprobe的注册函数kprobe_register

2) 标记call->flags是TRACE_EVENT_FL_KPROBE(这个event是一个kprobe类型的)

f6d02eb1c1585509a67eb450dcebab42.png

3、__register_trace_kprobe

1) 设置kprobe的flags,默认是KPROBE_FLAG_DISABLED

2) 注册kprobe,register_kprobe

98d79ca132c1ff0d8f7727cc93850bd1.png

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

74b723e391704175bed9b61cc533a410.png

181355babf3789fe20cae56d6a514211.png

4.4.4 perf_trace_event_init

这里主要是trace event的注册perf_trace_event_reg

68de77ed1e17713d8e7a8a061eeba64f.png

1、perf_trace_event_reg

在章节4.4.2的第2小点init_trace_event_call有设置kprobe的reg函数为kprobe_register

此处tp_event->class->reg调用的是kprobe_register

1d24967a3d7b6f889c4a931ccc5c05eb.png

2、kprobe_register

type传递的是TRACE_REG_PERF_REGISTER,于是运行的是enable_trace_kprobe

4b06a8693a9f1dacda39b78ea8f24718.png

3、enable_trace_kprobe

1) 设置TP_FLAG_PROFILE的flag

2) 调用__enable_trace_kprobe使能enable kprobe

9588bfbf5084a60929656a9d4003b116.png

4、__enable_trace_kprobe

判断kprobe是否已经注册了,是否设置了KPROBE_FLAG_GONE(移除kprobe的flag)

cb1060df68d70f5add00a3c025048354.png

5、enable_kprobe

使能kprobe,如果kprobe被disable了,则去掉disable(KPROBE_FLAG_DISABLED)标签,其中被移除(KPROBE_FLAG_GONE)的kprobe将不能再enable

b585b89bf98933ec0ddecc30ff13e9ba.png

6、arm_kprobe

加锁text_mutex调用__arm_kprobe

4b1bd9761cb4ee89d74b90a09f2d048c.png

7、__arm_kprobe/arch_arm_kprobe

在register_kprobe中已经找到函数"vfs_read"对应的符号地址p->addr,

arch_arm_kprobe会在该地址处插入BRK64_OPCODE_KPROBES指令,

这样在触发blk中断异常后,首先进入的是中断异常处理函数。

b3831b42c468e0b5bddde24f8cc703f3.png

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程序是怎么注入到内核函数中的

e45233c55c08984e6efb8cca566d9a1c.png

42a1b650a7b76bc19fcdf490dd952ae0.png

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

)

f9d3a6aafbb542d2b918748dcc115b20.png

4.5.2 link_create

系统调用link_create

f78cce4108495fdbb43fcef49902724e.png

70b8a09634005d41e32d93cfdfb96a44.png

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程序)

e892b95d963e0f07c2e7d9f442a0d6e4.png

a4b719b12680d45dd3acdcdf26267e7f.png

4.5.4 perf_event_set_bpf_prog

kprobe和uprobe都走的这里,最后调用的是

perf_event_attach_bpf_prog将bpf程序attach到perf_event中

1a1b76653d18625660785119453ebe98.png

d6e86d643351df8c4fdbd34eaa8fc703.png

4.5.5 perf_event_attach_bpf_prog

这里除了将bpf程序放入(perf_event event)->prog中,

还将bpf程序放入event->tp_event->prog_array(这个是blk中断会执行的指令集)

29a07676228bd0c4804f0ff0d307f5a1.png

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函数

4aa3d73cc2c7b80029a6f2b12cecc83b.png

5.2 call_break_hook

找到注册到kernel_break_hook链表的kprobe处理函数kprobe_breakpoint_handler

55144d00d70a57afcb9f83a23001f2cd.png

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

2d724e25f10e128c9f96e0d3391d1315.png

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)

3e17e36a5ba5139397d280e1f9c93a75.png

9f3bdf01c67147383d39956ff13e4eba.png

5.4 kprobe_dispatcher

在章节4.4.4中的enable_trace_kprobe设置了TP_FLAG_PROFILE,于是运行的是kprobe_perf_func

c045b300aa65d70ee6d6e224cd81fff0.png

5.5 kprobe_perf_func

1) 检查bpf程序的指令集合call->prog_array是否有效

2) trace_call_bpf运行bpf程序

8a1ddef9d9746b6d21e4cbe1d12a8b53.png

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程序了

f2f5438254dded0adc8c50921e16b84f.png

5.7 bpf_prog_run

最后运行的是prog->bpf_func(ctx, insnsi),

bpf_func是load的时候针对注入函数jit编译后的指令集(加入堆栈保存,执行bpf程序,堆栈还原的操作),

insnsi是bpf程序本身的指令

c9665fb5babb57490092a96722fa97de.png

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解密

874b20fb6bb8d93925e2bc614e6f2af8.gif

长按关注内核工匠微信

Linux内核黑科技| 技术文章| 精选教程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

OPPO内核工匠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值