このチュートリアルでは、メモリセーフ エラーを検出して報告するチェック ビルドを作成します。
メモリの解放後にメモリを使用する、メモリの読み取りと書き込みなどのメモリセーフ エラー、ポインターの逆参照などを NULL することは、C/C++ コードにとって最も重要な問題です。 AddressSanitizer (ASAN) は、このような見つけにくいバグを公開し、誤検知をゼロにして実行するコンパイラおよびランタイム テクノロジです。 ASAN の概要については、「 AddressSanitizerを参照してください。
エラー時の続行 (COE) は、アプリの実行時にメモリの安全性エラーを自動的に診断して報告する新しい ASAN 機能です。 プログラムが終了すると、固有のメモリー・セーフ・エラーの要約が、 stdout、 stderr、または選択したログ・ファイルに出力されます。
-fsanitizer=addressを使用して標準の C++ チェック ビルドを作成すると、アロケーター、free、memcpy、memsetなどの割り当て解除子の呼び出しが ASAN ランタイムに転送されます。 ASAN ランタイムは、これらの関数に対して同じセマンティクスを提供しますが、メモリで何が起こるかを監視します。 ASAN は、アプリの実行時に、非表示のメモリ安全性エラーを診断して報告します。誤検知は 0 個です。
COE の大きな利点は、以前の ASAN 動作とは異なり、最初のメモリ エラーが見つかったときにプログラムの実行が停止しないということです。 代わりに、ASAN によってエラーが記録され、アプリは引き続き実行されます。 アプリが終了すると、すべてのメモリの問題の概要が出力されます。
ASAN を有効にして C または C++ アプリのチェック ビルドを作成し、テスト ハーネスでアプリを実行することをお勧めします。 テストでバグを探してアプリ内のコード パスを実行すると、テストに干渉することなく、それらのコード パスでメモリの安全性の問題が発生するかどうかを確認することもできます。
アプリが完了すると、メモリの問題の概要が表示されます。 COE を使用すると、既存のアプリケーションをコンパイルして限られた運用環境にデプロイして、メモリの安全性の問題を見つけることができます。 ASAN インストルメンテーションが原因でアプリの実行速度が低下しますが、チェックされたビルドを数日実行してコードを完全に実行できます。
この機能を使用して、新しい配送ゲートを作成できます。 すべての既存のテストに合格しても、COE がメモリセーフ エラーまたはリークを報告する場合は、新しいコードを出荷したり、親ブランチに統合したりしないでください。
COE が有効になっているビルドを運用環境にデプロイしないでください。 COE は、テスト環境と開発環境でのみ使用することを目的としています。 メモリ エラーを検出するために追加されたインストルメンテーションのパフォーマンスへの影響、エラーが報告された場合に内部実装を公開するリスク、および ASAN がメモリ割り当て、解放、および などなど。
次の例では、チェックビルドを作成し、アドレスサニタイザー情報を出力する環境変数を stdout に設定して、ASAN が報告するメモリ安全性エラーを確認します。
前提条件
このチュートリアルを完了するには、Visual Studio 2022 17.6 以降と Desktop 開発と C++ ワークロード がインストールされている必要があります。
Double free の例
この例では、ASAN を有効にしてビルドを作成し、メモリが 2 倍解放されたときに何が起こるかをテストします。 ASAN はこのエラーを検出して報告します。 この例では、エラーが検出された後もプログラムは引き続き実行され、2 つ目のエラーを使用するメモリが解放されます。 エラーの概要は、プログラムの終了時に stdout に出力されます。
例を作成します。
開発者コマンド プロンプトを開きます。 Start メニューを開き、「 Developer」と入力し、一致する一覧から Developer Command Prompt for VS 2022 などの最新のコマンド プロンプトを選択します。
この例を実行するディレクトリをコンピューターに作成します。 たとえば、
%USERPROFILE%\Desktop\COEのようにします。そのディレクトリに、空のソース ファイルを作成します。 たとえば、
doublefree.cppのように指定します。以下のコードをファイルに貼り付けます。
#include <stdio.h> #include <stdlib.h> void BadFunction(int *pointer) { free(pointer); free(pointer); // double-free! } int main(int argc, const char *argv[]) { int *pointer = static_cast<int *>(malloc(4)); BadFunction(pointer); // Normally we'd crash before this, but with COE we can see heap-use-after-free error as well printf("\n\n******* Pointer value: %d\n", *pointer); return 1; }
上記のコードでは、 pointer は 2 回解放されます。 これは複雑な例ですが、二重フリーは、より複雑な C++ コードで簡単に間違いを犯します。
次の手順で COE を有効にして、上記のコードのビルドを作成します。
- 前に開いた開発者コマンド プロンプト (
cl -fsanitize=address -Zi doublefree.cpp) でコードをコンパイルします。-fsanitize=addressスイッチは ASAN をオンにし、-Ziは、AddressSanitizer がメモリ エラーの場所情報を表示するために使用する個別の PDB ファイルを作成します。 - 開発者コマンド プロンプトで次のように
stdout環境変数を設定して、ASAN 出力をASAN_OPTIONSに送信します。set ASAN_OPTIONS=continue_on_error=1 - 次のコマンドを使用してテスト コードを実行します。
doublefree.exe
出力には、二重の空きエラーと、発生した呼び出し履歴が示されています。 レポートは、 BadFunctionで発生したエラーを示す呼び出し履歴から始まります。
==22976==ERROR: AddressSanitizer: attempting double-free on 0x01e03550 in thread T0:
#0 free D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp(69)
#1 BadFunction C:\Users\xxx\Desktop\COE\doublefree.cpp(8)
#2 main C:\Users\xxx\Desktop\COE\doublefree.cpp(14)
#3 __scrt_common_main_seh D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
#4 BaseThreadInitThunk Windows
#5 RtlInitializeExceptionChain Windows
次に、解放されたメモリと、メモリが割り当てられた場所の呼び出し履歴に関する情報があります。
0x01e03550 is located 0 bytes inside of 4-byte region [0x01e03550,0x01e03554)
freed by thread T0 here:
#0 free D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp(69)
#1 BadFunction C:\Users\xxx\Desktop\COE\doublefree.cpp(7)
#2 main C:\Users\xxx\Desktop\COE\doublefree.cpp(14)
#3 __scrt_common_main_seh D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
#4 BaseThreadInitThunk Windows
#5 RtlInitializeExceptionChain Windows
previously allocated by thread T0 here:
#0 malloc D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp(85)
#1 main C:\Users\xxx\Desktop\COE\doublefree.cpp(13)
#2 __scrt_common_main_seh D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
#3 BaseThreadInitThunk Windows
#4 RtlInitializeExceptionChain Windows
次に、ヒープの使用後のエラーに関する情報があります。 これは、*pointer呼び出しでprintf()を使用することを指します。これは、参照pointerメモリが以前に解放されたためです。 このメモリが割り当てられ解放された呼び出し履歴と同様に、エラーが発生する呼び出し履歴が一覧表示されます。
==35680==ERROR: AddressSanitizer: heap-use-after-free on address 0x02a03550 at pc 0x00e91097 bp 0x012ffc64 sp 0x012ffc58READ of size 4 at 0x02a03550 thread T0
#0 main C:\Users\xxx\Desktop\Projects\ASAN\doublefree.cpp(18)
#1 __scrt_common_main_seh D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
#2 BaseThreadInitThunk Windows
#3 RtlInitializeExceptionChain Windows
0x02a03550 is located 0 bytes inside of 4-byte region [0x02a03550,0x02a03554)
freed by thread T0 here:
#0 free D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp(69)
#1 BadFunction C:\Users\xxx\Desktop\Projects\ASAN\doublefree.cpp(7)
#2 main C:\Users\xxx\Desktop\Projects\ASAN\doublefree.cpp(14)
#3 __scrt_common_main_seh D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
#4 BaseThreadInitThunk Windows
#5 RtlInitializeExceptionChain Windows
previously allocated by thread T0 here:
#0 malloc D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp(85)
#1 main C:\Users\xxx\Desktop\Projects\ASAN\doublefree.cpp(13)
#2 __scrt_common_main_seh D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
#3 BaseThreadInitThunk Windows
#4 RtlInitializeExceptionChain Windows
次に、バッファー オーバーフローの近くにあるシャドウ バイトに関する情報があります。 シャドウ バイトの詳細については、「 AddressSanitizer シャドウ バイトを参照してください。
シャドウ バイト情報の後に、プログラムからの出力が表示されます。これは、ASAN がエラーを検出した後も実行が継続したことを示します。
******* Pointer value: xxx
その後、メモリ エラーが発生したソース ファイルの概要が表示されます。 これは、そのファイル内のメモリ エラーの一意の呼び出し履歴によって並べ替えられます。 一意の呼び出し履歴は、エラーの種類と、エラーが発生した呼び出し履歴によって決まります。
この並べ替えは、最も重要なメモリの安全性の問題に優先順位を付けます。 たとえば、5 つの一意の呼び出し履歴により、同じファイル内のメモリセーフ エラーが異なると、複数回ヒットする 1 つのエラーよりも心配になる可能性があります。 概要は次のようになります。
=== Files in priority order ===
File: D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp Unique call stacks: 1
File: C:\Users\xxx\Desktop\COE\doublefree.cpp Unique call stacks: 1
最後に、このレポートには、メモリ エラーが発生した場所の概要が含まれています。
=== Source Code Details: Unique errors caught at instruction offset from source line number, in functions, in the same file. ===
File: D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp
Func: free()
Line: 69 Unique call stacks (paths) leading to error at line 69 : 1
Bug: double-free at instr 19 bytes from start of line
File: C:\Users\xxx\Desktop\COE\doublefree.cpp
Func: main()
Line: 18 Unique call stacks (paths) leading to error at line 18 : 1
Bug: heap-use-after-free at instr 55 bytes from start of line
>>>Total: 2 Unique Memory Safety Issues (based on call stacks not source position) <<<
#0 C:\Users\xxx\Desktop\COE\doublefree.cpp Function: main(Line:18)
Raw HitCnt: 1 On Reference: 4-byte-read-heap-use-after-free
#1 D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp Function: free(Line:69)
Raw HitCnt: 1
範囲外のメモリ アクセスの例
この例では、ASAN を有効にしてビルドを作成し、アプリが範囲外のメモリにアクセスした場合の動作をテストします。 ASAN はこのエラーを検出し、プログラムの終了時に stdout するエラーの概要を報告します。
例を作成します。
開発者コマンド プロンプトを開きます。 Start メニューを開き、「 Developer」と入力し、一致の一覧から Developer Command Prompt for VS 2022 などの最新のコマンド プロンプトを選択します。
この例を実行するディレクトリをコンピューターに作成します。 たとえば、
%USERPROFILE%\Desktop\COEのようにします。そのディレクトリにソース ファイル (
coe.cppなど) を作成し、次のコードを貼り付けます。#include <stdlib.h> char* func(char* buf, size_t sz) { char* local = (char*)malloc(sz); for (auto ii = 0; ii <= sz; ii++) // bad loop exit test { local[ii] = ~buf[ii]; // Two memory safety errors } return local; } char buffer[10] = {0,1,2,3,4,5,6,7,8,9}; int main() { char* inverted_buf= func(buffer, 10); }
前のコードでは、パラメーター sz は 10 で、元のバッファーは 10 バイトです。 メモリの安全性エラーが 2 つあります。
-
bufループ内のforからの範囲外の読み込み -
localループ内でforする境界外ストア
バッファー オーバーフローは、ループ終了テストの <=szが原因です。 この例を実行すると、偶然によって安全。 これは、ほとんどの C++ ランタイム実装によって行われる過剰割り当てとアラインメントが原因です。
sz % 16 == 0すると、local[ii]への最終的な書き込みによってメモリが破損します。 その他のケースでは、"malloc slop" に対する読み取り/書き込みのみが行われます。これは、C ランタイム (CRT) が 0 mod 16 境界への割り当てを埋め込む方法によって割り当てられる追加のメモリです。
エラーは、割り当ての後のページがマップされていない場合、または破損したデータを使用した場合にのみ監視できます。 この例では、他のすべてのケースはサイレントです。 [エラー時に続行] では、プログラムの実行が完了すると、エラーが概要に表示されます。
COE を有効にして、上記のコードのビルドを作成します。
-
cl -fsanitize=address -Zi coe.cppを使用してコードをコンパイルします。-fsanitize=addressスイッチは ASAN をオンにし、-Ziは、AddressSanitizer がメモリ エラーの場所情報を表示するために使用する個別の PDB ファイルを作成します。 - 開発者コマンド プロンプトで次のように
stdout環境変数を設定して、ASAN 出力をASAN_OPTIONSに送信します。set ASAN_OPTIONS=continue_on_error=1 - 次のコマンドを使用してテスト コードを実行します。
coe.exe
出力は、2 つのメモリ バッファー オーバーフロー エラーが発生したことを示し、発生した場所の呼び出し履歴を提供します。 レポートは次のように開始されます。
==9776==ERROR: AddressSanitizer: global-buffer-overflow on address 0x0047b08a at pc 0x003c121b bp 0x012ffaec sp 0x012ffae0
READ of size 1 at 0x0047b08a thread T0
#0 func C:\Users\xxx\Desktop\COE\coe.cpp(8)
#1 main C:\Users\xxx\Desktop\COE\coe.cpp(18)
#2 __scrt_common_main_seh D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
#3 BaseThreadInitThunk Windows
#4 RtlInitializeExceptionChain Windows
次に、バッファー オーバーフローの近くにあるシャドウ バイトに関する情報があります。 シャドウ バイトの詳細については、「 AddressSanitizer シャドウ バイトを参照してください。
シャドウ バイト レポートの後に、メモリ エラーが発生したソース ファイルの概要が表示されます。 これは、そのファイル内のメモリ エラーの一意の呼び出し履歴によって並べ替えられます。 一意の呼び出し履歴は、エラーの種類と、エラーが発生した呼び出し履歴によって決まります。
この並べ替えは、最も重要なメモリの安全性の問題に優先順位を付けます。 たとえば、5 つの一意の呼び出し履歴により、同じファイル内のメモリセーフ エラーが異なると、複数回ヒットする 1 つのエラーよりも心配になる可能性があります。
概要は次のようになります。
=== Files in priority order ===
File: C:\Users\xxx\Desktop\COE\coe.cpp Unique call stacks: 2
最後に、メモリ エラーが発生した場所の概要がレポートに含まれています。 [エラー時に続行] は、同じソース行で発生する 2 つの異なるエラーを報告します。 最初のエラーは、 .data セクションのグローバル アドレスでメモリを読み取り、他のエラーはヒープから割り当てられたメモリに書き込みます。
レポートは次のようになります。
=== Source Code Details: Unique errors caught at instruction offset from source line number, in functions, in the same file. ===
File: C:\Users\xxx\Desktop\COE\coe.cpp
Func: func()
Line: 8 Unique call stacks (paths) leading to error at line 8 : 2
Bug: heap-buffer-overflow at instr 124 bytes from start of line
>>>Total: 2 Unique Memory Safety Issues (based on call stacks not source position) <<<
#0 C:\Users\xxx\Desktop\COE\coe.cpp Function: func(Line:8)
Raw HitCnt: 1 On Reference: 1-byte-read-global-buffer-overflow
#1 C:\Users\xxx\Desktop\COE\coe.cpp Function: func(Line:8)
Raw HitCnt: 1 On Reference: 1-byte-write-heap-buffer-overflow
既定の AddressSanitizer ランタイム動作は、最初に検出されたエラーを報告した後、アプリを終了します。 "不適切な" マシン命令の実行は許可されません。 新しい AddressSanitizer ランタイムは、エラーを診断して報告しますが、その後の命令を実行します。
COE は、各メモリの安全性エラーを報告した後、自動的に制御をアプリケーションに返そうとします。 メモリ アクセス違反 (AV) やメモリ割り当ての失敗など、できない場合があります。 COE は、プログラムの構造化例外処理でキャッチされないアクセス違反の後も続行されません。 COE がアプリに実行を返さない場合は、 CONTINUE CANCELLED - Deadly Signal. Shutting down. メッセージが出力されます。
ASAN 出力を送信する場所を選択する
ASAN_OPTIONS環境変数を使用して、次のように ASAN 出力を送信する場所を決定します。
- stdout への出力:
set ASAN_OPTIONS=continue_on_error=1 - stderr への出力:
set ASAN_OPTIONS=continue_on_error=2 - 選択したログ ファイルへの出力:
set COE_LOG_FILE=yourfile.log
未定義の動作の処理
ASAN ランタイムは、C および C++ の割り当て/割り当て解除関数のすべての未定義の動作を模倣するわけではありません。 次の例では、 _alloca の ASAN バージョンと C ランタイム バージョンの違いを示します。
#include <cstdio>
#include <cstring>
#include <malloc.h>
#include <excpt.h>
#include <windows.h>
#define RET_FINISH 0
#define RET_STACK_EXCEPTION 1
#define RET_OTHER_EXCEPTION 2
int foo_redundant(unsigned long arg_var)
{
char *a;
int ret = -1;
__try
{
if ((arg_var+3) > arg_var)
{
// Call to _alloca using parameter from main
a = (char *) _alloca(arg_var);
memset(a, 0, 10);
}
ret = RET_FINISH;
}
__except(1)
{
ret = RET_OTHER_EXCEPTION;
int i = GetExceptionCode();
if (i == EXCEPTION_STACK_OVERFLOW)
{
ret = RET_STACK_EXCEPTION;
}
}
return ret;
}
int main()
{
int cnt = 0;
if (foo_redundant(0xfffffff0) == RET_STACK_EXCEPTION)
{
cnt++;
}
if (cnt == 1)
{
printf("pass\n");
}
else
{
printf("fail\n");
}
}
main()では、foo_redundantに多数が渡され、最終的に_alloca()に渡され、_alloca()が失敗します。
次の使用例は、ASAN なしでコンパイルした場合 (つまり、pass スイッチなし) に-fsanitize=address出力しますが、ASAN をオン (つまり、fail スイッチ) でコンパイルすると、-fsanitize=address出力されます。 ASAN がないと、例外コードが RET_STACK_EXCEPTION 一致するため、 cnt は 1 に設定されるためです。 スローされた例外が AddressSanitizer エラー (dynamic-stack-buffer-overflow) であるため、ASAN をオンにしてコンパイルすると、動作が異なります。 つまり、コードはRET_OTHER_EXCEPTIONではなくRET_STACK_EXCEPTIONを返すので、cntは 1 に設定されません。
その他の利点
新しい ASAN ランタイムでは、追加のバイナリをアプリと共にデプロイする必要はありません。 これにより、追加のバイナリを管理する必要がないため、通常のテスト ハーネスで ASAN をさらに簡単に使用できます。
関連項目
AddressSanitizer Continue on Error ブログの投稿
メモリ安全性エラーの例
-Zi コンパイラ フラグ
-fsanitize=address コンパイラ フラグ
上位 25 の最も危険なソフトウェアの弱点