DebugHacks ネタ – メモリリーク退治

前回のエントリで Debug Hacks Conference 2009 の参加報告 (?) を書きましたが、会場で著者のよしおかさんが「参加した人はブログ等で自分のDebugネタを書いてみんなで共有しよう」とおっしゃっていたので、私のDebugネタをちょっと書いてみます。

以前、ダウンが許されないサーバプログラムで数日間稼働させていると、僅かずつながらもメモリがリークしていくという現象があり非常に困りました。その時のHackです。

Hackその1  mallocカウンタを用意

malloc, free をマクロ定義して独自のmalloc, free で置き換えます。その独自 malloc, free でカウンタの増減を行います。そのカウンタは共有メモリ上に置いておくことでプログラム外から参照できるようにしていました。(コードは雰囲気で、未検証です)

/* 全てのソースがincludeしているヘッダに以下を記述 */
#define malloc(size) __my_malloc(size)
#define free(ptr) __my_free(ptr)
/* 置き換えられるmalloc */
#undef malloc
#undef free
void *__my_malloc(size_t size)
{
void *ptr;
ptr = malloc(size);
if(ptr)
_malloc_count++;
return ptr;
}

void __my_free(void *ptr)
{
free(ptr);
_malloc_count--;
}

これで malloc カウンタを見るとリークが発生しているかどうかが分かります。

ただし、ソフトウェアに定常状態があって、定常状態は常に同じmalloc数の場合でしか使えません。状況に応じてメモリ確保の状況が変わるとダメですね。あと、ライブラリ内でmallocしているようなものはカウントできない欠点もあります。

この方法は、リークの事実は分かっても特定するだけの情報がないので、「リークがない」という事実を確認するには使えますが、リークを潰す目的には使えません。

Hackその2  malloc領域にデバッグ情報を埋め込む

これは比較的メモリに余裕があるときにしか使えないのですが、Hackその1に関連して、mallocをフックした時に実際に要求された領域より+α を余分にmallocしておき、余剰部分にメモリ確保時の状況を書き込んでおくことで、後でリークを検出した時にトレース出来るようにしました。(以下未検証コード)

/* 全てのソースがincludeしているヘッダに以下を記述 */
#define malloc(size) __my_malloc(size, __func__, __LINE__)
#define free(ptr) __my_free(ptr, __func__, __LINE__)
struct malloc_debug_header {
int magic;
const char *func;
int line;
int size;
void *caller;
};
/* 置き換えられるmalloc */
#undef malloc
#undef free
void *__my_malloc(size_t size, const char *func, int line)
{
void *ptr;
struct malloc_debug_header *hdr;
ptr = malloc(size + sizeof(struct malloc_debug_header)); /* 余分にmalloc */
if(!ptr)
return ptr;
_malloc_count++;

hdr = ptr;
hdr->magic = 0xbf8129de; /* magic バイナリに出てこなそうな数値を指定 */
hdr->func = func;
hdr->line = line;
hdr->size = size;
hdr->caller = __builtin_return_address(1);
return ptr + sizeof(struct malloc_debug_header); /* offset を足してreturn */
}

void __my_free(void *ptr)
{
free(ptr - sizeof(struct malloc_debug_header)); /* offset を引いて free */
_malloc_count--;
}

これで各malloc領域にデバッグ情報は埋め込めたけど、じゃぁどうやってそれを読み出すんだよ!と思われるかもしれませんが、それは力技で。

  1. coreダンプさせて、coreファイルからmagicを検索して特定する方法
  2. attatchして、/proc/<PID>/mem をmmapして、そこからmagicを検索する方法

1つ目は、リーク発生後、kill -ABRT <PID> でcoreダンプさせてしまい、coreファイルからmagicナンバーを検索する方法です。

coreファイルはメモリイメージをベタに書き出しているので、gdb で core を開かずとも、バイナリファイルとして magic ナンバーを検索してしまえば、malloc された領域を特定できます。そこからデバッグ情報が書かれているので、line番号やサイズを取得します。

関数名についてはポインタなので、ポインタ値をメモっておいて、gdb でターゲットプログラムを開いて、そのポインタを表示させれば取得できます。

2つ目は、別途プログラムを作る必要がありますが、gdbのようにプロセスにattachして、そのプロセスの /proc/<PID>/mem を mmap することで、ターゲットプログラムのメモリ空間から直接magicナンバーを検索して解析する方法です。この方法では、detach 後、SIGCONTを発行すれば再開するので、プロセスが死なずにすみます。

caller も保存してあるので、おおよそどのルートでmallocされたメモリなのかが分かり、リーク退治に非常に便利でした。

 

今までの私が行ったデバッグの中で一番派手なのが上記の例ですね。後は泥まみれのデバッグばかりです。

私のデバッグスタイルとしては、使えるものを探し(時には作り)、使えるものは全て使ってバグ退治をするという感じですかね。バグを退治するのが目的なので、手段は選びません。

最近は printf デバッグ=悪といった風潮もありますが、printf デバッグも有効な手段である場面も多いと私は思います。私自身結構使いますし、必ずしも悪とは思ってません。その時の状況に応じて使えるものは全て使います。バグ退治に有効ならば。

私の場合、組み込み系の仕事がメインなのですが、i386以外のアーキテクチャではgdbが当てにならない場合が結構あります。マルチスレッドプログラムがgdb上ではうまく動かなかったり、Linuxカーネル起動後はICEなしで開発になったりと、デバッガがまともに使えないケースも結構あるので、デバッガは使えればラッキーくらいな感覚ですね。

なので、あまり手段を選んでられないのです。組み込み系でもスタイリッシュにグラフィカルなデバッガでスイスイデバッグができると楽なんですけどねぇ。組み込み系のようなプアな環境でのデバッグは十数年前からあまり変わってないのかもしれません。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です