valgrind を使用してメモリ リークを見つけるにはどうすればいいですか? 質問する

valgrind を使用してメモリ リークを見つけるにはどうすればいいですか? 質問する

valgrind を使用してプログラム内のメモリ リークを見つけるにはどうすればよいですか?

私は Ubuntu 10.04 を使用しており、プログラムがありますa.c

ベストアンサー1

Valgrindの実行方法

まず、Valgrind がインストールされているかどうかを確認します。インストールされていない場合は、次の手順を実行します。

sudo apt install valgrind  # Ubuntu, Debian, etc.
sudo yum install valgrind  # RHEL, CentOS, Fedora, etc.
sudo pacman -Syu valgrind  # Arch, Manjaro, Garuda, etc.
sudo pkg ins valgrind      # FreeBSD

ValgrindはC/C++コードですぐに使用できますが、適切に設定すれば他の言語でも使用できます(これPython の場合)。

Valgrind を実行するには、実行可能ファイルを引数として渡します (プログラムへのパラメータも渡します)。

valgrind --leak-check=full \
         --show-leak-kinds=all \
         --track-origins=yes \
         --verbose \
         --log-file=valgrind-out.txt \
         ./executable exampleParam1

フラグは、簡単に言うと次のとおりです。

  • --leak-check=full: 「個々のリークが詳細に表示されます」
  • --show-leak-kinds=all: 「完全」レポートに「明確、間接、可能性、到達可能」のすべてのリークの種類を表示します。
  • --track-origins=yes: 速度よりも有用な出力を優先します。これは初期化されていない値の発生源を追跡します。これはメモリ エラーの検出に非常に役立ちます。Valgrind が許容できないほど遅い場合は、オフにすることを検討してください。
  • --verbose: プログラムの異常な動作について通知します。より詳細な情報を表示するにはこれを繰り返します。
  • --log-file: ファイルに書き込みます。出力が端末のスペースを超える場合に便利です。

最後に、次のような Valgrind レポートを確認します。

HEAP SUMMARY:
    in use at exit: 0 bytes in 0 blocks
  total heap usage: 636 allocs, 636 frees, 25,393 bytes allocated
 
All heap blocks were freed -- no leaks are possible
 
ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

水漏れがありますが、どこでしょうか?

つまり、メモリ リークが発生しており、Valgrind は何も意味のあることを言っていません。おそらく、次のような内容です。

5 bytes in 1 blocks are definitely lost in loss record 1 of 1
   at 0x4C29BE3: malloc (vg_replace_malloc.c:299)
   by 0x40053E: main (in /home/Peri461/Documents/executable)

私が書いた C コードも見てみましょう:

#include <stdlib.h>

int main() {
    char* string = malloc(5 * sizeof(char)); //LEAK: not freed!
    return 0;
}

5 バイトが失われました。どうしてそうなったのでしょうか。エラー レポートには と とだけ書かれていますmainmalloc大規模なプログラムでは、これを突き止めるのは非常に困難です。これは、実行可能ファイルがコンパイルされた方法によるものです。実際に、何が間違っていたのかを行ごとに詳しく知ることができます。デバッグ フラグを使用してプログラムを再コンパイルします (ここでは以下を使用していますgcc)。

gcc -o executable -std=c11 -Wall main.c         # suppose it was this at first
gcc -o executable -std=c11 -Wall -ggdb3 main.c  # add -ggdb3 to it

このデバッグ ビルドでは、Valgrind はリークされたメモリを割り当てるコードの正確な行を示します。(言葉遣いは重要です。リークが正確にどこで発生しているかではなく、何がリークされたかがわかる可能性があります。トレースは、リークがどこにあるかを見つけるのに役立ちます。)

5 bytes in 1 blocks are definitely lost in loss record 1 of 1
   at 0x4C29BE3: malloc (vg_replace_malloc.c:299)
   by 0x40053E: main (main.c:4)

-ggdb3バイナリに詳細なデバッグ注釈が含まれ、Valgrind はこれを使用してより正確な情報を提供できます。この方法でコンパイルすると、GCC はデフォルトの最適化レベル ( -O2) を使用し、コードを変換して効率を改善します。場合によっては、詳細に調査したいコードの一部が最適化によって削除されたり、他の意外な結果が生じたりすることがあります。そのような場合は、-Og代わりに フラグを試してみるとよいでしょう。このフラグは詳細なデバッグ注釈を含め抑制されたデバッグに適した最適化のみを使用するため、コンパイルされたバイナリは記述したものとより一致するようになります。これは次のように実行できます。

gcc -o executable -std=c11 -Wall -Og main.c  # add -Og

