Linux

Linux ValgrindツールでC/C++のメモリリークを検出する方法

*記事内に商品プロモーションを含む場合があります

C言語やC++で作成したLinuxプログラムのメモリリークやメモリアクセス違反の検出に使えるツール(Valgrind)を紹介します。

Valgrind(ヴァルグリンド)とは

C言語やC++で作成したLinuxプログラムのメモリリークや不正なメモリアクセスを検出可能な無料で使えるツールです。

以下のようなプログラムのデバッグ時や運用時に発生する問題の解析に役立ちます。

・プロセスの使用メモリが時間が経つと増える
・突然、メモリアクセス違反が発生してプロセスが異常終了する

Valgrindの使い方

Valgrindのインストール

■Red Hat系(Red Hat Enterprise / CentOS / Rocky Linux/AlmaLinux)
sudo yum install valgrind

■Ubuntu
sudo apt install valgrind

デバッグビルド

コンパイル時に「-g」オプションで、デバッグ情報付きでプログラムを作成します。

■C言語
gcc ソース.c -g

■C++
g++ ソース.cpp -g

実行

valgrind 経由で実行します。

valgrind –leak-check=full プログラム名

「-s」を付けるとエラーの情報が最後に出力され、少しだけ見やすくなります。
valgrind –leak-check=full -s プログラム名

今回、確認に使ったソースコードは長いので、この記事の最後に載せておきます。
gcc と Visual Studio の両方で実行確認ができます。

Valgrindの検出結果の見方

自分が作成したソース行を探して、ソースコードを確認しましょう。

「★xxxx」は、私が説明用に追加したコメントです。

$

Invalid write of size 8         ★不正な書き込み
at 0x4008DB: func2() (memoryLeak.cpp:43) ★問題のソース行
by 0x40095E: main (memoryLeak.cpp:75)
Address 0x5b4e290 is 0 bytes after a block of size 80 alloc'd
at 0x4C38B6F: operator new[](unsigned long) (vg_replace_malloc.c:640) ★Cランタイムのソース行なので無視
by 0x4008CE: func2() (memoryLeak.cpp:42) ★問題のソース行
by 0x40095E: main (memoryLeak.cpp:75)

HEAP SUMMARY:
in use at exit: 256 bytes in 4 blocks
total heap usage: 8 allocs, 4 frees, 74,120 bytes allocated

8 bytes in 1 blocks are definitely lost in loss record 1 of 4 ★解放漏れ
at 0x4C378C3: operator new(unsigned long) (vg_replace_malloc.c:422) ★Cランタイムのソース行なので無視
by 0x400915: func3() (memoryLeak.cpp:58) ★問題のソース行
by 0x400963: main (memoryLeak.cpp:77)

40 bytes in 1 blocks are definitely lost in loss record 2 of 4 ★解放漏れ
at 0x4C38B6F: operator new[](unsigned long) (vg_replace_malloc.c:640)
by 0x400949: func4() (memoryLeak.cpp:68) ★問題のソース行
by 0x400968: main (memoryLeak.cpp:79)

メモリチェックツールの注意事項

Valgrindを使う上での注意事項を紹介します。

Valgrindに限らず、この手のメモリチェックツールに対して一般的に言えることです。

・実行速度が、数倍から数十倍遅くなる

・スレッドで並列で動くようなプログラムの場合、メモリ操作のタイミングが変わり、検出できないときがある

・動的解析ツールなので実行されたソース行しかチェックできない
→ 検査パターンを網羅して、ソースコードのカバレッジを上げる工夫が必要

・複雑なソースプログラムだと、検出された場所がなぜリークするのか分からないことがある

・複雑(汚い)なソースコードだと、たまに誤判定する
ポインタをコピーして、コピー側を変なところで解放(free/delete)している場合など

・実行中はメモリに溜め込んで、最後にメモリ解放する造りのプログラムだと一見リークしていないように見える(valgrindの問題ではない)

・常駐型のプログラムだと途中で、プロセスの停止(KILL)などが必要になり、本来の動作と異なるので、本当のリークが見つけにくくなる。

Valgrindは動的解析ツールですが、静的解析ツールも組み合わせて利用したほうがいい。

静的解析ツールで以下のようなことをチェックできます。

・メモリリーク/リソースリーク
・バッファオーバーフロー
・NULLポインターの参照
・未初期化変数の参照
・整数オーバーフロー
・ゼロ除算
・配列の境界外アクセス
・デッドロック
・不適切な排他制御

(おまけ)Visual Studiioの静的解析

Windowsの開発統合環境(IDE)のVisual Studioでも実行前に簡単なものなら、静的解析で問題を検出してくれます。

赤線は私が説明用に追加しました。「エラー一覧」に問題点が検出されています。
メモリリークは無理ですが、一部の不正アクセスは検出可能。

GUIでの開発環境はWindowsのほうが優れているので、LinuxのC言語やC++のプログラムもWindowsで開発して最終確認はLinuxで実施するのが開発効率が上がります。

Windowsから、Linux上のプログラムを直接デバッグする方法もあります。

Visual StudioでLinuxプログラムをWindowsでリモートデバックする方法Visual Studioで、Linuxのアプリ(プログラム)をWindowsからリモートでデバッグする方法を紹介します。 Lin...

最後に

Windowsのメモリチェックツールは、DevPartnerのBoundsCheckerを使ったことがあります。
インストールするとVisualStudioの画面と統合されて、検出箇所にジャンプできたりと非常に便利でした。

ただし、残念ながら、最近、DevPartner製品は販売終了になっているようです。

WindowsでGUIを持ったものは使い勝手はいいですが、Valgrindでもメモリリークやアクセス違反の検出度などの違いはありません。

BoundsCheckerは、Valgrindより、チェックに必要な実行時間が長い傾向にあります。

ソースコード

今回使用したC++のソースコードを載せておきます。

//
// g++ memoryLeak.cpp  -g
//
// valgrind --leak-check=full  ./a.out
//
// -g -O0 オプションでコンパイルすると、valgrind の出力にファイル名と行番号が含まれるようになる。
// 
//  # すべての種類について表示する
//  $ valgrind --leak-check=full --show-leak-kinds= all PROGRAM-NAME
// 
// マニュアル
// https://valgrind.org/docs/manual/mc-manual.html
//


#include <stdlib.h>
#include <stdio.h>
#include <iostream>
#include <new>


void func1()
{
    printf("func1()\n");

    char* p1 = (char*)malloc(sizeof(char) * 128);
    char* p2 = (char*)malloc(sizeof(char) * 128);

    free(p2);

    // p2を二回 freeで解放。二重解放。
    free(p2);  // コアダンプする場合がある。その場合はコメントアウトする

    // p1は解放漏れ。free(p1)なし。
}

// definitely lost
void func2()
{
    printf("func2()\n");

    long* pp = new long[10];
    pp[10] = 1;  //  バッファオーバラン。 コアダンプの可能性

    // delete pp; 漏れ
}

// indirectly lost
void func3()
{
    printf("func3()\n");

    struct Node {
        Node* ptr;
    };

    Node* np = new Node();
    np->ptr = new Node();

    delete np;

    // free(np->ptr);の解放漏れ
}

// still reachable
void func4()
{
    int* intArray = new int[10];
}

int main() {

    func1();

    func2();

    func3();

    func4();

    return 0;
}