1 つの記事の分析 - Linux のメモリ リーク検出方法を例を通じて説明

1. mtrace はメモリ リークを分析します

mtrace (メモリ トレース) は、GNU Glibc に付属するメモリ問題検出ツールで、メモリ リークの問題を特定するのに役立ちます。その実装ソース コードは glibc ソース コードの malloc ディレクトリにあります。その基本的な設計原則は、関数 void mtrace () を設計することです。この関数は、libc ライブラリ内の malloc/free およびその他の関数の呼び出しを追跡し、それによって、libc ライブラリに存在するかどうかを検出します。メモリリークの状態です。mtrace は C 関数であり、<mcheck.h> で宣言および定義されています。関数のプロトタイプは次のとおりです。

void mtrace(void);

mtrace の原理

mtrace() この関数は、動的メモリ割り当てに関連する関数 (malloc()、realloc()、memalign()、free() など) の「フック」関数をインストールします。これらのフック関数は、関連するすべてのメモリ割り当てとリリースされた追跡情報を記録します。 、および muntrace() は、対応するフック関数をアンロードします。

これらのフック関数によって生成されるデバッグトレース情報に基づいて、「メモリリーク」などの問題が発生していないかを分析できます。

ログ生成パスの設定

mtrace メカニズムでは、トレース ログを生成する前にプログラムを実際に実行する必要がありますが、プログラムを実際に実行する前に行うべきもう 1 つの作業は、ログ ファイルを生成するパスを mtrace (前述のフック関数) に伝えることです。

ログ生成パスを設定するには 2 つの方法があり、1 つは環境変数を設定する方法、もうexport MALLOC_TRACE=./test.log // 当前目录下 1 つはコード レベルで設定する方法 (setenv("MALLOC_TRACE", "output_file_name", 1);``output_file_name検出結果を保存するファイルの名前) です。

テスト例

#include <mcheck.h>
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char **argv)
{
    mtrace();  // 开始跟踪

    char *p = (char *)malloc(100);
    free(p);
    p = NULL;
    p = (char *)malloc(100);

    muntrace();   // 结束跟踪,并生成日志信息
    return 0;
}

上記のコードから、プログラムの最初から最後までメモリ リークが発生しているかどうかを確認できればと考えています。例は簡単です。メモリ リークがあることが一目でわかります。 mtrace がメモリ リークをチェックできるかどうかを確認し、結果を分析して配置する方法を確認します。 gcc -g test.c -o test実行可能ファイルを生成します。

ログ

プログラムの実行が完了すると、現在のディレクトリに test.log ファイルが生成され、それを開くと、次の内容が表示されます。

= Start
@ ./test:[0x400624] + 0x21ed450 0x64
@ ./test:[0x400634] - 0x21ed450
@ ./test:[0x400646] + 0x21ed450 0x64
= End

このファイルから、中央の 3 行がソース コードの malloc -> free -> malloc 操作に対応していることがわかります。解釈: ./test は実行するプログラムの名前を指します、[0x400624] は malloc 関数への最初の呼び出しのマシンコード内のアドレス情報です、+ はメモリの適用を意味します (- は解放を​​意味します)、0x21ed450 はアドレスですmalloc 関数によって適用される情報。0x64 は要求されたメモリ サイズを表します。この分析から、最初のアプリケーションはリリースされましたが、2 番目のアプリケーションはリリースされておらず、メモリ リークの問題が発生しています。

リーク分析

addr2line ツールを使用してソース コードの場所を特定します。

「addr2line」コマンド ツールを使用すると、ソース ファイルの行番号を取得できます (これを使用して、マシン コード アドレスに基づいて特定のソース コードの場所を見つけることができます)。

# addr2line -e test 0x400624
/home/test.c:9

mtrace ツールを使用してログ情報を分析する

mtrace + 実行ファイルパス + ログファイルパス mtrace test ./test.logが実行され、以下の情報が出力されます。

Memory not freed:
-----------------
           Address     Size     Caller
0x00000000021ed450     0x64  at /home/test.c:14

2. Valgrind がメモリ リークを分析する

Valgrindツールの紹介

Valgrind は、Linux 上のオープン ソース (GPL V2) シミュレーションおよびデバッグ ツールのコレクションです。Valgrind は、コアと、コアに基づくその他のデバッグ ツールで構成されます。カーネルは、CPU 環境をシミュレートし、他のツールにサービスを提供するフレームワークに似ています。他のツールはプラグインに似ており、カーネルによって提供されるサービスを使用して、さまざまな特定のメモリ デバッグ タスクを実行します。Valgrind のアーキテクチャを次の図に示します。

