簡単Boehm GCによるC/C++メモリリーク検知

Boehm GCはガベージコレクションの標準実装がないC/C++でガベージコレクションのようなことを実現可能にしてくれるライブラリ。 一度確保されたリソースは明示的に解放処理が行われない限りメモリリークが発生してしまうけれどもBoehm GCで用意されている関数群(通常はGC_MALLOC)を使ってリソースを確保すれば不要になった段階で自動的に解放してくれる。 ソースコード全体で統一してこのライブラリを利用していればオブジェクトの寿命管理の手間は減るだろうし自分がコードの責任者であるならば難しいところを枯れたライブラリにお任せできるので安心感はあるのだろう。ちなみにこのライブラリ、以前からその存在は知ってはいたけれどスマートポインタ派の自分としてはオブジェクトの寿命管理のためにこれを使うモチベーションはいまいち感じられない。

そんなBoehm GCは「Using the Garbage Collector as Leak Detector」で紹介されているようにメモリリーク検知用ユーティリティとしても使える。ドキュメントやソースコードを覗いてみて少し工夫すれば手軽に既存のコードに殆ど手を加えること無くメモリリークの検知ができそうだったので試しにサンプルコードを書いてみた。以下、Boehm GCのインストールからC/C++コードでのメモリリーク検知について。

Boehm GCインストール

まずは自分の環境(debina5.0.1″lenny”)にBoehm GCをインストールしてみる。 Boehm GCライブラリをつかって開発するにはlibgc-devlibgc1c2が必要。もし自分の環境に合うパッケージが存在しない場合は直接ここから tar.gzボールをダウンロードして例のconfigure、make、make install。

