EN JP CN

C/C++ 解析のチューニング

C/C++ 解析のチューニング

C/C++ 解析のチューニング

C/C++ 解析のチューニング

チューニングにより真の欠陥を検出するか、誤検知と重要でない指摘の数を低減することで、Klocwork 解析の精度を高めることができます。チューニングには、解析中にKlocwork が認識するコードの変更と Klocwork によるコードの解釈方法の変更という 2 種類の見方があります。

Klocwork のチューニング方法

この記事では、3 種類の一般的な Klocwork 解析のチューニング方法について説明します。

  • _KLOCWORK_ マクロを使用する場合は、Klocwork 解析中の条件付きコンパイルを利用して、コンパイラに別のバージョンのソースコードを提供します。
  • #kw_override プリプロセッサディレクティブを使用してマクロ定義をオーバーライドする場合は、Klocwork がコード内で認識するマクロを抑制または置換します。このチューニング方法はコンパイラごとの変更の点で _KLOCWORK_ マクロよりも多少制限されていますが、ソースコードの変更が不要です。
  • knowledge base (ナレッジベース) レコードを使用する場合は、Klocwork によるコード動作の解釈を変更するために、コードの特定関数をチューニングします。このメソッドは最も柔軟なチューニング方法で、Klocwork による自動生成レコードに代わる knowledge base (ナレッジベース) レコードを作成したり、ソースコードが存在しない場合に新規モデルを提供する knowledge base (ナレッジベース) レコードを作成することができます。

この記事のウォークスルー例では、これらのチューニング方法を使用するための一般的なシナリオについて説明します。

ウォークスルー 1:__KLOCWORK__ マクロを使用した NPD 誤検知の処理

__KLOCWORK__ マクロの典型的な用途は、カスタムアサートにより発生した NPD 誤検知の処理です。

アサート

アサートは有効性チェック、および関数の事前条件と事後条件を実装するために、C/C++ コードで広く使用されています。たとえば、関数が非 NULL ポインターの引数を予期する場合は、次のようなコードを使用します。

1  void my_function(my_data_t *ptr)
2  {
3     MY_ASSERT(ptr != NULL);
4     char *name = ptr->name;
5  }

静的解析ツールがコード内のアサートを認識し、条件チェックとして適切に処理することが重要になります。たとえば、上のコード例の MY_ASSERT が "NULL のチェック ptr" として認識されない場合、Klocwork により 4 行目で NULL ポインター逆参照の指摘が報告されます (誤検知)。

標準 C/C++ ライブラリでは、assert.h に assert() のデフォルト実装が提供されていますが (通常、条件をチェックし、エラーの場合に停止するマクロ)、次の目的のためにカスタム ASSERT マクロを定義することが一般的です。

  • 診断の向上 - 通常標準バージョンはエラーが発生した条件、ファイル名、および行番号を表示しますが、カスタムアサートでは、エラーメッセージに関数またはモジュール名を追加したり、一部のロギングシステムからファイルにエラーを報告することができます。
  • ビルド構成の柔軟性を向上 - 製品のデバッグバージョンでは、アサートを定義して拡張エラーレポートを実装するか、デバッガーを自動的に起動できますが、アサートのリリースバージョンでは、空のステートメントに拡張して追加健全性チェックのオーバーヘッドを排除できます。

__KLOCWORK__ マクロの使用

条件に応じてコンパイルしたコードをソースに追加する手段は、異なる環境に対応するために一般的に使用されています。Klocwork コンパイラによって常に定義される __KLOCWORK__ マクロを使用すると、Klocwork の実行中のみに特定のコードが有効になり、それ以外の場合はコードが無視されます。__KLOCWORK__ ブロックにアサートタイプのマクロの別の定義を追加する場合は、コードを変更する必要がなく、管理がシンプルになります。

たとえば、次のコードがあるとします。

1   MY_ASSERT(a!=NULL);
2   use(a->mm);

Klocwork が不正に生産定義 MY_ASSERT を処理して、abort 条件を理解しないと、誤ってポジティブな NPD を行 2 に報告する可能性があります。Klocwork により簡単で、より標準的なカスタムアセットの定義を送ることで、これが発生する確率を下げることができます。

