コード読解演習 Step 6【鑑識捜査】:メモリ破壊コードの鑑識捜査(Use-After-Free / ASan)

「落ちた場所」と「真犯人」が乖離する怪現象を解明!AddressSanitizerレポートの逆算特定術

C/C++開発で最も恐ろしいバグは「メモリを不正に解放・破壊した瞬間にはクラッシュせず、数分後や全く無関係な関数の中で突然死ぬ」というメモリ破壊です。現代の開発現場で必須の AddressSanitizer (ASan) のスタックトレースとヒープ割り当て/解放ログをプロの眼力で読み解き、Use-After-Free(解放後メモリ参照)の真犯人を逆算特定する鑑識技術を体得します。

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

6.1 メモリ破壊の「潜伏期間」と AddressSanitizer(ASan)の基礎知識

なぜC++のメモリ破壊は迷宮入りするのか?クラッシュダンプから「犯人の生存ライン」を逆引きする手法を学びます。

🐧 ペンギン先輩
シロクマ先生!自機が敵を倒して爆発エフェクトが出た後、全く関係ないUI描画関数(drawScore)の中で突然 segmentation fault で落ちました!drawScore のコードを何度見てもポインタ操作なんてしていないのに何故ですか!?
🐻‍❄️ シロクマ先生
ふぉっふぉっふぉ!それこそが全プログラマを恐怖のどん底に叩き落とす【Use-After-Free(解放後メモリ参照)】の遅延地雷じゃ!すでに delete された敵オブジェクトの残骸ポインタを爆発エフェクトが握り続け、別の誰かがそのメモリを再利用した瞬間にデータが壊れたのじゃ!
🐧 ペンギン先輩
ひええっ!落ちた場所(drawScore)には何の罪もなかったんですか!?じゃあどうやって本当の犯人を見つければいいんですか!?
🐻‍❄️ シロクマ先生
現代の開発には【AddressSanitizer(ASan)】という最強の鑑識ツールがある!メモリ確保・解放・アクセスの全履歴を記録し、不正アクセスの瞬間に「いつ誰が確保し、いつ誰が解放し、誰が不当に読んだか」の3大証拠を一瞬で出力してくれるのじゃ!

### AddressSanitizer (ASan) レポート読解の「3大ブロック検証法」

ASan のクラッシュレポート(`heap-use-after-free`)は、必ず以下の3つのスタックトレースで構成されています。上から順に読むのではなく、**「2番目(解放した奴)」** にまず目を光らせるのがプロの鑑識眼です。

1. **ブロック1:【不正アクセスした現場(Crash Point)】**:
- `READ of size 4 at 0x603000000040 thread T0`
- 誰が死んだメモリを読もうとしたか?(被害者・引き金を引いた関数)
2. **ブロック2:【メモリを解放した現場(The Killer)★最重要★】**:
- `0x603000000040 is located 0 bytes inside of 32-byte region [...] freed by thread T0 here:`
- **真犯人はここ!** 誰がどの関数のどの行でこのメモリを `delete` したのか?
3. **ブロック3:【メモリが確保された現場(Origin)】**:
- `previously allocated by thread T0 here:`
- このメモリの正体は何だったのか?(どのクラスのインスタンスだったのか?)
💡 このセクションの重要ポイント
  • ASan レポートは「真ん中の freed by」を真っ先に見よ: クラッシュした場所(最上部)は単にゾンビメモリを触ってしまっただけの被害者です。「そのメモリがどこでフライング解放されたか(freed by)」こそが修正すべきバグの発生源です。

6.2 演習コード:敵撃破パーティクルシステム(Use-After-Free 潜伏版)

以下のC++ゲームループコードには、致命的な Use-After-Free バグが組み込まれています。生ポインタの寿命を追跡してください。

📄 GameParticleSystem.cpp
#include <iostream>
#include <vector>
#include <memory>
#include <string>

struct Enemy {
    int id;
    int hp;
    float x, y;
    Enemy(int _id, float _x, float _y) : id(_id), hp(100), x(_x), y(_y) {}
    ~Enemy() {
        std::cout << "[Destructor] Enemy " << id << " destroyed!\n";
    }
};

struct Particle {
    float vx, vy;
    int life;
    const Enemy* sourceEnemy; // ⚠️ 危険:敵への生ポインタ(所有権なし)

    Particle(float _vx, float _vy, const Enemy* src)
        : vx(_vx), vy(_vy), life(3), sourceEnemy(src) {}

    void update() {
        // 敵の最新座標に追従してエフェクトを出す処理
        if (sourceEnemy) {
            // ⚠️ ここで死んだ Enemy のメモリを読み出す!
            std::cout << "Particle tracking Enemy at (" 
                      << sourceEnemy->x << ", " << sourceEnemy->y << ")\n";
        }
        life--;
    }
};

class GameScene {
private:
    std::vector<std::unique_ptr<Enemy>> m_enemies;
    std::vector<Particle> m_particles;

public:
    void init() {
        m_enemies.push_back(std::make_unique<Enemy>(1, 100.0f, 200.0f));
        // パーティクル生成:敵の生ポインタを渡す
        m_particles.emplace_back(1.0f, 1.0f, m_enemies[0].get());
    }