これらのデバッグオプションやその他のデバッグオプションについては、3.10 プログラムのデバッグオプションGCCドキュメントの を-Og参照してください。最適化に及ぼす影響の詳細については、3.11 最適化を制御するオプション


メモリリークとエラーをデバッグするテクニック

  • 活用するcppreference! CとC++の関数に関する優れたドキュメントがあります。ホームページ

  • メモリリークに関する一般的なアドバイス:

  • 可能であれば、RAII を使用すると、ほとんどの問題は解決します。

  • 動的に割り当てられたメモリが実際に解放されることを確認してください。

  • メモリを割り当てず、ポインターを割り当てるのを忘れないでください。

  • 古いメモリが解放されない限り、ポインタを新しいポインタで上書きしないでください。

  • メモリエラーに関する一般的なアドバイス:

  • 自分に属することが確実なアドレスとインデックスにアクセスして書き込みます。メモリ エラーはリークとは異なります。多くの場合、単なるIndexOutOfBoundsException型の問題です。

  • メモリを解放した後は、メモリにアクセスしたり、書き込んだりしないでください。

  • IDE が閉じ括弧がまだ入力されていないことを検出するのと同じように、リークやエラーが互いにリンクしている場合があります。1 つの問題を解決すると他の問題も解決できるため、原因になりそうな問題を探して、次のアイデアをいくつか適用してください。

  • メモリ エラーのある「問題の」コードに依存する/依存しているコード内の関数をリストします。プログラムの実行を追跡し (場合によっては実行中もgdb)、前提条件/事後条件エラーを探します。割り当てられたメモリの有効期間に焦点を当てながら、プログラムの実行を追跡することが目的です。

  • 「問題のある」コード ブロックをコメント アウトしてみてください (コードがコンパイルされるよう、妥当な範囲内で)。Valgrind エラーが消えれば、問題の場所がわかります。

  • それでもダメなら、調べてみてください。Valgrindはドキュメンテーションあまりにも!


よくある漏れとエラーについて

指示に注意してください

60 bytes in 1 blocks are definitely lost in loss record 1 of 1
   at 0x4C2BB78: realloc (vg_replace_malloc.c:785)
   by 0x4005E4: resizeArray (main.c:12)
   by 0x40062E: main (main.c:19)

コードは次の通りです:

#include <stdlib.h>
#include <stdint.h>

struct _List {
    int32_t* data;
    int32_t length;
};
typedef struct _List List;

List* resizeArray(List* array) {
    int32_t* dPtr = array->data;
    dPtr = realloc(dPtr, 15 * sizeof(int32_t)); //doesn't update array->data
    return array;
}

int main() {
    List* array = calloc(1, sizeof(List));
    array->data = calloc(10, sizeof(int32_t));
    array = resizeArray(array);

    free(array->data);
    free(array);
    return 0;
}

ティーチング アシスタントとして、私はこの間違いを何度も目にしてきました。学生はローカル変数を使用して、元のポインターを更新することを忘れています。ここでのエラーは、realloc割り当てられたメモリを実際に別の場所に移動し、ポインターの位置を変更できることに気付いたことです。その後、配列がどこに移動されたかresizeArrayを伝えずに終了します。array->data

無効な書き込み

1 errors in context 1 of 1:
Invalid write of size 1
   at 0x4005CA: main (main.c:10)
 Address 0x51f905a is 0 bytes after a block of size 26 alloc'd
   at 0x4C2B975: calloc (vg_replace_malloc.c:711)
   by 0x400593: main (main.c:5)

コードは次の通りです:

#include <stdlib.h>
#include <stdint.h>

int main() {
    char* alphabet = calloc(26, sizeof(char));

    for(uint8_t i = 0; i < 26; i++) {
        *(alphabet + i) = 'A' + i;
    }
    *(alphabet + 26) = '\0'; //null-terminate the string?

    free(alphabet);
    return 0;
}

Valgrind が上記のコメント行のコードを指し示していることに注目してください。サイズ 26 の配列は [0,25] のインデックスが付けられているため、*(alphabet + 26)無効な書き込みとなります。つまり、範囲外です。無効な書き込みは、off-by-one エラーの一般的な結果です。代入演算の左側を見てください。

無効な読み取り

1 errors in context 1 of 1:
Invalid read of size 1
   at 0x400602: main (main.c:9)
 Address 0x51f90ba is 0 bytes after a block of size 26 alloc'd
   at 0x4C29BE3: malloc (vg_replace_malloc.c:299)
   by 0x4005E1: main (main.c:6)

コードは次の通りです:

