目次へ戻る §27へ戻る §29へ進む

28. C/C++ 静的解析ツール cppcheck

2013.8.23 (鈴)

28.1 はじめに

Cygwin の標準パッケージに C/C++ の静的解析ツール cppcheck があります。 Cygwin の setup.ini ファイルによれば:

This program tries to detect bugs that your C/C++ compiler doesn't see. The goal is no false positives.

Cppcheck is versatile. You can check non-standard code that includes various compiler extensions, inline assembly code, etc.

訳せば,

このプログラムはあなたの C/C++ コンパイラが見つけないバグを検出しようとします。 目標はフォールス・ポジティブ [偽陽性,問題ないのに問題ありと検査結果を出すこと] 一切なしです。

cppcheck は全方位的です。 様々なコンパイラ拡張やインライン・アセンブリ・コードなどを含む非標準的なコードを検査できます。

実際には Windows で cppcheck を使うとき Cygwin は必要ではありません。 cppcheck本家ページ [sourceforge.net] には Windows ネイティブのインストーラがあるほか,各種開発環境へのプラグインも紹介されています。 しかし,今 Cygwin を使っているならばその統合的に管理されたパッケージ・システムから試しにインストールしても損はありません。 とりわけ Cygwin の gcc の補助ツールとして使うつもりならばそうです。

その利点として gcc と同じように Cygwin のファイル・パスを解釈することが挙げられます。 ただし,それ以上なにか特別に gcc のために便宜を図っているわけではありません。 gcc の既定の include パスやシンボル定義を作り付けているわけではありません。 Cygwin の cppcheck パッケージは素のソースをそのままコンパイルしたバイナリです。 それでも §28.3 で説明するように簡易的に使うことはできますが,検査能力を発揮させるには §28.4 で説明するように gcc の内側に踏み込んだやや煩雑なオプション指定が必要です。

28.2 インストール

Cygwin のインストーラ (setup-x86_64.exe または setup-x86.exe) のパッケージ選択画面で Devel カテゴリの cppcheck を選択してインストールします。 (64 ビット版,32 ビット版ともに) 執筆時現在のバージョンは 1.60.1-1 でした。 8 月はじめに公開された本家ページの最新版 1.61 より少し後れています。

インストール後,構成ファイルとバージョンを確認してみます。 実質的には実行ファイル cppcheck.exe 1本とその man ページ cppcheck.1.gz だけです。

01:~$ zcat /etc/setup/cppcheck.lst.gz
usr/
usr/bin/
usr/bin/cppcheck.exe
usr/lib/
usr/share/
usr/share/doc/
usr/share/doc/cppcheck/
usr/share/doc/cppcheck/AUTHORS
usr/share/doc/cppcheck/ChangeLog
usr/share/doc/cppcheck/COPYING
usr/share/doc/cppcheck/README.md
usr/share/doc/Cygwin/
usr/share/doc/Cygwin/cppcheck.README
usr/share/man/
usr/share/man/man1/
usr/share/man/man1/cppcheck.1.gz
01:~$ cppcheck --version
Cppcheck 1.60.1
01:~$  

28.3 簡単な使用例

下記のようなファイル a.c を作ります。

#include <string.h>

void baz(char* bb);

void bar(char* bb)
{
    memset(bb, 0xFF, 100);
}

void foo()
{
    char a[99];
    bar(a);
    baz(a);
}

これをオプションなしで cppcheck にかけてみます。

01:~/tmp$ cppcheck a.c
Checking a.c...
[a.c:13] -> [a.c:7]: (error) Buffer is accessed out of bounds: a
01:~/tmp$  

[a.c:13]a.c の 13 行目を意味します。 確かめてみると,

01:~/tmp$ cat -n a.c
     1  #include <string.h>
     2
     3  void baz(char* bb);
     4
     5  void bar(char* bb)
     6  {
     7      memset(bb, 0xFF, 100);
     8  }
     9
    10  void foo()
    11  {
    12      char a[99];
    13      bar(a);
    14      baz(a);
    15  }
01:~/tmp$  

確かに 13 行目の bar(a) で呼び出された関数 barmemset (a.c の 7 行目) で配列 a の境界を超えたアクセスをしています。

これが本当にそう検査しているのかどうか,12 行目の配列の宣言を

    char a[100];

と変えてみます。

01:~/tmp$ cppcheck a.c
Checking a.c...
01:~/tmp$  

今度は何も言いません。

28.4 --enable=all オプション

cppcheck は選択可能な検査を --enable=id オプションで選ぶことができます。 指定できる id として all, style, performance, portability, information, unusedFunction, missingInclude があります。 all はすべての検査を指定します。 無指定時は,選択可能な検査はどれも選択されません。

つまり,前節のようなオプションなしの使い方は cppcheck の検査能力のすべてを出していたわけではありません。

前節の例を 12 行目を

    char a[99];

