2008-05-25

Failgrind: Failmalloc on Valgrind framework

以前, Failmalloc がなかなか良いという話をした. その中で書いた "malloc() の失敗するタイミングを呼出元の関数名で制限する" 機能. スタックを覗いたりが面倒で vaporware のまま放置してたんだけど, Valgrind を使うとあっさり実現できた. 本家リスペクトで Failgrind と命名. (レポジトリ, スナップショット)

インストール

Failgrind は Valgrind に対する patch になっている. patch といっても中のコードは手つかずで, ビルドシステムに相乗りするだけ. Valgrind は マニュアルに拡張の仕方が載っている だけあって, フレームワークとしての利用を前提としている. なので patch という響きを嫌がらないでください.

まず valgrind-3.3.0 を展開:

$ tar xvjf ~/Download/valgrind-3.3.0.tar.bz2

次に failgrind を展開して valgrind のディレクトリにうつす:

$ tar xvzf ~/Download/failgrind-3336efc98b98.tar.gz
$ mv failgrind-3336efc98b98 valgrind-3.3.0/failgrind

パチる(patch のファイル名は .diff が業界標準なのを思いだしたけど後の祭り..) :

$ cd valgrind-3.3.0
$ patch -p1 < failgrind/3_3_0_root.patch

あとは autotools で configure をつくりなおし, 普通にビルド.

$ ./failgrind/autogen.sh # つくりなおしスクリプト同梱
$ ./configure --prefix=`pwd`/inst
$ make && make install

これで準備ができた.

実行

まず failgrind 上で動かすテストコードを用意する. 私の使ったプログラムが failgrind/tests/hello.c にある. こんなの.

#include <stdlib.h>
#include <stdio.h>

void* fine() { return malloc(16); } // 16 には意味なし.

void* how_are_you() { return fine(); }

void* hello() { return malloc(16); }

void* bye() { return malloc(16); }

int main(int argc, char* argv[])
{
    void* h = 0;
    void* b = 0;
    void* hau = 0;

    h = hello();
    if (!h) {
        printf("failed to alloc hello\n");
        return 1;
    }

    hau = how_are_you();
    if (!hau) {
        printf("failed to alloc how_are_you\n");
        free(h);
        return 1;
    }

    b = bye();
    if (!b) {
        printf("failed to alloc bye\n");
        free(hau);
        free(h);
        return 1;
    }

    printf("all allocted successfully\n");

    free(b);
    free(hau);
    free(h);

    return 0;
}

いくつかの関数の中で malloc() を呼び, NULL だったらエラー処理をして終了する. とりあえずビルドして動かすと...

$ gcc failgrind/tests/hello.c
$ ./a.out
all allocated successfully

当然エラーもなく終了する.

次に failgrind を使う.

$ ./inst/bin/valgrind -q --tool=failgrind --fail-conds=bye ./a.out
failed to alloc bye

bye() の中で malloc() に失敗したのがわかる. 関数はカンマ区切りで複数個指定できる. いずれかにヒットすると fail する.

$ ./inst/bin/valgrind -q --tool=failgrind --fail-conds=how_are_you,bye ./a.out
failed to alloc how_are_you

スラッシュで区切ると, 関数が呼び出しスタックにその順序で現れた時に fail する.

$ ./inst/bin/valgrind -q --tool=failgrind --fail-conds=bye/how_are_you ./a.out
all allocated successfully # bye がマッチしてない.
$ ./inst/bin/valgrind -q --tool=failgrind --fail-conds=fine/how_are_you ./a.out
failed to alloc how_are_you # マッチした
$ ./inst/bin/valgrind -q --tool=failgrind --fail-conds=how_are_you/fine ./a.out
all allocated successfully # 順序が異る

こんなかんじ. というか, 今のところこれが全機能. 地味だなー... malloc() だけでなく, calloc() や rellaloc(), C++ の operator new() も対象になる.

仕組み

valgrind は実行ファイルのバイナリに細工をして動かすためのフレームワークで, 実態は x86(など)の機械語をバイトコートとする JIT つき VM. この VM は様々なデバッグ機構を備えている. (機械語を動かすだけなら CPU にもできるからね.) Valgrind の代表的な機能であるメモリリーク検出は, そのデバッグ機構を利用して作られている. Failgrind もその仕組みに載っている.

Valgrind が備えるデバッグ機構の基礎をなすのは, 強力なバイナリ書き換え支援. Valgrind はネイティブの機械語をアーキテクチャ中立な内部表現 (IR) に変換し, フレームワークの利用者はその IR を書き換えて様々なフックを仕掛けることができる. キャッシュミスの検出や呼び出しグラフの構築 (Callgrind) も, こうした枠組みの上に作られている. より詳しい議論は Valgrind のページにある記事 を 参照してほしい.