1、メムチェック

最も一般的に使用されるツールは、プログラム内のメモリの問題を検出するために使用され、メモリに対するすべての読み取りと書き込みが検出され、malloc() / free() / new / delete のすべての呼び出しがキャプチャされます。

したがって、次の問題を検出できます: 未初期化メモリの使用、解放されたメモリ ブロックの読み取り/書き込み、malloc 割り当てを超えたメモリ ブロックの読み取り/書き込み、スタック内の不適切なメモリ ブロックの読み取り/書き込み、ブロックを指すメモリ リーク、メモリ ポインタ永久に失われる、間違った malloc/free または new/delete の一致、memcpy() 関連関数の dst および src ポインタが重複する。

2、コールグラインド

gprof に似た分析ツールですが、プログラムの実行をより詳細に観察し、より多くの情報を提供します。gprof とは異なり、ソース コードをコンパイルするときに特別なオプションは必要ありませんが、デバッグ オプションを追加することをお勧めします。

Callgrind は、プログラムの実行中にデータを収集し、関数呼び出しグラフを構築し、オプションでキャッシュ シミュレーションを実行できます。実行の最後に、分析データがファイルに書き込まれます。callgrind_annotate は、このファイルの内容を読み取り可能な形式に変換できます。

3、キャッシュグラインド

キャッシュ アナライザは、CPU 内の 1 次キャッシュ I1、D1、および 2 次キャッシュをシミュレートし、プログラム内のキャッシュ ミスとヒットを正確に指摘できます。必要に応じて、キャッシュ ミスの数、メモリ参照の数、コードの各行、各関数、各モジュール、およびプログラム全体によって生成された命令の数も提供できます。これはプログラムを最適化するのに非常に役立ちます。

4、ヘルグラインド

主にマルチスレッドプログラムで発生する競合問題をチェックするために使用されます。Helgrind は、複数のスレッドによってアクセスされ、一貫してロックされていないメモリ領域を探します。これらの領域はスレッド間の同期が失われることが多く、発見が難しいエラーにつながる可能性があります。

Helgrind は、「Eraser」と呼ばれる競合検出アルゴリズムを実装し、報告されるエラーの数を減らすためにさらなる改良を加えました。ただし、Helgrind はまだ実験段階にあります。

5、山塊

スタック アナライザーは、プログラムがスタック内で使用するメモリの量を測定し、ヒープ ブロック、ヒープ管理ブロック、およびスタックのサイズを示します。

Massif はメモリ使用量の削減に役立ち、仮想メモリを備えた最新のシステムでは、プログラムの実行速度を向上させ、プログラムがスワップ領域に留まる可能性を減らすこともできます。

さらに、レイキーとヌルグラインドも提供されます。Lackey はめったに使用されない小さなツールですが、Nulgrind は開発者にツールの作成方法を示すだけです。

 Information Direct: Linux カーネル ソース コード テクノロジ学習ルート + ビデオ チュートリアル カーネル ソース コード

Learning Express: Linux カーネル ソース コード メモリ チューニング ファイル システム プロセス管理 デバイス ドライバー/ネットワーク プロトコル スタック

メムチェックの原理

この記事はメモリ リークの検出に焦点を当てているため、valgrind の他のツールについてはあまり説明せず、主に Memcheck の動作について説明します。Memcheck がメモリの問題を検出する原理を次の図に示します。

Memcheck がメモリの問題を検出できる鍵となるのは、2 つのグローバル テーブルを作成することです。

  • Valid-Value テーブルには、プロセスのアドレス空間全体の各バイトに対応する 8 ビットがあり、CPU の各レジスタに対応するビット ベクトルもあります。これらのビットは、バイトまたはレジスタ値が有効な初期化された値を持つかどうかを記録する役割を果たします。
  • Valid-Address テーブルには、プロセスのアドレス空間全体の各バイトに対応するビットがあり、アドレスが読み取り可能か書き込み可能かを記録する役割を果たします。
  • 検出原理: メモリ内のバイトを読み書きしたい場合は、まず、このバイトに対応する有効アドレス テーブルの A ビットをチェックします。A ビットがその位置が無効な位置であることを示している場合、memcheck は読み取りおよび書き込みエラーを報告します。コアは仮想 CPU 環境に似ているため、メモリ内の特定のバイトが実際の CPU にロードされると、そのバイトに対応する有効値テーブルの V ビットも仮想 CPU 環境にロードされます。レジスタ内の値がメモリ アドレスの生成に使用されるか、値がプログラム出力に影響を与える可能性がある場合、memcheck は対応する V ビットをチェックします。値が初期化されていない場合は、初期化されていないメモリ エラーが報告されます。