1   #ifdef __KLOCWORK__
2   # define MY_ASSERT(x) do { if (!(x)) abort(); } while (0)
3   #else
4   # define MY_ASSERT(x) ... production, advanced definition of assert ...
5   #endif

ウォークスルー 2:NPD 誤検知の処理 #kw_override

__KLOCWORK__ マクロは標準プリプロセッサメカニズムを使用した汎用コード置換をサポートしており、非常に強力である一方で、やはり Klocwork に合わせてソースコードを変更する必要があります。コードに必要な変更がいくつかのマクロ定義に限定されている場合は、オーバーライドファイルを使用してソースの変更を回避できます。

オーバーライドファイルは、Klocwork プロジェクトにインポートするヘッダーファイルです。Klocwork コンパイラは、プロジェクトにビルドされたすべてのソースモジュールに自動的にオーバーライドファイルを含めて処理します。このファイルには、ソースのマクロ定義を変更するための Klocwork 専用のプリプロセッサディレクティブ、#kw_override が使用されています。

運用中のソースに次のマクロ定義が含まれるとします。

# define MY_ASSERT(x) (void) ((x) __handle_failed_assert(#x, __FILE__, __LINE__, __FUNCTION__))

コンパイラによりシンプルで直接的なアサートを使用するには、次の定義を含むオーバーライドファイルを作成できます。

# kw_override MY_ASSERT(x) do { if( !(x) ) abort(); } while(0) 

このオーバーライドファイルを Klocwork プロジェクトにインポートしてプロジェクトをビルドすると、次のようなソース関数は

1   void myFunction(char *aString)
2   {
3     MY_ASSERT(aString); 
4   }

Klocwork コンパイラによって、MY_ASSERT の Klocwork 専用定義に拡張されます。

1    void myFunction(char* aString)
2    {
3      do { if( (!aString) ) abort(); } while(0);
4    }

その一方で、運用中のコンパイラは引き続き元の定義を使用してマクロを拡張します。

#kw_override ディレクティブは、運用中のソースに定義されたマクロのみに影響します。

オーバーライドファイルを統合ビルド解析に適用するには、プロジェクトまたは projects_root ディレクトリに インポート します。オーバーライドファイルをスタンドアロンデスクトップに適用するには、kwcheck import を使用します。

ウォークスルー 3:knowledge base (ナレッジベース) レコードによるチューニング

Klocwork では、ソースコードの初回解析時に自動的に knowledge base (ナレッジベース) を生成し、解析を行うたびにこの情報を更新します。ネイティブシステムの関数の動作は、デフォルトの knowledge base (ナレッジベース) で指定されます。誤検知と検知漏れを処理するには、Klocwork によるコードの解釈方法を変更するために独自の knowledge base (ナレッジベース) レコードを作成して、結果を微調整できます。

このウォークスルーでは、knowledge base (ナレッジベース) レコードの 4 つのチューニング例を示し、knowledge base (ナレッジベース) レコードの追加の実装方法について説明します。

  • 例 1:ALLOC ignore による誤検知の削減
  • 例 2:XMRF によるメモリリーク指摘のチューニング
  • 例 3:NORET による NPD チェッカーのチューニング
  • 例 4:ALLOC によるメモリリークのチューニング
  • knowledge base (ナレッジベース) への追加の実装

knowledge base (ナレッジベース) のレコード構文の詳細については、C/C++ knowledge base (ナレッジベース) リファレンス を参照してください。

例 1:ALLOC ignore による誤検知の削減

時によって、考えられるメモリ割り当て関数に関する指摘レポートを回避したい場合があります。たとえば、次の抜粋の Klocwork メモリリークチェッカー、MLK は通常 9 行目で 'p' のメモリリークを報告します。

1   void* C::custom_alloc(unsigned int n)
2   {
3       return malloc(n);
4   }
5
6   void C::foo()
7   {
8       void* p = custom_alloc(10);
9       return;
10  }

解析に ALLOC ignore という knowledge base (ナレッジベース) レコードを追加することで、検出を抑制できます。

    C::custom_alloc - ALLOC ignore

このレコードは、完全修飾関数名、関数のキー (常にハイフン '-')、レコードの種類 (この場合は ALLOC)、およびこの knowledge base (ナレッジベース) レコードの指定、'ignore' を指定します。

