SAL について
Microsoft ソース・コード注釈言語 (SAL の) は、関数のパラメーターをどのように使用するかを記述するために使用できる一連の注釈、それらについて行う前提、および終了を使用したという保証が用意されています。注釈は、ヘッダー ファイル <sal.h>で定義されます。C++ の Visual Studio コード分析は関数の分析を変更するには、SAL 注釈を使用します。Windows ドライバー開発に対する SAL 2.0 の詳細については、参照します SAL Windows ドライバーでは 2.0 の注釈。
ネイティブ コードでは、C および C++ の空白の明確な意図や不変性に開発者に限られた方法のみです。SAL 注釈を使用すると、これらを使用して、その使用方法を開発者をより的確に把握できるように関数をさらに詳細に記述できます。
SAL の概要とそれを使用する理由
簡単に指定すると、コンパイラは、SAL であるようにコードを検証することを許可する負荷の方法です。
SAL でコードの価値を高める
SAL でコード設計を人間とコード分析ツールについて、理解しやすくすることができます。C のランタイム関数 memcpyを説明する例を示します。:
void * memcpy(
void *dest,
const void *src,
size_t count
);
この関数の動作を確認できます。関数が実装されるか、呼び出されるプログラムの正当性を保証するために、特定のプロパティを保持する必要があります。、例のような宣言を調べると、何かを認識しません。SAL 注釈がないと、ドキュメントまたはコード コメントにはなりません。ここで memcpy の MSDN ドキュメントを理解しています:
「dest へのソースのコピーのバイト数。ソースとターゲットが重なり合って、memcpy 関数の動作は未定義です。重なり合う領域を処理するには、memmove を使用します。セキュリティ メモ: は 変換先バッファーがソース バッファーのサイズ以上であることを確認してください。詳細については、参照します Avoiding Buffer Overruns "を」
ドキュメントは、プログラムの正当性を確認するコードは、特定のプロパティを保持する必要があることを提案する情報のいくつかのビットが含まれます。:
memcpy は ソース バッファーからの変換先バッファーにバイトの count をコピーします。
変換先バッファーはソース バッファー少なくとも大きくなければなりません。
ただし、コンパイラは、ドキュメントまたは非公式のコメントを読み取ることができません。2 種類のバッファーと count間にリレーションシップが存在する場合や、関係について効率的に推測できないことを認識します。SAL は次に示すように、関数のプロパティと実装の詳細については、"わかりやすさを提供する:
void * memcpy(
_Out_writes_bytes_all_(count) void *dest,
_In_reads_bytes_(count) const void *src,
size_t count
);
これらの注釈は、MSDN ドキュメントの情報に注意して、より簡潔に似ていますが、パターン、続行します。このコードを読み取る場合は、バッファー オーバーランのセキュリティ上の問題を回避する方法について、この関数のプロパティを理解します。、SAL は潜在的なバグの初期の検出の自動コード分析ツールの効率と効率を向上できる提供する意味パターンにします。ユーザーが wmemcpyのこのおかしな実装を記述することが想定する:
wchar_t * wmemcpy(
_Out_writes_all_(count) wchar_t *dest,
_In_reads_(count) const wchar_t *src,
size_t count)
{
size_t i;
for (i = 0; i <= count; i++) { // BUG: off-by-one error
dest[i] = src[i];
}
return dest;
}
この実装はエラー以外に共通する 1 が含まれます。しかし、コード作成者は単独でこの関数の分析によって SAL バッファー サイズで注釈のコード分析ツールをバグをキャッチできますが含まれていました。
SAL の基礎
SAL で使用パターンに分類される 4 種類の基本型のパラメーターを定義します。
分類 |
パラメーター注釈 |
説明 |
---|---|---|
呼び出された関数への入力 |
_In_ |
データは、呼び出された関数に渡して、読み取り専用として扱われます。 |
呼び出し元の関数を呼び出した関数および出力への入力 |
_Inout_ |
使用可能なデータは関数に渡されます (通常は変更されます。 |
呼び出し元への出力 |
_Out_ |
呼び出し元は、関数に対してに記述する領域を提供します。呼び出された関数はその領域にデータを書き込みます。 |
呼び出し元へのポインターの出力 |
_Outptr_ |
Output to callerのです。呼び出された関数によって返される値は、ポインターです。 |
この 4 種類の基本的な注釈は、さまざまな方法で明示的に行うことができます。既定では成功することが必須にあり、指定した関数ポインター パラメーターに null 以外であると見なされます。基本的な注釈で最もよく使用されるバリエーションが NULL の場合は省略可能パラメーター、関数ポインターが成功する可能性のあることを、作業をすることを示します。
このテーブルで必須と省略可能なパラメーターを区別しています:
パラメーターが必要です。 |
パラメーターは省略可能です |
|
---|---|---|
呼び出された関数への入力 |
_In_ |
_In_opt_ |
呼び出し元の関数を呼び出した関数および出力への入力 |
_Inout_ |
_Inout_opt_ |
呼び出し元への出力 |
_Out_ |
_Out_opt_ |
呼び出し元へのポインターの出力 |
_Outptr_ |
_Outptr_opt_ |
これらの注釈は形式的で正確な方法で有効で初期化されていない値と無効な null ポインターの使用を特定するのに役立ちます。必須パラメーターに NULL を渡すと、クラッシュが起こる可能性がある場合、またはにより失敗しました」返されるエラー コードを「可能性があります。いずれの場合も、関数は仕事をすることに成功することはできません。
SAL の例
このセクションでは、基本的な SAL 注釈のコード例を示します。
Visual Studio のコード分析ツールを使用して問題を検出する
例では、コード障害を見つけるために、SAL 注釈とともに Visual Studio のコード分析ツールが使用されます。次に実行する方法を示します。
Visual Studio のコード分析ツールと SAL を使用するには
Visual Studio では、SAL 注釈を含む開きます。. C++ のプロジェクト。
メニュー バーで、[ソリューションでコード分析を実行][ビルド] をクリックします。
このセクションの_In_の例を示します。このファイル内のコード分析を実行すると、この警告が表示されます。:
C6387 無効なパラメーター値「パイント」は「0 "である可能性があります: これは InCallee 「関数"の指定に従っていません。
例: _In_ 注釈
_In_ の注釈を示します:
パラメーターで有効であり、変更されません。
関数は、単一の要素から成るバッファーから読み取ります。
呼び出し元がバッファーを提供し、初期化する必要があります。
_In_ は 「読み取り専用」を指定します。よくある間違いは _Inout_ の注釈が代わりに必要なパラメーターに _In_ を適用することです。
_In_ は 許可されますが、ポインターのスカラーのアナライザーでは無視されます。
void InCallee(_In_ int *pInt)
{
int i = *pInt;
}
void GoodInCaller()
{
int *pInt = new int;
*pInt = 5;
InCallee(pInt);
delete pInt;
}
void BadInCaller()
{
int *pInt = NULL;
InCallee(pInt); // pInt should not be NULL
}
この例の Visual Studio コード分析を使用している場合、呼び出し元が pIntの初期化されたバッファーに NULL 以外のポインターを渡すことを検証します。この場合、pInt のポインターを null にすることはできません。
例: _In_opt_ 注釈
_In_opt_ は _In_と同じですが、入力パラメーターが null であり、この関数はこれをチェックする必要があります。
void GoodInOptCallee(_In_opt_ int *pInt)
{
if(pInt != NULL) {
int i = *pInt;
}
}
void BadInOptCallee(_In_opt_ int *pInt)
{
int i = *pInt; // Dereferencing NULL pointer ‘pInt’
}
void InOptCaller()
{
int *pInt = NULL;
GoodInOptCallee(pInt);
BadInOptCallee(pInt);
}
Visual Studio コード分析がバッファーにアクセスする前に、null の関数チェックを検証します。
例: _Out_ 注釈
_Out_ は 一般的なシナリオをバッファーを指す NULL 以外のポインターをサポートし、渡された関数は要素を初期化します。呼び出し元は、呼び出しの前にバッファーを初期化する必要はありません; 呼び出された関数が戻る前に初期化すると約束します。
void GoodOutCallee(_Out_ int *pInt)
{
*pInt = 5;
}
void BadOutCallee(_Out_ int *pInt)
{
// Did not initialize pInt buffer before returning!
}
void OutCaller()
{
int *pInt = new int;
GoodOutCallee(pInt);
BadOutCallee(pInt);
delete pInt;
}
Visual Studio のコード分析ツールは、呼び出し元が pInt のバッファーに非 null ポインターを渡すと、戻る前にバッファーが関数で初期化されることを検証します。
例: _Out_opt_ 注釈
_Out_opt_ は _Out_と同じですが、パラメーターが NULL の場合、この関数はこれをチェックする必要があります。
void GoodOutOptCallee(_Out_opt_ int *pInt)
{
if (pInt != NULL) {
*pInt = 5;
}
}
void BadOutOptCallee(_Out_opt_ int *pInt)
{
*pInt = 5; // Dereferencing NULL pointer ‘pInt’
}
void OutOptCaller()
{
int *pInt = NULL;
GoodOutOptCallee(pInt);
BadOutOptCallee(pInt);
}
Visual Studio コード分析は pInt が逆参照される前に、戻る前にバッファーが関数で初期化される pInt が null であるか null のこの関数チェックを検証します。
例: _Inout_ 注釈
_Inout_ が 関数によって変更される可能性があるポインター パラメーターに注釈を付けるために使用されます。ポインターを呼び出す前に有効な初期化されたデータ ポイントを変更しても、戻り値の有効な値が必要です。注釈は、関数がバッファーからの読み込み、要素 1 バッファーに書き込む可能性があることを指定します。呼び出し元がバッファーを提供し、初期化する必要があります。
[!メモ]
_Out_と同様に、_Inout_ は変更可能な値に適用する必要があります。
void InOutCallee(_Inout_ int *pInt)
{
int i = *pInt;
*pInt = 6;
}
void InOutCaller()
{
int *pInt = new int;
*pInt = 5;
InOutCallee(pInt);
delete pInt;
}
void BadInOutCaller()
{
int *pInt = NULL;
InOutCallee(pInt); // ‘pInt’ should not be NULL
}
Visual Studio コード分析は、呼び出し元が pIntの初期化されたバッファーに非 null ポインターを渡すと、バッファーが初期化されることを、返される前に、pInt がまだ null 以外であることを検証します。
例: _Inout_opt_ 注釈
_Inout_opt_ は _Inout_と同じですが、入力パラメーターが null であり、この関数はこれをチェックする必要があります。
void GoodInOutOptCallee(_Inout_opt_ int *pInt)
{
if(pInt != NULL) {
int i = *pInt;
*pInt = 6;
}
}
void BadInOutOptCallee(_Inout_opt_ int *pInt)
{
int i = *pInt; // Dereferencing NULL pointer ‘pInt’
*pInt = 6;
}
void InOutOptCaller()
{
int *pInt = NULL;
GoodInOutOptCallee(pInt);
BadInOutOptCallee(pInt);
}
Visual Studio コード分析が返される前にバッファーが関数で初期化されること pInt が null でない場合は、バッファーにアクセスする前に、null のこの関数検証チェックします。
例: _Outptr_ 注釈
_Outptr_ が ポインターを返すように設計されたパラメーターに注釈を付けるために使用されます。パラメーター自体が初期化されたデータに NULL で呼び出された関数の戻り値は NULL 以外のポインターを、そのポインターが必要です。
void GoodOutPtrCallee(_Outptr_ int **pInt)
{
int *pInt2 = new int;
*pInt2 = 5;
*pInt = pInt2;
}
void BadOutPtrCallee(_Outptr_ int **pInt)
{
int *pInt2 = new int;
// Did not initialize pInt buffer before returning!
*pInt = pInt2;
}
void OutPtrCaller()
{
int *pInt = NULL;
GoodOutPtrCallee(&pInt);
BadOutPtrCallee(&pInt);
}
Visual Studio コード分析は、呼び出し元が *pIntに非 null ポインターを渡すと、戻る前にバッファーが関数で初期化されることを検証します。
例: _Outptr_opt_ 注釈
_Outptr_opt_ は _Outptr_と同じですが、パラメーターはパラメーターの Null ポインターで省略可能な呼び出し元を渡すことができます。
void GoodOutPtrOptCallee(_Outptr_opt_ int **pInt)
{
int *pInt2 = new int;
*pInt2 = 6;
if(pInt != NULL) {
*pInt = pInt2;
}
}
void BadOutPtrOptCallee(_Outptr_opt_ int **pInt)
{
int *pInt2 = new int;
*pInt2 = 6;
*pInt = pInt2; // Dereferencing NULL pointer ‘pInt’
}
void OutPtrOptCaller()
{
int **ppInt = NULL;
GoodOutPtrOptCallee(ppInt);
BadOutPtrOptCallee(ppInt);
}
Visual Studio コード分析が返される前に *pInt が逆参照される前に、バッファーは関数によって初期化されることが null のこの関数チェックを検証します。
例: _Out_ と _Success_ 注釈の組み合わせ
注釈は、ほとんどのオブジェクトに適用できます。特に、関数全体に注釈を付けることができます。関数の最も明白な特性の 1 つが成功または失敗する可能性があることです。ただし、バッファー サイズとの間の関連付けのように、C/C++ は関数の成功または失敗を表現できません。_Success_ のコメントを使用して、関数の、成功と表示されるかを確認できます。_Success_ の注釈へのパラメーターは関数は、成功したことをこのプロパティが true のときに示す式です。式は処理注釈パーサーが挙げられますでもかまいません。関数が成功した場合のみ関数の戻り値が適用された後、注釈の効果。正しいことを確認するに _Success_ が _Out_ とどのように対話する例を次に示します。戻り値を表すために return キーワードを使用できます。
_Success_(return != false) // Can also be stated as _Success_(return)
bool GetValue(_Out_ int *pInt, bool flag)
{
if(flag) {
*pInt = 5;
return true;
} else {
return false;
}
}
_Out_ の注釈により Visual Studio コード分析は、呼び出し元が pIntのバッファーに非 null ポインターを渡すと、戻る前にバッファーが関数で初期化されることを検証します。
SAL のベスト プラクティス
既存のコードに注釈を追加する
SAL でコードのセキュリティと信頼性を向上させるのに役立つ強力な手法です。SAL を学んだ後、毎日の作業に新しいスキルを適用できます。新しいコードでは、何らかの SAL ベースの仕様を意図的に使用できる; 古いコードで、更新するたびにより注釈を段階的に追加し、利点を向上させることができます。
Microsoft のパブリックなヘッダーによって指定されます。したがって、プロジェクトに多くの機能を利用するために Win32 API を呼び出すリーフ関数や関数を使用することをお勧めします。
注釈を付けるタイミング
ガイドラインを次に示します。:
すべてのポインター パラメーターを指定します。
コード分析がバッファーやポインターの安全性を確認できるように、値の範囲の注釈を使用します。
規則のロック、副作用をロックする Annotate。詳細については、「ロック動作に注釈を付ける」を参照してください。
ドライバー プロパティなどのドメイン固有のプロパティを使用してください。
また、注釈が発生したことを意図を明確にし、全体を簡単にするために、すべてのパラメーターに注釈を付けることができます。