メモリリークの種類

valgrind はメモリ リークを 4 つのカテゴリに分類します。

  • 確実に失われた: メモリは解放されていませんが、メモリを指すポインタがなく、メモリにアクセスできません。リークした実行メモリにはパッチを適用することが強く必要であることが確認されています。
  • 間接リーク(間接消失):リークしたメモリにはリークしたメモリポインタが格納されており、リークしたメモリにはアクセスできないため、間接リークの原因となったメモリにはアクセスできません。例えば:
struct list {
 struct list *next;
};

int main(int argc, char **argv)
{
 struct list *root;
 root = (struct list *)malloc(sizeof(struct list));
 root->next = (struct list *)malloc(sizeof(struct list));
 printf("root %p roop->next %p\n", root, root->next);
 root = NULL;
 return 0;
}

ここで欠けているのはルート ポインタ (確立されたリーク タイプ) であり、ルートに格納されている次のポインタが間接リークになります。間接的にリークしたメモリには必ずパッチを適用する必要がありますが、通常は確立されたリークのパッチと同時にパッチが適用されます。

  • 紛失した可能性があります。針はメモリ ヘッダー アドレスではなく、メモリ内の位置を指しています。Valgrind は、針が既にバイアスされており、メモリ ヘッドに向かってバイアスされていないものの、メモリの内部部分にバイアスがかかっているため、リークが発生しているのではないかと疑うことがよくあります。このプログラムは、たとえばメモリ アライメントを達成するために、アライメントされたメモリ アドレスに追加のアプリケーション処理メモリが返されるように設計されているため、場合によっては、これはリークではありません。
  • まだ到達可能: ポインタは常に存在し、メモリの上部に向かって傾いており、プログラムが終了するまでメモリは解放されません。

Valgrindパラメータ設定

  • --leak-check=<no|summary|yes|full> yes または full に設定すると、呼び出されたプログラムの終了後、valgrind は各メモリ リークを詳細に説明します。デフォルトは概要で、いくつかのメモリ リークのみを報告します。
  • --log-fd= [デフォルト: 2、stderr] valgrind はログを出力し、指定されたファイルまたはファイル記述子にダンプします。このパラメータを指定しないと、valgrind のログがユーザー プログラムのログと一緒に出力され、非常に乱雑に表示されます。
  • --trace-children=<yes | no> [デフォルト: no] 子プロセスを追跡するかどうか、マルチプロセス プログラムの場合は、この機能を使用することをお勧めします。ただし、単一のプロセスが有効になっている場合は、大きな影響はありません。
  • --keep-debuginfo=<yes | no> [デフォルト: no] プログラムが動的にロードされるライブラリ (dlopen) を使用している場合、動的ライブラリがアンロードされる (dlclose) ときにデバッグ情報がクリアされます。このオプションを有効にすると、ダイナミック ライブラリがアンロードされてもコール スタック情報が保持されます。
  • --keep-stacktraces=<alloc | free | alloc-and-free | alloc-then-free | none> [デフォルト: alloc-and-free] メモリ リークは、アプリケーションとリリース、および関数呼び出しの不一致にすぎません。スタックは適用時に記録するか、リリース申請時に記録するだけです。メモリ リークだけに注目する場合、実際にはリリース申請時に両方を記録する必要はありません。これは、多くの余分なメモリと CPU 消費量を占有することになるためです。ただでさえ遅い実行に、プログラムはさらなる侮辱を加える。
  • --freelist-vol= クライアント プログラムが free または delete を使用してメモリ ブロックを解放する場合、メモリ ブロックはすぐには再割り当てに使用できなくなります。メモリ ブロックは空きブロック キュー (フリーリスト) に配置され、使用不可としてマークされるだけです。これは、非常に重要な時間が経過した後にクライアント プログラムが解放されたブロックにアクセスするときのエラーを検出するのに役立ちます。このオプションは、キューが占めるバイト ブロック サイズを指定します。デフォルトは 20MB です。このオプションを増やすと、memcheck のメモリ オーバーヘッドが増加しますが、そのようなエラーを検出する機能も向上します。
  • --freelist-big-blocks= 再割り当てのためにフリーリスト キューから利用可能なメモリ ブロックを取得するとき、memcheck は、number より大きいメモリ ブロックから優先順位に従ってブロックを取得します。このオプションにより、フリーリスト内の小さなメモリ ブロックへの頻繁な呼び出しが防止され、小さなメモリ ブロックに対するワイルド ポインタ エラーが検出される可能性が高まります。このオプションを 0 に設定すると、すべてのブロックが先入れ先出しベースで再割り当てされます。デフォルトは 1M です。参考:valgrind(メモリチェックツール)の紹介

