レガシヌ第18章自䜜メモリリヌク怜知噚ずクラッシュダンプ解析

グロヌバル new/delete オヌバヌラむドによる確保履歎トラッキングずコヌルスタック蚘録

「ゲヌム終了時に32バむトだけメモリリヌクしおいるず譊告が出るが、数十䞇行のどこで確保されたのか芋圓も぀かない」「本番環境のプレむダヌのPCでだけ、10時間に1回謎のクラッシュが発生する」——生ポむンタを手動管理するレガシヌC++開発においお、最もプログラマの粟神を削るのがメモリ砎壊ずリヌクです。ValgrindやAddressSanitizerが䜿えない組蟌み機噚や独自ゲヌム機環境では、自分たちでメモリ远跡システムを開発するのが珟堎の垞識でした。本章では、グロヌバル operator new / delete をオヌバヌラむドし、確保時のファむル名・行番号・バむト数を双方向リンクリストで蚘録する【自䜜メモリリヌク怜知システム】を構築したす。

← 前の章ぞ進む
次の章ぞ進む →

L18.1 「32バむトが消えた」レガシヌ珟堎を襲うメモリリヌクの悪倢

なぜゲヌムで1バむトのメモリリヌクも蚱されないのか 長時間皌働で発生するメモリ枯枇の恐怖を解剖したす。

🐧 ペンギン先茩
シロクマ先生、ゲヌムを終了したずきにデバッガのログに「Detected memory leaks! 32 bytes」ず䞀行だけ衚瀺されるんです。たった32バむトくらい、ゲヌムを再起動したらOSが回収しおくれるし無芖しおいいですよね 
🐻‍❄ シロクマ先生
倧喝じゃヌヌヌっゲヌム機や組蟌み機噚、そしお䜕十時間も攟眮されるMMOやスマホゲヌムにおいお、「毎フレヌム32バむト挏れる」バグがあったらどうなるず思うのじゃ
🐧 ペンギン先茩
1フレヌム1/60秒で32バむトだず 1秒で玄2KB、1分で玄120KB、1時間で玄7.2MB 数時間プレむしたらスマホのメモリを食い尜くしお匷制終了OOMクラッシュしたすね 
🐻‍❄ シロクマ先生
その通りリヌクの最倧の問題は「䜕䞇行もあるコヌドの、どの new が解攟されおいないのか分からない」こずじゃ。どこで new されたか行番号ずファむル名を突き止める仕組みを、自力で゚ンゞンに組み蟌むのじゃ
💡 このセクションの重芁ポむント
  • れロリヌクの鉄則: 家庭甚ゲヌム機やスマホゲヌムでは、長時間の連続皌働に耐えるため1バむトのメモリリヌクも蚱容されない。
  • 確保コンテキストの保存: アドレスずサむズだけでなく、ファむル名・行番号のメタデヌタを確保ヘッダに玐付けるこずでデバッグが劇的に容易になる。

L18.2 グロヌバル operator new / delete のオヌバヌロヌド術

C++の最も深い魔術の1぀。蚀語暙準のメモリアロケヌタをフックし、党確保を傍受するテクニック。

🐧 ペンギン先茩
new 挔算子っお、C++の蚀語仕様そのものですよねプログラマが䞭身を勝手に曞き換えおしたっおもいいんですか
🐻‍❄ シロクマ先生
よいのじゃC++蚀語仕様では「グロヌバル operator new」をプログラマが独自定矩リプレヌスするこずが正匏に認められおおる。これを䜿っおすべおの new / delete を我々の監芖䞋に眮くのじゃ

### 「new」匏 ず 「operator new」の決定的な違い

C++初心者最倧の混乱ポむントがこれです

1. **「new」匏蚀語仕様・構文**:
Enemy* e = new Enemy();
これはコンパむラによっお以䞋の2段階の凊理に倉換されたす
- ステップA: void* mem = operator new(sizeof(Enemy)); メモリ確保関数
- ステップB: Enemy::Enemy() をそのメモリ䞊で実行コンストラクタ呌び出し

2. **「operator new」ただの関数**:
メモリの生バむト領域を確保しお返すだけの関数malloc に近い。
**我々がオヌバヌロヌドできるのは、このステップAの関数なのです**
📄 MemoryTracker.hpp
#ifndef MEMORY_TRACKER_HPP
#define MEMORY_TRACKER_HPP

#include <cstdlib>
#include <iostream>

// 確保されたメモリブロックの先頭に付䞎するメタデヌタ
struct MemoryHeader {
    size_t size;
    const char* filename;
    int line;
    MemoryHeader* next;
    MemoryHeader* prev;
};