    void updateFrame() {
        std::cout << "--- Frame Start ---\n";

        // 1. 敵が倒されてリストから消去(ここで Enemy が delete される!)
        std::cout << "Action: 敵を撃破して消去します\n";
        m_enemies.clear(); // ⚠️ メモリ解放!

        // 2. パーティクルの更新処理(既に死んだ Enemy を見に行く!)
        std::cout << "Action: 残留パーティクルを更新します\n";
        for (auto& p : m_particles) {
            p.update(); // 💥 Use-After-Free 発生!
        }

        std::cout << "--- Frame End ---\n";
    }
};

int main() {
    GameScene scene;
    scene.init();
    scene.updateFrame();
    return 0;
}
💡 このセクションの重要ポイント
  • コンテナの clear() や erase() はポインタ所有者を破壊する: `m_enemies.clear()` を呼んだ瞬間、中身の `std::unique_ptr<Enemy>` がすべて即時デストラクトされます。外部に渡していた `sourceEnemy` 生ポインタは瞬時に「ダングリングポインタ(墓場へのポインタ)」と化します。

6.3 鑑識眼の証明:実際の ASan ログからの逆引き特定プロファイル

本コードを `-fsanitize=address` 付きで実行した際に出力される本物の鑑識レポートの読み解きです。

📝 理解度チェッククイズ

Q1. Use-After-Free(解放後メモリ参照)のバグが「デバッグが極めて難しい」と言われる最大の理由は何でしょう?
  • 1. コンパイルが100%通らないから
  • 2. メモリを delete した瞬間にはクラッシュせず、OSがその領域を別のオブジェクトに再割り当てするまで正常に動いているように見え、後から全く無関係な場所で突然発症するから
  • 3. C++の規格書に記述がないから
  • 4. Linuxでは絶対に発生しないから
正解と解説を見る
正解:メモリを delete した瞬間にはクラッシュせず、OSがその領域を別のオブジェクトに再割り当てするまで正常に動いているように見え、後から全く無関係な場所で突然発症するから
正解です!解放されたメモリは即座にゼロクリアされるわけではないため、直後はたまたま昔の値が読めて動いてしまい、数秒後や数分後に別のデータで上書きされた瞬間に謎のクラッシュを引き起こします。
Q2. AddressSanitizer(ASan)のレポートにおいて、「真犯人(そのメモリを不当に delete した箇所)」を特定するために最も注目すべきセクションはどれでしょう?
  • 1. 最上行の `SUMMARY: AddressSanitizer:`
  • 2. `previously allocated by thread T0 here:`
  • 3. `freed by thread T0 here:` の直後にあるスタックトレース
  • 4. CPUレジスタ(RSP, RBP)の値
正解と解説を見る
正解:`freed by thread T0 here:` の直後にあるスタックトレース
正解です!`freed by thread T0 here:` には、その不正メモリ領域が「どの関数のどの行番号で解放されたのか」の完全なコールスタックが記録されており、真犯人の特定に直結します。
Q3. GameParticleSystem.cpp において、Particle が Enemy の解放後もアクセスを試みてしまった根本的な設計欠陥は何でしょう?
  • 1. Enemy が struct ではなく class で書かれていたこと
  • 2. Particle が生存期間の保証されていない Enemy への生ポインタ(const Enemy*)を保持し、Enemy のライフサイクル(m_enemies.clear())と同期していなかったこと
  • 3. float ではなく double を使うべきだったこと
  • 4. Particle のコンストラクタで std::move を使わなかったこと
正解と解説を見る
正解:Particle が生存期間の保証されていない Enemy への生ポインタ(const Enemy*)を保持し、Enemy のライフサイクル(m_enemies.clear())と同期していなかったこと
正解です!生ポインタは相手の消滅を検知できません。親コンテナがクリアされて Enemy が破棄されたのに、Particle 側がそれを知らずに参照し続けたことが原因です。
Q4. この Use-After-Free を撲滅するための最もクリーンかつ実践的なゲームプログラミング設計はどれでしょう?
  • 1. Enemy を一切 delete しないようにメモリリークさせる
  • 2. Particle に生ポインタを持たせず、生成時に「その時点の敵の座標(Vec2 position)」を値コピーして渡すか、生存判定が可能な「EntityID(整数識別子)」で管理する
  • 3. delete の直後に sleep_for(10ms) を挟む
  • 4. デバッガを繋ぎっぱなしにする
正解と解説を見る
正解:Particle に生ポインタを持たせず、生成時に「その時点の敵の座標(Vec2 position)」を値コピーして渡すか、生存判定が可能な「EntityID(整数識別子)」で管理する
正解です!エフェクトに必要な情報が座標だけであれば値コピーで渡すのが最も安全です。動的追従が必要な場合でも、生ポインタではなく EntityID で検索し、既に敵が消滅していればエフェクトも自然消滅させる設計が現場の鉄則です。
← 前の章へ進む
次の章へ進む →

🐻‍❄️ シロクマC++ラボ インタラクティブ学習システム

ブラウザでJavaScriptを有効にすると、ブラウザ内インベーダーゲームエミュレータ、メモリマップ可視化、UMLクラス図、対話型解説、理解度クイズが起動します。

インタラクティブ学習を開始する →