推奨されるコンパイルパラメータ

問題が発生したときにデスタック情報を詳細に出力するには、実際にはプログラムのコンパイル時に -g オプションを追加するのが最善です。動的にロードされるライブラリがある場合は追加する必要があります --keep-debuginfo=yes が、動的にロードされるライブラリのリークが判明した場合、動的ライブラリがアンインストールされているためシンボル テーブルが見つかりません。コード コンパイラの最適化では、-O2 以降の使用は推奨されません。-O0 を指定すると動作が遅くなる可能性があるため、-O1 を使用することをお勧めします。

検出例の説明

メモリを解放せずに申請する

#include <stdlib.h>
#include <stdio.h>
void func()
{
  //只申请内存而不释放
    void *p=malloc(sizeof(int));
}
int main()
{
    func();
    return 0;
}

valgrindコマンドを使用してプログラムを実行し、ログをファイルに出力します。

valgrind --log-file=valReport --leak-check=full --show-reachable=yes --leak-resolution=low ./a.out

パラメータの説明:

  • –log-file=valReport は、現在の実行ディレクトリに分析ログ ファイルを生成することを指定します。ファイル名は valReport です。
  • –leak-check=full は各リークの詳細を表示します
  • –show-reachable=yes グローバル ポインタ、静的ポインタなどの制御範囲外のリークを検出し、すべてのメモリ リーク タイプを表示するかどうか
  • –leak-resolution=低メモリ リーク レポートのマージ レベル
  • –track-origins=yes は、「未初期化メモリの使用」検出機能をオンにし、詳細な結果を開くことを意味します。該当する文がない場合、デフォルトでこの領域の検出が行われますが、詳細な結果は出力されません。出力実行後、レポートが解釈されます。54017 はプロセス番号を示します。プログラムが複数のプロセスを使用して実行される場合、複数のプロセスの内容が表示されます。
==54017== Memcheck, a memory error detector
==54017== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==54017== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==54017== Command: ./a.out
==54017== Parent PID: 52130

2 番目の段落はヒープ メモリ割り当ての概要で、プログラムがメモリを 1 回申請し、そのうち 0 バイトが解放され、4 バイトが割り当てられたことが記載されています ( ) 1 allocs, 0 frees, 4 bytes allocated

ヘッドサマリーには、プログラムが使用するヒープメモリの総量、メモリ割り当て回数、メモリ解放回数が表示されます。メモリ割り当て回数とメモリ解放回数が一致しない場合は、メモリ不足が発生していることを意味します。メモリリーク。

==54017== HEAP SUMMARY:
==54017==   in use at exit: 4 bytes in 1 blocks
==54017==   total heap usage: 1 allocs, 0 frees, 4 bytes allocated

3 番目の段落では、メモリ リークの具体的な情報が説明されています。4 バイト ( ) を占有するメモリの一部があります4 bytes in 1 blocks。これは、malloc を呼び出すことによって割り当てられます。コール スタックでは、func 関数が最終的に malloc を呼び出したことがわかります。したがって、この情報はこれにより、リークされたメモリがどこに適用されているかが特定されます。

==54017== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==54017==    at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==54017==    by 0x40057E: func() (in /home/oceanstar/CLionProjects/Share/src/a.out)
==54017==    by 0x40058D: main (in /home/oceanstar/CLionProjects/Share/src/a.out)

最後の段落は要約であり、4 バイトのメモリ リークです。

==54017== LEAK SUMMARY:
==54017==    definitely lost: 4 bytes in 1 blocks  // 确立泄露
==54017==    indirectly lost: 0 bytes in 0 blocks  // 间接性泄露
==54017==    possibly lost: 0 bytes in 0 blocks   // 很有可能泄露
==54017==    still reachable: 0 bytes in 0 blocks // 仍可访达
==54017==    suppressed: 0 bytes in 0 blocks

境界を超えた読み書き

#include <stdio.h>
#include <iostream>
int main()
{
    int len = 5;
    int *pt = (int*)malloc(len*sizeof(int)); //problem1: not freed
    int *p = pt;
    for (int i = 0; i < len; i++){
        p++;
    }
    *p = 5; //problem2: heap block overrun
    printf("%d\n", *p); //problem3: heap block overrun
    // free(pt);
    return 0;
}

問題 1: ポインタ pt が空間に適用されましたが、解放されませんでした; 問題 2: pt が 5 int の空間に適用され、5 サイクル後に p が p[5] の位置に到達したとき、アクセスが範囲外になりました (書き込みは範囲外)  *p = 5(以下の valgrind レポートのサイズ 4 の無効な書き込み)

