当前位置: 首页 > news >正文

使用bcc/memleak定位C/C++应用的内存泄露问题

C/C++应用的内存泄露

在笔者之前的一篇文章中,提到了通过每隔一段时间抓取应用的/proc/XXX/maps文件对比得到进程的内存增长区域,然后调用gdb调试工具的dump binary memory命令将增长的内存(即对应着泄露的内存数据)导出到文件,之后根据文件中的内存数据,结合代码推测出可能出现内存泄露的代码。在笔者的那篇文章后面也提到,这种方法对泄露的内存数据很敏感,数据的“质量”直接决定了能否定位到存在缺陷的C/C++代码;这是一种很不可靠的定位分析方法。

本文提供一种比较有效的C/C++应用内存泄露的定位方法,主要是使用到开源调试工具bcc/memleak,笔者也在之前一篇博客文章中提到该bcc/bpftrace工具对嵌入式设备的编译构建过程,这里不再重复。

准备泄露内存的应用

为了展示使用bcc/memleak的内存泄露定位的过程,笔者修改了dropbear源码,引入了一处内存泄露缺陷:

diff --git a/src/svr-main.c b/src/svr-main.c
index 6373e59..79899ea 100644
--- a/src/svr-main.c
+++ b/src/svr-main.c
@@ -247,7 +247,8 @@ static void main_noinetd(int argc, char ** argv, const char* multipath) {if (childpipes[i] >= 0 && FD_ISSET(childpipes[i], &fds)) {m_close(childpipes[i]);childpipes[i] = -1;
-                               m_free(preauth_addrs[i]);
+                               // m_free(preauth_addrs[i]);
+                               preauth_addrs[i] = NULL;}}@@ -327,7 +328,8 @@ static void main_noinetd(int argc, char ** argv, const char* multipath) {/* child */getaddrstring(&remoteaddr, NULL, &remote_port, 0);dropbear_log(LOG_INFO, "Child connection from %s:%s", remote_host, remote_port);
-                               m_free(remote_host);
+                               // m_free(remote_host);
+                               remote_host = NULL;m_free(remote_port);#if !DEBUG_NOFORK

按上面修改dropbear代码,笔者编译该应用放置于树莓派设备上运行(推荐包含编译参数-g -fno-omit-frame-pointer,并保留调试信息):

./dropbear -R -F -E -p 23

之后笔者编写了下面的脚本initiate-ssh.sh,它会反复调用ssh客户端连接独立运行的dropbear服务器开放监听的23端口:

#!/bin/shCNT=1
while true ; dossh -y -y -p 23 -i dropbear.key root@127.0.0.1 "exit ${CNT}" 2>/dev/nullif [ $? -ne ${CNT} ] ; thenecho "Error, invalid exit code found."exit 1fiecho "ssh client terminated with ${CNT}"CNT=$((CNT + 1))[ ${CNT} -ge 128 ] && CNT=1usleep 230000
done

以上脚本在树莓派设备上运行结果如下:

root@OpenWrt:~# cat /etc/openwrt_version
r23868-02214ab8dc
root@OpenWrt:~# uname -a
Linux OpenWrt 6.1.50 #0 SMP Fri Sep  1 21:45:47 2023 aarch64 GNU/Linux
root@OpenWrt:~# ./initiate-ssh.sh
ssh client terminated with 1
ssh client terminated with 2
ssh client terminated with 3
ssh client terminated with 4
ssh client terminated with 5
ssh client terminated with 6

这样,我们就创造了一个内存不断泄露的dropbear服务进程,它使用的内存会随着客户端的连接而不断增加。

dropbear进程泄露的内存数据

在之前一篇博客文章中,笔者提到了通过对比不同时间段的/proc/XXX/maps文件,并调用gdb来抓取dropbear泄露的内存,这里直接给出抓取到的泄露内存数据:

00000000   31 32 37 2E  30 2E 30 2E  31 00 00 00  00 00 00 00  127.0.0.1.......
00000010   00 00 00 00  00 00 00 00  21 00 00 00  00 00 00 00  ........!.......
00000020   31 32 37 2E  30 2E 30 2E  31 00 00 00  00 00 00 00  127.0.0.1.......
00000030   00 00 00 00  00 00 00 00  21 00 00 00  00 00 00 00  ........!.......
00000040   31 32 37 2E  30 2E 30 2E  31 00 00 00  00 00 00 00  127.0.0.1.......
00000050   00 00 00 00  00 00 00 00  21 00 00 00  00 00 00 00  ........!.......
00000060   31 32 37 2E  30 2E 30 2E  31 00 00 00  00 00 00 00  127.0.0.1.......
00000070   00 00 00 00  00 00 00 00  21 00 00 00  00 00 00 00  ........!.......
00000080   31 32 37 2E  30 2E 30 2E  31 00 00 00  00 00 00 00  127.0.0.1.......
00000090   00 00 00 00  00 00 00 00  21 00 00 00  00 00 00 00  ........!.......
000000A0   31 32 37 2E  30 2E 30 2E  31 00 00 00  00 00 00 00  127.0.0.1.......
000000B0   00 00 00 00  00 00 00 00  21 00 00 00  00 00 00 00  ........!.......
000000C0   31 32 37 2E  30 2E 30 2E  31 00 00 00  00 00 00 00  127.0.0.1.......
000000D0   00 00 00 00  00 00 00 00  21 00 00 00  00 00 00 00  ........!.......
000000E0   31 32 37 2E  30 2E 30 2E  31 00 00 00  00 00 00 00  127.0.0.1.......
000000F0   00 00 00 00  00 00 00 00  21 00 00 00  00 00 00 00  ........!.......
00000100   31 32 37 2E  30 2E 30 2E  31 00 00 00  00 00 00 00  127.0.0.1.......
00000110   00 00 00 00  00 00 00 00  21 00 00 00  00 00 00 00  ........!.......
00000120   31 32 37 2E  30 2E 30 2E  31 00 00 00  00 00 00 00  127.0.0.1.......
---  dropbear-32516-55b930f000-55b932f000.bin       --0x0/0x20000--0%----------

由以上的数据,结合dropbear源码可知,泄露的内存数据很符合我们之前修改的代码引入的内存泄露问题。注意到上面数据中的0x21,这一数据表明每一段泄露的数据长度为0x20字节,这个信息接下来我们会用到(了解glibc的开发者应该清楚,0x21对应结构体malloc_chunk中的成员变量mchunk_size)。下面我们需要使用bcc/memleak来定位该问题,该工具会一步到位地定位到泄露内存在分配时的调用栈回溯。

使用bcc/memleak调试dropbear的内存泄露问题

笔者的树莓派设备使能了BPF/KPROBE/UPROBE相关的内核选项,这里不再重复。因bcc/memleak脚本中包含了C语言代码,它的执行要求安装了内核头文件的内核模块kheaders.ko,这个内核模块可以通使能内核选项CONFIG_IKHEADERS=m生成。以下是笔者在树莓派设备上使用bcc/memleak的调试操作:

insmod kheaders.ko
cd /opt/bpftrace/share/bcc/tools
export LD_LIBRARY_PATH=/opt/bpftrace/lib
export PYTHONPATH=/opt/bpftrace/lib/python3/dist-packages
python3 memleak -p 32516 -z 9 -Z 32 300

注意到,笔者因已知道泄露内存的大小,上面的命令指定了bcc/memleak脚本监测dropbear服务进程调用的内存最小值和最大值(分别为9字节和32字节),这样可以过滤掉很多不必要的应用内存监测,降低调试带来的系统负载。调试结果如下:

root@OpenWrt:/opt/bpftrace/share/bcc/tools# python3 memleak -p 32516 -z 9 -Z 32 300
Attaching to pid 32516, Ctrl+C to quit.
[17:43:01] Top 10 stacks with outstanding allocations:7000 bytes in 700 allocations from stack0x000000558aacd098      m_malloc+0x18 [dropbear]0x000000558aacd0f8      m_strdup+0x24 [dropbear]0x000000558aad58c0      getaddrstring+0x80 [dropbear]0x000000558aadb8cc      main_noinetd.constprop.0+0x4b8 [dropbear]0x000000558aac4c64      [unknown] [dropbear]0x0000007fad90b5ac      __libc_start_call_main+0x5c [libc.so.6]0x0000007fad90b690      __libc_start_main@@GLIBC_2.34+0xa0 [libc.so.6]0x000000558aac4c98      [unknown] [dropbear]
[17:48:01] Top 10 stacks with outstanding allocations:14450 bytes in 1445 allocations from stack0x000000558aacd098      m_malloc+0x18 [dropbear]0x000000558aacd0f8      m_strdup+0x24 [dropbear]0x000000558aad58c0      getaddrstring+0x80 [dropbear]0x000000558aadb8cc      main_noinetd.constprop.0+0x4b8 [dropbear]0x000000558aac4c64      [unknown] [dropbear]0x0000007fad90b5ac      __libc_start_call_main+0x5c [libc.so.6]0x0000007fad90b690      __libc_start_main@@GLIBC_2.34+0xa0 [libc.so.6]0x000000558aac4c98      [unknown] [dropbear]

以上结果直接给出了bcc/memleak确认的疑似内存泄露的调用栈回溯。因笔者在调试过程中停止过initiate-ssh.sh脚本,因此两次结果统计的内存泄露总大小不一致。因我们事先修改过代码,知道这个结果是准确的;但在实际的内存泄露问题定位过程中,我们只能通过这种调试方式得知内存分配的调用栈回溯,具体问题的解决还要根据这一栈信息确定遗忘释放内存的相关代码;依笔者的经验,有时需要多次修改才能确定内存不再泄露,因为同一处申请的内存,在应用代码中可能有多个地方泄露掉。

最后值得强调的是,这一调试方法是笔者用过最有效的内存泄露定位方法,效率远比valgrind/uftrace等工具高(事实上笔者从未用过这两种工具解决过内存泄露问题)。其缺点也比较明显,一方面需要Linux内核的支持,另一方面bcc/bpftrace在嵌入式设备上的移植也确实比较麻烦;不过参考笔者之前的博客文章,为嵌入式设备编译bcc/bpftrace应该不会消耗太多精力。


http://www.mrgr.cn/news/77838.html

相关文章:

  • 【网络】网络抓包与协议分析
  • 企业信息化-走进身份管理之搭建篇
  • Qt交叉编译x86和arm心得
  • 流形与对抗性样本本质的关系
  • 淘宝商品评论爬虫:Java版“窃听风云”
  • 操作系统大会2024 | 麒麟信安根植openEuler社区,持续技术创新 共拓新应用 探索新机遇
  • #Verilog HDL# 谈谈代码中如何跨层次引用
  • 下载安装Android Studio
  • #Verilog HDL# Verilog中的ifdef/ifndef/else等用法
  • 每日一练:位运算-消失的两个数字
  • CNN—LeNet:从0开始神经网络学习,实战MNIST和CIFAR10~
  • 第三十四篇 MobileNetV1、V2、V3模型解析
  • 【计算机网络】数据链路层
  • 算法(Algorithm)
  • Playwright(Java版) - 7: Playwright 页面对象模型(POM)
  • 使用 Spring Boot 和 GraalVM 的原生镜像
  • win10局域网加密共享设置
  • 《计算力学学报》
  • MCSA --- make coding simple again
  • JavaFX 实现文件夹和文件选择功能及常见问题解决方案
  • 动态规划子数组系列一>最长湍流子数组
  • 高频面试题(含笔试高频算法整理)基本总结回顾6
  • 【模块一】kubernetes容器编排进阶实战之pod的调度流程,pause容器及init容器
  • Vue.js基础——贼简单易懂!!(响应式 ref 和 reactive、v-on、v-show 和 v-if、v-for、v-bind)
  • Spring学习笔记_41——@RequestBody
  • HarmonyOS4+NEXT星河版入门与项目实战(11)------Button组件