Address Sanitizerの使い方を紹介 MSVC編【Visual Studio・C++】

C++,MSVC,Visual Studio

MSVCがx64のAddressSanitizerにも対応しました。

随分前からx86には対応していたのですが、今時x64に対応していないので使い物にならないので、x64の対応を待っていましたが、遂に対応されたので紹介したいと思います。


Address Sanitizer(ASan)

Address Sanitizerは元々はClangに実装された不正なメモリ操作を検知する為の仕組みです。
AddressSanitizer — Clang 13 documentation

MSVCもClangもAddressSanitizerの機能には大きな違いはありません。違うのはコンパイル・リンクオプションだけです。エラー発生時の表示内容、デバッグ方法は全く同じです。

通常であれば発見が難しいこバグも、AddressSanitizerを利用すれば簡単に検知する事が出来ます。

AddressSanitizerで検知できるバグ
  • Heapの範囲外アクセス
  • 解放済みメモリへのアクセス
  • Stack変数のreturn後参照
  • Stackのスコープ外参照
  • Heapの二重開放
  • メモリリーク

開発環境の構築

インストール

Viual Studio InstallerでAddressSanitizerをインストール

Visual Studio Installerを起動してAddress Sanitizerをインストールします。
「C++ によるデスクトップ開発」の中にある「C++ AddressSanitizer(試験段階)」にチェックを入れます。

注意

利用するには、Visual Studio 2019 16.7以上のバージョンが必要です。

Visual Studio プロジェクトの設定

C++のプロジェクトを作成して、プロジェクトのプロパティからコンパイルオプションを変更します。

Visual Studio C++ Project Properties

最近のプロジェクトだと、「Debug Information Format」が、「Program Database for Edit & Continue (/ZI)」になっているので「Program Database (/Zi)」に変更します。
次に、「Enable Address Sanitizer (Experimental)」を「Yes(/fsanitize=address)」に変更します。

Heapの範囲外アクセスを検知する

プロジェクトの設定が出来たので、簡単なHeapの範囲外アクセスを起こして検知してみましょう。

int main()
{
  int *array = new int[100];
  array[100] = 1; // 0-99以外のアクセスなので、範囲外アクセス
}

例えば上記のようなコードを書いた場合、通常であれば何もエラーが出ずに配列の範囲外のメモリを壊したまま処理は続きます。

AddressSanitizerビルドで実行した場合、以下のようなエラーが出ます。Debuggerの停止ポイントで、まずどの辺りでエラーが出ているのかは直ぐに分かるかと思います。

Visual Studio Debug

そしてログにこのようなエラーメッセージが出力されます。

Address Sanitizer Error: Heap buffer overflow
Full error details can be found in the output window
=================================================================
==19260==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x12c9acc001d0 at pc 0x7ff7ac08109b bp 0x00d67c52f930 sp 0x00d67c52f938
WRITE of size 4 at 0x12c9acc001d0 thread T0
==19260==WARNING: Failed to use and restart external symbolizer!
#0 0x7ff7ac08109a in main D:\samples\AddressSanitizer\AddressSanitizer.cpp:7
#1 0x7ff7ac0830c8 in invoke_main D:\agent\_work\9\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:78
#2 0x7ff7ac08301d in __scrt_common_main_seh D:\agent\_work\9\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288
#3 0x7ff7ac082edd in __scrt_common_main D:\agent\_work\9\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:330
#4 0x7ff7ac083138 in mainCRTStartup D:\agent\_work\9\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp:16
#5 0x7ffeafdd6fd3 in BaseThreadInitThunk+0x13 (C:\WINDOWS\System32\KERNEL32.DLL+0x180016fd3)
#6 0x7ffeb149cec0 in RtlUserThreadStart+0x20 (C:\WINDOWS\SYSTEM32\ntdll.dll+0x18004cec0)
0x12c9acc001d0 is located 0 bytes to the right of 400-byte region [0x12c9acc00040,0x12c9acc001d0)
allocated by thread T0 here:
#0 0x7ffe39677e91 in _asan_loadN_noabort+0x55555 (C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.27.29110\bin\HostX86\x64\clang_rt.asan_dbg_dynamic-x86_64.dll+0x180057e91)
#1 0x7ff7ac081035 in main D:\samples\AddressSanitizer\AddressSanitizer.cpp:6
#2 0x7ff7ac0830c8 in invoke_main D:\agent\_work\9\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:78
#3 0x7ff7ac08301d in __scrt_common_main_seh D:\agent\_work\9\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288
#4 0x7ff7ac082edd in __scrt_common_main D:\agent\_work\9\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:330
#5 0x7ff7ac083138 in mainCRTStartup D:\agent\_work\9\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp:16
#6 0x7ffeafdd6fd3 in BaseThreadInitThunk+0x13 (C:\WINDOWS\System32\KERNEL32.DLL+0x180016fd3)
#7 0x7ffeb149cec0 in RtlUserThreadStart+0x20 (C:\WINDOWS\SYSTEM32\ntdll.dll+0x18004cec0)
SUMMARY: AddressSanitizer: heap-buffer-overflow D:\samples\AddressSanitizer\AddressSanitizer.cpp:7 in main
Shadow bytes around the buggy address:
0x04fae257ffe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x04fae257fff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x04fae2580000: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
0x04fae2580010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x04fae2580020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x04fae2580030: 00 00 00 00 00 00 00 00 00 00[fa]fa fa fa fa fa
0x04fae2580040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x04fae2580050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x04fae2580060: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x04fae2580070: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x04fae2580080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable:           00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone:       fa
Freed heap region:       fd
Stack left redzone:      f1
Stack mid redzone:       f2
Stack right redzone:     f3
Stack after return:      f5
Stack use after scope:   f8
Global redzone:          f9
Global init order:       f6
Poisoned by user:        f7
Container overflow:      fc
Array cookie:            ac
Intra object redzone:    bb
ASan internal:           fe
Left alloca redzone:     ca
Right alloca redzone:    cb
Shadow gap:              cc
=================================================================

