Windows でメモリ リークを検出する方法

       私は数年前から C++ を使用してプログラムを作成しており、多くのプログラムを作成し、多くのバグを変更してきました。その中でも、メモリ リークの追跡、デバッグ、修正は最も時間のかかるタスクの 1 つです。長年にわたり、私はいくつかの経験を蓄積してきましたので、それを要約としてここに記録します。

        現在の高級言語の多くはガベージコレクション機構を備えており、言語自体に欠陥がない限り、メモリリークの問題は通常発生しません。しかしC/C++は違い、申請したリソースは自分で解放しなければ、まるで慢性病のようにゆっくりとプログラムを侵食し、意図せずプログラムに致命的な打撃を与え、そして静かに消滅して途方に暮れてしまいます。対策。自分がプログラムを最初から最後まで担当していれば解決しても大丈夫ですが、他人から引き継いだプログラムに問題があった場合、それを解決するのは体力的にも精神的にも大変ですし、どのくらい毛が抜けるかわかりません。

        しかし、問題は解決されなければならず、問題が解決されて初めて、私たちはよく眠り、よく食べることができるのです。では、メモリリークなどの問題を解決するにはどうすればよいでしょうか?

最初の方法: コードのバックトラッキング

       これは最も簡単で時間を節約できる方法です。数日前までは問題なかったのに、今日になって突然問題が発生する、ということもあり、デバッグの負荷が比較的大きいため、この方法が使えます。バージョン管理システムでは、問題のないバージョンの時点から現在まで、二分法を使用してデバッグ用の履歴コードを取得し、問題の時点​​を見つけて、さらにデバッグします。実際、メモリリークだけでなく、厄介なクラッシュやその他の問題もこれらの方法で解決できます。このようにして、特に他の人のコードを引き継ぐなど、一見面倒に見えるいくつかの問題を解決しました。

2 番目の方法: ログ

        これは、プログラマが最も一般的に使用する方法です。問題が何であっても、特にサーバー プログラムや長時間実行する必要があるプログラムなど、ログを記録する方法を使用して問題を特定できます。ただし、問題を正確に特定することはできません。 、分析問題の範囲を狭めることができます。ログにはメモリ使用量を定期的に出力できます。メモリ リークが発生した場合でも、クエリできるトレースがいくつか残っています。

3 番目の方法: コードのデバッグ

VC のデバッグ モードでは、プログラム エントリに次のコードを追加できます。

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF)

このようにして、プログラムが終了するときに、リークされたコードを出力できます。次のコードを参照してください。

int main()
{
    #ifdef _DEBUG
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
    #endif
    char* p = new char[1000];
    char* q = new char[500];
    return 0;
}

デバッグ後、出力には次が出力されます。

1 つは 1000、もう 1 つは 500 です。しかし、これは漏れがあることだけを知っており、漏れがどこにあるのかは知りません。プログラムがより複雑な場合は、ゆっくりと解析する必要があります。

       新しく書き換えることができます:

#ifdef _DEBUG
#define MY_NEW new(_NORMAL_BLOCK, __FILE__, __LINE__) 
#else
#define MY_NEW new
#endif
 
 
int main()
{
    #ifdef _DEBUG
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
    #endif
    char* p = MY_NEW char[1000];
    char* q = MY_NEW char[500];
    return 0;
}

これにより、割り当てが解放されていない場所がわかります

        ただし、これは私が最初から最後まで参加し、私がメイン開発者であるプロジェクトにのみ適しています。中途半端に引き継いでしまうと調整の負担がかなり大きくなります。しかし、プロジェクトには繰り返しがあり、プロジェクト チームのメンバーはあなただけではないため、誰かがあなたの要件に従わない場合、漏洩のリスクが依然として残ります。

最後にズームインします。WinDbg を使用してデバッグする

       次に、WinDbg を通じてメモリ リークを特定する方法に焦点を当てます。

       WinDbg は、Windows プラットフォームでの非常に強力なデバッグ ツールで、ユーザー モードでプログラムをデバッグできるだけでなく、カーネル モードでもデバッグできます。このツールについてはここではあまり紹介しませんし、オンライン チュートリアルもたくさんありますが、ここでは (私の通常のやり方で) メモリ リークを検出するためにこのツールを使用する方法についてのみ説明します。

        WinDbg をインストールした後、その後の作業を開始する前に、いくつかの簡単な設定を行う必要があります。

        1. WinDbg 拡張コマンド !heap を使用したいのですが、このコマンドは ntdll.dll と kernel32.dll のシンボルをダウンロードする必要があるため、Windows のシンボル サーバーを構成する必要があります。環境変数を追加する必要があります。

