前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于ebpf的性能工具-bpftrace实战(内存泄漏)

基于ebpf的性能工具-bpftrace实战(内存泄漏)

作者头像
Rice加饭
发布2023-09-21 19:52:24
5090
发布2023-09-21 19:52:24
举报
文章被收录于专栏:Rice嵌入式Rice嵌入式

在实际的软件开发过程中,内存问题常常是耗费大量时间进行分析的挑战之一。为了更有效地定位和解决与内存相关的难题,一系列辅助工具应运而生,其中备受赞誉的Valgrind工具便是其中之一。事实上,笔者本人曾利用Valgrind工具成功地发现并解决了一个隐藏在软件中的bug,这充分体现了工具在开发过程中的重要性。

然而,同样强大的bpftrace工具同样具备简洁而直观的特点,能够协助我们高效地追踪内存泄漏问题。在这方面,bpftrace提供了一种更加精细的、实时的分析方式,帮助开发人员准确地定位代码中可能存在的内存泄漏情况。

构建样例

  • 我们编写一个程序--mem_check.c,代码中包含正确的申请内存和释放内存的逻辑,同时包含存在内存泄露的代码代码。。
代码语言:javascript
复制
#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文件,生成可执行文件:
代码语言:javascript
复制
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还没有运行,显然还没有探测我们的可执行程序。
代码语言:javascript
复制
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
代码语言:javascript
复制
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的内存空间大小。
  1. malloc的原型:
代码语言:javascript
复制
void *malloc(size_t size);
  1. bpftrace的uprobe和kprobe可以通过内置变量arg0、arg1 ··· ··· 访问函数参数,对bpf_test.bt修改就可以打印malloc申请内存的大小:
代码语言:javascript
复制
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");
}
  1. 如下图可以看到mem_check中申请内存的情况,最后一个malloc size 1024是mem_check自动创建输出缓冲区申请的内存,不用理会。
  • 探测mem_check中malloc的返回值
  1. malloc的返回值是地址,需借助uretprobe进行探测,函数返回值可通过内置变量retval访问。uretprobe的filter与malloc参数探测时类似,脚本修改为:
代码语言:javascript
复制
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");
}
  1. 运行结果:
  • 探测mem_check中free
  1. 我们已经探测到mem_check的malloc的内存大小,内存的地址,我们通过探测free,然后匹配malloc和free的情况就可以查找内存的泄漏点。脚本修改为:
代码语言:javascript
复制
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");
}
  1. 运行结果:

探测内存泄露

  • 上面我们已经探测到了mem_check中的malloc,free情况。我们可以通过malloc和free的地址集合差,就可以得到内存泄露的地址位置。
  • bpftrace底层使用的是eBPF的map作为存储结构,可以简单的看作K-V存储,我们可以利用map来统计地址集合差,步骤如下:
    1. 定义一个map变量@mem:保存malloc返回的内存地址。
    2. 当探测到free调用时,将@mem对应地址删除。
    3. 最后@mem剩下的就是内存泄露的地址。
  • 内存泄露检测脚本如下:
代码语言:javascript
复制
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脚本,我们就可以监视应用程序的内存分配和释放事件,捕获内存泄漏的迹象。这种直接的实时监控方式,使得开发者能够在问题出现时即刻获得反馈,从而更加迅速地解决潜在的内存问题,提升软件的稳定性和性能。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-09-04 12:00,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Rice 嵌入式开发技术分享 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 构建样例
  • 探测mem_ckeck可执行文件
  • 使用bpftrace脚本进一步探测
  • 探测内存泄露
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档