どうしてエラーが発生しているのかは、ログから追ってみましょう。重要なのは、ログの頭の方に表示されるバイト操作です。
今回だと、4バイト分不正なメモリ書き込みがあった。とありますね。

WRITE of size 4 at 0x12c9acc001d0 thread T0

次に見るべきなのは、shaow byteのログです。

Shadow bytes around the buggy address:
0x04fae257ffe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x04fae257fff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x04fae2580000: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
0x04fae2580010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x04fae2580020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x04fae2580030: 00 00 00 00 00 00 00 00 00 00[fa]fa fa fa fa fa
0x04fae2580040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x04fae2580050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x04fae2580060: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x04fae2580070: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x04fae2580080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa

AddressSanitizerは、Heap 8バイト毎に “Shadow byte" 作成してそのメモリの正当性をチェックする仕組みになっています。

=>0x04fae2580030: 00 00 00 00 00 00 00 00 00 00[fa]fa fa fa fa fa

この行の [fa] が不正メモリアクセスの箇所です。"0″ と “fa" の部分がありますが、"fa" とはなんでしょうか?
ログの下の方に説明が書いてありますね。

Addressable:           00
Heap left redzone:     fa

AddressSanitizerはHeapの前後に自動で redzone と呼ばれる領域を確保し、そのアドレスにアクセスしたらエラーを吐きます。今回は redzone の意味を持つ fa の領域にアクセスしたのでエラーという事になります。

サンプルプログラムでは、int(4 bytes)の配列を100個Heapから確保しました。丁度、アクセスしている直前の “00" の領域は、Shadow byte で見ると50個あります。

Shadow byte 1つは 8 Bytesなので 50個あれば 400 Bytes 分となります。つまり、0x04fae2580000 から始まる 50個の Shadow bytes は、new int[100] で確保したメモリ空間という事です。そして、0x04fae2580000 より前の Shadow byte も redzone という事で “fa" の空間が存在しますね。試しに、array[-1] = 1; とアクセスしても範囲外アクセスでエラーが出ます。

ここで注意したいのは、redzone は無限にある訳ではないので、shadow byte から見る限り array[-17]とアクセスしてしまうと有効なメモリ領域を壊してしまうので、AddressSanitizer ではエラーを検知出来ないはずです。試しに “array[-17] = 1;" に書き換えたら AddressSanitizer でのエラーは発生せずに普通の Access Violation が出ました。丁度、不正な領域だったので良かったですが、生きているメモリ領域だとそのままプログラムが続行してしまいます。

x64 Access Violation

x64だと、対応が不十分なのか起動時に Access Violation が発生する事があります。とりあえず、無視しています。

x64 Access Violation when runch

AddressSanitizerの注意点

AddressSanitizerは非常に優秀なデバッグツールですが、メモリ使用量は増えますし処理速度が低下します。

ですので、処理速度が低下すると発生確率が低下するようなバグの検知は難しくなります。AddressSanitizerとは別にメモリアロケーターにバグ検知用の仕組みを実装する事をお勧めします。

関連リンク

AddressSanitizerを使った解放済みメモリアクセスの効率的な検知方法をこちらの記事で紹介しています。普通に利用しただけでは、検知率はそれ程高くありませんが、こちらの方法を利用すると検知率を格段に上げる事が出来ます。

Visual Studio公式サイトの解説記事

AddressSanitizer (ASan) for Windows with MSVC – C++ Team Blog

AddressSanitizer for Windows: x64 and Debug Build Support – C++ Team Blog