Leaktrace 使用方法
概述
LeakTracer 是在检查 C++ 程序内存泄漏时编写的一个小工具。我无法让 dmalloc 显示我想要的内容,我只看到了提到的 __builtin_return_address gcc-extension。
要使用 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 信息表,添加-rdynamic
- 链接选项中添加-Wl,-Bstatic -lleaktracer 静态链接 libleaktracer,最好加上-g3,处理报告时可以显示对应的代码
- 编译后保存生成的 bin 文件,备用
生成报告及处理:
生成的报告中一行信息如下:
1 | # LeakTracer report diff_utc_mono=1588164717.365890 |
- 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 使用与 leaktracer 设计与实现。在实际使用过程中,基本上都会碰到一些小问题。需要对 leaktracer 进行定制化修改。以下的定制化请根据各自项目实际需求酌情进行修改。
callstack 深度问题
callstack 深度默认只有 5 个。1
2
3因此当 callstack 深度较大时无法显示详细的调用关系。如下:
1
stack=0x7f05d63801d5 0x7f05d638e594 0x7f05d638d8f8 0x7f05d638c28d 0x7f05d6382738
将默认 callstack 深度 5 修改为较大值,本文是 50。请根据实际情况调整,一般情况下对整个情况影响不大
1
2
3callstatck 地址问题
无论是 leak-analyze-addr2line,leak-analyze-gdb 二个工具进行解析。或者借助 gdb、objdump 或 map 文件等手段 得到该泄露源的真正文件/行号或函数范围。因多个动态库及其他原因,或多或少都会碰上各种库引用等问题导 致???。会很麻烦1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19$ ./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
??:?文件路径不存在无法生成文件问题
使用 void writeLeaksToFile(const char* reportFileName); 接口生成文件时,若文件夹路径不存在,则会生成文件失败。然而实际在较大工程中使用时,均会将内存泄露文件统一到某个指定路径。可能存在文件夹路径不存在的情况。未内存泄露确生成文件问题
在较大工程中使用时,进程数较多。若未内存泄露确产生文件,对 UI 交互及直观上总有些不协调。解决办法参考 https://github.com/carlyleliu/LeakTracer/commit/49513580316bf998796b19ae7587b1a3e32bbfac
原理
leaktracer 主要的设计思路为:
- 实现一组内存的分配/释放函数,这组函数的函数原型与系统的那一组完全一样,让被 trace 的 library 对于内存的分配/释放函数的调用都链接到自己实现的这一组函数中以 override 掉系统的那组内存/分配释放函数;
- 自己实现的这组函数中的内存分配函数记录分配相关的信息,包括分配的内存的大小,callstack 等,并调用系统本来的内存分配函数去分配内存;
- 自己实现的这组函数中的内存释放函数则销毁内存分配的相关记录,并使用系统的内存释放函数真正的释放内存;
- 在 trace 结束时,遍历所有保存的内存分配记录的信息,并把这些信息保存进文件以供进一步的分析
override 系统内存分配/释放函数
LeakTracer 实现的用于 override 系统内存分配/释放函数的那组函数在 AllocationHandlers.cpp 中定义:
1 | c++ |
1 | // |
内存分配函数记录分配相关的信息
inline void registerAllocation(void *p, size_t size, bool is_array); 记录每一次内存分配的相关信息
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);inline void registerReallocation(void *p, size_t size, bool is_array); 记录每一次内存分配的相关信 息,限 realloc
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();
}
}内存释放函数则销毁内存分配的相关记录
inline void registerReallocation(void *p, size_t size, bool is_array); 记录每一次内存释放的相关信息
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);
}
}
}遍历所有保存的内存分配记录的信息,并把这些信息保存
void writeLeaksToFile(const char* reportFileName); 保存内存分配记录信息到文件。
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=";
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);
}
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 << ", ";
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。