変数名は _NT_SYMBOL_PATH_ である必要があります。

変数値は SRV*f:\symbols* http://msdl.microsoft.com/download/symbols です。f:\symbols はシンボルを配置したパスです。もちろん、WinDbg でシンボル サーバーを読み込むコマンドを使用することもできますが、便利ではないと思うので、ここでは紹介しません。ただし、新しいバージョンの WinDbg を使用した後は、設定する必要がないようですが、以前の習慣を尊重するために、追加しましょう。

        2. 次に、プログラムのメモリ割り当てを監視する必要があります。WinDbg ディレクトリには gflags.exe プログラムがあります。コマンド ラインを使用してプログラムを設定します。

Gflags.exe /i F:\Test\Debug\Test.exe +ust

このうち、/i はどのファイルを指定するかを示しており、例えばプログラムが F:\Test\Debug ディレクトリにある場合は、F:\Test\Debug\Test.exe と指定します。+ust は、ユーザーモードのスタック トレース データベースを作成することを意味します。実行後、コマンドラインに次のプロンプトが表示されます。

成功を示します。gflags の詳細設定については、コマンド ラインに gflags.exe /? と入力して確認できます。

(メモリ監視をオンにすると、プログラムが大量のメモリを消費するため、デバッグが完了したら監視をオフにする必要があります。コマンドも非常に簡単です。Gflags.exe /i F:\ Test\Debug\Test.exe -ust、+ ust は -ust に変更できます)

        さて、これでプログラムを調整できるようになりました。

        まず、WinDbg を開き、次に [ファイル]、[実行可能ファイルを開く] メニューからプログラムを開きます。プログラムが実行中の場合は、「ファイル」->「プロセスにアタッチ」を使用してプログラムをマウントします。

         次に、メニューの「ファイル」->「シンボルファイルパス」を使用して、Test.exe (独自のプログラム) のシンボルをロードします。プログラムがデバッグ モードでコンパイルされると、プログラムのデバッグ シンボルを含む PDB ファイルが生成されます。プログラムのシンボルをロードした後、プログラムにブレークポイントを設定したり、シングルステップ操作を実行したりできます。それ以外の場合は、アセンブリ コードに移動してブレークポイントを設定する場所を見つけるか、プログラムにブレークを追加するしかありません。

 シンボル パスはセミコロン「;」で区切られています。ここではパスは (srv*;C:\Users\zy099\source\repos\Test\x64\Debug) で、[OK] をクリックします。次に、WinDbg コマンド ラインで次のように入力します。

.reload /f

シンボルをロードし、シンボルがロードされるまで待ちます。WinDbgコマンドラインでも操作できますが、記憶力があまり良くなく、調べるのも面倒なので、最も簡単な方法を使います。

シンボルサーバーが海外にあるため、中国でのダウンロードは遅くなりますので、しばらく気長に待つ必要があります...

WinDbg コマンド ラインを入力できる場合は、シンボルが正常にダウンロードされたことを意味します。この時点で、「lm」コマンドを使用してシンボルの読み込みステータスを確認します。

ここで、ntdll、KERNEL32、およびテスト プログラムのシンボルがロードされていないことに注意してください。上記のプロンプトに従っていれば、シンボルは正常にロードされています。ntdll と KERNEL32 は主に拡張コマンドを使用する必要があり、Test はプログラムのデバッグに使用されます。

        次に、メニューの [ファイル] -> [ソース ファイルを開く] を使用して、デバッグするソース ファイルを開きます。一般に、シンボルが正しくロードされ、コードに中断がある場合、自動的にソース コードにジャンプします。ここでは手動で開きます。

Test.exe のシンボルが正常に読み込まれた場合は、F9 キーを押して、指定した場所にブレークポイントを設定できます。ここでは、main の戻り時にブレークポイントを設定します。

        この後、プログラムのリーク箇所を確認していきますが、ここではWinDbgの拡張コマンドを使ってみます!時間があればまた紹介したいと思います。