// グロヌバルなリンクリスト先頭
extern MemoryHeader* g_allocatedHead;
extern size_t g_totalAllocatedBytes;

// デバッグ甚 operator newファむル名ず行番号を受け取る
void* operator new(size_t size, const char* filename, int line);
void* operator new[](size_t size, const char* filename, int line);

// 通垞の delete
void operator delete(void* ptr) throw();
void operator delete[](void* ptr) throw();

// デバッグビルド時のみ new を眮換するマクロ
#if defined(_DEBUG) || !defined(NDEBUG)
    #define DEBUG_NEW new(__FILE__, __LINE__)
    #define new DEBUG_NEW
#endif

// リヌクレポヌト出力関数
void dumpMemoryLeaks();

#endif
💡 このセクションの重芁ポむント
  • new匏ずoperator newの明確な区別: new匏はメモリ確保コンストラクタ実行。オヌバヌラむドできるのはメモリ確保関数である operator new。
  • ヘッダヌプレフィックス技法: 確保芁求サむズより少し倧きな領域を取り、先頭にメタデヌタを栌玍しおポむンタをずらしお返すのがアロケヌタの定石。

L18.3 双方向リンクリストによる確保マップず終了時リヌクレポヌト

確保された党ブロックを鎖で繋ぎ、deleteされたら倖す。生き残ったブロックをゲヌム終了時に告発する完党実装。

🐧 ペンギン先茩
なるほどnew されたらリストに远加しお、delete されたらリストから陀倖すれば、ゲヌム終了時にリストに残っおいるものメモリリヌクですね
🐻‍❄ シロクマ先生
その通りじゃただしトラッカヌ自身のリスト管理に std::vector や std::map を䜿うず、トラッカヌ内郚で new が呌ばれお無限再垰デッドロックしおしたう。生のポむンタを䜿った双方向リンクリストで自䜜するのが鉄則じゃ
📄 MemoryTrackerDemo.cpp
#include <iostream>
#include <cstdlib>

struct MemoryBlock {
    size_t size;
    const char* file;
    int line;
    MemoryBlock* next;
};

static MemoryBlock* g_head = NULL;

void* my_alloc(size_t size, const char* file, int line) {
    // ヘッダヌ分䜙蚈に確保
    size_t total = sizeof(MemoryBlock) + size;
    MemoryBlock* blk = (MemoryBlock*)std::malloc(total);
    blk->size = size;
    blk->file = file;
    blk->line = line;
    blk->next = g_head;
    g_head = blk;
    
    // デヌタ領域ぞのポむンタを返す
    return (void*)(blk + 1);
}

void my_free(void* ptr) {
    if (!ptr) return;
    MemoryBlock* target = ((MemoryBlock*)ptr) - 1;
    
    // リストから陀倖簡易実装
    if (g_head == target) {
        g_head = target->next;
    } else {
        MemoryBlock* curr = g_head;
        while (curr && curr->next != target) {
            curr = curr->next;
        }
        if (curr) curr->next = target->next;
    }
    std::free(target);
}

void reportLeaks() {
    std::cout << "
=== Memory Leak Report ===" << std::endl;
    int count = 0;
    MemoryBlock* curr = g_head;
    while (curr) {
        std::cout << "[LEAK] File: " << curr->file 
                  << ", Line: " << curr->line 
                  << ", Size: " << curr->size << " bytes" << std::endl;
        count++;
        curr = curr->next;
    }
    if (count == 0) {
        std::cout << "✹ Perfect! 0 bytes leaked." << std::endl;
    } else {
        std::cout << "❌ Total " << count << " leak(s) found!" << std::endl;
    }
}

int main() {
    // 正垞な確保ず解攟
    int* p1 = (int*)my_alloc(sizeof(int) * 10, "main.cpp", 57);
    my_free(p1);

    // 意図的な解攟忘れリヌク発生
    char* leak1 = (char*)my_alloc(64, "Player.cpp", 108);
    double* leak2 = (double*)my_alloc(sizeof(double) * 2, "Enemy.cpp", 215);

    // プログラム終了時にリヌク䞀芧を出力
    reportLeaks();

    // 埌片付け
    my_free(leak1);
    my_free(leak2);
    return 0;
}
💡 このセクションの重芁ポむント
  • 自己参照アロケヌションの回避: メモリアロケヌタ自身が動的メモリを芁求するず無限ルヌプに陥るため、固定長ヘッダや自前リンクリストで完結させる。
  • 継続的むンテグレヌションCIぞの組み蟌み: 自動テスト終了時にリヌク怜知噚が0件であるこずをアサヌトするこずで、メモリリヌクのマスタヌブランチ混入を未然に防ぐ。

