经典 libbpf 范例: uprobe 分析 - eBPF基础知识 Part4
《eBPF基础知识》 系列简介:
《eBPF基础知识》系列目标是整理一下 BPF 相关的基础知识。主要聚焦程序与内核互动接口部分。文章使用了 libbpf,但如果你不直接使用 libbpf,看本系列还是有一定意义的,因为它聚焦于程序与内核互动接口部分,而非 libbpf 封装本身。而所有 bpf 开发框架,都要以相似的方式跟内核互动。甚至框架本身就是基于 libbpf。哪怕是 golang/rust/python/BCC/bpftrace。
国际习惯:尽量多图少文字。以下假设读者已经对 BPF 有一定的了解,或者阅读过之前的 《eBPF基础知识》系列文章。
libbpf 提供了一个使用 libbpf 的示例:https://github.com/libbpf/libbpf-bootstrap。其中的 uprobe 程序示范了一个最简单的 BPF uprobe 程序加载、绑定到 user space ELF 函数、与内核互动的过程。下面将图解分析这个程序与内核的互动过程。
动机:为何我想学习 BPF uprobe
开始分析前,我想说几句废话:为何我想学习 BPF uprobe?
-
大部分应用的行为,都以函数为单元来划分,跟踪应用的函数是跟踪应用很好的切入点
例如我之前写的:
-
stack 性能分析
火焰图数据之源
-
Troubleshooting
生产上遇到问题,不太可能用 gdb 对分析,但有的情况下可以用 BPF uprobe 去拦截函数调用和获取入参出参。
uprobe 示例程序功能
uprobe 程序是一个用户空间(user-space)函数进入(entry)和退出(exit) 探针示例,在 libbpf 术语中称为 uprobe
和 uretprobe
。 它将 uprobe
和 uretprobe
BPF 程序绑定到它自己的函数(uprobed_add()
和 uprobed_sub()
),并使用 bpf_printk()
宏记录输入参数和返回值。 用户空间函数每秒触发一次:
$ sudo ./uprobe
libbpf: loading object 'uprobe_bpf' from buffer
...
Successfully started!
...........
你可以这样监视程序的输出:
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
uprobe-1809291 [007] .... 4017233.106596: 0: uprobed_add ENTRY: a = 0, b = 1
uprobe-1809291 [007] .... 4017233.106605: 0: uprobed_add EXIT: return = 1
uprobe-1809291 [007] .... 4017233.106606: 0: uprobed_sub ENTRY: a = 0, b = 0
uprobe-1809291 [007] .... 4017233.106607: 0: uprobed_sub EXIT: return = 0
uprobe-1809291 [007] .... 4017234.106694: 0: uprobed_add ENTRY: a = 1, b = 2
uprobe-1809291 [007] .... 4017234.106697: 0: uprobed_add EXIT: return = 3
uprobe-1809291 [007] .... 4017234.106700: 0: uprobed_sub ENTRY: a = 1, b = 1
uprobe-1809291 [007] .... 4017234.106701: 0: uprobed_sub EXIT: return = 0
程序主流程
我一直努力避免在文章直接上代码。原因是,我自己的体验是,在文章中读代码太难了…… 不过有时还是要贴。目标不是让读者完全一次看懂代码,而是对主要逻辑和命名符号有个感性的了解。我尽量精简一下吧。不要被这纸老虎吓跑。后面有图解的。
内核态 BPF 字节码程序
先看 BPF 内核字节码程序部分:
|
|
可见,这里包括 uprobe_add
与 uprobe_sub
两个 user space 函数的入口与出口探针。这个示例是用户态进程自己探测自己(这个样的示例其实不太好,不现实)。进程自己探测自己,所以可以用 /proc/self/exe
。熟识 Linux proc
目录的同学都知道,这个文件是指向访问这个文件的进程本身的 symbol link:
|
|
同是 uprobe 实现函数 的 section 定义,上面代码有两种表达方法:
-
SEC(“uprobe”)
这种没指定目标函数。由用户态 bpf 程序加载和动态绑定到探测目标函数。上层用户态 bpf 程序需要自行计算函数在 ELF 文件中的 offset。
-
SEC(“uprobe//proc/self/exe:uprobed_sub”)
这种指定了目标 elf 路径和函数。可由用户态 libbpf 自动加载和绑定到探测目标函数。上层用户态 bpf 程序不需要计算函数在 ELF 文件中的 offset。由 libbpf 自动计算。
在 make 的过程中,实际上是执行了:
|
|
最后一行就是重点。输入是 uprobe.bpf.c
。输出是 uprobe.bpf.o
。这是一个 ELF 格式的文件。这个文件将会嵌入到应用中。uprobe.bpf.o
section 如下:
$ readelf -aW examples/c/.output/uprobe.bpf.o
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .strtab STRTAB 0000000000000000 000c41 00014f 00 0 0 1
[ 2] .text PROGBITS 0000000000000000 000040 000000 00 AX 0 0 4
[ 3] uprobe PROGBITS 0000000000000000 000040 000040 00 AX 0 0 8
[ 4] .reluprobe REL 0000000000000000 000aa8 000010 10 I 18 3 8
[ 5] uretprobe PROGBITS 0000000000000000 000080 000038 00 AX 0 0 8
[ 6] .reluretprobe REL 0000000000000000 000ab8 000010 10 I 18 5 8
[ 7] PROGBITS 0000000000000000 0000b8 000040 00 AX 0 0 8
[ 8] .reluprobe//proc/self/exe:uprobed_sub REL 0000000000000000 000ac8 000010 10 I 18 7 8
[ 9] uretprobe//proc/self/exe:uprobed_sub PROGBITS 0000000000000000 0000f8 000038 00 AX 0 0 8
[10] .reluretprobe//proc/self/exe:uprobed_sub REL 0000000000000000 000ad8 000010 10 I 18 9 8
[11] license PROGBITS 0000000000000000 000130 00000d 00 WA 0 0 1
[12] .rodata PROGBITS 0000000000000000 00013d 000080 00 A 0 0 1
[13] .BTF PROGBITS 0000000000000000 0001c0 000635 00 0 0 4
[14] .rel.BTF REL 0000000000000000 000ae8 000050 10 I 18 13 8
[15] .BTF.ext PROGBITS 0000000000000000 0007f8 000148 00 0 0 4
[16] .rel.BTF.ext REL 0000000000000000 000b38 000100 10 I 18 15 8
[17] .llvm_addrsig LOOS+0xfff4c03 0000000000000000 000c38 000009 00 E 0 0 1
[18] .symtab SYMTAB 0000000000000000 000940 000168 18 1 10 8
如果你不太了解 ELF 格式,建议先看看,因为理解这个格式很重要。可以参考我的《ELF 格式简述 - eBPF 基础知识》
上面可见,uprobe//proc/self/exe:uprobed_sub
与 uretprobe//proc/self/exe:uprobed_sub
section 的名字指明了相应 BPF program 要绑定的目标用户态 ELF 与函数名。
用户态 bpf 程序
|
|
看看输出的 ELF 内容:
$ readelf -aW uprobe
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x0002d8 0x0002d8 R 0x8
INTERP 0x000318 0x0000000000000318 0x0000000000000318 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x004828 0x004828 R 0x1000
LOAD 0x005000 0x0000000000005000 0x0000000000005000 0x02ba29 0x02ba29 R E 0x1000
# 关注上面的 Offset: 0x005000
# 只关注 .symtab 部分 的 uprobed_sub
Symbol table '.symtab' contains 720 entries:
Num: Value Size Type Bind Vis Ndx Name
470: 000000000000646b 24 FUNC GLOBAL DEFAULT 16 uprobed_add
523: 0000000000006483 22 FUNC GLOBAL DEFAULT 16 uprobed_sub
程序架构
uprobe 与内核互动概述
如上图排版有问题,请点这里用 Draw.io 打开。部分带互动链接和 hover tips
图中是我跟踪的结果。用 Draw.io 打开后,每一步均有 link,点击可看到代码。鼠标放到连接线上,会 hover 出 stack(调用堆栈)。
图中的说明已经比较详细。其中包括重要的数据结构和步骤。
上图 file descriptor
之间的连线,反映了它们之间的关联。这里简单列一下上图的流程:
-
1 ~ 5 建立 libbpf 用到的数据结构。
-
6 ~ 12 加载 BPF program 与 BPF Map
-
13 ~ 16 开启动态 uprobe
-
- 计算动态
int uprobed_add(int a, int b)
的 offset - 创建
函数 perf event probe
- 函数 perf event probe 绑定到 BPF program
- 启动
函数 perf event probe
- 计算动态
-
-
- 开启静态 uprobe
- 利用 libbpf 根据 uprobe.bpf.o ELF 文件的 section 信息,自动计算
int uprobed_sub(int a, int b)
offset。之后完成类似上面开启动态 uprobe
的过程。最终 函数 perf event probe 绑定到 BPF programBPF_KPROBE(uprobe_sub, int a, int b)
-
- 用户态应用定时调用函数
uprobed_add(int a, int b)
和uprobed_sub(int a, int b)
触发了 BPF progam
- 用户态应用定时调用函数
我 fork 了项目到这里:
https://github.com/labilezhu/libbpf-bootstrap/tree/20230226
后记
**这个后记和本文没什么相关了,不喜可跳过。**最近看了一部 1994 年的电影。