本サイトでは、アフィリエイト広告およびGoogleアドセンスを利用しています。

VisualStudio 2012でSALを使う

SALとは、Sourcecode Annotation LanguageといってMicrosoftが定義しているコンパイラで使用できる注釈のことです。
これは注釈という名が示すとおり、プログラムコードに注釈という形で情報を付加する仕組みです。

出来るようになることは割と多くあるのですが、主に以下のようなことが出来るようになります。

  • 関数戻り値の確認をチェックするしているコードか判定
  • 関数入力に渡されたバッファに対する範囲内アクセスかどうかの確認
  • 関数出力用の変数に値を格納しているか、格納しないままの実行パスはないか、の確認
  • バッファが書式指定を持つバッファの場合の引数個数確認

大雑把に言えば、渡されるバッファの境界チェックが出来るという感じでしょうか。
あと、エラーを返すような関数で、呼出元が戻り値チェックを省いていることを検知できるとか。安全なプログラムを作るためにこれらの機能をうまく使っていきたいですね。

ちなみにこのSALは、WindowsSDKやDirectX SDKで提供されるヘッダファイルらに既に適用されています。
さて、なぜ今SALなのかといいますと、VisualStudio 2012 Professional にて分析機能が使えるようになり、この分析という部分がSALを使用しているプログラムソースコードを解析することが出来るようになるためです。

詳しいSALの使い方はMSDNのヘルプを参照してもらうことにして、ここではどんな感じに使えるのかを例を出したいと思います。
例として、引数に指定された文字列を関数内部でメモリ確保して、呼出元に返すような関数を考えます。SALを考慮しないと下記のようなコードになるかと思います。

void allocAndCopy( char** ppResultBuf, const char* szSrc ) {
    int size = strlen(szSrc)+1;
    char* pAlloc = (char*)malloc( size );
    strcpy_s( pAlloc, size, szSrc );
    (*ppResultBuf) = pAlloc;
}

この状態で分析を行っても何も警告は表示されません。

まず関数にとってのIn/Outを識別するためのSALを引数に設定してみます。

void allocAndCopy( _Deref_out_z_ char** ppResultBuf, _In_z_ const char* szSrc ) {
    int size = strlen(szSrc)+1;
    char* pAlloc = (char*)malloc( size );
    strcpy_s( pAlloc, size, szSrc );
    (*ppResultBuf) = pAlloc;
}

ここで追加した、_Deref_out_z_, _In_z_ らはそれぞれ、関数内でメモリ確保された出力バッファ(NULL終端)、入力バッファはNULL終端、であるという意味を持っています。
この状態で分析を行うと、次のような警告メッセージが表示されます。

warning : C6387: '*ppResultBuf' は '0' である可能性があります:
この動作は、関数 'allocAndCopy' の指定に従っていません。
warning : C28196: '*_Param_(1)!=0' は満たされない要件です
(式は true に評価されません)。

つまり、mallocに失敗してNULLが入ってくる場合を警告しています。
NULLが入ってきた場合には *ppResultBuf にNULLが格納されます(その前にstrcpy_sでアクセス違反すると思いますが)。
このNULLが格納されるという可能性が、_Deref_out_z_ の仕様に合っていないという警告です。

では、この関数がエラー状態を返すように戻り値boolを追加してみます。
ついでに関数が予期しない動作をしないようにもう少しチェックを追加しておきます。

bool allocAndCopy( _Deref_out_z_ char** ppResultBuf, _In_z_ const char* szSrc ) {
    int size = strlen(szSrc)+1;
    char* pAlloc = (char*)malloc( size );
    if( pAlloc == NULL ) {
        return false;
    }
    strcpy_s( pAlloc, size, szSrc );
    (*ppResultBuf) = pAlloc;
    return true;
}

この状態で分析を行うと次のような警告が出てきます。

warning : C6101: 初期化されていないメモリ '*ppResultBuf' を返しています。
関数を通じた正規のパスが、指定された _Out_ パラメーターを設定しません。
FALSE を返すことが失敗を示す場合は、この関数に _Success_(return) の注釈を付けてください。
warning : C6054: 文字列 '*ppResultBuf' は 0 で終了しない可能性があります。

つまり、return falseを追加して関数失敗を返すようにしたことで、
出力バッファが確保されていないという先ほど追加したSALの仕様に合っていないと警告されています。

そこで、上記でも指摘されていますが、関数が成功の時にこのチェックが有効となるようにSALを追加します。

_Success_(return!=false)
bool allocAndCopy( _Deref_out_z_ char** ppResultBuf, _In_z_ const char* szSrc ) {
    int size = strlen(szSrc)+1;
    char* pAlloc = (char*)malloc( size );
    if( pAlloc == NULL ) {
        return false;
    }
    strcpy_s( pAlloc, size, szSrc );
    (*ppResultBuf) = pAlloc;
    return true;
}

これで分析を行うと、警告は出てこなくなりました。
しかし考えてみるとまだ不十分です。
この関数はエラーを返しているのに、そのエラーを呼出元で検知できなかったらどうでしょうか。結局プログラムは誤動作してしまいます。
このようなケースに対応するために戻り値を省略しないように警告するSALを追加してみます。

_Success_(return!=false) _Check_return_
bool allocAndCopy( _Deref_out_z_ char** ppResultBuf, _In_z_ const char* szSrc ) {
    int size = strlen(szSrc)+1;
    char* pAlloc = (char*)malloc( size );
    if( pAlloc == NULL ) {
       return false;
    }
    strcpy_s( pAlloc, size, szSrc );
    (*ppResultBuf) = pAlloc;
    return true;
}

これで、戻り値を無視してこの関数を呼んでいる部分で下記のような警告が表示されます。

warning : C6031: 戻り値が無視されました: 'allocAndCopy'

次にもう1つ呼出元を変更して警告の状況を確認しておきましょう。

allocAndCopy( &pBuffer, NULL );

この呼出があると分析の結果では次のように表示されます。

warning : C6387: '_Param_(2)' は '0' である可能性があります:
この動作は、関数 'allocAndCopy' の指定に従っていません。

これは _In_z_ による注釈が有効になっているため検知できた部分です。
これは、バッファは呼び出し元で確保されていることが条件で関数内部では読み取り動作を行う、という意図を表しているためです。
SALの追加と分析という機能の様子を簡単ですが紹介してみましたが、どうでしょうか。
これでこの関数としてはおよそ追加が終わった状態ですが、分析による警告のことを考えると追加しただけの手間はあったのではないでしょうか。
簡単なチェックではありますが、使用する側も関数を実装する側もつい忘れてしまいがちなエラーケースをツールから警告して貰うことにより安全なプログラムを忘れずにかけるようになるかと思います。

プログラミング
すらりんをフォローする
すらりん日記
タイトルとURLをコピーしました