#include <stdlib.h>
#include <stdint.h>

int main() {
    char* destination = calloc(27, sizeof(char));
    char* source = malloc(26 * sizeof(char));

    for(uint8_t i = 0; i < 27; i++) {
        *(destination + i) = *(source + i); //Look at the last iteration.
    }

    free(destination);
    free(source);
    return 0;
}

Valgrind は、上記のコメント行を指摘します。ここで最後の反復、 を見てください
*(destination + 26) = *(source + 26);。ただし、*(source + 26)は無効な書き込みと同様に、再び範囲外です。無効な読み取りも、off-by-one エラーの一般的な結果です。代入演算の右側を見てください。


オープンソース(U/Dys)トピア

リークが自分のものであるかどうかは、どうすればわかりますか? 他の人のコードを使用しているときに、自分のリークを見つけるにはどうすればよいですか? 自分のものではないリークを見つけました。何かすべきでしょうか? これらはすべて正当な質問です。まず、一般的な 2 つの遭遇を示す 2 つの実際の例を示します。

ヤンソン: JSONライブラリ

#include <jansson.h>
#include <stdio.h>

int main() {
    char* string = "{ \"key\": \"value\" }";

    json_error_t error;
    json_t* root = json_loads(string, 0, &error); //obtaining a pointer
    json_t* value = json_object_get(root, "key"); //obtaining a pointer
    printf("\"%s\" is the value field.\n", json_string_value(value)); //use value

    json_decref(value); //Do I free this pointer?
    json_decref(root);  //What about this one? Does the order matter?
    return 0;
}

これは単純なプログラムです。JSON 文字列を読み取って解析します。作成時に、ライブラリ呼び出しを使用して解析を行います。JSON には自身のネストされた構造が含まれることがあるため、Jansson は必要な割り当てを動的に行います。ただし、これはすべての関数から割り当てられたメモリを「解放」するという意味ではありません。実際、上で書いたこのコードは、「無効な読み取り」と「無効な書き込み」の両方をスローします。これらのエラーは、の行decrefを削除するとなくなります。decrefvalue

valueなぜでしょうか? Jansson APIでは、変数は「借用参照」とみなされます。Janssonはメモリを追跡しているので、 decrefJSON構造を互いに独立させるだけで済みます。ここでの教訓は、ドキュメントを読むことです。本当に。理解しにくいこともありますが、ドキュメントには、なぜこのようなことが起こるのかが書かれています。代わりに、既存の質問このメモリエラーについて。

SDL: グラフィックスとゲームライブラリ

#include "SDL2/SDL.h"

int main(int argc, char* argv[]) {
    if (SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO) != 0) {
        SDL_Log("Unable to initialize SDL: %s", SDL_GetError());
        return 1;
    }

    SDL_Quit();
    return 0;
}

どうしたのこのコード? 私の場合、常に約 212 KiB のメモリがリークされます。少し考えてみてください。SDL をオンにしてからオフにします。答えは? 何も問題はありません。

最初は奇妙に聞こえるかもしれない正直に言うと、グラフィックスは乱雑で、標準ライブラリの一部としてメモリリークを受け入れる必要がある場合もあります。ここでの教訓は、すべてのメモリリークを抑える必要はないということです漏れを抑える これらは既知の問題なので、どうすることもできません。(これは、あなた自身のリークを無視する許可を私が与えたわけではありません!)

空虚への答え

漏れが自分のものであるかどうかは、どうすればわかるのでしょうか?
そうです。(少なくとも 99% は確実です)

他の人のコードを使用しているときに、リークを見つけるにはどうすればよいですか?
おそらく、すでに誰かが見つけているでしょう。Google を試してください。それでも見つからない場合は、上で紹介したスキルを使用してください。それでも見つからない場合、主に API 呼び出しが表示され、自分のスタック トレースがほとんど表示されない場合は、次の質問を参照してください。

自分のものではないリークを発見しました。何かすべきでしょうか?
はい! ほとんどの API にはバグや問題を報告する方法があります。それらを使用してください! プロジェクトで使用しているツールに貢献しましょう!


参考文献

ここまでお付き合いいただきありがとうございました。この回答にたどり着いた幅広い層の人々に配慮しようと努めた結果、皆さんが何かを学んでくれたことを願っています。途中で次のような疑問が湧いていたと思います。C のメモリ アロケータはどのように動作するのか? メモリ リークとメモリ エラーとは実際何なのか? これらはセグメント エラーとどう違うのか? Valgrind はどのように動作するのか? これらの疑問が 1 つでもあれば、ぜひ好奇心を満たしてください。

おすすめ記事