【经验分享】如何有效地排查内存泄露的疑难问题?
摘要:在嵌入式开发中,相信大家都遇到过内存泄露这类疑难问题,你的排查方法和解决思路是怎么样的呢?本文将给大家分享一种我个人常用的一种方法,这个方法看似很“挫”,but it works well !
1 写在前面
最近博主在实际的项目开发中,又遇到了有关【内存泄露】的问题。作为C语言开发程序员,可能从接触C语言的那会起,就比较怕这类【内存】相关的问题;但是怕归怕,遇到问题还是得想办法解决,及时把项目给交付了才是王道。
本文将从一个简单的案例讲起,逐步还原给出一个可有效解决【内存泄露】的思路方案,也正是这个解决方案,帮我打开了对【内存管理】的一些谜团,也希望本文的介绍能给大家带来更多的思考和启发。
通过本文的阅读,你将可以了解到以下几部分核心内容:
- 一种业内常见常用的【内存管理】方案介绍;
- 判断【内存泄露】的简单方法;
- 如何通过钩子操作替换原生的内存操作接口;
- 如果通过编译器的一些特殊功能,缩减排查方案的实施难度;
- 如何通过脚本工具高效追踪内存泄露的问题点代码。
2 问题描述
回到我前段时间接手的一个项目,这是一个【未使用操作系统】的单片机项目,在执行一些异常测试和性能压测的时候,会偶现一个内存问题,日志打印就是类似【malloc error】之类的。很快,我们跟进这个日志信息,断定【出问题的时间节点下,很有可能系统的堆内存不够了】,由此可以将问题定义为一个【内存泄露】问题。
但是,要知道,我们的整个工程代码比较庞大,少说也是好几W行的规模,这些代码大致分为三大类:
1)芯片原厂提供的代码,包括:芯片驱动、协议栈代码(库形式存在,没有源码)、芯片二次开发SDK等;
2)第三方平台的SDK代码,包括:与第三方平台的对接,数据加解密及交互逻辑等;
3)我方开发的上层应用逻辑代码,包括:私有化的数据处理,对接场测,功能逻辑封装,独有的应用逻辑等。
同时,第1块和第2块非我方开发的地方,我们并没有很熟练,并且也不太轻易去改动里面的代码,这无疑也增大了排查难度。在这样的背景下,要完成从“复杂度”如此高的代码堆里面找出可能出现【内存泄露】的几行问题代码,需要有点手段才行。
3 解决思路
根据多年对【内存】问题的排查经验,我提供了几点思路,后续的实践证明,正是这几点思路帮助了我们顺利排查出问题代码。
1)内存堆分离
这样做到目的是为了做个排除,究竟是芯片底层出现了内存问题,还是上层的逻辑代码引入了内存问题。原生的方案,肯定是上层应用使用底层提供的内存管理API,公用一个内存堆,但这样就无法精确地分析某一处内存申请调用,因为底层的代码并不对我们公开。
2)内存引用计数
这样做的目的就是通过一个很简单的引用计数原理,在申请内存的时候执行**+1**,而在内存释放的时候执行**-1**,通过判断引用计数的变化关系,即可大致判断当前内存情况:引用计数在一定时间内维持一个值附近,证明当前内存使用没有大问题;如果引用计数在一定时间内持续上升且未有下降的趋势,证明十有八九就是【内存泄露】了。
这个方法在问题排查的前期,还是一个非常有效的判断方法。
3)内存钩子
为何要做【内存钩子】?原因在于,我们DEBUG出具体哪一处代码申请了内存、哪一处代码释放了内存、同时还需要知道它申请了多少个字节。
由于系统的内存管理机制,很大可能是对我们闭源的,我们可能都无法看到内存分配和内存释放的代码,这个时候要在它的代码内部加入DEBUG代码,这是不现实的。
所以这里采用的方法是,使用宏定义替换的原理,把整个工程中调用malloc/free/calloc/realloc等内存操作接口,直接替换成对应的内存管理钩子接口,从而接管调用内存管理的实现,达到DEBUG的目的。
4)脚本化分析
当代码逻辑复杂了,调用内存申请、释放的地方肯定非常多,从而打印的LOG也会显著增加。同时,我们的LOG中,将会大量出现地址、内存大小、计数值等内容,简单来说就是一堆的数字,如果这个时间仅仅靠人工去完成LOG的排查和帅选,恐怕效率会非常的低下,并且还非常可能会出错,看花眼的情况比较普遍。
所以,这样的情形下,务必使用脚本程序来介入,辅助完成LOG的筛选,如果可以的话,直接定位出问题代码。
整体的思路,如下图所示:
要实现以上几个解决思路提及的点,需要一些前置的储备知识,详见下文介绍。
4 前置知识
4.1 GCC编译器的编译选项特性
在我们开发中,由于整个团队对GCC的熟悉程度较高,所以我们优先会选用支持GCC编译器环境的开发芯片。在这个解决方案中,我们充分地利用了GCC编译器支持的一个编译选项:-include xxx.h
这个选项一般存在于 CFLAGS 或 ASMFLAGS 中,即对C源码文件或汇编文件编译时生效。
这个选项的作用就是类似于C语言的 #include xxx.h ,但是它的好处就是无需在每个C文件的头部写上 #include xxx.h,而是通过 CFLAGS 的形式在命令行中就传递给了GCC,达到的效果就是只要参与了编译的C源文件或汇编文件,都会间接引用 xxx.h,就相当于在每个.c文件的开头写了一句 #include xxx.h。
这个可太方便了,试想下,如果没有这个选项,难度你真的要每个.c都去新增一行include吗?
就这样通过编译选项传递进去,既不用修改一行代码,又能够达到预期的效果,何乐而不为呢?
4.2 脚本能力
这里说的脚本能力,作为嵌入式开发,我们常见的就3种脚本:
Python脚本:作为一个支持跨平台的脚本语言,它极其容易上手,同时也有大量的第三方库来支持完成一些复杂的功能,可以说有Python的存在,实则是降低了脚本语言的使用门槛,从而更大地解放开发者的双手。
Shell脚本:作为Linux系统的忠实粉丝,对Linux命令行环境非常熟悉,所以很多情况下,我第一时间想到的会是使用Shell脚本来实现一些功能;但是它也有一个弊端,就是需要一个Linux开发环境,但这个对我这个常备Linux云开发主机的开发人员来说,显然不是什么难事。另一方面,虽然Windows下也有类似 git bash 之类的类shell环境,但真实的操作体验还是差一大截。
BAT脚本:这个是Windows下的脚本语言,有些操作不方便使用Linux环境或Python脚本时,可以使用它来应急一下。
4.3 一种常用的内存管理方案
为何要引入新的内存方案,这样为了要解决【内存堆分离】的问题,我们必须能够保证芯片底层使用的内存堆跟上层应用使用的内存堆分离开来,从而定位哪一块引入的内存问题。
综合考虑,采用的是【FreeRTOS的heap_4.c】的内存管理方案,主要的考虑点是:
- 网上参考的资料比较多,大量的开发人员去研究它的实现源码,能够有效地保证方案的可实施性;
- 这个内存管理方案自带了相邻内存块合并的处理逻辑,一定程度上可以避免内存浪费和内存碎块;
- 采用链式存储,对空闲内存块的管理比较合理,在申请内存查找时有比较高的效率;
- 支持自定义内存堆的空间,方便做内存堆替换;
- 用的人多,【从众心理】作祟。
关于它的更多介绍,可以参考:【内存管理】freeRTOS的5种内存管理的实现原理及源码分析
4.4 ANSI C语言的内置宏定义
这里主要介绍会使用到的两个内置宏定义:
__FILE__ :表示正在编译文件对应正在编译文件的路径和文件的名称,注意返回值是一个字符串;在C代码中引入它,可以获取到调用代码的绝对路径文件名。
__LINE__ :表示调用代码的当前行号,一个十进制常量。
正是由于这两个内置宏定义的存在,给了我们一种定位内存申请、内存释放的具体代码点的方法。
5 方案实施
下面以 Windows10 Qemu-vexpress-a9 模拟环境,搭载RT-Thread操作系统,还原并实现整一个解决方案。
5.1 方案实现
搭建基于env的开发环境,此处省略,有兴趣可以参考RT-Thread的教程完成。
假设已经有了现成的开发验证环境,我们在原生的 bsp/qemu-vexpress-a9 工程中新增一个 mem_leak_debug 模块,里面实现几个文件:
mem_leak_debug$ tree
.
├── mem_heap_hook.c 使用FreeRTOS的heap_4.c实现memory alloc、free这两个基础操作
├── mem_heap_hook.h 对外开放memory alloc、free等几个接口
├── mem_leak_debug.c 内存钩子的调用实现,以及引用计数的实现等
├── mem_leak_debug.h 全局引用的内存泄露debug头文件
├── mem-leak-debug.sh 脚本分析工具
├── SConscript Scons构建脚本
└── test.log 测试log文件
以上实现中,mem_heap_hook.c基本沿用了FreeRTOS的heap_4.c,只不过新增了一个 memory trace的分析接口:
void vPortMemTrace(void)
{
uint8_t *start = g_pucAlignedHeapStart;
uint8_t *end = g_pucAlignedHeapEnd;
BlockLink_t * pxBlock = NULL;
uint16_t cnt = 0;
while (start < end) {
pxBlock = (BlockLink_t *)start;
if (!pxBlock->pxNextFreeBlock) {
cnt++;
RT_PRINTF("mem block %3d : %p (%5d) %p %p\n",
cnt,
(uint8_t *)(pxBlock) + xHeapStructSize + sizeof(uint32_t),
pxBlock->xBlockSize & (~xBlockAllocatedBit),
pxBlock,
pxBlock->pxNextFreeBlock);
}
start += pxBlock->xBlockSize & (~xBlockAllocatedBit);
}
}
而作为最重要的两个文件:mem_leak_debug.h 和 mem_leak_debug.c,则是全新的实现。
#ifndef __MEM_LEAK_DEBUG_H__
#define __MEM_LEAK_DEBUG_H__
#include "rtdef.h"
#include "rtthread.h"
#define PORT_ENTER_CRITICAL() do { } while(0)
#define PORT_EXIT_CRITICAL() do { } while(0)
#define TOTAL_HEAP_SIZE (100 * 1024)
#define RT_PRINTF(fmt, arg...) rt_kprintf(fmt, ##arg)
#define RT_MEM_LEAK_DEBUG_ON 1
#if 0
#define __FILENAME__ __FILE__
#else
extern char *_rt_strrchr(const char *str, char c);
#define __FILENAME__ _rt_strrchr(__FILE__, '\\') + 1
#endif
#if (RT_MEM_LEAK_DEBUG_ON)
extern void *_rt_malloc(rt_size_t nbytes, const char *file, uint32_t line);
extern void _rt_free(void *ptr, const char *file, uint32_t line);
extern void *_rt_realloc(void *ptr, rt_size_t nbytes, const char *file, uint32_t line);
extern void *_rt_calloc(rt_size_t count, rt_size_t size, const char *file, uint32_t line);
extern void *_rt_malloc_align(rt_size_t size, rt_size_t align, const char *file, uint32_t line);
extern void _rt_free_align(void *ptr, const char *file, uint32_t line);
#define rt_malloc(nbytes) _rt_malloc(nbytes, __FILENAME__, __LINE__)
#define rt_free(ptr) _rt_free(ptr, __FILENAME__, __LINE__)
#define rt_realloc(ptr, nbytes) _rt_realloc(ptr, nbytes, __FILENAME__, __LINE__)
#define rt_calloc(count, size) _rt_calloc(count, size, __FILENAME__, __LINE__)
#define rt_malloc_align(size, align) _rt_malloc_align(size, align, __FILENAME__, __LINE__)
#define rt_free_align(ptr) _rt_free_align(ptr, __FILENAME__, __LINE__)
#endif
#endif
#include <stdio.h>
#include "rtthread.h"
#include "mem_heap_hook.h"
#include "mem_leak_debug.h"
#if (RT_MEM_LEAK_DEBUG_ON)
#define ALIGN_SIZE(size, align_n) (((size) + (align_n - 1)) & ~(align_n - 1))
static uint32_t g_mem_cnt = 0;
char *_rt_strrchr(const char *str, char c)
{
char *end = (char *)str + rt_strlen(str);
while(*end != c) {
end--;
if (end == str) {
return NULL;
}
}
return end;
}
void _debug_printf(const char *msg)
{
RT_PRINTF("%s", msg);
}
static int cmd_mem_leak_debug(int argc, char **argv)
{
vPortMemTrace();
return 0;
}
MSH_CMD_EXPORT_ALIAS(cmd_mem_leak_debug, leak, Debug for memory leak.);
static void *_malloc_hook(rt_size_t nbytes, uint8_t align, const char *file, uint32_t line)
{
uint8_t *ptr = RT_NULL;
rt_size_t align_nbytes;
align_nbytes = ALIGN_SIZE(nbytes, align);
ptr = (uint8_t *)pvPortMalloc(align_nbytes + sizeof(uint32_t));
if (ptr) {
g_mem_cnt++;
RT_PRINTF("+++[%5d][%25s:%-5d]+++ %p %d(%d)\n",
g_mem_cnt, file, line, ptr + sizeof(uint32_t), align_nbytes, nbytes);
rt_memcpy(ptr, &align_nbytes, sizeof(align_nbytes));
ptr += sizeof(uint32_t);
} else {
RT_PRINTF("MALLOC NULL !!!!!!!!!!!!!!!!!\n");
}
return ptr;
}
static void _free_hook(void *ptr, const char *file, uint32_t line)
{
if (ptr) {
uint32_t align_nbytes = 0;
uint8_t * p_free = (uint8_t *)ptr - sizeof(uint32_t);
g_mem_cnt--;
rt_memcpy(&align_nbytes, p_free, sizeof(align_nbytes));
RT_PRINTF("---[%5d][%25s:%-5d]--- %p %d\n",
g_mem_cnt, file, line, ptr, align_nbytes);
vPortFree(p_free);
}
}
void *_rt_malloc(rt_size_t nbytes, const char *file, uint32_t line)
{
return _malloc_hook(nbytes, sizeof(uint32_t), file, line);
}
void _rt_free(void *ptr, const char *file, uint32_t line)
{
_free_hook(ptr, file, line);
}
void *_rt_realloc(void *ptr, rt_size_t nbytes, const char *file, uint32_t line)
{
_rt_free(ptr, file, line);
return _malloc_hook(nbytes, sizeof(uint32_t), file, line);
}
void *_rt_calloc(rt_size_t count, rt_size_t size, const char *file, uint32_t line)
{
uint8_t *ptr = NULL;
ptr = _malloc_hook(count * size, sizeof(uint32_t), file, line);
if (ptr) {
rt_memset(ptr, 0, count * size);
}
return ptr;
}
void *_rt_malloc_align(rt_size_t size, rt_size_t align, const char *file, uint32_t line)
{
return _malloc_hook(size, (uint8_t)align, file, line);
}
void _rt_free_align(void *ptr, const char *file, uint32_t line)
{
_free_hook(ptr, file, line);
}
#endif
这里比较核心的地方有2点:
1)重新封装malloc/free等内存操作接口,传入file和line,记录内存操作的位置;
2)在malloc内存时,多申请4个字节,并将内存申请的字节大小填入申请内存的前4字节,返回随后4字节之后的内存地址给应用层。
同时在debug代码中,还封装了一个命令行 leak,用于最后显示当前内存节点数据:
static int cmd_mem_leak_debug(int argc, char **argv)
{
vPortMemTrace();
return 0;
}
MSH_CMD_EXPORT_ALIAS(cmd_mem_leak_debug, leak, Debug for memory leak.);
脚本的实现,这里主要利用了几个关键命令行:cat、grep、awk等。
#!/bin/bash
file_name=$0
log_file=$1
unfreed_node_list=()
unfreed_node_key_word="mem block"
malloc_mem_key_words="]+++"
echo input-log-file: $log_file
function get_unfreed_node_list()
{
result=`cat $1 | grep "$unfreed_node_key_word" | awk '{print $5}'`
unfreed_node_list=(`echo $result | sed -e 's/^[ ]*//g' | sed -e 's/[ ]*$//g' | tr '\r' ' '`)
echo "unfreed_node_list: ${unfreed_node_list[@]}"
}
function find_unfreed_node()
{
echo "============================================"
echo "There ${#unfreed_node_list[@]} unfreed node:"
echo "============================================"
for node in `echo ${unfreed_node_list[@]}`; do
echo -n "node=[$node] ==> ";cat $log_file | grep -n "$node" | grep "$malloc_mem_key_words" | tail -n 1
done
echo "============================================"
}
get_unfreed_node_list $log_file
find_unfreed_node $log_file
以上所有工程代码,可以在我的代码仓库中找到:
仓库:https://gitee.com/recan-li/rt-thread.git
分支:mem-leak-debug
5.2 方案验证
为了辅助验证该方案的有效性,我刻意在application/main.c中产生了一处内存泄露的异常代码,for循环10次,产生10次内存泄露:
int main(void)
{
int cnt = 10;
printf("Hello RT-Thread!\n");
while (cnt--)
{
uint8_t *p = rt_malloc(100);
*p = 0x12;
}
return 0;
}
我们把整个工程重新编译一下,起始运行的结果如下:
> .\qemu-nographic.bat
WARNING: Image format was not specified for 'sd.bin' and probing guessed raw.
Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
Specify the 'raw' format explicitly to remove the restrictions.
\ | /
- RT - Thread Operating System
/ | \ 5.0.0 build Aug 31 2022 01:04:35
2006 - 2022 Copyright by RT-Thread team
+++[ 1][ object.c:445 ]+++ 600a400c 52(52)
+++[ 2][ mempool.c:225 ]+++ 600a404c 512(512)
+++[ 3][ object.c:445 ]+++ 600a425c 160(160)
+++[ 4][ thread.c:468 ]+++ 600a430c 2048(2048)
+++[ 5][ kservice.c:622 ]+++ 600a4b1c 8(5)
+++[ 6][ object.c:445 ]+++ 600a4b34 48(48)
+++[ 7][ ipc.c:1903 ]+++ 600a4b74 32(32)
+++[ 8][ object.c:445 ]+++ 600a4ba4 36(36)
+++[ 9][ object.c:445 ]+++ 600a4bd4 160(160)
+++[ 10][ thread.c:468 ]+++ 600a4c84 2048(2048)
lwIP-2.0.3 initialized!
+++[ 11][ workqueue.c:253 ]+++ 600a5494 56(56)
+++[ 12][ object.c:445 ]+++ 600a54dc 160(160)
+++[ 13][ thread.c:468 ]+++ 600a558c 2048(2048)
+++[ 14][ mmcsd_core.c:697 ]+++ 600a5d9c 144(144)
+++[ 15][ drv_sdio.c:414 ]+++ 600a5e3c 44(44)
+++[ 16][ drv_sdio.c:435 ]+++ 600a5e74 4(4)
+++[ 17][ workqueue.c:253 ]+++ 600a5e84 56(56)
+++[ 18][ object.c:445 ]+++ 600a5ecc 160(160)
+++[ 19][ thread.c:468 ]+++ 600a5f7c 2048(2048)
+++[ 20][ syscalls.c:39 ]+++ 600a678c 428(428)
+++[ 21][ dfs.c:144 ]+++ 600a6944 16(16)
+++[ 22][ dfs.c:160 ]+++ 600a6964 36(36)
+++[ 23][ kservice.c:622 ]+++ 600a6994 12(11)
+++[ 24][ kservice.c:622 ]+++ 600a69ac 8(7)
---[ 23][ dfs_file.c:78 ]--- 600a6994 12
+++[ 24][ serial.c:635 ]+++ 600a69c4 76(76)
+++[ 25][ sal_socket.c:127 ]+++ 600a6a1c 16(16)
[I/sal.skt] Socket Abstraction Layer initialize success.
+++[ 26][ sd.c:546 ]+++ 600a6a3c 164(164)
[I/SDIO] SD card capacity 65536 KB.
+++[ 27][ sd.c:159 ]+++ 600a6aec 64(64)
[I/SDIO] switching card to high speed failed!
---[ 26][ sd.c:238 ]--- 600a6aec 64
+++[ 27][ block_dev.c:439 ]+++ 600a6aec 512(512)
found part[0], begin: 32256, size: 63.992MB
+++[ 28][ block_dev.c:360 ]+++ 600a6cfc 104(104)
+++[ 29][ object.c:445 ]+++ 600a6d74 32(32)
+++[ 30][ block_dev.c:360 ]+++ 600a6da4 104(104)
+++[ 31][ object.c:445 ]+++ 600a6e1c 32(32)
---[ 30][ block_dev.c:498 ]--- 600a6aec 512
+++[ 31][ kservice.c:622 ]+++ 600a6994 4(2)
+++[ 32][ dfs_elm.c:124 ]+++ 600a6e4c 4156(4156)
+++[ 33][ object.c:445 ]+++ 600a6aec 36(36)
+++[ 34][ dfs_elm.c:139 ]+++ 600a6b1c 48(48)
+++[ 35][ dfs_elm.c:1008 ]+++ 600a7e94 512(512)
---[ 34][ dfs_elm.c:1014 ]--- 600a7e94 512
---[ 33][ dfs_elm.c:155 ]--- 600a6b1c 48
[I/FileSystem] file system initialization done!
+++[ 34][ ethernetif.c:549 ]+++ 600a6b1c 84(84)
+++[ 35][ ethernetif.c:391 ]+++ 600a6b7c 68(68)
rt_hw_us_delay() doesn't support for this board.Please consider implementing rt_hw_us_delay() in another file.
rt_hw_us_delay() doesn't support for this board.Please consider implementing rt_hw_us_delay() in another file.
rt_hw_us_delay() doesn't support for this board.Please consider implementing rt_hw_us_delay() in another file.
+++[ 36][ sys_arch.c:548 ]+++ 600a6bcc 64(64)
+++[ 37][ sys_arch.c:548 ]+++ 600a7e94 380(380)
---[ 36][ sys_arch.c:553 ]--- 600a7e94 380
+++[ 37][ shell.c:774 ]+++ 600a7e94 524(524)
+++[ 38][ object.c:445 ]+++ 600a6c1c 160(160)
+++[ 39][ thread.c:468 ]+++ 600a80ac 4096(4096)
Hello RT-Thread!
+++[ 40][ main.c:24 ]+++ 600a90bc 100(100)
+++[ 41][ main.c:24 ]+++ 600a912c 100(100)
+++[ 42][ main.c:24 ]+++ 600a919c 100(100)
+++[ 43][ main.c:24 ]+++ 600a920c 100(100)
+++[ 44][ main.c:24 ]+++ 600a927c 100(100)
+++[ 45][ main.c:24 ]+++ 600a92ec 100(100)
+++[ 46][ main.c:24 ]+++ 600a935c 100(100)
+++[ 47][ main.c:24 ]+++ 600a93cc 100(100)
+++[ 48][ main.c:24 ]+++ 600a943c 100(100)
+++[ 49][ main.c:24 ]+++ 600a94ac 100(100)
+++[ 50][ sys_arch.c:548 ]+++ 600a951c 380(380)
---[ 49][ sys_arch.c:553 ]--- 600a951c 380
+++[ 50][ sys_arch.c:548 ]+++ 600a951c 612(612)
---[ 49][ sys_arch.c:553 ]--- 600a951c 612
+++[ 50][ sys_arch.c:548 ]+++ 600a951c 612(612)
+++[ 51][ sys_arch.c:548 ]+++ 600a978c 380(380)
---[ 50][ sys_arch.c:553 ]--- 600a978c 380
---[ 49][ sys_arch.c:553 ]--- 600a951c 612
+++[ 50][ sys_arch.c:548 ]+++ 600a951c 612(612)
+++[ 51][ sys_arch.c:548 ]+++ 600a978c 60(60)
---[ 50][ sys_arch.c:553 ]--- 600a978c 60
---[ 49][ sys_arch.c:553 ]--- 600a951c 612
msh />---[ 48][ idle.c:243 ]--- 600a430c 2048
---[ 47][ object.c:519 ]--- 600a425c 160
+++[ 48][ sys_arch.c:548 ]+++ 600a425c 60(60)
---[ 47][ sys_arch.c:553 ]--- 600a425c 60
+++[ 48][ sys_arch.c:548 ]+++ 600a425c 60(60)
---[ 47][ sys_arch.c:553 ]--- 600a425c 60
+++[ 48][ sal_socket.c:293 ]+++ 600a425c 68(68)
+++[ 49][ sys_arch.c:548 ]+++ 600a42ac 60(60)
---[ 48][ sys_arch.c:553 ]--- 600a42ac 60
---[ 47][ sal_socket.c:179 ]--- 600a425c 68
+++[ 48][ object.c:445 ]+++ 600a425c 32(32)
+++[ 49][ sys_arch.c:548 ]+++ 600a428c 108(108)
+++[ 50][ sys_arch.c:548 ]+++ 600a4304 60(60)
---[ 49][ sys_arch.c:553 ]--- 600a4304 60
+++[ 50][ sys_arch.c:548 ]+++ 600a4304 96(96)
---[ 49][ sys_arch.c:553 ]--- 600a428c 108
+++[ 50][ sys_arch.c:548 ]+++ 600a428c 84(84)
---[ 49][ sys_arch.c:553 ]--- 600a4304 96
---[ 48][ sys_arch.c:553 ]--- 600a428c 84
+++[ 49][ sys_arch.c:548 ]+++ 600a428c 116(116)
---[ 48][ sys_arch.c:553 ]--- 600a428c 116
+++[ 49][ sys_arch.c:548 ]+++ 600a428c 60(60)
---[ 48][ sys_arch.c:553 ]--- 600a428c 60
---[ 47][ object.c:519 ]--- 600a425c 32
+++[ 48][ object.c:445 ]+++ 600a425c 48(48)
+++[ 49][ ipc.c:1903 ]+++ 600a429c 4(4)
+++[ 50][ object.c:445 ]+++ 600a42ac 32(32)
+++[ 51][ sys_arch.c:548 ]+++ 600a42dc 60(60)
+++[ 52][ sys_arch.c:548 ]+++ 600a4324 60(60)
+++[ 53][ sys_arch.c:548 ]+++ 600a436c 84(84)
---[ 52][ sys_arch.c:553 ]--- 600a4324 60
+++[ 53][ sys_arch.c:548 ]+++ 600a43cc 72(72)
---[ 52][ sys_arch.c:553 ]--- 600a42dc 60
---[ 51][ sys_arch.c:553 ]--- 600a43cc 72
---[ 50][ sys_arch.c:553 ]--- 600a436c 84
+++[ 51][ sys_arch.c:548 ]+++ 600a42dc 64(64)
---[ 50][ sys_arch.c:553 ]--- 600a42dc 64
---[ 49][ ipc.c:1957 ]--- 600a429c 4
---[ 48][ object.c:519 ]--- 600a425c 48
---[ 47][ object.c:519 ]--- 600a42ac 32
+++[ 48][ sys_arch.c:548 ]+++ 600a425c 60(60)
---[ 47][ sys_arch.c:553 ]--- 600a425c 60
+++[ 48][ sys_arch.c:548 ]+++ 600a425c 60(60)
---[ 47][ sys_arch.c:553 ]--- 600a425c 60
msh />
这时,我们可以看到内存引用计数没有再显著增加了,当然这里也不是说全部都是【内存泄露】,因为有些是常驻内存,不会轻易释放的。
这时候使用 leak 命令行,查看一下当前的内存节点分布:
msh />
msh />
msh />leak
mem block 1 : 600a400c ( 64) 600a4000 00000000
mem block 2 : 600a404c ( 528) 600a4040 00000000
mem block 3 : 600a4b1c ( 24) 600a4b10 00000000
mem block 4 : 600a4b34 ( 64) 600a4b28 00000000
mem block 5 : 600a4b74 ( 48) 600a4b68 00000000
mem block 6 : 600a4ba4 ( 48) 600a4b98 00000000
mem block 7 : 600a4bd4 ( 176) 600a4bc8 00000000
mem block 8 : 600a4c84 ( 2064) 600a4c78 00000000
mem block 9 : 600a5494 ( 72) 600a5488 00000000
mem block 10 : 600a54dc ( 176) 600a54d0 00000000
mem block 11 : 600a558c ( 2064) 600a5580 00000000
mem block 12 : 600a5d9c ( 160) 600a5d90 00000000
mem block 13 : 600a5e3c ( 56) 600a5e30 00000000
mem block 14 : 600a5e74 ( 16) 600a5e68 00000000
mem block 15 : 600a5e84 ( 72) 600a5e78 00000000
mem block 16 : 600a5ecc ( 176) 600a5ec0 00000000
mem block 17 : 600a5f7c ( 2064) 600a5f70 00000000
mem block 18 : 600a678c ( 440) 600a6780 00000000
mem block 19 : 600a6944 ( 32) 600a6938 00000000
mem block 20 : 600a6964 ( 48) 600a6958 00000000
mem block 21 : 600a6994 ( 24) 600a6988 00000000
mem block 22 : 600a69ac ( 24) 600a69a0 00000000
mem block 23 : 600a69c4 ( 88) 600a69b8 00000000
mem block 24 : 600a6a1c ( 32) 600a6a10 00000000
mem block 25 : 600a6a3c ( 176) 600a6a30 00000000
mem block 26 : 600a6aec ( 48) 600a6ae0 00000000
mem block 27 : 600a6b1c ( 96) 600a6b10 00000000
mem block 28 : 600a6b7c ( 80) 600a6b70 00000000
mem block 29 : 600a6bcc ( 80) 600a6bc0 00000000
mem block 30 : 600a6c1c ( 176) 600a6c10 00000000
mem block 31 : 600a6cfc ( 120) 600a6cf0 00000000
mem block 32 : 600a6d74 ( 48) 600a6d68 00000000
mem block 33 : 600a6da4 ( 120) 600a6d98 00000000
mem block 34 : 600a6e1c ( 48) 600a6e10 00000000
mem block 35 : 600a6e4c ( 4168) 600a6e40 00000000
mem block 36 : 600a7e94 ( 536) 600a7e88 00000000
mem block 37 : 600a80ac ( 4112) 600a80a0 00000000
mem block 38 : 600a90bc ( 112) 600a90b0 00000000
mem block 39 : 600a912c ( 112) 600a9120 00000000
mem block 40 : 600a919c ( 112) 600a9190 00000000
mem block 41 : 600a920c ( 112) 600a9200 00000000
mem block 42 : 600a927c ( 112) 600a9270 00000000
mem block 43 : 600a92ec ( 112) 600a92e0 00000000
mem block 44 : 600a935c ( 112) 600a9350 00000000
mem block 45 : 600a93cc ( 112) 600a93c0 00000000
mem block 46 : 600a943c ( 112) 600a9430 00000000
mem block 47 : 600a94ac ( 112) 600a94a0 00000000
msh />
这时候可以看到剩余 47 个内存节点还未释放。随后把从开机到当前的所有日志,存入一个文本文件,比如叫 test.log,然后在 Linux的命令行环境 下执行脚本分析:
$ ./mem-leak-debug.sh test.log
input-log-file: test.log
unfreed_node_list: 600a400c 600a404c 600a4b1c 600a4b34 600a4b74 600a4ba4 600a4bd4 600a4c84 600a5494 600a54dc 600a558c 600a5d9c 600a5e3c 600a5e74 600a5e84 600a5ecc 600a5f7c 600a678c 600a6944 600a6964 600a6994 600a69ac 600a69c4 600a6a1c 600a6a3c 600a6aec 600a6b1c 600a6b7c 600a6bcc 600a6c1c 600a6cfc 600a6d74 600a6da4 600a6e1c 600a6e4c 600a7e94 600a80ac 600a90bc 600a912c 600a919c 600a920c 600a927c 600a92ec 600a935c 600a93cc 600a943c 600a94ac
============================================
There 47 unfreed node:
============================================
node=[600a400c] ==> 10:+++[ 1][ object.c:445 ]+++ 600a400c 52(52)
node=[600a404c] ==> 11:+++[ 2][ mempool.c:225 ]+++ 600a404c 512(512)
node=[600a4b1c] ==> 14:+++[ 5][ kservice.c:622 ]+++ 600a4b1c 8(5)
node=[600a4b34] ==> 15:+++[ 6][ object.c:445 ]+++ 600a4b34 48(48)
node=[600a4b74] ==> 16:+++[ 7][ ipc.c:1903 ]+++ 600a4b74 32(32)
node=[600a4ba4] ==> 17:+++[ 8][ object.c:445 ]+++ 600a4ba4 36(36)
node=[600a4bd4] ==> 18:+++[ 9][ object.c:445 ]+++ 600a4bd4 160(160)
node=[600a4c84] ==> 19:+++[ 10][ thread.c:468 ]+++ 600a4c84 2048(2048)
node=[600a5494] ==> 21:+++[ 11][ workqueue.c:253 ]+++ 600a5494 56(56)
node=[600a54dc] ==> 22:+++[ 12][ object.c:445 ]+++ 600a54dc 160(160)
node=[600a558c] ==> 23:+++[ 13][ thread.c:468 ]+++ 600a558c 2048(2048)
node=[600a5d9c] ==> 24:+++[ 14][ mmcsd_core.c:697 ]+++ 600a5d9c 144(144)
node=[600a5e3c] ==> 25:+++[ 15][ drv_sdio.c:414 ]+++ 600a5e3c 44(44)
node=[600a5e74] ==> 26:+++[ 16][ drv_sdio.c:435 ]+++ 600a5e74 4(4)
node=[600a5e84] ==> 27:+++[ 17][ workqueue.c:253 ]+++ 600a5e84 56(56)
node=[600a5ecc] ==> 28:+++[ 18][ object.c:445 ]+++ 600a5ecc 160(160)
node=[600a5f7c] ==> 29:+++[ 19][ thread.c:468 ]+++ 600a5f7c 2048(2048)
node=[600a678c] ==> 30:+++[ 20][ syscalls.c:39 ]+++ 600a678c 428(428)
node=[600a6944] ==> 31:+++[ 21][ dfs.c:144 ]+++ 600a6944 16(16)
node=[600a6964] ==> 32:+++[ 22][ dfs.c:160 ]+++ 600a6964 36(36)
node=[600a6994] ==> 51:+++[ 31][ kservice.c:622 ]+++ 600a6994 4(2)
node=[600a69ac] ==> 34:+++[ 24][ kservice.c:622 ]+++ 600a69ac 8(7)
node=[600a69c4] ==> 36:+++[ 24][ serial.c:635 ]+++ 600a69c4 76(76)
node=[600a6a1c] ==> 37:+++[ 25][ sal_socket.c:127 ]+++ 600a6a1c 16(16)
node=[600a6a3c] ==> 39:+++[ 26][ sd.c:546 ]+++ 600a6a3c 164(164)
node=[600a6aec] ==> 53:+++[ 33][ object.c:445 ]+++ 600a6aec 36(36)
node=[600a6b1c] ==> 60:+++[ 34][ ethernetif.c:549 ]+++ 600a6b1c 84(84)
node=[600a6b7c] ==> 61:+++[ 35][ ethernetif.c:391 ]+++ 600a6b7c 68(68)
node=[600a6bcc] ==> 65:+++[ 36][ sys_arch.c:548 ]+++ 600a6bcc 64(64)
node=[600a6c1c] ==> 69:+++[ 38][ object.c:445 ]+++ 600a6c1c 160(160)
node=[600a6cfc] ==> 46:+++[ 28][ block_dev.c:360 ]+++ 600a6cfc 104(104)
node=[600a6d74] ==> 47:+++[ 29][ object.c:445 ]+++ 600a6d74 32(32)
node=[600a6da4] ==> 48:+++[ 30][ block_dev.c:360 ]+++ 600a6da4 104(104)
node=[600a6e1c] ==> 49:+++[ 31][ object.c:445 ]+++ 600a6e1c 32(32)
node=[600a6e4c] ==> 52:+++[ 32][ dfs_elm.c:124 ]+++ 600a6e4c 4156(4156)
node=[600a7e94] ==> 68:+++[ 37][ shell.c:774 ]+++ 600a7e94 524(524)
node=[600a80ac] ==> 70:+++[ 39][ thread.c:468 ]+++ 600a80ac 4096(4096)
node=[600a90bc] ==> 72:+++[ 40][ main.c:24 ]+++ 600a90bc 100(100)
node=[600a912c] ==> 73:+++[ 41][ main.c:24 ]+++ 600a912c 100(100)
node=[600a919c] ==> 74:+++[ 42][ main.c:24 ]+++ 600a919c 100(100)
node=[600a920c] ==> 75:+++[ 43][ main.c:24 ]+++ 600a920c 100(100)
node=[600a927c] ==> 76:+++[ 44][ main.c:24 ]+++ 600a927c 100(100)
node=[600a92ec] ==> 77:+++[ 45][ main.c:24 ]+++ 600a92ec 100(100)
node=[600a935c] ==> 78:+++[ 46][ main.c:24 ]+++ 600a935c 100(100)
node=[600a93cc] ==> 79:+++[ 47][ main.c:24 ]+++ 600a93cc 100(100)
node=[600a943c] ==> 80:+++[ 48][ main.c:24 ]+++ 600a943c 100(100)
node=[600a94ac] ==> 81:+++[ 49][ main.c:24 ]+++ 600a94ac 100(100)
============================================
脚本分析显示,仍有 47 个内存节点未释放,同时还把每个未释放内存节点的代码行数,内存地址及内存大小给输出了,我们利用这个信息,就可以逐步去排查我们的代码,找到对应的问题代码。
当然,执行上面的脚本,你也可以在 ENV环境中执行,不过运行方法有点区别,同时,脚本运行会非常慢,需要忍受下!
bash mem-leak-debug.sh test.log
比如向前面提及的main.c中的内存泄露,在这里刚好有10处,与代码情况是符合的。
6 经验总结
- 内存泄露并不可怕,可怕的是没有找到有效的排查方法,徒劳无功;
- 掌握核心的思路原理,方可切换到不同的芯片平台,不同的操作系统,游刃有余;
- 文中采用HOOK钩子操作,比较好地避开了深究具体平台的内存管理原理,达到了排查方案的通用性;
- 排查问题讲究【抽茧剥丝】,层层递进,良好的逻辑判断思路,能够更快地找准排查的方向;
- 大胆使用排除法,可以尽快缩小问题范围;
- 【脚本能力】不可缺失,关键时候,能帮大忙;
- 方法不怕【丑陋】,贵在能有效解决问题。
7 参考链接
8 课后思考
本方案基本介绍完毕,回想一下,我给大家留几个问题,感兴趣的可以一起讨论讨论:
- 这个方案有什么缺陷?
- 多线程(任务)环境下,会有影响吗?
- 过多地打印LOG,会不会影响系统响应的效率?有什么解决办法?
- 这个方案能移植其他系统平台吗?比如嵌入式Linux?比如Windows平台?
9 更多分享
架构师李肯
架构师李肯(全网同名),一个专注于嵌入式IoT领域的架构师。有着近10年的嵌入式一线开发经验,深耕IoT领域多年,熟知IoT领域的业务发展,深度掌握IoT领域的相关技术栈,包括但不限于主流RTOS内核的实现及其移植、硬件驱动移植开发、网络通讯协议开发、编译构建原理及其实现、底层汇编及编译原理、编译优化及代码重构、主流IoT云平台的对接、嵌入式IoT系统的架构设计等等。拥有多项IoT领域的发明专利,热衷于技术分享,有多年撰写技术博客的经验积累,连续多月获得RT-Thread官方技术社区原创技术博文优秀奖,荣获CSDN博客专家、CSDN物联网领域优质创作者、2021年度CSDN&RT-Thread技术社区之星、2022年RT-Thread全球技术大会讲师、RT-Thread官方嵌入式开源社区认证专家、RT-Thread 2021年度论坛之星TOP4、华为云云享专家(嵌入式物联网架构设计师)等荣誉。坚信【知识改变命运,技术改变世界】!
欢迎关注我的gitee仓库01workstation ,日常分享一些开发笔记和项目实战,欢迎指正问题。
同时也非常欢迎关注我的CSDN主页和专栏:
【CSDN主页-架构师李肯】
【RT-Thread主页-架构师李肯】
【C/C++语言编程专栏】
【GCC专栏】
【信息安全专栏】
【RT-Thread开发笔记】
【freeRTOS开发笔记】
有问题的话,可以跟我讨论,知无不答,谢谢大家。
|