经典 libbpf 范例: uprobe 剖析 – eBPF 基础知识 Part4
《eBPF 基础知识》系列简介:
《eBPF 基础知识》系列指标是整顿一下 BPF 相干的基础知识。次要聚焦程序与内核互动接口局部。文章应用了 libbpf,但如果你不间接应用 libbpf,看本系列还是有肯定意义的,因为它聚焦于程序与内核互动接口局部,而非 libbpf 封装自身。而所有 bpf 开发框架,都要以类似的形式跟内核互动。甚至框架自身就是基于 libbpf。哪怕是 golang/rust/python/BCC/bpftrace。
- 《ELF 格局简述 – eBPF 基础知识 Part1》
- 《BPF 零碎接口 与 libbpf 示例剖析 – eBPF 基础知识 Part2》
- 《经典 libbpf 范例: bootstrap 剖析 – eBPF 基础知识 Part3》
国内习惯:尽量多图少文字。以下假如读者曾经对 BPF 有肯定的理解,或者浏览过之前的《eBPF 基础知识》系列文章。
libbpf 提供了一个应用 libbpf 的示例:https://github.com/libbpf/libbpf-bootstrap。其中的 uprobe 程序示范了一个最简略的 BPF uprobe 程序加载、绑定到 user space ELF 函数、与内核互动的过程。上面将图解剖析这个程序与内核的互动过程。
动机:为何我想学习 BPF uprobe
开始剖析前,我想说几句废话:为何我想学习 BPF uprobe?
-
大部分利用的行为,都以函数为单元来划分,跟踪利用的函数是跟踪利用很好的切入点
例如我之前写的:
-
逆向工程与云原生现场剖析 Part1 —— eBPF 跟踪 Istio/Envoy 之学步
-
逆向工程与云原生现场剖析 Part2 —— eBPF 跟踪 Istio/Envoy 之启动、监听与线程负载平衡
-
逆向工程与云原生现场剖析 Part3 —— eBPF 跟踪 Istio/Envoy 事件驱动模型、连贯建设、TLS 握手与 filter_chain 抉择
-
逆向工程与云原生现场剖析 Part4 —— eBPF 跟踪 Istio/Envoy 之 upstream/downstream 事件驱动合作下的 HTTP 反向代理流程
-
-
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
程序主流程
<mark> 我始终致力防止在文章间接上代码。起因是,我本人的体验是,在文章中读代码太难了…… </mark> 不过有时还是要贴。指标不是让读者齐全一次看懂代码,而是对次要逻辑和命名符号有个理性的理解。我尽量精简一下吧。不要被这纸老虎吓跑。前面有图解的。
内核态 BPF 字节码程序
先看 BPF 内核字节码程序局部:
uprobe.bpf.c
SEC("uprobe")
int BPF_KPROBE(uprobe_add, int a, int b)
{bpf_printk("uprobed_add ENTRY: a = %d, b = %d", a, b);
return 0;
}
SEC("uretprobe")
int BPF_KRETPROBE(uretprobe_add, int ret)
{bpf_printk("uprobed_add EXIT: return = %d", ret);
return 0;
}
SEC("uprobe//proc/self/exe:uprobed_sub")
int BPF_KPROBE(uprobe_sub, int a, int b)
{bpf_printk("uprobed_sub ENTRY: a = %d, b = %d", a, b);
return 0;
}
SEC("uretprobe//proc/self/exe:uprobed_sub")
int BPF_KRETPROBE(uretprobe_sub, int ret)
{bpf_printk("uprobed_sub EXIT: return = %d", ret);
return 0;
}
可见,这里包含 uprobe_add
与 uprobe_sub
两个 user space 函数的入口与进口探针。这个示例是用户态过程本人探测本人(这个样的示例其实不太好,不事实)。过程本人探测本人,所以能够用 /proc/self/exe
。熟识 Linux proc
目录的同学都晓得,这个文件是指向拜访这个文件的过程自身的 symbol link:
$ ls -l /proc/self/exe
lrwxrwxrwx 1 labile labile 0 Apr 2 22:40 /proc/self/exe -> /usr/bin/ls
同是 uprobe 实现函数 的 section 定义,下面代码有两种表达方法:
-
SEC(“uprobe”)
这种没指定指标函数。由用户态 bpf 程序加载和动静绑定到探测指标函数。下层用户态 bpf 程序须要自行计算函数在 ELF 文件中的 offset。
-
SEC(“uprobe//proc/self/exe:uprobed_sub”)
这种指定了指标 elf 门路和函数。可由用户态 libbpf 主动加载和绑定到探测指标函数。下层用户态 bpf 程序不须要计算函数在 ELF 文件中的 offset。由 libbpf 主动计算。
在 make 的过程中,实际上是执行了:
clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -I.output -I../../libbpf/include/uapi -I../../vmlinux/x86/
-idirafter /usr/lib/llvm-14/lib/clang/14.0.0/include -idirafter /usr/local/include -idirafter
/usr/include/x86_64-linux-gnu -idirafter /usr/include -c uprobe.bpf.c -o .output/uprobe.bpf.o
llvm-strip -g .output/uprobe.bpf.o
最初一行就是重点。输出是 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 程序
uprobe.c
// 计算本过程的一个函数在 ELF 文件的 offset
ssize_t get_uprobe_offset(const void *addr)
{
size_t start, end, base;
char buf[256];
bool found = false;
FILE *f;
f = fopen("/proc/self/maps", "r");
...
while (fscanf(f, "%zx-%zx %s %zx %*[^\n]\n", &start, &end, buf, &base) == 4) {if (buf[2] == 'x' && (uintptr_t)addr >= start && (uintptr_t)addr < end) {
found = true;
break;
}
}
fclose(f);
...
return (uintptr_t)addr - start + base;
}
// 探测指标函数
int uprobed_add(int a, int b)
{return a + b;}
// 探测指标函数
int uprobed_sub(int a, int b)
{return a - b;}
int main(int argc, char **argv)
{
struct uprobe_bpf *skel;
long uprobe_offset;
int err, i;
...
/* 1. 加载内核态 BPF 程序 */
skel = uprobe_bpf__open_and_load();
if (!skel) {fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
/* uprobe/uretprobe expects relative offset of the function to attach
* to. This offset is relateve to the process's base load address. So
* easy way to do this is to take an absolute address of the desired
* function and substract base load address from it. If we were to
* parse ELF to calculate this function, we'd need to add .text
* section offset and function's offset within .text ELF section.
* 计算 uprobed_add 函数在本过程的 ELF 中的 offset
*/
uprobe_offset = get_uprobe_offset(&uprobed_add);
/* 2. 绑定 BPF 程序 BPF_KPROBE(uprobe_add, int a, int b) 到函数 uprobed_add */
skel->links.uprobe_add = bpf_program__attach_uprobe(skel->progs.uprobe_add,
false /* not uretprobe */,
0 /* self pid */,
"/proc/self/exe",
uprobe_offset);
...
/* Let libbpf perform auto-attach for uprobe_sub/uretprobe_sub
* NOTICE: we provide path and symbol info in SEC for BPF programs
* 3. 让 libbpf 主动依据 uprobe.bpf.c 的 section 定义(如:SEC("uprobe//proc/self/exe:uprobed_sub")),去加载和绑定 bpf 程序到过程用户态函数
*/
err = uprobe_bpf__attach(skel);
...
printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe`"
"to see output of the BPF programs.\n");
for (i = 0; ; i++) {
/* trigger our BPF programs */
fprintf(stderr, ".");
uprobed_add(i, i + 1);
uprobed_sub(i * i, i);
sleep(1);
}
...
}
看看输入的 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 年的电影。