前言

最近在工作中遇到了一些内存泄露问题,虽然泄露速度很慢,但是对于小型嵌入式设备而言资源本身就很紧张而且过 72h 老化测试过不了。于是寻找一些内存泄露检测工具辅助查找内存泄露问题,此处只使用了 leaktracer 这个开源库来帮助查找内存泄露问题。

leaktracer 概述

LeakTracer 是在检查 C/C++ 程序内存泄漏时编写的一个小工具。 要使用 LeakTracer,请使用提供的 LeakCheck 脚本运行您的程序。它使用 LD_PRELOAD 特性在你的函数之上“覆盖”一些函数(不需要重新编译)。如果您的平台不支持 LD_PRELOAD,您可以将 LeakTracer.o 对象文件添加到 Makefile 中的对象并运行您的应用程序。 LeakTracer 利用 gdb 去输出发生内存泄露所发生的位置,它是通过 override operator new, operator delete, operator malloc, operator free 来实现检测。

用法

加载 leaktracer 库的 3 种方法:

  • 将您的程序链接到 libleaktracer.a。
  • 将您的程序链接到 libleaktracer.so。您需要将 -lleaktracer 选项作为链接命令的第一个选项。
  • 使用 LD_PRELOAD 环境变量以确保它在任何其他库之前加载。

将 leaktracer 添加到程序中:

  • 添加头文件 MemoryTrace.hpp。
  • 添加 leaktracer::MemoryTrace::GetInstance().startMonitoringAllThreads() 函数,作为起始检测位置。
  • 添加 leaktracer::MemoryTrace::GetInstance().writeLeaksToFile("/mnt/extsd/leaks.out") 函数,作为结束位置,生成检测报告。
  • 编译选项中添加-funwind-tables 生成 backtrace 信息表。
  • 链接选项中添加-Wl,-Bstatic -lleaktracer 静态链接 libleaktracer,最好加上-g3,处理报告时可以显示对应的代码。
  • 编译后保存生成的 bin 文件,备用。

生成报告及处理:

生成的报告中一行信息如下:

1
2
3
4
5
6
# LeakTracer report diff_utc_mono=1588164717.365890
leak, time=1434.190531, stack=0x7f05d63801d5 0x7f05d638e594 0x7f05d638d8f8 0x7f05d638c28d 0x7f05d6382738, size=1024, data=***********************************************...
leak, time=1444.194638, stack=0x7f05d70f9c58 0x4027f5 0x7f05d6333830 0x402369, size=100, data=."%...............................................
leak, time=1434.190625, stack=0x4024d7 0x7f05d6333830 0x402369, size=100, data=..................................................
leak, time=1444.194222, stack=0x7f05d70f9a5c 0x4027f0 0x7f05d6333830 0x402369, size=100, data=. %...............................................
leak, time=1434.190651, stack=0x402500 0x7f05d6333830 0x402369, size=100,
  • leak:代表内存泄露。
  • time:代表调用分配内存函数的时间(开机到当前的时间)。
  • stack:调用栈。
  • size:泄露的内存大小。
  • data:申请时内存中的数据。

明显可以看到调用栈,callstack 全是地址,我们以使用 helpers 文件夹中的 leak-analyze-addr2line,leak-analyze-gdb 二个工具进行解析。或者借助 gdb、objdump 或 map 文件等手段得到该泄露源的真正文件/行号或函数范围。

报告处理

  • 使用 addr2line 处理 leaks.out,生成可读的调用栈。
1
leak-analyze-addr2line {bin} leaks.out > leaks.addr2line.txt

生成的报告示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ ./helpers/leak-analyze-addr2line memleak_test_so leaks.out 
Processing "leaks.out" log for "memleak_test_so"
Matching addresses to "memleak_test_so"
found 49 leak(s)
100 bytes lost in 1 blocks (one of them allocated at 1444.194222), from following call stack:
??:0
/home/cll/99_temp/memory_leak/leaktracer/memleak_way1/memleak_test.cpp:110
??:0
??:?
400 bytes lost in 1 blocks (one of them allocated at 1444.194550), from following call stack:
??:0
/home/cll/99_temp/memory_leak/leaktracer/memleak_way1/memleak_test.cpp:110
??:0
??:?
328 bytes lost in 1 blocks (one of them allocated at 1434.190960), from following call stack:
/home/cll/99_temp/memory_leak/leaktracer/memleak_way1/memleak_test.cpp:83
??:0
??:?

解析:49 个重复调用,泄露 100 个字节的内存,其中一个泄漏点在 leaks.out 中的时间是 06219.879013,调用栈从下往上看。

  • 使用 gdb 处理 leaks.out,生成可读的调用栈及源码对应:
1
leak-analyze-gdb {bin} leaks.out > leaks.gdb.txt