コードは以下のように表示されます:

class Bad
{
public:
    void AllocMemory()
    {
        for (auto i = 0; i < 100; ++i)
        {
            char* p = new char[5000];
        }
    }
};
 
int main()
{
    Bad b;
    b.AllocMemory();
    return 0;
}

これは非常に簡単です。どこにメモリ リークがあるかが一目でわかります。次に、WinDbg がメモリ リークをどのように見つけるかを見てみましょう。

プログラムを実行する前に、ヒープを見てみましょう。

WinDbg コマンド ラインで !heap -s と入力すると、すべてのヒープの概要情報が表示されます。

0:000> !heap -s
       Failed to read heap keySEGMENT HEAP ERROR: failed to initialize the extention
       NtGlobalFlag enables following debugging aids for new heaps:
       stack back traces
       LFH Key                   : 0xe48d63c61a6de263
       Termination on corruption : ENABLED
          Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                            (k)     (k)    (k)     (k) length      blocks cont. heap 
-------------------------------------------------------------------------------------
000001e134530000 08000002    1220     60   1020      2     2     1    0      0   LFH
000001e134500000 08008000      64      4     64      2     1     1    0      0      
-------------------------------------------------------------------------------------

次に、F5 キーを押してプログラムを実行し、ブレークポイントに到達したら停止します。ヒープ情報をもう一度見てみましょう。

0:000> !heap -s
        Failed to read heap keySEGMENT HEAP ERROR: failed to initialize the extention
        NtGlobalFlag enables following debugging aids for new heaps:
        stack back traces
        LFH Key                   : 0xe48d63c61a6de263
        Termination on corruption : ENABLED
          Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                            (k)     (k)    (k)     (k) length      blocks cont. heap 
-------------------------------------------------------------------------------------
000001e134530000 08000002    1220    652   1020     24     8     1    0      0   LFH
000001e134500000 08008000      64      4     64      2     1     1    0      0      
-------------------------------------------------------------------------------------

ここでは、アドレス 0x000001e134530000 のヒープが大幅に増加していることがわかります。コミットは以前は 60K でしたが、現在は 652K になっています。

       次に、コマンド !heap -stat -h 000001e134530000 を使用して表示します。ここで、パラメータ -stat は指定したヒープの使用量統計を表示することを意味し、-h は表示するヒープ アドレスを指定します。ここでは 0x000001e134530000 です。

0:000> !heap -stat -h 000001e134530000
     heap @ 000001e134530000
 group-by: TOTSIZE max-display: 20
    size     #blocks     total     ( %) (percent of total busy bytes)
    13bc 64 - 7b570  (90.02)
    1cf0 1 - 1cf0  (1.32)
    30 8d - 1a70  (1.21)
    1234 1 - 1234  (0.83)
    1034 1 - 1034  (0.74)
    df4 1 - df4  (0.64)
    400 2 - 800  (0.36)
    100 8 - 800  (0.36)
    7c4 1 - 7c4  (0.35)
    7a2 1 - 7a2  (0.35)
    138 6 - 750  (0.33)
    390 2 - 720  (0.33)
    695 1 - 695  (0.30)
    628 1 - 628  (0.28)
    1d8 3 - 588  (0.25)
    25c 2 - 4b8  (0.22)
    470 1 - 470  (0.20)
    168 2 - 2d0  (0.13)
    50 8 - 280  (0.11)
    238 1 - 238  (0.10)

サイズ 0x13bc の 0x64 ブロックがあり、合計サイズは 0x7B570 で、使用中のブロック全体の 90.02% を占めていることがわかります。これらのブロックが漏洩したブロックであると考えられます。

       次に、これらのブロックのアドレスを取得します。コマンド !heap -flt s 13bc を使用します。このうち -flt は表示範囲を指定サイズまたはサイズ範囲のヒープに限定し、パラメータ s 13bc は指定サイズ 0x13bc のブロックになります。

