[HOWTO] UMDH を用いたメモリリーク調査技法


Platform SDK (Windows SDK) サポートの平田です。私は、元 ADSI/ILM/FIM を担当しておりました。ひょっとするとhttp://blogs.technet.com/b/jpilmblg をご覧になられた方がいらっしゃるかもしれません。ぴろととして記事を書いておりましたが、今後は SDK サポートの平田として鋭意皆様のお役に立てればと考えております。

ところで先日、調布にあるオフィス近くのビアガーデンに行ってまいりました。思ったより混んでいたものの、運よく座ることができました。最近暑い日が多いので、外で飲むビールが美味しくて夏を全身で感じることができました。皆様も、水分と塩分をしっかり補給していただいて体調管理には十分お気を付けください。私たちも、気持ちも体もベストコンディションでお客様の問題を解決できるよう、日々体調管理に気を付けてまいります。

汗で水分がリークすると熱中症になるかなぁとか思い立ったので、本日は、メモリ リークに焦点を当てて調査を行う方法についてご紹介いたします。

概要説明
多くの場合、アプリケーションのメモリ リーク箇所の特定は非常に難しいトラブルシューティングとなります。しかしながら、本日ご紹介させていただく User-Mode Dump Heap (以下、UMDH) というツールを用いることで、メモリ リークの発生箇所や、リークの大きさを特定することができます。私たちもお客様からのお問い合わせを調査する際に利用しているツールですので、ひょっとしたらメモリ リークしてるかも?という場合にぜひともご利用いただきたいツールです。

UMDH ツールは、Windows SDK に含まれる Debugging Tools for Windows の一部として提供されており、簡単に利用できるのですが、VirtualAlloc() や VirtualAllocEx() で確保されたメモリのメモリ リークは残念ながら検出することができません。その点だけお気を付けいただければ便利に使えるツールです。

ツールのインストール
自分のプログラムにメモリ リークが疑われたら、まず、パフォーマンス モニターでメモリに関する情報を採取してみましょう。これには、パフォーマンス モニターの Process カテゴリの情報を採取し、 Private Bytes の値を監視します。この値が右肩上がりの傾向であれば、メモリ リークが発生している可能性あるため、より詳しく調査をするために UMDH の出番となります。

1)   UMDH のインストール方法
以下の SDK をダウンロードしてインストールします。この時、[Debugging Tools for Windows] をインストールするようにチェックボックスにチェックを入れます。

Windows 8 用 Windows ソフトウェア開発キット (SDK)
http://msdn.microsoft.com/ja-jp/library/windows/desktop/hh852363.aspx

 

 

 

2) インストールが成功すると、以下のフォルダーに Debugger Tools for Windows がインストールされます。
C:\Program Files (x86)\Windows Kits\8.0\Debuggers\x64
C:\Program Files (x86)\Windows Kits\8.0\Debuggers\x86

 

使ってみよう

今回は、以下のようなサンプル アプリケーションを使って UMDH の使い方を簡単にご説明します
このサンプルコードを  LeakSample.cpp として保存します。

#include “stdafx.h”

#include “Leak.h”

#include <conio.h>

#include <stdlib.h>

  

void heapleak()

{

        CLeak *p;

        p = new CLeak();

}

 

int _tmain(int argc, _TCHAR* argv[])

{

        _getch();

        for(int i=0;i<100;i++)

        {

                heapleak();

        }

        _getch();

        return 0;

}

また、以下のヘッダファイルを Leak.h として保存してプロジェクトに入れてください。

#pragma once

class CLeak

{

public:

        int a;

        int b;

        int c;

        int d;

        CLeak() {};

        ~CLeak() {};

};

LeakSample.exeとしてコンパイルしましょう。コンパイル時に生成されたシンボルファイル (拡張子が .pdb のファイル) を、C:\symbols.cache_pubにコピーしてください。

2)   使用方法

UMDH による調査では、最低 2 回 UMDH を実行して、実行した時点で確保されているメモリの差分よりメモリ リークを調査します。UMDH が実行されて、次に実行される間に確保、解放されたメモリは適切に解放されたメモリですので  UMDH では検出することができません。

また、UMDH は管理者権限で実行する必要があります。そのため、以下の手順はすべて管理者権限をもつユーザーとして実行してください。