生成的报告示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ ./helpers/leak-analyze-gdb memleak_test_so  leaks.out 
found 49 leak(s)
(gdb) Reading symbols from memleak_test_so...done.
16 bytes lost in 1 blocks (one of them allocated at 1434.190803), from following call stack:
main + 364 in section .text
0x4025e4 is in main() (memleak_test.cpp:80).
80 new_delete_test *p_new_class_no_free = new new_delete_test;
No symbol matches 0x7f05d6333830.
_start + 41 in section .text

1 bytes lost in 1 blocks (one of them allocated at 1444.194798), from following call stack:
No symbol matches 0x7f05d70f9cee.
main + 893 in section .text
0x4027f5 is in main() (memleak_test.cpp:111).
111 leaktracer::MemoryTrace::GetInstance().stopAllMonitoring();
No symbol matches 0x7f05d6333830.

无论是 leak-analyze-addr2line,leak-analyze-gdb 二个工具进行解析。或者借助 gdb、objdump 或 map 文件等手段得到该泄露源的真正文件/行号或函数范围。其中都会碰上各种库引用等问题导致???。都会很麻烦,然而 leaktracer 提供了相关源码,我们可以对 leaktracer 进行改造成符合我们的定制化需求。

原理

leaktracer 主要的设计思路为:

  • 实现一组内存的分配/释放函数,这组函数的函数原型与系统的那一组完全一样,让被 trace 的 library 对于内存的分配/释放函数的调用都链接到自己实现的这一组函数中以 override 掉系统的那组内存/分配释放函数;
  • 自己实现的这组函数中的内存分配函数记录分配相关的信息,包括分配的内存的大小,callstack 等,并调用系统本来的内存分配函数去分配内存;
  • 自己实现的这组函数中的内存释放函数则销毁内存分配的相关记录,并使用系统的内存释放函数真正的释放内存;
  • 在 trace 结束时,遍历所有保存的内存分配记录的信息,并把这些信息保存进文件以供进一步的分析;

override 系统内存分配/释放函数

LeakTracer 实现的用于 override 系统内存分配/释放函数的那组函数在 AllocationHandlers.cpp 中定义:

1
2
3
4
5
6
7
8
9
10
c++
void* operator new(size_t size)
void* operator new[] (size_t size)
void operator delete (void *p)
void operator delete[]
c
void *malloc(size_t size)
void free(void* ptr)
void* realloc(void *ptr, size_t size)
void* calloc(size_t nmemb, size_t size)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//
// LeakTracer
// Contribution to original project by Erwin S. Andreasen
// site: http://www.andreasen.org/LeakTracer/
//
// Added by Michael Gopshtein, 2006
// mgopshtein@gmail.com
//
// Any comments/suggestions are welcome
//

#include "MemoryTrace.hpp"
#include "LeakTracer_l.hpp"

void* (*lt_malloc)(size_t size);
void (*lt_free)(void* ptr);
void* (*lt_realloc)(void *ptr, size_t size);
void* (*lt_calloc)(size_t nmemb, size_t size);

void* operator new(size_t size) {
void *p;
leaktracer::MemoryTrace::Setup();

p = LT_MALLOC(size);
leaktracer::MemoryTrace::GetInstance().registerAllocation(p, size, false);

return p;
}

void* operator new[] (size_t size) {
void *p;
leaktracer::MemoryTrace::Setup();

p = LT_MALLOC(size);
leaktracer::MemoryTrace::GetInstance().registerAllocation(p, size, true);

return p;
}

void operator delete (void *p) {
leaktracer::MemoryTrace::Setup();

leaktracer::MemoryTrace::GetInstance().registerRelease(p, false);
LT_FREE(p);
}

void operator delete[] (void *p) {
leaktracer::MemoryTrace::Setup();

leaktracer::MemoryTrace::GetInstance().registerRelease(p, true);
LT_FREE(p);
}

/** -- libc memory operators -- **/

/* malloc
* in some malloc implementation, there is a recursive call to malloc
* (for instance, in uClibc 0.9.29 malloc-standard )
* we use a InternalMonitoringDisablerThreadUp that use a tls variable to prevent several registration
* during the same malloc
*/
void *malloc(size_t size)
{
void *p;
leaktracer::MemoryTrace::Setup();

leaktracer::MemoryTrace::GetInstance().InternalMonitoringDisablerThreadUp();
p = LT_MALLOC(size);
leaktracer::MemoryTrace::GetInstance().InternalMonitoringDisablerThreadDown();
leaktracer::MemoryTrace::GetInstance().registerAllocation(p, size, false);

return p;
}

void free(void* ptr)
{
leaktracer::MemoryTrace::Setup();

leaktracer::MemoryTrace::GetInstance().registerRelease(ptr, false);
LT_FREE(ptr);
}

