一段很典型的内存泄漏C++ 代码如下:

1
2
3
4
5
6
7
8
int main(int argc, const char **argv)
{
auto *p = new int(10);
// other codes ...
p = nullptr; // leaked
// ...
return 0;
}

如何在庞大的 C++ 项目代码中找出类似的问题呢?

libgmalloc

苹果提供了内存调试功能:Guard Malloc,用于debug 内存问题。 man libgmalloc 可以查看更多使用信息。

开启libgmalloc的记录 malloc 调用日志的功能,在执行程序前设置环境变量MallocStackLogging=1,如:

1
MallocStackLogging=1 ./my_tool

日志会写到一个临时文件中:

my_tool(38364) MallocStackLogging: stack logs being written to /private/tmp/stack-logs.38364.103f3a000.my_tool.19n2JH.index
my_tool(38364) MallocStackLogging: recording malloc and VM allocation stacks to disk using standard recorder

需要注意的是,在程序退出时,这个调用日志文件会被自动删除:

my_tool(38364) MallocStackLogging: stack logs deleted from /private/tmp/stack-logs.38364.103f3a000.my_tool.19n2JH.index

所以需要将程序在结束时最好 block 住,以便分析日志文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
static void wait_for_input()
{
std::cout << "Press Enter to exit." << std::endl;
char b[1];
std::cin.read(b, 1);
}
int main(int argc, const char **argv)
{
auto *p = new int(10);
// other codes ...
p = nullptr; // leaked
// ...

// blocking program for analyzing malloc stack history
wait_for_input();
return 0;
}

The leaks Tools

另外,可以直接用 macOS 检测内存泄漏:/usr/bin/leaks (详见:the leaks Tool

终端中执行 man leaks 查看使用手册。

leaks 使用方式很简单,指定 pid 即可 attach 到执行中的程序:

1
2
3
4
# pid=38364
leaks $pid --outputGraph=$pid.memgraph
# open memory graph file with Xcode
open $pid.memgraph

Xcode Memory Graph Debugger

结合起来就是:

  1. 修改 C++ 程序 main 函数,使其在结束时 block,重编程序
  2. 设置环境变量MallocStackLogging=1,执行程序。
  3. 在程序执行结束时,亦即 block 时,执行 leaks,保存 memory graph文件
  4. ⌃+C 结束程序
  5. 使用 Xcode Memory Graph Debugger 打开 memory graph文件,分析内存泄漏

Xcode Instruments

另外一个内存检测工具是使用Xcode Instruments

从命令行启动 Instruments, 并使用内置的 Leaks 模板:

1
xcrun xctrace record --template 'Leaks' --target-stdout - --launch -- ./my_tool

在程序退出时,自动保存trace文件:

Starting recording with the Leaks template. Launching process: my_tool.
Ctrl-C to stop the recording
Target app exited, ending recording…
Recording completed. Saving output file…
Output file saved as: Launch_my_tool_2023-07-03_15.21.06_AA4C3897.trace

需要注意的是,程序不能退出的太快,不然可能来不及注入分析工具就退出了╮(╯_╰)╭ :

Run issues were detected (trace is still ready to be viewed):

  • [Error] Failed to attach to target process

    * [Error] Failed to execute loader thread for /Applications/Xcode.app/Contents/SharedFrameworks/DVTInstrumentsFoundation.framework/Resources/liboainject.dylib in target; target process 82675 likely exited

另外,Instruments 每 10 秒执行一次内存泄漏检测,所以程序执行时间最好大于 10 秒。

在 main 函数中添加一个 sleep 等待 attach,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <thread>
void wait_seconds(int seconds)
{
std::this_thread::sleep_for(std::chrono::seconds(seconds));
}
int main(int argc, const char **argv)
{

std::cout << "Waiting a moment for Allocations instrument attached..." << std::endl;
wait_seconds(3);
for (size_t i = 0; i < 30; i++)
{
auto *p = new int(10);
*p = (int)i;
wait_seconds(1);
}

return 0;
}

Instruments

LSan (LeakSanitizer)

x86_64 的 macOS 可以使用LeakSanitizerAddressSanitizer 内置的一个内存检测工具)。

1
2
clang++ -std=c++17 -fsanitize=address -g main.cc -o my_tool
ASAN_OPTIONS=detect_leaks=1 ./my_tool

但比较遗憾的是,Apple 的 Clang 阉割掉了 AddressSanitizer 的 LeakSanitizer,所以需要用 homebrew 安装 llvm 的 clang,详见: https://github.com/google/sanitizers/issues/1026#issuecomment-997350809

Refs