2-1) コマンドプロンプトを起動して、環境変数 _NT_SYMBOL_PATH にシンボル ファイルの参照元を設定します。
>set _NT_SYMBOL_PATH=srv*C:\symbols.cache_pub*http://msdl.microsoft.com/download/symbols

2-2) gflags コマンドを使い、スタックトレースの取得を有効にします。
「Leaksample.exe」がメモリ リークの調査対象なので、以下のように gflags コマンドを実行します。
gflags -i Leaksample.exe +ust

2-3) メモリ リークの調査対象となるアプリケーションを実行します。

2-4) タスクマネージャーや tlist コマンドなどを使い、調査対象アプリケーションのプロセス ID を特定します。
tlist コマンドを使って Leaksample.exe のプロセス PID を確認した例がこちらになります。この場合、プロセス ID は 6248 であることがわかります。
>tlist
0 System Process
4 System
<…
省略 …>
6248 LeakSample.exe    LeakSample.exe
2704 conhost.exe

2-5) UMDH による 1 回目の情報採取をします。
プロセス ID 6248 のアプリケーションに対する情報を C:\TEMP\log1.log として保存するために次のコマンドを実行します。
C:\Program Files (x86)\Windows Kits\8.0\Debuggers\x86>umdh -p:6248 -f:C:\TEMP\log1.log

2-6) アプリケーションをしばらく実行し続けて、メモリ リークの現象を発生させます。

2-7) 2 回目の情報採取をします。
C:\Program Files (x86)\Windows Kits\8.0\Debuggers\x86>umdh -p:6248 -f:C:\TEMP\log2.log

 

解析してみよう

1 回目と 2 回目での間にて確保されたメモリの差分がリークしたメモリになります。この差分の検出するときも UMDH を使います。

3-1) UMDH で情報採取結果を取得するために、以下のコマンドにて結果を取得いたします。
C:\Program Files (x86)\Windows Kits\8.0\Debuggers\x86>umdh C:\TEMP\log1.log C:\TEMP\log2.log > C:\TEMP\diff12.txt

3-2) diff12.txt をテキストエディタで開きます

//                                                                         
// Each log entry has the following syntax:                                
//                                                                         
// + BYTES_DELTA (NEW_BYTES – OLD_BYTES) NEW_COUNT allocs BackTrace TRACEID
// + COUNT_DELTA (NEW_COUNT – OLD_COUNT) BackTrace TRACEID allocations     
//     … stack trace …                                                 
//                                                                         
// where:                                                                   
//                                                                         
//     BYTES_DELTA – increase in bytes between before and after log        
//     NEW_BYTES – bytes in after log                                       
//     OLD_BYTES – bytes in before log                                     
//     COUNT_DELTA – increase in allocations between before and after log  
//     NEW_COUNT – number of allocations in after log                      
//     OLD_COUNT – number of allocations in before log                     
//     TRACEID – decimal index of the stack trace in the trace database    
//         (can be used to search for allocation instances in the original 
//         UMDH logs).                                                      
//                                                                         

+    1450 (  1450 –     0)     64 allocs        BackTraceA51584
+      64 (    64 –     0)        BackTraceA51584     allocations

      ntdll!RtlAllocateHeap+1CC
      MSVCR110D!_heap_alloc_base+51
      MSVCR110D!_free_dbg_nolock+6FF
      MSVCR110D!_nh_malloc_dbg+7D
      MSVCR110D!_nh_malloc_dbg+2A
      MSVCR110D!malloc+19
      MSVCR110D!operator new+F
      LeakSample!heapleak+44 (C:\leaksample.cpp, 13)
      LeakSample!wmain+4A (c:\leaksample.cpp, 22)
      LeakSample!__tmainCRTStartup+199
      LeakSample!wmainCRTStartup+D
      KERNEL32!BaseThreadInitThunk+E
      ntdll!__RtlUserThreadStart+72
      ntdll!_RtlUserThreadStart+1B

Total increase ==   1450 requested +    af0 overhead =   1f40

3-3) 上記ログから、LeakSample!heapleak+44 (c:\leaksample.cpp, 13) にてリークしていることがわかります。実際にリークするサンプルコードを確認してみると、

           p = new CLeak();

      となっておりますが、該当ポインタに対して、delete がありません。これにより、メモリリークのコード箇所を確認することができました!