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

2021年8月14日C++,MSVC,Visual Studio

1年以上前にx86のAddressSanitizerには対応していましたがx64には未対応でした。それが最近になってx64にも対応したのでMSVCでAddressSanitizerを使う方法を紹介します。

Visual StudioでMSVCを使う方法を紹介していますが、Clangの時はコンパイルオプション等の設定が違うだけで、Address Sanitizerが出すエラー内容やデバッグ方法は同じです。

注意

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

Address Sanitizer(ASan)

AddressSanitizerは元々はClangに実装された不正なメモリ捜査を検知する為の仕組みです。

これらのバグの解消に効果的
  • Heapの範囲外アクセス
  • 解放済みメモリへのアクセス
  • Stack変数のreturn後参照
  • Stackのスコープ外参照
  • 二重開放
  • メモリリーク

インストール

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

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

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

まとめ

NOTIFICATION

Address Sanitizerは非常に有用なデバッグ手段です。
使った事がない人は、ぜひ試してみる事をお勧めします。
メモリフィルよりも効果的ですが、処理速度が低下して、メモリ使用量も増えます。