に戻して試してみます。

01:~/tmp$ cppcheck --enable=all a.c
Checking a.c...
[a.c:13] -> [a.c:7]: (error) Buffer is accessed out of bounds: a
Checking usage of global functions..
[a.c:10]: (style) The function 'foo' is never used.
(information) Cppcheck cannot find all the include files (use --check-config for details)
01:~/tmp$  

これまでの指摘に追加して,関数 foo が決して使われないと指摘されます。 また,include ファイルが見つけられないようです。 --check-config を与えてみます。

01:~/tmp$ cppcheck --enable=all a.c --check-config
Checking a.c...
[a.c:1]: (information) Include file: <string.h> not found.
Checking usage of global functions..
01:~/tmp$  

なんと,今まで <string.h> が読めていなかったようです。 include パスとして /usr/include を与えてみます。

01:~/tmp$ cppcheck --enable=all -I/usr/include a.c --check-config
Checking a.c...
[/usr/include/cygwin/config.h:42]: (information) Include file: "../tlsoffsets64.h" not found.
[/usr/include/cygwin/config.h:53]: (information) Include file: "../tlsoffsets.h" not found.
[/usr/include/sys/cdefs.h:44]: (information) Include file: <stddef.h> not found.
[/usr/include/sys/_types.h:72]: (information) Include file: <stddef.h> not found.
[/usr/include/sys/types.h:69]: (information) Include file: <stddef.h> not found.
[/usr/include/string.h:17]: (information) Include file: <stddef.h> not found.
Checking usage of global functions..
01:~/tmp$  

<stddef.h> のため,さらに /usr/lib/gcc/i686-pc-cygwin/4.7.3/include も追加してみます。

01:~/tmp$ cppcheck --enable=all -I/usr/include -I/usr/lib/gcc/i686-pc-cygwin/4.7.3/include a.c --check-config
Checking a.c...
[/usr/include/cygwin/config.h:42]: (information) Include file: "../tlsoffsets64.h" not found.
[/usr/include/cygwin/config.h:53]: (information) Include file: "../tlsoffsets.h" not found.
Checking usage of global functions..
01:~/tmp$  
この例では 32 ビット版の Cygiwn を使っていますから gcc のバージョンは 4.7.3 です。 64 ビット版では /usr/lib/gcc/x86_64-pc-cygwin/4.8.1/include にします。
gcc が実際にどのような include パスを使っているかは
01:~/tmp$ gcc -c -v a.c
のように -v オプションをつけてコンパイルすると表示されます。

まだ Cygwin 構築時の設定のために参照されるファイルが二つ見つからないようですが,それらは本来,普通のコンパイルには無関係のはずです。 ここで --check-config オプションを外してみます。

01:~/tmp$ cppcheck --enable=all -I/usr/include -I/usr/lib/gcc/i686-pc-cygwin/4.7.3/include a.c
Checking a.c...
[a.c]: (information) Too many #ifdef configurations - cppcheck only checks 12 of 128 configurations. Use --force to check all configurations.
Checking a.c: DEBUG...
Checking a.c: DEFS_H...
Checking a.c: LLONG_MAX...
Checking a.c: NDEBUG...
Checking a.c: NO_ANSI_KEYWORDS...
Checking a.c: _AM29K...
[a.c:13] -> [a.c:7]: (error) Buffer is accessed out of bounds: a
Checking a.c: _ANSI_H_...
Checking a.c: _BIG_ENDIAN;__TMS320C6X__...
Checking a.c: _CALL_SYSV;__PPC__...
Checking a.c: _COMPILING_NEWLIB...
Checking a.c: _COMPILING_NEWLIB;__x86_64__...
Checking usage of global functions..
[a.c:10]: (style) The function 'foo' is never used.
(information) Cppcheck cannot find all the include files (use --check-config for details)
01:~/tmp$  

コンパイル時の #ifdef の組み合わせが 12 通りまで試されます。 しかし,普通のコンパイルでは標準ヘッダファイルのあらゆる可能性をいちいち試すことは無駄です。 試しに -DDEBUG を与えて1通りだけに確定させてみます。

01:~/tmp$ cppcheck --enable=all -I/usr/include -I/usr/lib/gcc/i686-pc-cygwin/4.7.3/include -DDEBUG a.c
Checking a.c...
[/usr/include/machine/ieeefp.h:418]: (error) Endianess not declared!!
[/usr/include/machine/ieeefp.h:418]: (error) #error Endianess not declared!!
Checking usage of global functions..
01:~/tmp$  

リトルエンディアンかビッグエンディアンかを決定するアーキテクチャ情報がない模様です。 当該ヘッダファイルを読んでみると,とりあえず -D_WIN32 だけ与えればよさそうです。

