kivantium活動日記

プログラムを使っていろいろやります

C/C++でのメモリリーク検出方法 〜AddressSanitizer, Valgrind, mtrace〜

C/C++でプログラムを書いているときに遭遇する厄介なバグの一つがメモリリークです。
今回はメモリリークを検出するのに使えるツールの使い方について書きます。

AddressSanitizer

コンパイルオプションをつけるだけで使えて出力も見やすいのでおすすめです。

AddressSanitizerはGCC 4.8以降かLLVM 3.1以降で使うことができます。
コンパイル時にオプションをつけるだけでメモリリークを検出してくれます。(若干実行時間が長くなります)

以下のメモリリークのあるプログラム leak.cpp を例に使い方を説明します。

int main() {
  int *a = new int[10];
}

newで作った動的配列をdeleteしていないのでメモリリークになります。

g++ -fsanitize=address -fno-omit-frame-pointer -g leak.cpp
./a.out

のようにオプションをつけてコンパイルして実行すると以下のような出力を得ます。(clangの場合もオプションは同じです)

=================================================================
==6027==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 40 byte(s) in 1 object(s) allocated from:
    #0 0x7f606cd7a6b2 in operator new[](unsigned long) (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x996b2)
    #1 0x4006f7 in main /home/kivantium/leak.cpp:2
    #2 0x7f606c93782f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)

SUMMARY: AddressSanitizer: 40 byte(s) leaked in 1 allocation(s).

leak.cppの2行目のメモリリークの存在が分かりました。

詳細はAddressSanitizer · google/sanitizers Wiki · GitHubが詳しいです。

Valgrind

仮想機械の上でプログラムを実行することでメモリリークの検出などを行うツールです。実行ファイルだけあれば使える点が便利ですが、実行はけっこう遅くなります。

インストールは基本的には

sudo apt install valgrind

でできるはずですが、僕の環境ではlinux-tools-4.13.0-45-genericのインストールも必要でした。

int main() {
  int *a = new int[10];
}

をValgrindでチェックするには、

g++ -g leak.cpp
valgrind --leak-check=full ./a.out

を実行します。結果は、

==6911== Memcheck, a memory error detector
==6911== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==6911== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==6911== Command: ./a.out
==6911== 
==6911== 
==6911== HEAP SUMMARY:
==6911==     in use at exit: 72,744 bytes in 2 blocks
==6911==   total heap usage: 2 allocs, 0 frees, 72,744 bytes allocated
==6911== 
==6911== 40 bytes in 1 blocks are definitely lost in loss record 1 of 2
==6911==    at 0x4C2E80F: operator new[](unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==6911==    by 0x400607: main (leak.cpp:2)
==6911== 
==6911== LEAK SUMMARY:
==6911==    definitely lost: 40 bytes in 1 blocks
==6911==    indirectly lost: 0 bytes in 0 blocks
==6911==      possibly lost: 0 bytes in 0 blocks
==6911==    still reachable: 72,704 bytes in 1 blocks
==6911==         suppressed: 0 bytes in 0 blocks
==6911== Reachable blocks (those to which a pointer was found) are not shown.
==6911== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==6911== 
==6911== For counts of detected and suppressed errors, rerun with: -v
==6911== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

のようになり、2行目にメモリリークがあることが分かります。

他にも機能があるようなので機会があったら追記します。

mtrace

メモリリークの位置を絞って探すときに使えます。glibcに入っているので使える環境が多いですが出力が見にくいです。

mtraceを使うにはmcheck.hをインクルードして、調べる範囲にmtrace()muntrace()を書く必要があります。

例としては

#include <stdlib.h>
#include <mcheck.h>
 
int main() {
  mtrace();
  int *a = (int*) malloc(sizeof(int)*10);
  muntrace();
}

のようになります。

実行するには

gcc -g leak.c
export MALLOC_TRACE=./mtrace.log
./a.out
mtrace a.out mtrace.log

のようにログ出力先を環境変数に指定したあとにプログラムを実行し、バイナリとログファイルを引数に与えてmtraceを実行します。

出力は

Memory not freed:
-----------------
           Address     Size     Caller
0x0000000001456450     0x28  at /home/kivantium/leak.cpp:6

のようになります。

C++

#include <mcheck.h>
 
int main() {
  mtrace();
  int *a = new int[10];
  muntrace();
}

のように書いた場合、出力は

Memory not freed:
-----------------
           Address     Size     Caller
0x00000000008b5060     0x28  at 0x7fe1589bbe78

のようになってしまい、どの行が問題だったのか分かりません。addr2lineを使って

addr2line -e a.out 0x7fe1589bbe78

とすればどの行か分かる場合もあるようですが、僕の環境では分かりませんでした。
C++ではかなり使いにくいようです……

特定商取引法に定められた事項は請求により遅滞なく提供する