0:000> !heap -flt s 13bc
    _HEAP @ 1e134530000
              HEAP_ENTRY Size Prev Flags            UserPtr UserSize - state
        000001e134546ce0 013f 0000  [00]   000001e134546d10    013bc - (busy)
          unknown!noop
        000001e1345480d0 013f 013f  [00]   000001e134548100    013bc - (busy)
        000001e1345494c0 013f 013f  [00]   000001e1345494f0    013bc - (busy)
        000001e13454a8b0 013f 013f  [00]   000001e13454a8e0    013bc - (busy)
        000001e13454bca0 013f 013f  [00]   000001e13454bcd0    013bc - (busy)
        000001e13454d090 013f 013f  [00]   000001e13454d0c0    013bc - (busy)
        000001e13454e480 013f 013f  [00]   000001e13454e4b0    013bc - (busy)
        000001e13454f870 013f 013f  [00]   000001e13454f8a0    013bc - (busy)
        000001e134550c60 013f 013f  [00]   000001e134550c90    013bc - (busy)
        000001e134552050 013f 013f  [00]   000001e134552080    013bc - (busy)
          unknown!noop
        000001e134553440 013f 013f  [00]   000001e134553470    013bc - (busy)
        000001e134554830 013f 013f  [00]   000001e134554860    013bc - (busy)
          unknown!printable
        000001e134555c20 013f 013f  [00]   000001e134555c50    013bc - (busy)
          unknown!printable

ここではデータの一部だけを抜粋しましたが、実際にはかなり長いです。ここでは、ビジー状態にあるヒープ ブロックが多数見られます。これらのヒープ ブロックは解放されていないメモリ空間であるはずです。

       !heap -p -a 000001e134546ce0 を使用して呼び出しスタックを出力します。

0:000> !heap -p -a 000001e134546ce0 
    address 000001e134546ce0 found in
    _HEAP @ 1e134530000
              HEAP_ENTRY Size Prev Flags            UserPtr UserSize - state
        000001e134546ce0 013f 0000  [00]   000001e134546d10    013bc - (busy)
          unknown!noop
        7ff9c9d3d6c3 ntdll!RtlpAllocateHeapInternal+0x00000000000947d3
        7ff9730dd480 ucrtbased!heap_alloc_dbg_internal+0x0000000000000210
        7ff9730dd20d ucrtbased!heap_alloc_dbg+0x000000000000004d
        7ff9730e037f ucrtbased!_malloc_dbg+0x000000000000002f
        7ff9730e0dee ucrtbased!malloc+0x000000000000001e
        7ff60b1c1f73 Test!operator new+0x0000000000000013
        7ff60b1c19f3 Test!operator new[]+0x0000000000000013
        7ff60b1c1e10 Test!Bad::AllocMemory+0x0000000000000040
        7ff60b1c4746 Test!main+0x0000000000000046
        7ff60b1c1eb9 Test!invoke_main+0x0000000000000039
        7ff60b1c1d5e Test!__scrt_common_main_seh+0x000000000000012e
        7ff60b1c1c1e Test!__scrt_common_main+0x000000000000000e
        7ff60b1c1f4e Test!mainCRTStartup+0x000000000000000e
        7ff9c83354e0 KERNEL32!BaseThreadInitThunk+0x0000000000000010
        7ff9c9c8485b ntdll!RtlUserThreadStart+0x000000000000002b

ここでは、このヒープの呼び出しスタック Test!Bad::AllocMemory が表示されます。これは実際に、割り当てられたものの解放されていないメモリ空間です。これは、このヒープ ブロックによって割り当てられたスタック情報であり、この情報を通じて、このメモリ ブロックがどこに割り当てられているかを特定し、対応する関数で分析することができます。

        実際のプロジェクトでは、状況はこれほど単純ではなく、プリントされたヒープ情報に長いリストが含まれる場合があるため、これらの情報の中から有用な情報を見つける必要があります。デバッグは頭の痛い作業ですが、難しい問題が解決されると、やはり達成感があります。

最後に書きます

C++ プログラマにとって、メモリ使用量を予想外に抑えることは実際には簡単ではないようです。ただし、C++ スマート ポインターの使用、メモリを均一に管理するメモリ プール テクノロジの使用など、メモリ リークのリスクを軽減するいくつかの方法を使用しています。

おすすめ

転載: blog.csdn.net/chenlycly/article/details/131578494