ヒープコラプションのデバッグ手順 ~ 例外 STATUS_HEAP_CORRUPTION (0xc0000374)
マイクロソフトのフォーラムにて、例外 0xc0000374 でクラッシュする問題が取り上げられていました。 私はスレッドに乗り遅れタイムリーに返信することができなかったのですが、興味があったので試しに動作確認をしてみました。
デバッグの基本のところを含め、簡単に解説します。
この資料では Debugging Tools for Windows はインストールされているものとしていますので、まだお持ちで無い方はインストールしてください。
例外 0xc0000374 でクラッシュする (落ちる)
例外 0xc0000374 は ntstatus.h にて STATUS_HEAP_CORRUPTION、メッセージテキスト "A heap has been corrupted" (ヒープが壊れています) と定義されています。 (日本語メッセージは私が訳してます)
すなわち、0xc0000374 で落ちるという問題の調査はヒープコラプションの調査をすればよいわけです。
一般的なアクセス違反の 0xc0000005 として問題が表面化するより、より特定されたメッセージを最初から出してもらえると調査はしやすいですね。
ヒープコラプションの調査で難しいところ
ヒープコラプションの調査で難しいところは、クラッシュするタイミングと、ヒープが壊れるタイミングが異なるところです。
何か悪いモジュールがいて、それがヒープを壊した瞬間にクラッシュしてくれれば、そのモジュールを現行犯逮捕しやすいわけです。 しかし、ヒープが壊れた瞬間にはクラッシュしないので、たいていはヒープは壊れた状態で処理が少しすすみ、全く別の問題の無いコードを実行している箇所でクラッシュしてしまいます。
従って、クラッシュした瞬間の箇所を調査しても意味が無く (そのコードには問題がない可能性が高いため)、ヒープを誰が壊したか?というところに焦点を当てて調査をしないといけないのです。
では、どうすればヒープを壊す箇所を特定できるでしょうか?
ページヒープを利用する
ヒープコラプション (ヒープ破壊) の調査には、ページヒープがしばしば利用されます。
簡単に言えば、ページヒープというのは、メモリ割り当ての前後にガードページという触れないページを配置することによって、 割り当てた領域の外を触ろうとした時に、アクセス違反が発生するようにする仕組みです。
ページの仕組みあるいはページヒープの詳細について興味がある方は、当サイト内の「ページヒープによるヒープオーバーランの検出」 や 「ヒープの仕組み」 を読んでください。
ヒープが壊れた時に本当に 0xc0000374 が出るのか?
以前 IIS のデバッグを担当していた頃は、正直 0xc0000374 というコードをみた記憶が無いです。新しいステータスコードなのか、私が忘れているだけなのか、 良くわかりませんが、それはさておき、「あれー、0xc0000005 じゃないんだ~」 と思ったので、さっそく自分で試してみることにしました。
私の実験の環境は、Windows 7 x64 です。
フォーラムの環境は Windows 2008 x86 っぽかったので、いろいろ条件は違いますが・・・、とりあえず試してみましょう。
ヒープを壊すコード
ヒープを壊すコードは、こんな感じで書きました。壊すだけなので簡単です(笑)
#include <windows.h> void DoBadThing() { char *p; p = (char*) HeapAlloc( GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS | HEAP_ZERO_MEMORY, 8 ); memset( p, 'x', 2048 ); } int main(int argc, char* argv[]) { Sleep( 15 * 1000 ); DoBadThing(); return 0; }
8 バイト割り当てて、memset で 2 kb 書き込んでます。
ちなみに、main で 15 秒スリープしているのは後でツールを仕掛けるための時間を稼ぐためです。Sleep でヒープが壊れると思っているわけではありませんよ (笑)
とりあえず、これでクラッシュするかどうか試してみましょう。
上のコードをビルドして、hc1.exe として実行すると、確かにクラッシュしました。
イベントログには次のように記録されています。
Faulting application name: hc1.exe, version: 0.0.0.0, time stamp: 0x4c1e50ab Faulting module name: ntdll.dll, version: 6.1.7600.16385, time stamp: 0x4a5be02b Exception code: 0xc0000374 Fault offset: 0x00000000000c6cd2 Faulting process id: 0x14bc Faulting application start time: 0x01cb109e8f9d82ca Faulting application path: C:\src\test\debug\hc1\chk\hc1.exe Faulting module path: C:\Windows\SYSTEM32\ntdll.dll Report Id: cd9e590f-7c91-11df-b8c3-463500000031
確かに 0xc0000374 が記録されています。
「へ~、便利だなぁ」と感心しつつ、それではこれをどうしたら捕まえられるでしょうか。
どうすればこのログから、おかしなコード、あるいは少なくとも誰がヒープを壊したかわかるでしょうか。
ADPlus によるダンプ採取
まず、ADPlus.exe でダンプを採ってみましょう。
ADPlus は最近のデバッガのアップデートで、なにやら exe 形式になったみたいですね。でも、単純なダンプ採取に関しては使い方は一緒のようです。
ダンプ採取の手順は次の通りです。
- 調査対象となるプログラムを開始する (今回の私の場合は hc1.exe というプログラム)
- ADPlus をクラッシュモードで、hc1.exe にアタッチする
- 問題が発生した時に自動的にダンプが生成される
これだけです。
ADPlus をアタッチする方法は、次の通り。
> adplus -crash -pn hc1.exe -o C:\temp
-crash オプションで 「クラッシュモード」 を指定、-pn オプションでプロセス名、 -o でダンプの出力場所を指定すれば OK です。
では、hc1.exe を起動。15 秒スリープしている間に、adplus のコマンドを実行。15 秒したらクラッシュ。その時にダンプ採取・・・という段取りでやってみましょう。
すると、確かにダンプが取得できました。
取得できたダンプを見てみましょう。
ご覧の通り、ダンプファイル名は Access Violation として採取されています。
ファーストチャンスの方を開いてみます。
ダンプファイルを開くコマンドは、次の通りです。
cdb -z filename.dmp
ここではデバッガとして cdb を使っていますが、好みで WinDbg などを使ってもいいとおもいます。
デバッガでダンプを開いて、k コマンドでスタックバックトレースを確認します。AV で採取されたダンプなら、コンテキストは問題のあるスレッドにあるはずですから、いきなりコマンドを打てば OK です。
0:000> k Child-SP RetAddr Call Site 00000000`0012f600 000007fe`e22c73c8 ntdll!RtlAllocateHeap+0xc5 00000000`0012f710 000007fe`fd9ecb3d AcXtrnal!NS_FaultTolerantHeap::APIHook_RtlAlloc... 00000000`0012f760 000007fe`fd9ed0af KERNELBASE!BasepComputeProcessPath+0x232 00000000`0012f7f0 000007fe`fd9ecc37 KERNELBASE!BasepComputeProcessDllPath+0x5b 00000000`0012f880 000007fe`fd9ec4f3 KERNELBASE!BasepGetCachedPath+0x1d9 00000000`0012f910 000007fe`fd9e9436 KERNELBASE!GetModuleHandleForUnicodeString+0xf3 00000000`0012f980 000007fe`fd9e9632 KERNELBASE!BasepGetModuleHandleExW+0xd6 00000000`0012fe50 00000001`400014d5 KERNELBASE!GetModuleHandleW+0x1e 00000000`0012fe80 00000001`40001844 hc1!__crtCorExitProcess+0x15 00000000`0012feb0 00000001`400013b0 hc1!doexit+0x168 00000000`0012ff20 00000000`7782f56d hc1!__tmainCRTStartup+0x16c 00000000`0012ff60 00000000`77963281 kernel32!BaseThreadInitThunk+0xd 00000000`0012ff90 00000000`00000000 ntdll!RtlUserThreadStart+0x1d 0:000>
上のテストコード内の DoBadThing 関数や memset といった問題箇所が見当たりません。
関数名から察するに、これはプロセスの終了処理をしているように思われます。
ここをいくらみていても、DoBadThing 関数で memset を呼んだ箇所が問題であるということは浮かび上がってきません。
そこで、ページヒープを利用しましょう。
ページヒープの設定
ページヒープ (PageHeap) は、問題の検出として優れているのですが、一方でデバッグ対象およびシステムで割り当てメモリが大幅に増えることによるインパクトが大きいので、設定には注意してください。
システムが使われていない時間帯を狙ってページヒープを設定して、テスト実行して、そこでダンプ採取できればベストです。
ページヒープを設定する簡単な方法は、デバッグツールに含まれる gflags を利用することです。下のスクリーンショットを参考にしてください。
この設定は直ちに有効になりますので、システムの再起動は不要です。
さて、ページヒープを有効にして、再度ダンプを採取しみてみましょう。
0:000> k Child-SP RetAddr Call Site 00000000`0012fea8 00000001`40001058 hc1!memset+0x87 00000000`0012feb0 00000001`4000108d hc1!DoBadThing+0x38 00000000`0012fef0 00000001`4000139f hc1!main+0x1d 00000000`0012ff20 00000000`7782f56d hc1!__tmainCRTStartup+0x15b 00000000`0012ff60 00000000`77963281 kernel32!BaseThreadInitThunk+0xd 00000000`0012ff90 00000000`00000000 ntdll!RtlUserThreadStart+0x1d 0:000>
今度はちゃんと DoBadThing での memset でアクセス違反が発生しているということがスタックバックトレースに見えてきました。
これで、ヒープを壊しているのは(少なくとも)この箇所であることがわかります。
以上、この資料ではヒープコラプションのときに 0xc0000374 が発生する場合があること、 およびヒープコラプションのときにページヒープを利用する方法について簡単に見てみました。