...のだけれど, Failgrind は別にそういうごつい機能は使っていない. 結局のところ Valgrind の目玉はメモリをめぐるあれこれで, そのへんは特に手厚くなっている. 動的なメモリ確保をフックするくらいなら自分でバイナリを書き換えるまでもない. そのままずばりな API がある.

// プラグインの初期化関数
static void fgr_pre_clo_init(void)
{
   ....
   // 各メモリ確保関数にフックを挿す
   VG_(needs_malloc_replacement)  (fgr_malloc,
                                   fgr___builtin_new,
                                   fgr___builtin_vec_new,
                                   fgr_memalign,
                                   fgr_calloc,
                                   fgr_free,
                                   fgr___builtin_delete,
                                   fgr___builtin_vec_delete,
                                   fgr_realloc,
                                   0 );
    ....
}

中身も簡単.

static void* fgr_malloc ( ThreadId tid, SizeT szB )
{
   return new_block( tid, NULL, szB, VG_(clo_alignment), /*is_zeroed*/False );
}

...

static
void* new_block ( ThreadId tid, void* p, SizeT req_szB, SizeT req_alignB,
                  Bool is_zeroed )
{
   ...
   if (is_to_fail(tid)) { // fail 条件にマッチしたら NULL を返す
       return NULL;
   }

   // Allocate and zero if necessary
   if (!p) {
      p = VG_(cli_malloc)( req_alignB, req_szB );
      ....
   }

   return p;
}

失敗条件を判定する is_to_fail() が核心なわけだけれど, これも大したことをしていない.

static
Bool is_to_fail(ThreadId tid)
{
    Int i = 0, j = 0, k = 0;
    Int n_ips = 0;
    Addr ips[MAX_IPS];
    Char fnname[MAX_FNNAME_LEN];
    XNameList* cand_names = 0;

    // スタックトレースの配列を取得する (Valgrind の API.)
    n_ips = VG_(get_StackTrace)( tid, ips, g_clo_depth, 0/*first_ip_delta*/ );
    tl_assert(n_ips > 0);

    for (i = 0; i < VG_(sizeXA)(g_failure_cond_list); i++) {
        cand_names = VG_(indexXA)(g_failure_cond_list, i);
        k = 0;

        for (j = 0; j < n_ips; j++)  {
            // 関数ポインタから関数名を取得する (Valgrind の API.)
            if (!VG_(get_fnname)(ips[j], fnname, MAX_FNNAME_LEN)) {
                continue;
            }

            for (/**/;k < cand_names->size; ++k) {
                // コマンドラインで渡しておいた関数名と比較し, よろしくやる.
                if (!VG_STREQ(fnname, cand_names->names[k])) {
                    break;
                } else if (k+1 == cand_names->size) { // all names mathced
                    VERB(0, "hit failure condition: %s", cand_names->names[k]);
                    return True;
                }
            }
        }
    }

    return False;
}

コードは短い. そのうえ中身も Massif という Valgrind のヒーププロファイラからコードをコピペし, 適当に削っただけ. コードを書く時間より automake と闘う時間の方が長いような有様だった.

ロードマップ

というわけで無事 vaporware の汚名を返上. 満足した. 自分では使い道がないから, 今のところ続けてがんばる気はない. でも Linux でばりばりコードを書く日のために, 欲しい機能は列挙しておく:

こうしてみると半月くらいで作れそうな雰囲気だなあ... Linux で仕事をしながらこの手のツールのない人がいたら, つくってみてはいかがでしょうか.

まとめ

追記

元ねたの開発者 okuji さん から反応が. ありがとうございます. そして netaware だったのか... (親戚は linux kernel にも入ってるのに!)

メモリに限らずわざとエラーを起こすのはソフトウェアテストの技法で fault injection という, らしい. ぐぐると色々でてくる.

通信のミドルウェアでもこれをサポートしているものは多い. (たとえば XNA の NetworkSession クラスは パケット損失や遅延のシミューレション機能を備えている.) そのほか組み込み界隈だと, たとえば Symbian OS のメモリアロケータ は 標準で failmalloc 的機能を備えている. これは GUI からも設定できる.

failgrind も fault injection という切り口で育ててくのがいいのかもしらん.

okuji さんは gdb の利用を提案している. 繋ぐのは面倒そうだけど, linux 以外でも動きそうなのはいい. gdb のドキュメントには libgdb というのが載っていて夢を煽るけれど, いかにも suspended な雰囲気. 世間の GUI frontend のようにプロセス間通信で頑張るのが正しい路線なのかなー. すげー大変そうだけど...