この新しいレコードのために、Klocwork では custom_alloc がメモリ割り当て関数ではないものと想定され、報告が行われません。

例 2:XMRF によるメモリリーク指摘のチューニング

デフォルトのメモリリークチェッカーは、割り当て済みメモリのポインターが不明な関数に渡される場合、ポインターが何らかの方法で変更される (エイリアス化、共有プールに格納、または解放) ことを想定しているため、追跡を停止します。

この動作は、XMRF レコードを使用してメモリのオーナーシップを保持するかどうかを指定することで変更できます。メモリのオーナーシップを保持する場合は、追跡を継続しますが、メモリのオーナーシップを保持しない場合は、呼び出された関数がメモリを引き継ぐことをエンジンが想定して追跡を停止します。

次の抜粋では、Klocwork は通常 8 行目の後に 'p' の追跡を停止し、9 行目で報告が行われません。

1    // some library function
2    // does not take ownership or free memory pointed to by 'ptr'
3    void process_ptr(void*);
4
5    void foo()
6    {
7       void* p = malloc(100);
8       process_ptr(p);         // Klocwork assumes that ownership of 'p' is not retained
9    }                          // FN: no memory leak reported here

この未検知を回避するには、解析にメモリ保持 (XMRF) レコードを追加できます。

    process_ptr - XMRF $1 : 1

このレコードは、呼び出し関数が 'process_ptr' に最初の引数 ($1) として渡されるメモリのオーナーシップを保持することをチェッカーに通知します。保持フラグは、呼び出し側がオーナーシップを保持しないことを示す 0 か、呼び出し側がオーナーシップを保持することを示す 1 のいずれかとすることができます。このレコードをサンプル抜粋の解析に適用すると、チェッカーが 8 行目の後でもポインター 'p' の追跡を継続し、9 行目でメモリリークを正しく報告します。

反対に、関数パラメーターが修飾された const、または非静的クラスメソッドの 'this' の場合は、チェッカーが不明とみなす関数に渡される場合でも、MLK チェッカーが割り当て済みメモリのポインターの追跡を継続します。次の 2 つの抜粋に示したように、これらの状況で誤検知が発生する場合があります。

1    // Collect 'ptr' for pool management
2    extern int pool_ptr(const void* ptr);
3
4    int foo()
5    {
6      void* p = malloc(100);
7      int res = pool_ptr(p);   //"const" heuristics: keep tracking 'p'
8      return res;              // FP: memory leak reported
9    }
1    class C
2    {
3    public:
4      // Collect 'this' for pool management
5      int pool_self();
6    };
7
8    int bar()
9    {
10     C* c = new C();
11     int res = c->pool_self();  // 'this' heuristics: keep tracking 'c'
12     return res;                // FP: memory leak reported
13   }

これらの場合は、次の XMRF レコードを解析に追加して、誤検知の報告を防止することができます。

    pool_ptr - XMRF $1 : 0
    C::pool_self - XMRF $0 : 0

これらのエントリは、呼び出し側がメモリのオーナーシップを保持せず、以降は追跡を行わないことをチェッカーに指示します。

例 3:NORET による NPD チェッカーのチューニング

NULL ポインター逆参照チェッカー、NPD は、関数呼び出しで戻り値がないことを認識していない場合に誤検知を行う可能性があります。たとえば、プログラムでは、エラーの重要度レベルを使用して、現在のコンテキストに関係なく、abort や longjmp() などのレポート関数の応答を判断することが一般的です。この点を認識しない場合、次の抜粋は、通常 15 行目で NULL のチェックに関する NPD を報告し、17 行目で逆参照を行います。

1   class C
2   {
3   public:
4     static C* findInstance(const char* name);
5     virtual void run();
6   };
7
8   // Error handling, abort() on ERROR or FATAL
9   enum { INFO, ERROR, FATAL };
10  void error_msg(int level, const char *msg);
11
12  void foo()
13  {
14    C *ptr = C::findInstance("default");
15    if( !ptr )
16        error_msg(FATAL, "Can't find default object");
17    ptr->run();
18  }

error_msg 関数が致命的エラーを報告して、必ず終了するような誤検知を防止するには、次の NORET レコードを使用できます。

    error_msg - NORET

