メモリリーク個所の特定

VisualC++に付属しているCランタイムライブラリにはヒープメモリのリークを検出する機能が備わっています。その機能をもちいて、メモリリーク個所を特定してみましょう。
メモリリークの検出
メモリリークを検出するには_CrtDumpMemoryLeaksを呼び出します。このとき未解放のメモリがあればデバッグコンソールにそのメモリアドレスと先頭16バイトのダンプが表示されます。

Dumping objects ->
{108} normal block at 0x00143FC0, 256 bytes long.
Data: 48 65 6C 6C 6F 20 57 6F 72 6C 64 00 CD CD CD CD
Object dump complete.

この中の{}で囲まれた数字は、起動後に何番目に確保さえたメモリかを示します。
メモリリーク個所の特定
メモリリーク箇所を特定するには_CrtSetBreakAllocを使用します。_CrtSetBreakAllocに先ほどの{}で囲まれた数字を指定します。これによりリークしたメモリを確保した個所でアプリケーションを中断することができます。
しかしこの方法ではアプリケーションの起動後、メモリを確保する順番が変動しないことが保障されていないと場所を特定できません。
そういった場合には_CrtMemCheckpointと_CrtMemDumpAllObjectsSinceを組み合わせて使用します。

int breakCnt;
_CrtMemState stateOld;
_CrtMemCheckpoint(&stateOld);
//ここで意図的にメモリを確保してリークさせる
_CrtMemDumpAllObjectsSince(&stateOld); // (1)
_CrtSetBreakAlloc(breakCnt);
//ここでデバッグ対象となる処理をおこなう。
_CrtMemDumpAllObjectsSince(&stateOld); // (2)

処理を行う前のメモリステータスをCrtMemCheckpoint関数で保存し、_CrtMemCheckpoint以降に確保したメモリのうち解放されていない分が_CrtDumpMemoryLeaksと同じように出力されます。
(1)で出力された{00}の番号と、(2)で出力された{00}の番号の差分を算出しておき、次回実行時に(1)の出力を確認したうえでデバッガから_CrtSetBreakAllocのパラメータを設定します。

アプリケーション側からデバッガを呼び出す。

プロセスを起動してからデバッガでプロセスにアタッチする方法では、プロセスの初期処理部分のデバッグを行えません。そのような場合にはDebugBreak APIを使用します。アプリケーション内部でDebugBreak APIを呼び出すと、DebugBreak APIを呼び出した個所でデバッガがアタッチするまで待たせることができます。
マネージドコードアプリケーションの場合には、DebugBreakの代わりにDebugger.Launch()を使用します。

マップファイルを使用したデバッグ

リリース後のアプリケーションにおいて不正なメモリアクセスなどのエラーが起こった場合には、マップファイルを用いてデバッグをおこないます。

マップファイルの作成

プロジェクトプロパティを開き、リンカのデバッグにあるマップファイルの生成を「はい」にしてください。これでビルド時にマップファイルが生成されます。

実行位置の特定

Windows Vistaでアプリケーション実行中に下のような画面が表示されます。詳細をクリックして詳しい情報を参照してください。
crash_vista.png
下記は実際に発生したエラーの詳細です。例外コードは例外の種類を表します。C00000005はWrite Protection Errorです。次の例外オフセットが例外が発生したコードの場所を示しています。マップファイルと照らし合わせて見てみます。

問題の署名:
問題イベント名: APPCRASH
アプリケーション名: crashTest.exe
アプリケーションのバージョン: 0.0.0.0
アプリケーションのタイムスタンプ: 498a7b3c
障害モジュールの名前: crashTest.exe
障害モジュールのバージョン: 0.0.0.0
障害モジュールのタイムスタンプ: 498a7b3c
例外コード: c0000005
例外オフセット: 00001064
OS バージョン: 6.0.6000.2.0.0.256.1
ロケール ID: 1041
追加情報 1: 1172
追加情報 2: 7912d3cce3b67ed49e553844317cb1a3
追加情報 3: c6b1
追加情報 4: 3dc30d5ca3a80f458b372b9da0535a3a

マップファイルには下記のような内容が含まれています。Publics by Valueが関数名。Rva+Baseがコードのアドレスです。ベースアドレス00400000+オフセットアドレス00001064=00401064が問題のおこったコードアドレスになります。

Address Publics by Value Rva+Base Lib:Object

0000:00000001 ___safe_se_handler_count 00000001
0000:00000000 ___ImageBase 00400000
0001:00000000 _wmain 00401000 f crashTest.obj
0001:00000020 ?funcA@@YAHXZ 00401020 f crashTest.obj
0001:00000050 ?funcB@@YAHH@Z 00401050 f crashTest.obj
0001:00000090 ?funcC@@YAXXZ 00401090 f crashTest.obj

Rva+Baseと照らし合わせると、00401064は「?funcB@@YAHH@Z」と「?funcC@@YAXXZ」の間にあることがわかります。Rva+Baseは関数の先頭を示すアドレスですから、00401064は「?funcB@@YAHH@Z」の内部だということがわかります。
ひとつだけ注意しなければならない点があります。コンパイラの最適化により関数がインラインに展開されたりした場合には、呼び出し元関数内でエラーが発生しているように表示されます。たとえば今回の場合「funcB」がインライン展開されていた場合、funcBの呼び出し元である「_tmain」がエラーの発生個所となります。

参考資料

KB196755 特定の場所、障害が発生コード内で表示されるエラー メッセージでアドレスを使用する方法

VisualStudio 2003 Remote Debugの設定

・OSの設定
アプリケーションを実行するコンピュータにユーザアカウントを追加します。開発環境を動作させているコンピュータにログインする時に使用している同じアカウント名とパスワードで管理者ユーザを作成します。
コントロールパネルの管理ツールにあるローカルセキュリティポリシーを起動して、ローカルポリシーのセキュリティオプションにある「ローカルアカウントの共有とセキュリティモデル」の設定を「クラシック」に変更します。
ファイアウォールが有効になっている場合にはリモートデバッグを実行できません。必ず無効にしてください。
・Remote Debuggerのインストール
Visual Studio 2003のインストーラを起動し、Visual Studio .Net Setupの画面から「リモートコンポーネントセットアップ」をクリックします。表示されるHTMLにリモートデバッグコンポーネントの導入手順が書かれているので、それに従ってインストールしてください。

vs2003_setup.png
vs2003_remote.png

・デバッグの手順
リモートデバッガはサービスとして起動しているので、別途起動する必要はありません。
VisualStudioの開発環境からデバッグのプロセスにアタッチを実行してください。修飾子にアプリケーションを実行するコンピュータのコンピュータ名を指定すると、実行されているプロセスの一覧が表示されるので、デバッグするプロセスを選択します。