01:~/tmp$ cppcheck --enable=all -I/usr/include -I/usr/lib/gcc/i686-pc-cygwin/4.7.3/include -D_WIN32 a.c
Checking a.c...
[a.c:13] -> [a.c:7]: (error) Buffer is accessed out of bounds: a
Checking usage of global functions..
[a.c:10]: (style) The function 'foo' is never used.
01:~/tmp$  

ヘッダファイルの読み込みで Cygwin 自身を構築するための分岐に入らなくなり,"cannot find all the include files" の指摘もされなくなりました。

結局,さしあたり,

--enable=all -I/usr/include -I/usr/lib/gcc/i686-pc-cygwin/4.7.3/include -D_WIN32

を与えるようにすればよさそうです。

これはあくまでさしあたりであって本来は gcc が既定で定義するシンボルに一致させるのが妥当です。 Cygwin パッケージならば include パスやシンボル定義は Cygwin の gcc にあわせて作り付けにしてくれてもよさそうなものですが面倒な話です。 しかし逆にいえば,汎用品として設定次第で他の環境のためのクロス開発にも使えるわけです。
ささやかな改善策として,いちいち -I オプションを与える代わりに include パスを1行ずつ書いたテキストファイルを用意し --includes-file=file のオプションでまとめて include パスを与えられます。 下記は incs.txt というテキストファイルを使った例です。
01:~/tmp$ cat incs.txt
/usr/include
/usr/lib/gcc/i686-pc-cygwin/4.7.3/include
01:~/tmp$ cppcheck --enable=all --includes-file=incs.txt -D_WIN32 a.c
Checking a.c...
[a.c:13] -> [a.c:7]: (error) Buffer is accessed out of bounds: a
Checking usage of global functions..
[a.c:10]: (style) The function 'foo' is never used.
01:~/tmp$  

28.5 --append=file オプション

前々節の例を a.c

void baz(char* bb);
void bar(char* bb);

void foo()
{
    char a[99];
    bar(a);
    baz(a);
}

b.c

#include <string.h>

void bar(char* bb)
{
    memset(bb, 0xFF, 100);
}

の二つのファイルに分割します。

このとき,普通に検査したのではバッファ・オーバーフローのエラーは検出されません。

01:~/tmp$ cppcheck --enable=all -I/usr/include -I/usr/lib/gcc/i686-pc-cygwin/4.7.3/include -D_WIN32 a.c
Checking a.c...
Checking usage of global functions..
[a.c:4]: (style) The function 'foo' is never used.
01:~/tmp$  

残念ながら検査対象をフォルダにしても同じです。

01:~/tmp$ cppcheck --enable=all -I/usr/include -I/usr/lib/gcc/i686-pc-cygwin/4.7.3/include -D_WIN32 .
Checking a.c...
1/2 files checked 57% done
Checking b.c...
2/2 files checked 100% done
Checking usage of global functions..
[a.c:4]: (style) The function 'foo' is never used.
01:~/tmp$  

しかし cppcheck はこうしたケースに対して無力ではありません。 妥当な呼び出しになっているか検査したい関数について,その実装が記述されたファイルを --append=file オプションで陽に与えれば,実装が同じファイルに書かれているときと同じように検査してくれます。

つまり,a.c での bar 関数の呼び出しの妥当性を検査するには --append=b.c を与えます。

01:~/tmp$ cppcheck --enable=all -I/usr/include -I/usr/lib/gcc/i686-pc-cygwin/4.7.3/include -D_WIN32 --append=b.c a.c
Checking a.c...
[a.c:7] -> [a.c:14]: (error) Buffer is accessed out of bounds: a
Checking usage of global functions..
[a.c:4]: (style) The function 'foo' is never used.
01:~/tmp$  

確かに検査できています。しかし b.c にあるはずの memset 関数の呼び出しがなぜか [a.c:14] と表示されています。 その種明かしは簡単です。

01:~/tmp$ cat -n a.c b.c
     1  void baz(char* bb);
     2  void bar(char* bb);
     3
     4  void foo()
     5  {
     6      char a[99];
     7      bar(a);
     8      baz(a);
     9  }
    10  #include <string.h>
    11
    12  void bar(char* bb)
    13  {
    14      memset(bb, 0xFF, 100);
    15  }
    16
01:~/tmp$  

つまり --append=file オプションとはその言葉のとおり file を検査対象ファイルに付加するだけのものです。

手抜きといえば手抜き,乱暴といえば乱暴な作りですが,当座の用には有用なオプションです。

28.6 おわりに

本章では C/C++ の静的解析ツール cppcheck を簡単に紹介しました。 ところどころ荒削りなところが垣間見えますが,プログラムのあらを取る補助的なツールとして有用です。 実際に使うときは cppcheck コマンドを素で使うのではなく,長々しいオプションをあらかじめ組み入れたスクリプトやエイリアスを用意すると実用的でしょう。




Copyright (c) 2013 OKI Software Co., Ltd.