void* realloc(void *ptr, size_t size)
{
void *p;
leaktracer::MemoryTrace::Setup();

leaktracer::MemoryTrace::GetInstance().InternalMonitoringDisablerThreadUp();

p = LT_REALLOC(ptr, size);

leaktracer::MemoryTrace::GetInstance().InternalMonitoringDisablerThreadDown();

if (p != ptr)
{
if (ptr)
leaktracer::MemoryTrace::GetInstance().registerRelease(ptr, false);
leaktracer::MemoryTrace::GetInstance().registerAllocation(p, size, false);
}
else
{
leaktracer::MemoryTrace::GetInstance().registerReallocation(p, size, false);
}

return p;
}

void* calloc(size_t nmemb, size_t size)
{
void *p;
leaktracer::MemoryTrace::Setup();

leaktracer::MemoryTrace::GetInstance().InternalMonitoringDisablerThreadUp();
p = LT_CALLOC(nmemb, size);
leaktracer::MemoryTrace::GetInstance().InternalMonitoringDisablerThreadDown();
leaktracer::MemoryTrace::GetInstance().registerAllocation(p, nmemb*size, false);

return p;
}

内存分配函数记录分配相关的信息

registerAllocation 记录每一次内存分配的相关信息:

1
2
3
4
5
6
7
/** registers new memory allocation, should be called by the
* function intercepting "new" calls */
inline void registerAllocation(void *p, size_t size, bool is_array);

/** registers memory reallocation, should be called by the
* function intercepting realloc calls */
inline void registerReallocation(void *p, size_t size, bool is_array);

registerReallocation 记录每一次内存分配的相关信息:

1
2
3
/** registers memory reallocation, should be called by the
* function intercepting realloc calls */
inline void registerReallocation(void *p, size_t size, bool is_array);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// adds all relevant info regarding current allocation to map
inline void MemoryTrace::registerAllocation(void *p, size_t size, bool is_array)
{
allocation_info_t *info = NULL;
if (!AllMonitoringIsDisabled() && (__monitoringAllThreads || getThreadOptions().monitoringAllocations) && p != NULL) {
MutexLock lock(__allocations_mutex);
info = __allocations.insert(p);
if (info != NULL) {
info->size = size;
info->isArray = is_array;
storeTimestamp(info->timestamp);
}
}
// we store the stack without locking __allocations_mutex
// it should be safe enough
// prevent a deadlock between backtrave function who are now using advanced dl_iterate_phdr function
// and dl_* function which uses malloc functions
if (info != NULL) {
storeAllocationStack(info->allocStack);
}

if (p == NULL) {
InternalMonitoringDisablerThreadUp();
// WARNING
InternalMonitoringDisablerThreadDown();
}
}

// adds all relevant info regarding current allocation to map
inline void MemoryTrace::registerReallocation(void *p, size_t size, bool is_array)
{
if (!AllMonitoringIsDisabled() && (__monitoringAllThreads || getThreadOptions().monitoringAllocations) && p != NULL) {
MutexLock lock(__allocations_mutex);
allocation_info_t *info = __allocations.find(p);
if (info != NULL) {
info->size = size;
info->isArray = is_array;
storeAllocationStack(info->allocStack);
storeTimestamp(info->timestamp);
}
}

if (p == NULL) {
InternalMonitoringDisablerThreadUp();
// WARNING
InternalMonitoringDisablerThreadDown();
}
}

内存释放函数则销毁内存分配的相关记录

registerReallocation 记录每一次内存释放的相关信息

1
2
3
/** registers new memory allocation, should be called by the
* function intercepting "new" calls */
inline void registerAllocation(void *p, size_t size, bool is_array);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// removes allocation's info from the map
inline void MemoryTrace::registerRelease(void *p, bool is_array)
{
if (!AllMonitoringIsDisabled() && __monitoringReleases && p != NULL) {
MutexLock lock(__allocations_mutex);
allocation_info_t *info = __allocations.find(p);
if (info != NULL) {
if (info->isArray != is_array) {
InternalMonitoringDisablerThreadUp();
// WARNING
InternalMonitoringDisablerThreadDown();
}
__allocations.release(p);
}
}
}

遍历所有保存的内存分配记录的信息,并把这些信息保存

writeLeaksToFile 保存内存分配记录信息到文件:

1
2
/** writes report with all memory leaks */
void writeLeaksToFile(const char* reportFileName);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// writes all memory leaks to given stream
void MemoryTrace::writeLeaksToFile(const char* reportFilename)
{
MutexLock lock(__allocations_mutex);
InternalMonitoringDisablerThreadUp();

std::ofstream oleaks;
if (!isFolderExist(reportFilename)) {
createDirectory(reportFilename);
}
if (__allocations.empty()) {
return; //no memory leak, not need to create leak file
}

oleaks.open(reportFilename, std::ios_base::out);
if (oleaks.is_open())
{
writeLeaksPrivate(oleaks);
oleaks.close();
}
else
{
std::cerr << "Failed to write to \"" << reportFilename << "\"\n";
}
InternalMonitoringDisablerThreadDown();
}

