C言語:メモリ破壊検出ツール electric fenceの使い方

2010年2月14日

C言語に限らず、プログラム開発においてデバッグに多くの時間がかかるバグの一つとして、 メモリ破壊系バグが挙げられます。


このバグは主に確保した配列を飛び越えてデータを書き込むことで 発生します。このようなプログラムは単純にprintf()等でシーケンスでバッグをしても、入力の種類に よって停止する位置が変わったりするので、まったくの無力です。逆にgdbでひとつひとつステップごとに 様々な変数やシーケンスをチェックしだすと、ものすごい時間を浪費します。

そんなとき、大活躍してくれるのがメモリ破壊検出ツールです。
いろんな検出ツールがあると思いますが、C言語で実装しているならば、手軽で実績もある electric fenceを試してみてください。


このツールは、プログラム中の動的配列(malloc等で確保した配列)の開始位置・終了位置を覚えて これらの配列の領域をポインタ等が踏み越えたときにセグフォで停止させてくれる ものです。

ですので、このセグフォによるコア(core)をgdb等のデバッガで確認すれば、 どこで、どの配列の領域侵害で停止したのかが一瞬で分かるようになります。
# 詳細な動作原理は後述します。


ライブラリ(libefence.so)をインストールするだけで実施可能で、かつ、デバッグ対象となるプログラムを 一切変更することなくメモリ破壊検出をすることができます。 

ちなみに、静的配列、スタック領域、mmap()でマップした領域等はチェック出来ませんので ご注意を。

また、デフォルトではメモリ領域のオーバーラン(=終了位置を踏み越えること)のみを 検出します。開始位置の検証をしたい場合(=メモリ領域のアンダーラン)は、 環境変数(EF_PROTECT_BELOW)を1と設定する必要があります。
# オーバーランとアンダーランを同時に検出することはできません。 

インストールは簡単で、ライブラリをインストールするだけです。
いつものごとく、Debianのインストール方法を以下に記述します。

1
$ sudo apt-get install electric-fence

これで、electric fenceが使用可能となります。


使い方はいくつもありますが、ここではお手軽な方法を紹介します。 まず、デバッグ対象となるバイナリがdo_hogeだとします。このdo_hogeのメモリオーバーランを 検出するには、

1
2
$ ulimit -c unlimited
$ LD_PRELOAD=/usr/lib/libefence.so do_hoge args

とします。これで、メモリオーバーランを検出するとその場でセグメンテーションフォルトで 停止してくれ、かつ、そのときのcoreを吐いてくれます。

一行目のulimitコマンドが、linuxシステムにコアを吐かせるようにするコマンドです。
あとは、実行バイナリの頭に「 LD_PRELOAD=/usr/lib/libefence.so 」を付ける だけです。 

これで、カレントディレクトリにcoreというファイルが生成されているので、あとは、gdbで どこで止まったか確認しましょう。
1
2
$ emacs
# emacs上で「M-x gdb "do_hoge" core」と入力する。

これだけで、プログラムのどの行で停止したのかをgdbが教えてくれます。

これ覚えると、デバッグにかかる時間が飛躍的に短縮されますよー。


では、なぜこのようなデバッグが可能となるのか? その原理について説明します(興味がない人は飛ばしてください)。 


まずelectric fenceを使用すると、malloc(), free()がlibefence.so上のものにリンクされます。 このmalloc/freeがデバッグ機能を持っていて、正確には、領域侵害したときにセグフォで落ちるような 設計になっています。


方法としては、1バイトのヒープを確保したとしても、必ず1回のmalloc()につき、ページ単位(4096KB) でメモリを確保し、ページの最後から数えて指定されたバイト数を配列として確保します。 


その次に領域侵害検出用のページ(アクセスした瞬間にセグフォで落ちるようにしてあるページ) が先ほどのページの次の領域に隣接して確保されます。


これによって、メモリのオーバーランが検出可能となるわけです。


アンダーラン時は、このセグフォ検出用ページが前に配置されることで実現されます。

以上、何となくイメージがつかめましたでしょうか?


このような方法をとっているからこそ、ソースを書き換える必要がなく、また、使い方も簡単なのです。


でも一方で、あり得ないくらいメモリ領域を消費します。ですので、もともとメモリを大量消費するプログラムの デバッグには向きません。