しかし、このコードの error_msg は 1 (ERROR または FATAL) より大きい最初の引数で呼び出された場合にのみ終了し、NORET ではこの状況に対して一般的過ぎるため、CONDNORET を代わりに使用する必要があります。

    error_msg - CONDNORET $1 > 1

例 4:ALLOC によるメモリリークのチューニング

一般的なシステムプリミティブを使用しない場合は、メモリ割り当てレコード ALLOC を使用してメモリの割り当て方法と、割り当てる条件を指定できます。

特に ALLOC では、関数について以下を定義することができます。

  • 関数がパラメーターに応じた条件で新規メモリを割り当てること (事前条件)
  • 関数が特定パラメーターまたは結果に割り当てられたアドレスを格納すること (返されたアドレス)
  • メモリ割り当てに成功した場合に関数が特定の値を返すこと (事後条件)

次の抜粋では、Klocworkが 'get_buf'、および戻り値コードと成功した割り当ての関係をどの程度判断できるかに応じて、10 行目で誤検知のメモリリークが報告される場合があります。

1   // get_buf allocates memory and returns 0 on success
2   extern int get_buf(void** buf);
3
4   void process_data()
5   {
6     void* buffer;
7     if( !get_buf(&buffer) )
8        free(buffer);
9     else
10       fprintf(stderr, "operation failed!\n");
11  }

このような報告を排除するために、ALLOC knowledge base (ナレッジベース) レコードを定義できます。

    get_buf - ALLOC stdc : 1 : *$1 : $$ EQ(0)

このレコードは以下を記述します。

  • 割り当てが 'stdc' グループに属し、'free' の自然ソースであることについては、'malloc' や関連関数のように動作することが予想できます。
  • 事前条件 (1) がなく、割り当てを常に試行します。
  • 割り当てに成功した場合、新規に割り当てられたメモリのポインターが最初のパラメーター (*$1) によりポイントされるアドレスに格納されます。
  • 戻り値コードが 0 ($$ EQ 0) の場合にのみ、割り当てに成功します。

knowledge base (ナレッジベース) への追加の実装

knowledge base (ナレッジベース) レコードを実装するには、knowledge base (ナレッジベース) ファイルを作成して Klocwork 解析に適用する必要があります。

knowledge base (ナレッジベース) ファイルの作成:

knowledge base (ナレッジベース) ファイルを作成するには:

  1. 拡張子 .kb の新規テキストファイルを作成します。
  2. 好みのエディターで必要なレコードを追加します。ファイルの 1 行に各レコードを指定します。
  3. 意図したとおりに生成される knowledge base (ナレッジベース) をオーバーライドしていることを確認してください。新規 .kb ファイルのレコードは、生成された .kb ファイル内で、特定関数の同じレコード名のみのすべてのレコードをオーバーライドします。たとえば、次のサンプルファイルに示した CONDNORET レコードは、生成された .kb 内で、error_msg 関数のすべての CONDNORET レコードをオーバーライドしますが、NORET、BO、CHECKRET、その他の生成レコードには影響を与えません。
以降には、この記事の 4 つの例のレコードを含む .kb ファイルの例を示します。
    C::custom_alloc - ALLOC ignore
    process_ptr - XMRF $1 : 1
    pool_ptr - XMRF $1 : 0
    C::pool_self - XMRF $0 : 0
    error_msg - CONDNORET $1 > 1
    get_buf - ALLOC stdc: 1 : *$1 : $$EQ(0)
新規レコードが及ぼす影響について不明な場合は、生成ファイルでオーバーライドしない関数の同じレコード名のすべてのレコードを新規 .kb ファイルにコピーします。

解析へのファイルの適用:

新しい構成ファイルを統合ビルド解析に適用するには、プロジェクトまたは projects_root にインポートします。たとえば、次のようになります。

    kwadmin import-config <projectname> <file> 

このファイルは connected desktop (コネクテッドデスクトップ) と同期されます。

knowledge base (ナレッジベース) ファイルをスタンドアロンデスクトッププロジェクトに適用するには、デスクトップ解析のカスタマイズ を参照してください。

完全ビルド解析を実行します。

チューニングの変更内容を有効にするには、全ビルド完 解析を実行する必要があります。