遍历自定义 MapMemoryInfo 中所有元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// writes all memory leaks to given stream
void MemoryTrace::writeLeaksPrivate(std::ostream &out)
{
struct timespec mono, utc, diff;
allocation_info_t *info;
void *p;
double d;
const int precision = 6;
int maxsecwidth;

clock_gettime(CLOCK_REALTIME, &utc);
clock_gettime(CLOCK_MONOTONIC, &mono);

if (utc.tv_nsec > mono.tv_nsec) {
diff.tv_nsec = utc.tv_nsec - mono.tv_nsec;
diff.tv_sec = utc.tv_sec - mono.tv_sec;
} else {
diff.tv_nsec = 1000000000 - (mono.tv_nsec - utc.tv_nsec);
diff.tv_sec = utc.tv_sec - mono.tv_sec -1;
}

maxsecwidth = 0;
while(mono.tv_sec > 0) {
mono.tv_sec = mono.tv_sec/10;
maxsecwidth++;
}
if (maxsecwidth == 0) maxsecwidth=1;

out << "# LeakTracer report";
d = diff.tv_sec + (((double)diff.tv_nsec)/1000000000);
out << " diff_utc_mono=" << std::fixed << std::left << std::setprecision(precision) << d ;
out << "\n";

__allocations.beginIteration();
while (__allocations.getNextPair(&info, &p)) {
d = info->timestamp.tv_sec + (((double)info->timestamp.tv_nsec)/1000000000);
out << "leak, ";
out << "time=" << std::fixed << std::right << std::setprecision(precision) << std::setfill('0') << std::setw(maxsecwidth+1+precision) << d << ", "; // setw(16) ?
out << "stack=";
#ifdef BACKTRACE_SYMBOLS_USED
unsigned int i_depth = 0;
for (i_depth = 0; i_depth < ALLOCATION_STACK_DEPTH; i_depth++) {
if (info->allocStack[i_depth] == NULL) break;

if (i_depth > 0) out << ' ';
out << info->allocStack[i_depth];
}
out << '\n';
char **trace_symbols = (char **)backtrace_symbols (info->allocStack, i_depth);
if (NULL != trace_symbols) {
size_t name_size = 64;
char *name = (char*)malloc(name_size);
for (unsigned int j = 0; j < i_depth; j++) {
char *begin_name = 0;
char *begin_offset = 0;
char *end_offset = 0;
for (char *p = trace_symbols[j]; *p; ++p) {
if (*p == '(') {
begin_name = p;
} else if (*p == '+' && begin_name) {
begin_offset = p;
} else if (*p == ')' && begin_offset) {
end_offset = p;
break;
}
}
if (begin_name && begin_offset && end_offset ) {
*begin_name++ = '\0';
*begin_offset++ = '\0';
*end_offset = '\0';
int status = -4;
char *ret = abi::__cxa_demangle(begin_name, name, &name_size, &status);
if (0 == status) {
name = ret;
out << trace_symbols[j] << ":" << name << "+" << begin_offset;
} else {
out << trace_symbols[j] << ":" << begin_name << "()+" << begin_offset;
}
} else {
out << trace_symbols[j];
}
out << '\n';
}
free(trace_symbols);
}
#else
for (unsigned int i = 0; i < ALLOCATION_STACK_DEPTH; i++) {
if (info->allocStack[i] == NULL) break;

if (i > 0) out << ' ';
out << info->allocStack[i];
}
out << ", ";
#endif
out << "size=" << info->size << ", ";

out << "data=";
const char *data = reinterpret_cast<const char *>(p);
for (unsigned int i = 0; i < PRINTED_DATA_BUFFER_SIZE && i < info->size; i++)
out << (isprint(data[i]) ? data[i] : '.');
out << '\n';
}
}

整体来看 LeakTracer 的设计与实现都并不复杂,因而能够 trace 的 memory issue 也就有限。比如,LeakTracer 就无法 trace 多次释放等问题。但我们可以通过源码编写更强大的内存相关的 trace 工具。

注意:对于应用,无论是静态链接或动态链接的库,若想能够生成 callstack,都需要添加-funwind-tables 重新编译一次 leak-analyze-addr2line 和 leak-analyze-gdb 在 libleaktracer.tar.gz 中,需要将其中的 addr2line 和 gdb 改成交叉编译工具链中的 arm-xxx-addr2line 和 arm-xxx-gdb

参考文献

https://blog.csdn.net/xiaoting451292510/article/details/105850409