疑うのは自分のコードから

コミケ77ではカタログビューワの機能はいじれなかったけど、懸案になってた異常終了バグだけは無事解決した。予想通りメモリ破壊だったわけだが、メモリ破壊って特定するのが本当に難しい。
コミケ前は立て込んでて書いてる余裕も無かったけど、実は12/22には解決してた。


最終的には、CreateDIBSectionで取得したDIBバッファに対して、範囲外の座標に書き込んでたというアホなバグが原因なのだが、そこにたどり着くまでにかなり苦労した。


まずは要所要所で確保してた動的メモリをプロセスヒープ(GetProcessHeap()のハンドルを使う)から、個別のヒープをHeapCreateでいちいち作ってヒープ空間を分離してみた。が、見つけられる限り分割してみても状況は変わらず。


次にライブラリ類を疑ってみる。
sqlite、zlib、libpngのメモリ関連のコードにデバッグコードを仕込む。


libpngはビルド時にPNG_USER_MEM_SUPPORTEDを定義しておくと、libpng内部のメモリ処理をユーザ定義のメモリ関数に置き換えることができる。
メモリ関数の置き換えはpng_set_mem_fn関数を使う。第1引数にはlibpngのコンテキスト、第2引数はユーザ定義の値(Windowsヒープを使う場合はヒープハンドルを放り込んでおくといいだろう)、第3引数、第4引数にユーザ定義の確保/解放ルーチンのポインタを指定する。こんな感じ。


// libpng処理開始
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
// メモリ処理の置き換え
png_set_mem_fn(png_ptr, hHeap, PngMalloc, PngMfree);

・・・libpngを使った処理・・・


// メモリ処理をデフォルトに戻す
png_set_mem_fn(*png_ptr, NULL, NULL, NULL);
// libpng処理終了
png_destroy_read_struct(png_ptr, NULL, &end_info);

ユーザ定義の関数はこんな感じに定義しておく。

png_voidp PngMalloc(png_structp png_ptr, png_size_t size)
{
HANDLE hHeap;

hHeap = (HANDLE) png_get_mem_ptr(png_ptr);
if(hHeap == NULL){
return NULL;
}

return HeapAlloc(hHeap, 0, size);
}

void PngMfree(png_structp png_ptr, png_voidp mp)
{
HANDLE hHeap;

hHeap = (HANDLE) png_get_mem_ptr(png_ptr);
if(hHeap == NULL){
return;
}

HeapFree(hHeap, 0, mp);
}


ZLibは最初mallocで検索したけどいまいち見つけ切れなくて、結局は呼び出し元(今回の場合、libpng)がz_stream構造体のzalloc/zfree/opaqueにユーザ定義(それぞれメモリ確保関数、メモリ解放関数、ユーザ定義の値)をセットして使う仕様だということが判明。
libpngは


png_ptr->zstream.zalloc = png_zalloc;
png_ptr->zstream.zfree = png_zfree;
png_ptr->zstream.opaque = (voidpf)png_ptr;
としていて、png_zalloc、png_zfreeはそれぞれpng_mallocpng_freeを呼び出し、これらはさらにpng_set_mem_fnで設定したユーザ定義関数を呼び出すという構造になっている。ので、libpngをユーザ定義関数に置き換えておけばZLib側はいじる必要が無い。


SQLiteは、内部でのメモリ確保はsqlite3Malloc_、sqlite3Free_、sqlite3Realloc_に集約されているので、ここのmalloc、freeを適当に書き換えてやる必要がある。
こうして見比べてみると、SQLiteに比べてZLib、libpngの方がメモリ関連で拡張性があるってことね。うむ。


で、こうやってやれることは一通りやったわけだけど、それでもメモリ破壊がなくならない。CreateDIBSectionは偶然のひらめきで気がついたからよかったものの、自分で明示的に確保しているわけではないメモリなので、なかなか気づきにくいよな…。


教訓。
まず自分のコードを疑え。疑って、疑って、疑いぬいて。
他人を疑うのはそれからだ。ということ。
大勢の人が開発にかかわり、大勢の人が使ってバグ出ししてきたコードにそうそうバグが残ってるはずも無い。実績の無い自分のコードが怪しいに決まってる、と疑った方がたぶんバグを見つける近道だ。


とはいえ、今回やった作業は無駄ではなった。機能単位でヒープを分割して、ライブラリのヒープも分割して、そうすることでバグを内在するヒープの範囲がどんどん狭まっていって、最終的にはちょっと操作するだけですぐ現象(画像が表示できない)となって見えるようになって再現が容易になったからだ。
エラーが発生する箇所もCreateCompatibleDCやCreateDIBSectionなどGDI関連に絞られていったため、DIBバッファでのメモリ破壊というひらめきに到達できたのは偶然の賜物だけによるものではない、と思う。99%の努力と1%の閃きという、アレなんだろう。きっと。
人生の経験値を上げるために必要なのは、結果ではなくて過程ということ。