0x00 前言

最近在开发一个 C 的分析库,发现在调用过程中出现内存占用随着请求不断增加的情况,基本可以断定代码存在内存泄露的情况,随后便开始了排查之路。

0x01 工具选择

在 C/C++ 开发中比较常用的内存检查工具是 Valgrind,但是由于支持 macOS 的版本 valgrind-macos 暂未支持 aarch64 处理器架构(Apple Silicon),只能寻找另外的分析工具。经过一番搜索,发现 macOS 在安装 Xcode ToolChain 之后,自带了一个内存检查工具 Leaks,便开始进行排查。

0x02 项目编译

需要注意的是,为了得到更详细的调试信息,尽量使用 Debug 构建,禁用编译器优化等功能。

# 示例配置,根据构建工具和编译器的不同按需配置
cmake_minimum_required(VERSION 3.10 FATAL_ERROR)

SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIC -Wall -O0 -ggdb")

0x03 Leaks

Leaks 识别无指针指向的缓冲区内存,这类内存区域由于指针丢失,无法通过调用用 free() 来释放。从而发现内存泄露的情况。

Leaks 命令 man:

leaks: Search through a process for leaked memory.

Usage: leaks [options] pid | partial-process-name | memory-graph-file
       leaks [options] --atExit -- <command-and-arguments>

        -e/--exclude <sym>     exclude leaked blocks whose backtraces include the specified symbol
        -h/--help              show this helpful usage message!
        -q/--quiet             suppress the process description header and footer
        --list                 print the leaks as a list ("classic"-style) rather than as a tree
        --groupByType          in leak trees, group children by type rather than showing individual instances
                                  (for normal leak detection output, and --referenceTree, --dominatorTree, and --autoreleasePools modes)
        --nostacks             do not print backtraces or save them in the memory graph file, even when available
        --fullStacks           print backtraces with one line per frame
        --nosources            do not show sourceFile:lineNumber in backtraces
        --outputGraph=<path>   save a memory graph file into the given directory or file
        --fullContent          print or save full allocation content descriptions
                                  (this is the default for printing output for live processes)
        --readonlyContent      print or save just readonly allocation content descriptions
                                  (this is the default for saving memory graphs)
        --noContent            do not save or print any allocation content descriptions
        --hex                  show the hex content of leaked allocations, if there is no description of content
        --forkCorpse           generate a corpse fork from process and run leaks on it
        --diffFrom=<memgraph>  show only the new leaks since the specified memgraph
        --trace=<address>      print chains of references from process 'roots' (e.g., global data) to the given block
        --traceTree=<address>  print a reverse tree of references, from the given block up to the process roots
        --referenceTree        print a reference tree of allocated memory starting at root nodes
        --autoreleasePools     print contents of autorelease pools, by thread
        --debug=[mode]         enable additional debugging modes; list available modes with --debug=help
        --atExit               launch the specified command and run leaks when that process exits.
                                  This should be the last argument; use '--atExit -- <command-and-arguments>'

0x04 泄露排查

1. 启动程序

$ MallocStackLogging=1 <command-and-arguments> # 启动需要排查的程序

# OR

$ leaks [options] --atExit -- <command-and-arguments> # 使用 leaks 直接启动需要排查的程序

2. 内存采样

如果不使用 leaks 直接启动程序,需要使用以下命令对内存情况进行收集,执行命令后,会有一个 <graph-name>.memgraph 文件生成到工作路径。

$ leaks --outputGraph=<graph-name> <pid|process-name>

3. 结果分析

命令行

$ leaks <graph-name>.memgraph # 分析单个 memgraph

$ leaks <graph-name-2>.memgraph --diffFrom=<graph-name-1>.memgraph # 对比两个 memgraph
# 进程环境信息
Process:         <ProcessName> [84122]
Path:            /Users/USER/Documents/*/<Executable>
Load Address:    0x1028c0000
Identifier:      <ProcessIdentifier>
Version:         0
Code Type:       ARM64
Platform:        macOS
Parent Process:  zsh [99192]

Date/Time:       2023-09-06 15:12:41.970 +0800
Launch Time:     2023-09-06 15:11:50.505 +0800
OS Version:      macOS 13.5.1 (22G90)
Report Version:  7
Analysis Tool:   /Applications/Xcode.app/Contents/Developer/usr/bin/leaks
Analysis Tool Version:  Xcode 14.3.1 (14E300c)

Physical footprint:         41.7M
Physical footprint (peak):  41.7M
Idle exit:                  untracked
----

# 统计信息
leaks Report Version: 4.0, multi-line stacks
Process 84122: 70099 nodes malloced for 5131 KB
Process 84122: 313 leaks for 24896 total leaked bytes.

# 堆栈信息
STACK OF 155 INSTANCES OF 'ROOT LEAK: <malloc in <malloc_call_function>>':
12  <ModuleName>              0x10292fb4c runtime.asmcgocall.abi0 + 124
11  <ModuleName>              0x1037ab2fc _cgo_86e3bac9422a_Cfunc_<FuncName> + 52
10  <ModuleName>              0x105abf6b8 <FuncName> + 32   <SourceFile>:48
9   <ModuleName>              0x105abcb44 <FuncName> + 660  <SourceFile>:110
8   <ModuleName>              0x105aa6560 <FuncName> + 88   <SourceFile>:112
7   <ModuleName>              0x105aa6124 <FuncName> + 168  <SourceFile>:101
6   <ModuleName>              0x1056bc75c <FuncName> + 384  <SourceFile>:35
5   <ModuleName>              0x1056c6b08 <FuncName> + 108  <SourceFile>:221
4   <ModuleName>              0x1056c6998 <FuncName> + 344  <SourceFile>:193
3   <ModuleName>              0x1056bf3a8 <FuncName> + 1040 <SourceFile>:567
2   <ModuleName>              0x1056bef80 <FuncName> + 108  <SourceFile>:507
1   <ModuleName>              0x1056beb94 <FuncName> + 3656 <SourceFile>:445
0   libsystem_malloc.dylib    0x19a4b0e10 _malloc_zone_malloc_instrumented_or_legacy + 264
====
    # 泄露内存列表
    155 (19.3K) << TOTAL >>
      1 (128 bytes) ROOT LEAK: <malloc in <malloc_call_function> 0x600000000180> [128]
      1 (128 bytes) ROOT LEAK: <malloc in <malloc_call_function> 0x600000002680> [128]
      1 (128 bytes) ROOT LEAK: <malloc in <malloc_call_function> 0x600000002700> [128]
      1 (128 bytes) ROOT LEAK: <malloc in <malloc_call_function> 0x600000002780> [128]
      1 (128 bytes) ROOT LEAK: <malloc in <malloc_call_function> 0x600000002800> [128]
      1 (128 bytes) ROOT LEAK: <malloc in <malloc_call_function> 0x600000002880> [128]
......

# 二进制信息
Binary Images:
......

Xcode

Xcode 可以直接打开对应的 memgraph 文件
XcodeExample

0x05 修复

借助 leaks 给出的信息排查相应的代码块,释放未释放的内存,解决内存泄露的问题。