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++ではかなり使いにくいようです……