==58261== Invalid write of size 4
==58261==    at 0x400707: main (main.cpp:12)
==58261==  Address 0x5a23054 is 0 bytes after a block of size 20 alloc'd
==58261==    at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==58261==    by 0x4006DC: main (main.cpp:7)

問題 1: 範囲外の読み取り (以下の valgrind レポートのサイズ 4 の無効な読み取り)

==58261== Invalid read of size 4
==58261==    at 0x400711: main (main.cpp:13)
==58261==  Address 0x5a23054 is 0 bytes after a block of size 20 alloc'd
==58261==    at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==58261==    by 0x4006DC: main (main.cpp:7)

繰り返し放出

#include <stdio.h>
#include <iostream>
int main()
{
    int *x;
    x = static_cast<int *>(malloc(8 * sizeof(int)));
    x = static_cast<int *>(malloc(8 * sizeof(int)));
    free(x);
    free(x);
    return 0;
}

報告書は以下の通りです。Invalid free() / delete / delete[] / realloc()

==59602== Invalid free() / delete / delete[] / realloc()
==59602==    at 0x4C2B06D: free (vg_replace_malloc.c:540)
==59602==    by 0x4006FE: main (main.cpp:10)
==59602==  Address 0x5a230a0 is 0 bytes inside a block of size 32 free'd
==59602==    at 0x4C2B06D: free (vg_replace_malloc.c:540)
==59602==    by 0x4006F2: main (main.cpp:9)
==59602==  Block was alloc'd at
==59602==    at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==59602==    by 0x4006E2: main (main.cpp:8)

アプリケーションのリリース インターフェイスが一致しません

アプリケーションとリリースのインターフェイスの不一致のレポートは次のとおりです。malloc を使用してスペースを適用するポインタは free を使用して解放され、new を使用して適用されたスペースは delete( ) を使用して解放されますMismatched free() / delete / delete []

==61950== Mismatched free() / delete / delete []
==61950==    at 0x4C2BB8F: operator delete[](void*) (vg_replace_malloc.c:651)
==61950==    by 0x4006E8: main (main.cpp:8)
==61950==  Address 0x5a23040 is 0 bytes inside a block of size 5 alloc'd
==61950==    at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==61950==    by 0x4006D1: main (main.cpp:7)

メモリ上書き

int main()
{
    char str[11];
    for (int i = 0; i < 11; i++){
        str[i] = i;
    }
    memcpy(str + 1, str, 5);
    char x[5] = "abcd";
    strncpy(x + 2, x, 3);
}

問題は memcpy にあり、str ポインタの位置から 5 文字を str+1 が指す領域にコピーすると、メモリの上書きが発生します。strncpy についても同様です。報告書は次のとおりですSource and destination overlap

==61609== Source and destination overlap in memcpy(0x1ffefffe31, 0x1ffefffe30, 5)
==61609==    at 0x4C2E81D: memcpy@@GLIBC_2.14 (vg_replace_strmem.c:1035)
==61609==    by 0x400721: main (main.cpp:11)
==61609== 
==61609== Source and destination overlap in strncpy(0x1ffefffe25, 0x1ffefffe23, 3)
==61609==    at 0x4C2D453: strncpy (vg_replace_strmem.c:552)
==61609==    by 0x400748: main (main.cpp:14)

3. まとめ

メモリ検出方法は 2 つあります。

1. メモリ操作リンク リストを維持し、メモリ アプリケーション操作がある場合はこのリンク リストに追加され、解放操作がある場合はアプリケーション操作からリンク リストから削除されます。プログラム終了後もリンク リストに内容が残っている場合はメモリ リークが発生していることを意味し、解放されるメモリ操作がリンク リスト内で対応する操作が見つからない場合は、複数回解放されたことを意味します。 。この方法は、組み込みのデバッグ ツール、Visual Leak Detecter、mtrace、memwatch、debug_new とともに使用します。2. プロセスのアドレス空間をシミュレートします。オペレーティング システムによるプロセス メモリ操作の処理に続いて、アドレス空間マッピングがユーザー モードで維持されますが、この方法にはプロセス アドレス空間の処理についての深い理解が必要です。Windows のプロセス アドレス空間配布はオープン ソースではないため、シミュレーションが困難なため、Linux でのみサポートされています。このアプローチを採用しているのが valgrind です。

原著者:一緒に埋め込まれた学習

おすすめ

転載: blog.csdn.net/youzhangjing_/article/details/132817245
おすすめ