Ker · 7月21日

BPF内部原理

1. 简介

Brendan最近在USENIX LISA2021大会上做了一篇关于BPF内部原理的演讲,这篇演讲把BPF的内部逻辑剖析地非常清楚,本文大部分素材来自Brendan的这篇PPT, 额外加了一些kprobe原理的解释,分享给大家。

文中用到的术语解释

  • AST: Abstract Syntax Tree
  • LLVM: A compiler
  • IR: Intermediate Representation
  • JIT: Just-in-time compilation
  • kprobes: Kernel dynamic instrumentation
  • uprobes: User-level dynamic instrumentation
  • tracepoints: Kernel static instrumentation

下图是BPF内部逻辑图,可以看出如何从bpftrace program经过AST,LLVM IR,BPF bytecode这几个阶段,最终得到machine code由CPU来执行。接下来我们详细介绍一下每个阶段。

bpftrace_mid-level_internals.png

step.png

2. bpftrace program → AST

Screen Shot 2021-07-20 at 1.19.55 PM.png

我们以下面这个bpftrace One-Liner作为例子,介绍一下如何把bpftrace program转化成抽象语法树AST。

# bpftrace -e 'kprobe:do_nanosleep {
printf("PID %d sleeping...\n", pid);
}'

Screen Shot 2021-07-20 at 1.21.33 PM.png

Screen Shot 2021-07-20 at 1.23.34 PM.png
如上图所示,中间有个Parser模块包含了lex(词法分析)和yacc(语法分析)模块,学过编译原理的都知道,编译器把文本格式的代码翻译为中间代码一般会经过词法分析,语法分析,语义分析这几个阶段,这块内容我们在这不详细讨论,感兴趣可以阅读本文后面的参考文献。

Screen Shot 2021-07-20 at 1.24.10 PM.png
经过lex和yacc处理后,我们就得到了抽象语法树AST,可以通过bpftrace -d参数打印出AST。

3. AST → LLVM IR

Screen Shot 2021-07-20 at 1.25.45 PM.png
有了AST后,我们接下来看如何得到LLVM IR, 这其中需要经过几个模块的处理,上图中的Tracepoint & Clang struct parser模块在我们这个One-Liner例子中用不到,因为没有用到相关的结构体。

Screen Shot 2021-07-20 at 1.27.03 PM.png
上图中的语义分析器,在本例中主要作用就是一些错误处理,例如错把pid写成了pidd,语义分析器会发现错误并打出错误日志。

Screen Shot 2021-07-20 at 1.28.23 PM.png

经过Code Generation & IR Builder模块的处理,AST会被转化成LLVM IR,具体处理如下图所示。

Screen Shot 2021-07-20 at 1.29.00 PM.png

Screen Shot 2021-07-20 at 1.29.50 PM.png

Screen Shot 2021-07-20 at 1.31.13 PM.png

Screen Shot 2021-07-20 at 1.32.37 PM.png
LLVM IR也可以通过bpftrace -d打印出来。

4. LLVM IR → BPF bytecode

Screen Shot 2021-07-20 at 1.33.41 PM.png
如何把LLVM IR转成BPF bytecode, 这里就用到了LLVM编译器。下图是BPF bytecode指令的格式,

Screen Shot 2021-07-20 at 1.34.19 PM.png

BFP_CALL&JMP组合是0x85, get_current_pid_tgid对应的No是14,得到相应的BPF bytecode为85 00 00 e0 00 00 00,如下图所示。

Screen Shot 2021-07-20 at 1.35.16 PM.png

Screen Shot 2021-07-20 at 1.35.46 PM.png

Screen Shot 2021-07-20 at 1.37.57 PM.png

5. BPF bytecode → machine code

得到了BPF bytecode后,接下来我们看如何得到machine code。

Screen Shot 2021-07-20 at 1.39.04 PM.png

Screen Shot 2021-07-20 at 1.39.44 PM.png

首先,第一个经过的模块为验证器Verifier, 验证器会拒绝那些不安全的操作,包括对无界循环的检查:BPF程序必须在有限的时间内完成。如下图。

Screen Shot 2021-07-20 at 1.40.39 PM.png

经过了验证器验证,JIT编译器负责生成处理器可直接执行的机器指令。

Screen Shot 2021-07-20 at 1.49.16 PM.png

因为要生成机器指令,所以JIT在不同的体系结构下处理方式是不一样的,下图罗列了x86/arm/sparc下的处理函数。

Screen Shot 2021-07-20 at 1.49.33 PM.png

x86是在arch/x86/net/bpf_jit_comp.c的do_jit()函数中处理。
Screen Shot 2021-07-20 at 1.49.55 PM.png

