使用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
应该不会消耗太多精力。