在之前的篇章中,我们已经详细阐述了bpftrace的操作原理,以及其脚本语法的特点。在本文中,我们将通过实际案例展示bpftrace这一强大工具的实际应用,以便更加深入地理解其在解决问题中的价值。
基于ubuntu22.04-深入浅出 eBPF
基于ebpf的性能工具-bpftrace
基于ebpf的性能工具-bpftrace脚本语法
在实际的软件开发过程中,内存问题常常是耗费大量时间进行分析的挑战之一。为了更有效地定位和解决与内存相关的难题,一系列辅助工具应运而生,其中备受赞誉的Valgrind工具便是其中之一。事实上,笔者本人曾利用Valgrind工具成功地发现并解决了一个隐藏在软件中的bug,这充分体现了工具在开发过程中的重要性。
然而,同样强大的bpftrace工具同样具备简洁而直观的特点,能够协助我们高效地追踪内存泄漏问题。在这方面,bpftrace提供了一种更加精细的、实时的分析方式,帮助开发人员准确地定位代码中可能存在的内存泄漏情况。
构建样例
我们编写一个程序--mem_check.c,代码中包含正确的申请内存和释放内存的逻辑,同时包含存在内存泄露的代码代码。。
#include <stdio.h>
#include <stdlib.h>
int main(){
char *p1 = NULL;
char *p2 = NULL;
for(int i = 0; i < 5; i++)
{
p1 = malloc(16);
}
for(int i = 0; i < 5; i++)
{
p2 = malloc(32);
free(p2);
}
getchar();
return 0;
}
上面的代码非常简单,我们申请了5次16个字节的内存,没有释放,存在内存泄露。申请5次32个字节的内存,有释放,没存在内存泄露。那么我们如何通过bpftrace定位呢?
我们通过bpftrace对mem_check.c进行动态的统计内存的申请和释放,定位内存泄露的问题。我们需要对关键的两个接口进行probe--malloc和free,这两个接口的实现在libc中。
编译mem_check.c文件,生成可执行文件:
gcc mem_check.c -o mem_check
探测mem_ckeck可执行文件
bpftrace可以对内核态进行探测也可以对用户态进行探测,其中探针如下:
内核态探针:kprobe/kretprobe
用户态探针:uprobe/uretprobe
mem_check.c是一个应用程序,显然我们需要使用用户态探针:uprobe/uretprobe
通过uprobe探测mem_check.c中的malloc函数,我们单行指令验证,参数格式是 uprobe:可执行文件:函数名:
理论是没有没有问题,但实际发生错误:No probes to attach。原因:可执行文件mem_check中找不到符号:malloc,我们可以通过nm命令确定一下:
我们发现malloc是一个链接自GLIBC_2.2.5的符号,并不是mem_ckeck自身的符号,所以我们探测的符号修改libc库中malloc符号,系统中可能存在多个c库,我们需要找到mem_ckeck程序使用的C库,通过ldd命令查看:
mem_check可执行文件使用的C库为:/lib/x86_64-linux-gnu/libc.so.6,我们将可以执行文件替换为/lib/x86_64-linux-gnu/libc.so.6。再次执行,会出现大量内容,显然是其他进程调用了malloc引起的,而我们的mem_ckeck还没有运行,显然还没有探测我们的可执行程序。
bpftrace -e 'uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc {printf("malloc call\n")}'
我们需要进行过滤,增加filter只保留我们关心的应用程序的调用探测。bpftrace提供了系统变量comm表示可执行文件名 (进程名),只需要在上述指令中增加 filter,只处理comm=="mem_check"的malloc调用事件。左边终端执行探测,右边终端执行可执行文件。每调用一次malloc函数,就能探测到一次:
使用bpftrace脚本进一步探测
将上面的单行命令变为bpftrace脚本--bpf_test.bt
BEGIN {
printf("start probe\n");
}
uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
printf("malloc call\n");
}
END {
printf("end probe\n");
}
探测mem_check中malloc的内存空间大小。
malloc的原型:
void *malloc(size_t size);
bpftrace的uprobe和kprobe可以通过内置变量arg0、arg1 ··· ··· 访问函数参数,对bpf_test.bt修改就可以打印malloc申请内存的大小:
BEGIN {
printf("start probe\n");
}
uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
printf("malloc size: %d\n", arg0);
}
END {
printf("end probe\n");
}
如下图可以看到mem_check中申请内存的情况,最后一个malloc size 1024是mem_check自动创建输出缓冲区申请的内存,不用理会。
探测mem_check中malloc的返回值
malloc的返回值是地址,需借助uretprobe进行探测,函数返回值可通过内置变量retval访问。uretprobe的filter与malloc参数探测时类似,脚本修改为:
BEGIN {
printf("start probe\n");
}
uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
printf("malloc size: %d\n", arg0);
}
uretprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
printf("addr = %p\n", retval);
}
END {
printf("end probe\n");
}
运行结果:
探测mem_check中free
我们已经探测到mem_check的malloc的内存大小,内存的地址,我们通过探测free,然后匹配malloc和free的情况就可以查找内存的泄漏点。脚本修改为:
BEGIN {
printf("start probe\n");
}
uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
printf("malloc size: %d\n", arg0);
}
uretprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
printf("addr = %p\n", retval);
}
uprobe:/lib/x86_64-linux-gnu/libc.so.6:free /comm == "mem_check"/{
printf("free addr = %p\n", arg0);
}
END {
printf("end probe\n");
}
运行结果:
探测内存泄露
上面我们已经探测到了mem_check中的malloc,free情况。我们可以通过malloc和free的地址集合差,就可以得到内存泄露的地址位置。
bpftrace底层使用的是eBPF的map作为存储结构,可以简单的看作K-V存储,我们可以利用map来统计地址集合差,步骤如下:
定义一个map变量@mem:保存malloc返回的内存地址。
当探测到free调用时,将@mem对应地址删除。
最后@mem剩下的就是内存泄露的地址。
内存泄露检测脚本如下:
BEGIN {
printf("start probe\n");
}
uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
printf("malloc size: %d\n", arg0);
@size = arg0;
}
uretprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /comm == "mem_check"/{
printf("addr = %p\n", retval);
@mem[retval] = @size;
}
uprobe:/lib/x86_64-linux-gnu/libc.so.6:free /comm == "mem_check"/{
printf("free addr = %p\n", arg0);
delete(@mem[arg0]);
}
END {
printf("end probe\n");
}
运行结果:
如上图,红色框中就是没有释放的内存和内存大小。
总结
通过编写一些简单的bpftrace脚本,我们就可以监视应用程序的内存分配和释放事件,捕获内存泄漏的迹象。这种直接的实时监控方式,使得开发者能够在问题出现时即刻获得反馈,从而更加迅速地解决潜在的内存问题,提升软件的稳定性和性能。