はじめに
ソフトウェアの開発終盤やリリース後に発覚する厄介な問題の中に「メモリリーク」があります。
「機器が動作しなくなる」や「アプリがクラッシュする」などの事象として表面に現れます。
リークしている原因箇所の特定には時間を要する場合が大半で、経験した方も多いかと思います。
また、「まだ直らないのか?」という上司からのプレッシャーとも戦う必要があります(笑)
この記事では、メモリリークの原因特定の一助となる valgrind についてご紹介いたします。
参考
解析の必要性にも触れている「メモリリークはナゼ起こるのか?」もご覧ください。
Valgrind の概要
概要と目的
Valgrind は、デバッグやプロファイリングのためのオープン・ソース・ソフトウェア(OSS)です。
こちらの sourceware.org で公開されています。
主な機能は、プログラムのメモリリーク、スレッドのメモリ競合アクセスなど、
実行時のメモリ操作に関する問題を検出することです。
主な機能とツール
Valgrind は、以下の主要ツールから構成されています。
また、Valgrind は、C/C++言語に特化していますが、他の言語にも一部対応しています。
- Memcheck
アクセスエラーやメモリリークを検出します。動的なメモリ割り当てと解放を追跡し、
未初期化のメモリの使用、不正メモリアクセス、二重解放などの問題を検出します。 - Cachegrind
キャッシュヒット率やミス率、メモリアクセスのパフォーマンスに関する情報を提供します。
プログラムのパフォーマンスのボトルネックを特定するのに役立ちます。 - Callgrind
プロファイリングを行い、関数の使用回数、時間、キャッシュ効率に関する情報を提供します。
プログラムのパフォーマンスの改善点を特定するのに役立ちます。 - Helgrind
スレッドのデッドロックやアクセス競合などの問題を検出します。
スレッド間の同期エラーや競合を特定し、修正するのに役立ちます。 - Massif
ヒープメモリの使用状況をプロファイリングし、ヒープメモリの割当/解放量、
プロファイルを提供し、メモリ使用量の削減やメモリリークの検出に役立ちます。
ポイント
Valgrind を使用すると、プログラムの品質とパフォーマンスを向上させるために、
実行時に検出されるさまざまなメモリ関連の問題を特定できます。
詳細は、公式ページをご確認ください。
Valgrindのインストールと設定
入手方法
Linux の場合はパッケージマネージャを通じで入手することができます。
また、ソースコードが公開されているため、コードからビルドしてインストールすることも可能です。
コードは sourceware.org で公開されており、様々なCPUアーキテクチャに対応した Makefile が用意されています。
インストール手順
Valgrind をインストールしてセットアップする手順は、使用している OS によって異なります。
ここでは、Ubuntu または Debian ベースの Linux ディストリビューションを想定して、
Valgrindのインストール手順を説明いたします。
step
1パッケージリポジトリの更新
command
$ sudo apt update
step
2Valgrind のインストール
command
$ sudo apt install valgrind
step
3インストールの確認
command
$ valgrind --version
メモリリークの検出と解析
Valgrind を使用した検出方法
Valgrind の主要なツールである Memcheck を使用して、メモリリークを検出します。
以下のコマンドを実行すると、Valgrind を使用してメモリリークを検出することができます。
command
$ valgrind --leak-check=full <プログラム> [<プログラムの引数>]
Valgrind が対象のプログラムを実行します。
あとは、メモリリークの再現手順や、普段行っている操作を行うだけです。
実行中にメモリリークが検出されると、Valgrind は詳細な情報を出力します。
メモリリークを発生させるサンプル
以下のような簡単なサンプルを用意して実行してみます。
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 |
#include <stdlib.h> #define MEM_SIZE (1024) static void catch_me(void) { unsigned char *buf = malloc(MEM_SIZE); if (!buf) return; /* leak here! */ buf = malloc(MEM_SIZE); } static void valgrind_test(void) { int i; for (i = 0; i < 100; i++) { catch_me(); } } int main(void) { valgrind_test(); return 0; } |
ここでリークが発生するサンプルです。
コンパイル
$ gcc -g -o test -O0 -Wall -Werror valgrind_test.c
解析実行
$ valgrind --leak-check=full ./test
解析結果と読み方
上記のサンプルの解析実行すると、以下のような解析結果が出力されます。
解析結果
==3930== Memcheck, a memory error detector
==3930== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==3930== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==3930== Command: ./test
==3930==
==3930==
==3930== HEAP SUMMARY:
==3930== in use at exit: 204,800 bytes in 200 blocks
==3930== total heap usage: 200 allocs, 0 frees, 204,800 bytes allocated
==3930==
==3930== 102,400 bytes in 100 blocks are definitely lost in loss record 1 of 2
==3930== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==3930== by 0x10915E: catch_me (main.c:7)
==3930== by 0x109196: valgrind_test (main.c:19)
==3930== by 0x1091B1: main (main.c:25)
==3930==
==3930== 102,400 bytes in 100 blocks are definitely lost in loss record 2 of 2
==3930== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==3930== by 0x109173: catch_me (main.c:12)
==3930== by 0x109196: valgrind_test (main.c:19)
==3930== by 0x1091B1: main (main.c:25)
==3930==
==3930== LEAK SUMMARY:
==3930== definitely lost: 204,800 bytes in 200 blocks
==3930== indirectly lost: 0 bytes in 0 blocks
==3930== possibly lost: 0 bytes in 0 blocks
==3930== still reachable: 0 bytes in 0 blocks
==3930== suppressed: 0 bytes in 0 blocks
==3930==
==3930== For lists of detected and suppressed errors, rerun with: -s
==3930== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
HEAP SUMMARY(ヒープサマリ)
ヒープを確保する関数(mallocなど)の呼び出し回数、ヒープを解放する関数(free)の呼び出し回数、
及び確保した容量の総数が最初に表示されます。
その後に、それぞれの詳細な情報が表示されます。
ヒープを操作した際の位置(ファイル名と行番号)や確保されたメモリのサイズなどが表示され、
スタックトレースからその関数の呼び出し経路も表示してくれます。
LEAK SUMMARY(リークサマリ)
リークサマリもヒープサマリと同様の情報が出力されます。
Valgrind の出力を注意深く解析し、リークが発生している場所や
関連するコードの特定に役立つ情報を探します。
メモリの割り当てや解放に関連する問題や、未解放のメモリ領域の特定など、
異常なメモリ操作を特定することができます。
おまけ(LD_PRELOAD)
command
at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
解析結果の出力の中で、アレっと思われた方もおられると思います。
Valgrind を経由して実行されたプログラムでは、libc の malloc/free ではなく、
Valgrind の malloc/free を利用していることが分かります。
LD_PRELOAD という仕組みを使っており、「malloc/freeをフックする方法」で紹介しておりますので、
興味がございましたらご覧ください。
Valgrindのオプション
基本的な書式
$ valgrind [options] <program name> [<arguments>]
オプション
メモリリーク解析で使用する主なオプションをご紹介します。
その他のオプションは、公式ページか --help でご確認ください。
- --leak-check=full
全てのメモリリークチェック機能を有効にします。 - --show-leak-kinds=all
「リークの可能性あり」を含めた全てのメモリリーク解析結果を表示します。
メモリリークの防止と予防策
C++言語を使用している場合は、スマートポインタを利用することで予防することができます。
「スマートポインタとは?」でも紹介していますので、ご覧ください。
C言語や他の言語を利用している場合は、以下のような手法・思想で実装すると予防効果が期待できます。
静的解析ツールの利用
静的解析ツールを利用することで、メモリリークの問題を検出できる場合があります。
ただし、検知漏れも当然ながらあります。(感触的には結構あります。)
スマートポインタと同等の機能を実装
スマートポインタと同等の機能を実装すれば、予防効果が期待できます。
スマートポインタはC++のテンプレートクラスで、内部にリファレンスカウンタを持っています。
参照者が増えるとインクリメントされ、参照が外れるとデクリメントされます。
カウンタが 0 になった際、参照者が居ない、すなわち今後利用されないと言えるため
自分自身でメモリを解放します。
構造体や関数ポインタを利用して、同様の仕組みを実装することができます。
メモリリークは起こるものという思想
注意深く設計・実装を行い、ツールやノウハウを活かしてもメモリリークは発生してしまうもの。
そこで、メモリリークは起こるものとして設計するのも一つの手法です。
OS を搭載している場合、アプリが扱えるメモリ空間は「仮想アドレス空間」です。
メモリが枯渇してプロセスが死ぬと、メモリの解放は OS が行ってくれます。
Linux では OOM-Killer(Out of Memory Killer)、Android では Low Memory Killer が実装されています。
どちらもメモリが枯渇した際に、一定のルールの元でプロセスが殺される仕組みなのですが、
Low Memory Killer の方は、殺すアプリをコントロール・選別に自由度がある点が異なります。
一般アプリかつ前面のアプリ以外を優先して殺し、HOME やシステムアプリを保護する考え方です。
このように、メモリが枯渇してプロセスが死んだとしても、再起動した際に元の状態に戻れれば良い、
という思想で設計・実装する方法です。