2010年3月20日土曜日

「mallocがダメならnewを使えばいいじゃない」

そこそこの大きさのソフトウェアを作った場合、メモリリークが問題になることがよくあります。(初期にメモリ管理についてしっかり設計し、エンジニアの合意を取れば、テストフェーズでメモリリークを気にすることはなくなるのかもしれませんが、残念ながらそのような素晴らしいプロジェクトを見たことがありません。)
普通の環境 (i386, amd64, ppc32, ppc64) ではValgrindを使ってメモリリークを検出するのが定石ですが、Valgrindが動かない環境ではどうしよう、という悩みがあります。

とりあえず、「glibcをdynamic linkして動作するアプリケーション」という仮定で、メモリリークの検出方法を考えました。この仮定はそんなに非現実的ではないはずです。
方針としては、愚直ですが、
malloc/freeをすべて記録し、対応するfreeが存在しないmallocを検出する
となります。

glibcには、__malloc_hook, __free_hookというフックが存在して、malloc/freeの呼び出しを横取りすることができます。がしかし、__[malloc|free]_hookはstatic変数でして、multi-threadedな環境では普通には使えません。

"man __malloc_hook"からの抜粋ですが、
static void
my_init_hook(void)
{
    old_malloc_hook = __malloc_hook;
    __malloc_hook = my_malloc_hook;
}

static void *
my_malloc_hook(size_t size, const void *caller)
{
    void *result;

    /* Restore all old hooks */
    __malloc_hook = old_malloc_hook;

    /* Call recursively */
    result = malloc(size);

    /* Save underlying hooks */
    old_malloc_hook = __malloc_hook;

    /* printf() might call malloc(), so protect it too. */
    printf("malloc(%u) called from %p returns %p\n",
            (unsigned int) size, caller, result);

    /* Restore our own hooks */
    __malloc_hook = my_malloc_hook;

    return result;
}
とやると、my_malloc_hookの"Restore all old hooks"の直後に別のスレッドがmallocを呼ぶと、hookされていない生のmallocを呼べてしまいます。
(my_malloc_hookをrecursive lockで囲ってやれば大丈夫な気がしますが、試してません。)

hookは何かと問題がありそうなので、hookを当てにせず、malloc/freeを乗っ取る方向で考えます。やりかたはBinary Hacks #61のとおり。

注意点としては、mallocを乗っとると(当然ながら)glibcのmallocを普通には呼び出せなくので、複雑な事ができなくなります。今回の場合では、要求されたサイズやアロケートされた領域のアドレス、コールスタックなどを、例えばstd::setに記録し、freeが呼び出されたら対応するmallocの記録をstd::setから削除する、ということをやりたいのですが、STLなどは当然のごとく使えないです。
しょうがないので、必要な情報はすべてファイルに書き出して、後からスクリプトで処理することにしました。ところが、fopen, fputs, fcloseなども使えない(内部でmallocを読んでいる?)ので、open, write, closeを使うことになりました。何という低レベルコーディング・・・。

ともあれこれで、malloc/freeが呼ばれたときに、要求されたサイズ、アロケートされたアドレス、コールスタックをファイルに書き出すことができました。
あとは、これを解析すればリーク箇所(対応するfreeが存在しないmalloc)を検出することができます。

ソースコードは、githubにアップしてみました。

なお、タイトルと本文とは(ほとんど)関係ありませんので、あしからず。

0 件のコメント:

コメントを投稿