概述

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
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 使用与 leaktracer 设计与实现。在实际使用过程中,基本上都会碰到一些小问题。需要对 leaktracer 进行定制化修改。以下的定制化请根据各自项目实际需求酌情进行修改。

  • callstack 深度问题
    callstack 深度默认只有 5 个。

    1
    2
    3
    #ifndef ALLOCATION_STACK_DEPTH
    #define ALLOCATION_STACK_DEPTH 5
    #endif

    因此当 callstack 深度较大时无法显示详细的调用关系。如下:

    1
    stack=0x7f05d63801d5 0x7f05d638e594 0x7f05d638d8f8 0x7f05d638c28d 0x7f05d6382738

    将默认 callstack 深度 5 修改为较大值,本文是 50。请根据实际情况调整,一般情况下对整个情况影响不大

    1
    2
    3
    #ifndef ALLOCATION_STACK_DEPTH
    #define ALLOCATION_STACK_DEPTH 100
    #endif

  • callstatck 地址问题
    无论是 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
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;
}
  • 内存分配函数记录分配相关的信息

    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=";
    #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。