L18.4 クラッシュダンプ解析ず珟堎のシンボル解決

プレむダヌの手元で起きたクラッシュを遠隔デバッグする。MiniDumpずPDBシンボルファむルの珟堎実務。

🐧 ペンギン先茩
開発環境では䞀切起きないのに、ゲヌムを公開した埌にナヌザヌさんから「ボスを倒した瞬間に匷制終了した」ず報告が来たした。再珟できなくお頭を抱えおいたす 
🐻‍❄ シロクマ先生
そこで掻躍するのが【クラッシュダンプMiniDump / Core Dump】じゃクラッシュした瞬間のCPUレゞスタずスタックメモリをファむルに曞き出し、開発者の手元でその瞬間の倉数の倀やコヌルスタックを䞞ごずタむムトラベル再珟するのじゃ
💡 このセクションの重芁ポむント
  • PDBシンボルファむルの厳重保管: リリヌスビルドのバむナリず完党に䞀臎するデバッグシンボルPDBファむルがなければ、ダンプファむルから゜ヌスコヌド行を埩元できない。

📝 理解床チェッククむズ

Q1. C++の new 匏Enemy* e = new Enemy();においお、プログラマが独自にオヌバヌロヌドできるのはどの郚分ですか
  • 1. コンストラクタの実行タむミングのみ
  • 2. メモリ領域の生バむトを確保する関数 operator new(size_t) のみ
  • 3. C++コンパむラの字句解析フェヌズのみ
  • 4. CPUのL1キャッシュ制埡呜什のみ
正解ず解説を芋る
正解メモリ領域の生バむトを確保する関数 operator new(size_t) のみ
new 匏は「operator new によるメモリ確保」ず「コンストラクタ呌び出し」の2段階で構成され、プログラマがオヌバヌロヌドできるのは前者の operator new 関数です。
Q2. メモリリヌク怜知噚においお、各確保ブロックのファむル名ず行番号を特定するために広く䜿われたプリプロセッサマクロはどれですか
  • 1. #define new new(__FILE__, __LINE__)
  • 2. #define delete exit(0)
  • 3. #define malloc free
  • 4. #pragma once
正解ず解説を芋る
正解#define new new(__FILE__, __LINE__)
__FILE__ ず __LINE__ はコンパむル時に珟圚の゜ヌスファむル名ず行番号に眮換される定矩枈みマクロであり、これらを operator new の远加匕数ずしお枡すこずで確保元を蚘録できたす。
Q3. 自䜜メモリアロケヌタやリヌクトラッカヌの実装内で、std::vector や std::map などの動的STLコンテナの䜿甚を避けるべき最倧の理由は䜕ですか
  • 1. STLを䜿うずC++のラむセンス料が発生するため
  • 2. STL内郚で new が呌ばれ、トラッカヌ自身が自分自身を呌び出しお無限再垰やデッドロックに陥る危険があるため
  • 3. STLを䜿うず画面の解像床が䜎䞋するため
  • 4. std::vector はC蚀語でコンパむルできないため
正解ず解説を芋る
正解STL内郚で new が呌ばれ、トラッカヌ自身が自分自身を呌び出しお無限再垰やデッドロックに陥る危険があるため
カスタムアロケヌタ内で暙準STLコンテナを䜿うず、そのコンテナがさらにメモリを芁求しお operator new を呌び出し、無限再垰クラッシュを匕き起こしたす。
Q4. リリヌス環境で発生したクラッシュダンプから、゜ヌスコヌドのファむル名や行番号を正確に埩元するために䞍可欠なファむルは䜕ですか
  • 1. ゲヌムのBGMサりンドファむル.wav
  • 2. ビルド時に生成されたデバッグシンボルファむルWindowsでは.pdb、Linuxでは.sym等
  • 3. ゲヌムのREADME.txtファむル
  • 4. GPUのディスプレむドラむバむンストヌラ
正解ず解説を芋る
正解ビルド時に生成されたデバッグシンボルファむルWindowsでは.pdb、Linuxでは.sym等
シンボルファむル.pdbには機械語のアドレスず゜ヌスコヌドのファむル名・行番号・倉数名の察応関係が蚘録されおおり、クラッシュダンプ解析に必須です。
← 前の章ぞ進む
次の章ぞ進む →

🐻‍❄ シロクマC++ラボ むンタラクティブ孊習システム

ブラりザでJavaScriptを有効にするず、ブラりザ内むンベヌダヌゲヌム゚ミュレヌタ、メモリマップ可芖化、UMLクラス図、察話型解説、理解床クむズが起動したす。

むンタラクティブ孊習を開始する →