メモリバグ検出ツールのAddressSanitizerについて

INDEX

はじめに

こんにちは。モノリスソフト プログラマーの木村です。
C++プログラムのメモリバグ検出ツールである Address Santizer(以降ASan)について調べましたので紹介します。
ASanを導入することで、コードを大きく変更することなく、容易かつ効果的にメモリバグを検出できる可能性があります。

ASanの概要

ASanは、ランタイムのメモリバグ検出ツールです。
LLVM(v3.1~)でサポートされ、VisualStudio2019(v16.9~)でもサポートされるようになりました。
ASanによって以下のようなメモリバグを検出できます。

  • 解放後アクセス
  • ヒープバッファオーバーフロー
  • スタックバッファオーバフロー
  • グローバルバッファオーバフロー
  • リターン後のアクセス
  • スコープ外後のアクセス
  • 初期化順序バグ

ASanの動作概説

ASanではざっくり以下の方法でメモリチェックを行います。

  • malloc/free、new/deleteなどのメモリ関連処理を、チェック機能付きの処理に置き換え(置き換える関数の例
    • ASanライブラリとのリンクやコンパイラによって実現
  • メモリ確保時の周辺メモリ領域や、メモリ解放後のメモリ領域へのアクセスをメモリチェック可能へ
    • 実際に確保する「メモリ」と、そのメモリに対応する「シャドウメモリ」を導入してチェック

ASanの制限

ASanには以下の制限があります。

  • 実行制限
    • ランタイムのパフォーマンス低下
      • 速度が2倍ほど遅くなる(類似ツールであるValgrindは20倍ほど遅いと記載があるので比較的実用的)
      • メモリ使用量の増加
    • バイナリサイズの増加
  • 機能制限
    • ASanのメモリチェック方法より、一般的なメモリ関数を用いない「カスタムアロケータ」ではメモリバグが検出できない
      • ゲーム開発ではそのようなカスタムアロケータを利用

カスタムアロケータの制限への対応

カスタムアロケータの制限については、カスタムアロケータ用の対応コードが提供されており、これを実装することでメモリバグの検出が可能になります。
後の章に参考情報を記載しています。

その他の特筆事項

  • メモリバグ検出時にフルダンプを出力可能
    • ダンプファイルに対して他PCなどからデバッグ実行が可能
  • メモリバグを検出をしたくないコードでは、__declspec(no_sanitize_address) で無効にできる

ASanの動作確認

VisualStudio2022(v17.5)でASanの動作確認を行いました。

ASanの有効化

  • VisualStudioから
    • 「AddressSanitizer を有効にする」を「はい」に設定する

      tech_20_02.png

    • エディット コンティニュをオフにする
      • 「C/C++ → 全般 → デバッグ情報の形式」を「エディット コンティニュのプログラム データベース (/ZI)」"以外"にする
    • ランタイムチェックをオフにする
      • 「C/C++ → コード生成 → 基本ランタイムチェック」を「既定」にする
    • インクリメンタルリンクをオフにする
      • 「リンカー → 全般 → インクリメンタルリンクを有効にする」を「いいえ (/INCREMENTAL:NO)」にする

(補足)コマンドラインから有効化したい

(補足)コマンドラインから有効化したい

  • 目的
    • 既存のプロジェクト構成(Debug/Releaseなど)に対してASanを有効化したい。プロジェクトファイルを編集せずコマンドラインからのビルド時オプションで有効化したい
  • 問題
    • msbuild.exeのオプション経由で設定可能か確認したところ「ランタイムチェックをオフにする」がmsbuild.exeから制御できなかった
  • 解決案

ASan動作確認:スタックとヒープで範囲外アクセス

サンプルコード

#include <memory>
 
void StackBufferOverflow() {
  int stack_buff[100];
  for (int i = 0; i <= 100; ++i) {
    stack_buff[i] = i;
  }
}
 
void HeapBufferOverflow() {
  int* heap_buff = static_cast<int*>(malloc(sizeof(int) * 100));
  *(heap_buff - 1) = 5;
  free(heap_buff);
}
 
int main() {
  StackBufferOverflow();
 
  HeapBufferOverflow();
 
  return 0;
}

StackBufferOverflow
エラー出力画面

tech_20_03.png

エラー出力画面の補足説明
青色ライン
 ・検出したメモリバグの概要
 ・スタックバッファオーバーフロー
緑色ライン
 ・検出した状況のスタック情報やコード情報
紫色ライン
 ・検出時のシャドウメモリの状態
 ・凡例よりスタックの右レッドゾーン(範囲外)アクセスとわかる

HeapBufferOverflow
エラー出力画面

tech_20_04.png

エラー出力画面の補足説明
青色ライン
 ・検出したメモリバグの概要
 ・ヒープバッファオーバーフロー
緑色ライン
 ・ヒープのメモリサイズやコールスタック
紫色ライン
 ・検出時のシャドウメモリの状態
 ・凡例よりヒープの左レッドゾーン(範囲外)アクセスとわかる

ASan動作確認:ヒープの解放後アクセス

ヒープの解放後アクセス
サンプルコード+エラー出力画面

tech_20_05.png

エラー出力画面の補足説明
青色ライン
 ・検出したメモリバグの概要
 ・解放後アクセス
緑色ライン
 ・フリー時と前アロケート時のコールスタック
 ・ヒープのメモリサイズ
紫色ライン
 ・検出時のシャドウメモリの状態
 ・凡例より解放後アクセスとわかる

カスタムアロケータのASan対応

Asanでは、カスタムアロケータ用にメモリ領域を毒化/無毒化するインタフェースを提供しています。
これを実装することでASanでメモリバグを検出できます。

【用語解説】毒化・無毒化

ASanのドキュメントやコードで使用されている POISON / UNPOISON を訳したものです。メモリの状態を表していて、POISONにしたメモリ領域にアクセスした場合はメモリバグとして検出されます。UNPOISONにすると、メモリ領域に問題なくアクセスできるようになります。

対応方法1

ASan有効時には、カスタムアロケータの実装で、ASanが提供している毒化/無毒化コードを呼び出します。

また、メモリ要件(メモリとシャドウメモリの対応)よりメモリ領域を8バイト境界に揃える必要があります。

カスタムアロケータへの組み込みサンプルとして、以下をご参考ください。

Microsoft Learn、AddressSanitizer ランタイム
https://learn.microsoft.com/ja-jp/cpp/sanitizers/asan-runtime?view=msvc-170#alignment-requirements-for-addresssanitizer-poisoning

カスタムアロケータでの毒化/無毒化アクセス
エラー出力画面

tech_20_06.png

エラー出力画面の補足説明
青色ライン
 ・検出したメモリバグの概要
 ・範囲外・解放後アクセスでも毒化後アクセスとして検出
緑色ライン
 ・アクセス情報やコールスタックやメモリサイズ
紫色ライン
 ・検出時のシャドウメモリの状態
 ・ユーザによる毒化とわかる

対応方法2

ASan有効時には、カスタムアロケータの実装を、malloc/freeなどの一般的なメモリ関数を呼び出すようにします。

組み込みは比較的容易ですが、パフォーマンスが大幅に低下する可能性があります。

特定のアロケータタイプやアロケータインスタンスだけで本対応を有効化できるようにするなど対策が考えられます。

まとめ

ASanの概要とVisualStudio2022での動作確認、カスタムアロケータでのASan対応などを紹介させていただきました。

ASanを有効化するだけでランタイムのメモリバグ検出ができること、また、カスタムアロケータを使う場合でもメモリバグを検出できることを確認しました。

調査して以下のように考えました。

  • ゲーム開発では、ASanを有効にした自動プレイ・自動テストで活用できそう
    • パフォーマンス制限やダンプファイル生成機能より自動化したい
  • ライブラリ開発では、積極的に活用したい
  • カスタムアロケータ対応は、毒化/無毒化でなくmalloc/freeの方が単純で導入しやすそう
    • 毒化/無毒化コードのミスやメモリ要件である8バイト境界を気にしなくてよい
    • この場合パフォーマンスが低下するが、ASanの有効化はテスト実行時なので運用で回避できることを期待

今回はASanを紹介しましたが、Sanitizerには UndefinedBehaviorSanitizer(未定義動作の検出)やMemorySanitizer(AddressSanitizerで検出できない未初期化メモリ読み取りの検出)などのデバッグツールもあります。
他のSanitizerも含めてデバッグツールの活用を検討いただければと思います。

本記事が皆様のゲーム開発ライフに役立てば幸いです。

参考

Microsoft Learn、AddressSanitizer
https://learn.microsoft.com/ja-jp/cpp/sanitizers/asan?view=msvc-170

Clang 18.0.0git documentation、AddressSanitizer
https://clang.llvm.org/docs/AddressSanitizer.html

GitHub、AddressSanitizer
https://github.com/google/sanitizers/wiki/AddressSanitizer

Microsoft Learn、AddressSanitizer ランタイム
https://learn.microsoft.com/ja-jp/cpp/sanitizers/asan-runtime?view=msvc-170#alignment-requirements-for-addresssanitizer-poisoning

執筆者:木村

家庭用ゲーム開発会社を経てモノリスソフトへ入社。 以来、プログラマーとして主にエフェクトエンジン開発、開発環境支援の業務を担当。 好きな食べ物はたこ焼き。

ABOUT

モノリスソフト開発スタッフが日々取り組んでいる技術研究やノウハウをご紹介

RECRUIT採用情報