$ sudo apt-get install libgc-dev
$ sudo apt-get install libgc1c2
$ dpkg --list |grep libgc
ii  libgc-dev                       1:6.8-1.1                conservative garbage collector for C (develo
ii  libgc1c2                        1:6.8-1.1                conservative garbage collector for C and C++

Cコードメモリリーク検知 – *alloc/freeをGC_関数に置換

Boehm GCにはGC_MALLOC等のリソース確保系関数でリソース確保されたにもかかわらずGC_FREE等の解放系関数でリソース解放されずにリークしてしまっているオブジェクトを検知してそれらをレポートする機能がある。 このメモリリーク検知レポートはGC_gcollect関数で実行される。ただしこのときメモリリーク検知レポート機能をオンにするためにはそもそもBoehm GC を-DFIND_LEAKオプションでコンパイルするか、または実行時にダイナミックにGC_find_leak=1をセットしてやる必要がある。

  • 対象オブジェクト: GC_MALLOC等でリソース確保されたにもかかわらずGC_FREE等で解放されないオブジェクト
  • 検知レポート実行関数: GC_gcollect
  • 前提条件: -DFIND_LEAKオプションでコンパイルされたBoehmGC or 実行時にGC_find_leak=1をセット

Using the Garbage Collector as Leak Detector」ではこのメモリリーク検知レポート機能を利用して実際のCコードのメモリリークを検知するために直接ヒープからメモリ確保、解放を行うための基本関数をBoehmGCの関数で置き換えるためのヘッダを用意している。

gc_detect_leaks.h

#define GC_DEBUG
#include "gc.h"
#define malloc(n) GC_MALLOC(n)
#define calloc(m,n) GC_MALLOC((m)*(n))
#define free(p) GC_FREE(p)
#define realloc(p,n) GC_REALLOC((p),(n))
#define CHECK_LEAKS() GC_gcollect()

試しにメモリリークを引き起こすコードに上記ヘッダをインクルードしたサンプルコード(t1.c)を用意してコンパイル+ 実行してみる。
サンプルコード: t1.c

#include <stdio.h>
#include "gc_detect_leaks.h"
int main() {
    GC_find_leak = 1;
    char *a;
    while(1){
        a = (char*)malloc(100);  // t1.c: line7
        //free(a);
        CHECK_LEAKS();
    }
}
[コンパイルと実行結果]
$ gcc -g -Wall t1.c -o t1 -I/usr/include/gc -L/usr/lib -lgc
$ ./t1
Leaked composite object at 0x805af90 (t1.c:7, sz=100, NORMAL)
Leaked composite object at 0x805af10 (t1.c:7, sz=100, NORMAL)
Leaked composite object at 0x805af90 (t1.c:7, sz=100, NORMAL)
...

無事メモリリークを検知。メモリリークの原因となっている箇所(t1.cの7行目)も特定されているので合格。ちなみに上記ヘッダでGC_DEBUGをdefineしているは理由はgc.hで定義されているGC_MALLOCをデバック用出力にモードにするため。以下gc.hの該当部分だけを略して抜粋。

gc6.8 – /usr/local/gc/gc.h

#ifdef GC_ADD_CALLER
#  define GC_EXTRAS GC_RETURN_ADDR, __FILE__, __LINE__
...略...
#else
#  define GC_EXTRAS __FILE__, __LINE__
...略...
#endif

# ifdef GC_DEBUG
#   define GC_MALLOC(sz) GC_debug_malloc(sz, GC_EXTRAS)
... 略...
# else
#   define GC_MALLOC(sz) GC_malloc(sz)
...略...
# endif

C++ コードメモリリーク検知 – gc/gc_cleanupクラスで継承

次にC++コードのメモリリーク検知を行う。C++のデータ構造の基本はクラス(細かいことを省けば構造体もクラス)でありクラスはnewオペレーターでメモリを動的に確保する。 当然のことながらデフォルトのnewオペレータでリソース確保した場合はGC回収不能であるためBoehm GCが用意しているC++用ライブラリを利用する。gc_cpp.hをみるとこれは単なるCライブラリのラップクラス。以下のようにgcやgc_cleanupクラスをベースクラスとすることでGC回収可能なオブジェクトとして扱うことができる。

サンプルコード: t2.cpp

#include <stdio.h>
#define GC_DEBUG
#include "gc_cpp.h"
#define CHECK_LEAKS() GC_gcollect()
class Foo: public gc{};
int main() {
    GC_find_leak = 1;
    Foo *f;
    while(1){
        f =new Foo;   // t2.cpp: line10
//        delete f;
        CHECK_LEAKS();
    }
}
[コンパイルと実行結果]
$ g++ -g -Wall t2.cpp -o t2 -I/usr/include/gc -L/usr/lib -lgc
$ ./t2
Leaked composite object at 0x805afe8 (/usr/include/gc/gc_cpp.h:274, sz=1, NORMAL)
Leaked composite object at 0x805afd0 (/usr/include/gc/gc_cpp.h:274, sz=1, NORMAL)
Leaked composite object at 0x805afe8 (/usr/include/gc/gc_cpp.h:274, sz=1, NORMAL)
....

これも無事メモリリーク検知はされている。ただしリーク箇所が/usr/include/gc/gc_cpp.hのL274行目を指している。これではリークの原因箇所がわからない。できればt2.cppのL10行目(f=new Foo)を指してもらいたい。さらにこの方法では単に既存のコードのメモリーリーク検知のみを行いたい場合でも、クラスをgc やgc_cleanupクラスで継承させるといったコードに手を加えていく作業が必要があるのでとても面倒でありコード量が多ければ多いほど手間がかかる。

C++コードメモリリーク検知 – new/deleteオペレータのオーバーロード

そこで既存のコードにあまり手を加えなくてもメモリリーク検知ができ、且つリークの原因箇所を特定できる方法を考えてみた。まずは既存のコードの手を加えないでメモリリーク検知する方法。クラスはリソースの確保、解放をそれぞれnew、 deleteオペレータで行うのでこれらをグローバルにオーバーロードしてやり内部のリソース確保、解放のための実装をGC関数で置き換えてやる。そしてそのヘッダを既存のコードにインクルードしてやれば既存のコードに手を加えないでメモリーリークの検知ができるのではないかと。 ヘッダは以下のような感じ。

#define GC_DEBUG
#include "gc.h"
void* operator new(size_t size) {
    return GC_MALLOC(size);
}
void* operator new[](size_t size) {
    return GC_MALLOC(size);
}
void operator delete(void* p) {
    if (p) GC_FREE(p);
}
void  operator delete[](void* p) {
    if (p) GC_FREE(p);
}

ただしこれだけではメモリリークの検知はできてもリークの原因箇所の特定が難しい。このままだと上記ヘッダのGC_MALLOC()コール箇所がリーク箇所としてレポートされてしまう。 理想はnewオペレータをコールしている箇所がレポートされて欲しい。ということで次のように GC_debug_malloc()にnewオペレータコール箇所のファイル名と行を渡すように変更してみる。

gc_detect_leaks.h

#define GC_DEBUG
#include "gc.h"
void* operator new(size_t size, const char * s, int i) {
    return GC_debug_malloc(size,s,i);
}
void* operator new[](size_t size,  const char * s, int i) {
    return GC_debug_malloc(size,s,i);
}
void operator delete(void* p) {
    if (p) GC_debug_free(p);
}
void  operator delete[](void* p) {
    if (p) GC_debug_free(p);
}
#   define new new(__FILE__, __LINE__)
#   define CHECK_LEAKS() GC_gcollect()
int GC_find_leak = 1;

ためしにメモリリークを引き起こすC++コードに上記ヘッダをインクルードしたものを用意してコンパイル+ 実行してみる。今度はヘッダにint GC_find_leak = 1を含めているのでサンプルコードにGC_find_leak = 1をセットする必要はない。

サンプルコード: t3.cpp

#include <stdio.h>
#include "gc_detect_leaks.h"
class Foo{};
int main() {
    Foo *f;
    while(1){
        f =new Foo;     // t3.cpp: line7
//        delete f;
        CHECK_LEAKS();
    }
}
[コンパイルと実行結果]
$ g++ -g -Wall t3.cpp -o t3 -I/usr/include/gc -L/usr/lib -lgc
$ ./t3
Leaked composite object at 0x805afe8 (t3.cpp:7, sz=1, NORMAL)
Leaked composite object at 0x805afd0 (t3.cpp:7, sz=1, NORMAL)
Leaked composite object at 0x805afe8 (t3.cpp:7, sz=1, NORMAL)
...

見事メモリリークを検知しnewオペレータをコールしている行がレポートされた。というわけでこのヘッダをインクルードするだけで他の既存のコードに手を加えることなくメモリリークを検知することができた。

最後に、今回使ったサンプルコードとここで紹介したヘッダを少し体裁を整えてgithubにあげておきました。ちなみにgithubにあげたgc_detect_leaks.hを使う際は-DGC_DETECT_MEM_LEAKでコンパイルすることをお忘れなく。

http://github.com/yokawasa/any/tree/master/boehmgc/

おわり

No related posts.

Posted in: Programming / プログラミング

Tags: , , , , ,



DeliciousFacebookRedditTwitterGoogle

addLeave a comment