arm64下是在arch/arm64/net/bpf_jit_comp.c的build_insn()函数中处理。
Screen Shot 2021-07-21 at 3.27.53 PM.png

我们可以用bpftool打印出最终的机器指令。
x86:
Screen Shot 2021-07-20 at 1.50.26 PM.png
arm64:
Screen Shot 2021-07-20 at 1.50.40 PM.png

6. kprobe

上面内容讲述了从bpftrace program到机器代码的转化过程,这时我们该考虑如何和kprobe结合了。
Screen Shot 2021-07-20 at 1.51.31 PM.png
Screen Shot 2021-07-20 at 3.25.20 PM.png
kprobe可以对任何内核函数进行插桩,可以实时在生产环境中启用,不需要重启系统,也不需要以特殊方式重启内核。
现在有以下三种接口可以访问kprobes.

  • kprobe API: 如register_kprobe()等
  • 基于Frace的,通过/sys/kernel/debug/tracing/kprobe_events:通过向这个文件写入字符串,可以配置开启和停止kprobes
  • perf_event_open(): 与perf工具所使用的一样,现在BPF跟踪工具也开始使用这些函数

通过strace可以看到这里是使用perf_event_open()接口来和kprobe打交道。

Screen Shot 2021-07-20 at 1.51.55 PM.png

kprobe工作原理

kprobe的工作过程如下(分几种情况):
一. 对于一个kprobe插桩来说:

1) 将要插桩的目标地址中的字节内容复制并保存;
2) 以单步中断指令覆盖目标地址:在ARM上是BRK指令,X86是int3;
3) 当指令流执行到断点时,断点处理函数会检查这个断点是否是由kprobes注册的,如果是,就会执行kprobes注册函数;
4) 原始的指令会接着执行,指令流继续
5) 当不在需要kprobes时,原始的字节内容会被复制回目标地址上,这样这些指令就回到了他们的初始状态。

二. 如果这个kprobe是一个Ftrace已经做过插桩的地址(一般位于函数入口处),那么可以基于Ftrace进行kprobe优化,过程如下:

1) 将一个Ftrace kprobe处理函数注册为对应函数的Ftrace处理器
2) 当在函数起始处执行内建入口函数时(x86架构上为__fentry__),该函数会调用Ftrace, Ftrace接下来会调用kprobe处理函数
3) 当kprobe不在被使用时,从Ftrace中移除Ftrace-kprobe处理函数

三. 如果是一个kretprobe:

1) 对函数入口进行kprobe插桩
2) 当函数入口被kprobe命中时,将返回地址保存并替换为一个「蹦床」(trampoline)函数:kretprobe_trampoline()
3) 当函数最终返回时(ret指令),CPU将控制交给蹦床函数处理
4) 在kretprobe处理完成之后再返回到之前保存的地址
5) 当不在需要kretprobe时,函数入口的kprobe就被移除了

Screen Shot 2021-07-20 at 1.52.38 PM.png

Screen Shot 2021-07-20 at 1.53.21 PM.png

最后我们看一下如何将结果返回给userspace, 这里用到了perf buffer空间。
Screen Shot 2021-07-20 at 3.25.48 PM.png

这块空间是per cpu的。
Screen Shot 2021-07-20 at 1.54.10 PM.png

Screen Shot 2021-07-20 at 1.54.29 PM.png
最终在userspace打印出来。这块不细说了,具体内容可以看后面参考文献。
Screen Shot 2021-07-20 at 1.54.59 PM.png

参考文献

https://www.brendangregg.com/blog/2021-06-15/bpf-internals.html
https://events.static.linuxfound.org/sites/events/files/slides/bpf_collabsummit_2015feb20.pdf
Linux include/uapi/linux/bpf_common.h
Linux include/uapi/linux/bpf.h
Linux include/uapi/linux/filter.h
https://docs.cilium.io/en/v1.9/bpf/#bpf-guide
BPF Performance Tools, Addison-Wesley 2020
https://ebpf.io/what-is-ebpf
http://www.brendangregg.com/ebpf.html
https://github.com/iovisor/bcc
https://github.com/iovisor/bpftrace
http://dinosaur.compilertools.net/

6 阅读 330
推荐阅读
0 条评论
关注数
1694
内容数
10
分享arm服务器软件应用经验、测试方法、优化思路、工具使用等。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
Arm中国学堂公众号
关注Arm中国学堂
实时获取免费 Arm 教学资源信息
Arm中国招聘公众号
关注Arm中国招聘
实时获取 Arm 中国职位信息