目次
cppcheckという静的解析ツールを試してみました。
cppcheckはC/C++言語向け静的解析ツールですが、今回メモリリーク検出を目的として試してみました。
ビルド~インストールや試してみた結果について書いておきたいと思います。
背景
C言語やC++でコードを書いていると否が応でもメモリリークの危険性と戦う必要があります。
リークをなくすためには、
- 動的メモリ確保を行わない
- メモリリークがないかしっかり試験する
の2つのアプローチがあります。
私が普段仕事をしている組み込みソフトの開発では1の「動的メモリ確保を行わない」というアプローチは一般的です。
そもそも組み込み開発の場合、OSがない=メモリ管理の機構(malloc,free)が存在しないというのは一般的です。
リアルタイムOSのitronを使う場合、固定長メモリプールや可変長メモリプールのAPIは利用できますが、メモリリークを嫌って使わないことも多いです。
最近では組み込みソフトの開発にLinuxを使われることが多く、さすがにその場合は「動的メモリ確保を行わない」というアプローチを取らなかったりします。
メモリリークというリスクを考慮すると、使わないで済むのであれば使わないに越したことはないと思いますが、Linuxを使うくらいですから扱うデータの規模もそれなりに大きかったりするので、スタックで確保するには大きすぎたり、静的変数で確保には使用効率が悪かったりします。
動的メモリ確保(malloc~free, new, delete)を使うとなると2の「メモリリークがないかしっかり試験する」が重要になってくるのですが、人手で試験をするのは手間も時間をかかりますので現実的とは言えません。
できる限り自動化したいところです。
メモリリークの検出にはいろいろなツールがあり、私も過去にvalgrindというツールを試してみたことがあります。
valgrindは非常に便利なツールなのですが、ビルド済みのバイナリ(デバッグビルド)を使ってメモリリークのチェックが行われるため、実は組み込み開発の実機では気軽に使えない(valgrind自体のクロスコンパイルが必要)という事情があります。
※組み込みLinuxの場合は開発中はセルフコンパイルで動作を確認したりするので、valgrindを使うメリットは十分享受できるのですが。
cppcheckを試してみる
いろいろ調べているとcppcheckという静的解析ツールでメモリリークも検知できるということを知り、今回ビルド・インストールして試してみました。
ビルド
こちらのサイトからソース一式をダウンロードします。
$ wget https://github.com/danmar/cppcheck/archive/1.80.zip
$ unzip unzip 1.80.zip
$ cd cppcheck-1.80
ビルド方法については readme.txt に簡単に説明があります。
※cppcheckはいろいろな環境に対応しており、Windows環境であればVisualStudioでビルドできるほか、cmakeを使ったビルドもできるそうです。
私の場合、手元のLinux(Mint)で試しました。恐らくUbuntuでも同じようにビルドできると思います。
$ uname -a
Linux mint 4.4.0-21-generic #37-Ubuntu SMP Mon Apr 18 18:33:37 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
ビルド時の注意点
ビルド時の注意点が1点あります。
cppcheckは静的解析ツールなので、チェックするルールを.cfgというファイルで指定できるのですが、このファイルの配置場所をビルド時に指定しておく必要があります。
指定方法は
ちなみに指定しないでmake;make install してcppcheckを動かすと、
cppcheck: Failed to load library configuration file 'std.cfg'. File not found
(information) Failed to load std.cfg. Your Cppcheck installation is broken, please re-install. The Cppcheck binary was compiled without CFGDIR set. Either the std.cfg should be available in cfg or the CFGDIR should be configured.
というメッセージが表示されてソースコードのチェックが正しくできません。
正しく解析するためには、makeする際に、 CFGDIR=cfgファイルのパス としてcfgの配置ディレクトリを指定する必要があります。
なので、makeの実行は以下のようなコマンドを実行しました。
$ make CFGDIR=/usr/local/cppcheck/cfg HAVE_RULES=yes
※readme.txt を読んでもHAVE_RULESの説明は詳しく書かれていませんでしたがとりあえずつけてみました。
ちなみにHAVE_RULES=yesを指定する場合は、依存するライブラリとしてpcreが必要になるため、インストールが必要です。
$ sudo apt install libpcre3-dev
cfgファイルは解凍したフォルダ内のcfgディレクトリ以下にいろいろなcfgファイルが入っています。
デフォルトでは、std.cfgが解析に使用されるようです。
インストール時に指定した/usr/local/cppcheck/cfg というディレクトリを作成し、これらのファイルをコピーします。
ビルドから、上記手順までのコマンドは、
$ make CFGDIR=/usr/local/cppcheck/cfg HAVE_RULES=yes
$ sudo make install;
$ sudo mkdir -p /usr/local/cppcheck/cfg
$ sudo cp ./cfg/* /usr/local/cppcheck/cfg
のような感じになります。
これでcppcheckを使う準備が整いました。
この時点で、cppcheck自体は /usr/bin にインストールされています。
cppcheckを動かしてみる
早速メモリリークの検出を試してみます。
今回、以下のようなコードでどんな感じにメモリリークが検出されるか試してみました。
#include <stdio.h> #include <stdlib.h> // ============================================================================= // define 定義 // ============================================================================= #define TEST_BUF_SIZE_MAX (16) // ============================================================================= // プロトタイプ宣言 // ============================================================================= void test_stack_overflow(void); // スタックオーバーフロー(直値・ループ) void test_stack_overflow_with_idx(int idx); // スタックオーバーフロー(変数アクセス) void test_heap_access(void); // メモリリーク(free漏れ) void test_mem_double_free(void); // メモリ2重解放 void test_buf_over_run(unsigned char *buf, int len); // バッファオーバーラン // ============================================================================= // static 変数 // ============================================================================= unsigned char s_test_buf[TEST_BUF_SIZE_MAX] = { 0 }; int main(int argc, char **argv) { // ===================================================== // スタックオーバーフロー // ===================================================== test_stack_overflow(); test_stack_overflow_with_idx(0); test_stack_overflow_with_idx(16); // ===================================================== // バッファオーバーラン // ===================================================== test_buf_over_run(s_test_buf, 4); test_buf_over_run(s_test_buf, 256); // ===================================================== // メモリリーク // ===================================================== test_heap_access(); // ===================================================== // 2重解放 // ===================================================== test_mem_double_free(); return 0; } // 直接・ローカル変数によるループ中での配列アクセス void test_stack_overflow(void) { unsigned char buf[TEST_BUF_SIZE_MAX]; int i; // インデックスアクセス:直値 buf[0] = 0x00; buf[15] = 0x00; buf[16] = 0x00; buf[-1] = 0x00; // ループ変数アクセス for (i = 0; i < 17; i++) { buf[i] = 0x00; } return; } // 変数による配列へのアクセス // 検出するとしたら呼び出し元で検出されるはず void test_stack_overflow_with_idx(int idx) { unsigned char buf[TEST_BUF_SIZE_MAX]; buf[idx] = 0x00; return; } // free漏れテスト void test_heap_access(void) { char *p; // 確保して解放しない p = malloc(1024); return; } // freeによる2重解放テスト void test_mem_double_free(void) { char *p; p = malloc(1024); // 2重解放 free(p); free(p); return; } // バッファオーバーラン void test_buf_over_run(unsigned char *buf, int len) { int i; // 引数で指定されたサイズ分だけループする // →呼び出し側でlenを間違えた場合ここではなく呼び出し側でエラーがでる? for (i = 0; i < len; i++) { buf[i] = i; } // 引数で指定されたサイズ分だけループする // →呼び出し側でlenを間違えた場合ここではなく呼び出し側でエラーがでる? for (i = 0; i < len; i++) { buf[i] = i; } return; }
cppcheckのシンプルな使い方として、引数にフォルダパスをしてするとそのパス内のファイルの解析をしてくれるようです。
今回は上記のコードを main.c として保存して試してみました。
$cppcheck .
出力は以下のような感じになります。
Checking main.c ...
[main.c:61]: (error) Array 'buf[16]' accessed at index 16, which is out of bounds.
[main.c:66]: (error) Array 'buf[16]' accessed at index 16, which is out of bounds.
[main.c:77]: (error) Array 'buf[16]' accessed at index 16, which is out of bounds.
[main.c:62]: (error) Array index -1 is out of bounds.
[main.c:88]: (error) Memory leak: p
[main.c:99]: (error) Memory pointed to by 'p' is freed twice.
[main.c:99]: (error) Deallocating a deallocated pointer: p
検出できたリークパターン
上記のコードはぱっと思いつくメモリリークのパターンをいくつか起してみたコードになりますがいい感じに検出してくれています。
検出内容として、
- 配列の添え字がおかしい場合(直値の場合)
- 配列の添え字がおかしい場合(マイナスの直値の場合)
- 配列の添え字がおかしい場合(ループ変数の場合)
- 配列の添え字がおかしい場合(引数の場合)
- mallocのfree忘れ
- 2重解放(freeしすぎ)
の6パターン検出しています。
なかなか優秀ですね。
検出できなかったリークパターン
上記のコードで検出できていないのは、 test_buf_over_run 関数の呼び出し、
test_buf_over_run(s_test_buf, 256);
のケースです。
16byteで確保しているバッファに対し、256byte分の書き込みを行うというリークですが、さすがにこのパターンは検出できていません。
manualを読んでみた。
cppcheckには簡単なマニュアルがついています。
http://cppcheck.sourceforge.net/manual.pdf
一通り読んでみましたが思っていたより高機能のようで驚きました。
面白いと思ったのは、ソースコードではなくライブラリとして提供される関数のチェックをしたい場合、チェックしたい関数について設定ファイルに情報を書いて置くことでその関数も設定に従ってチェック対象になるそうです。そういった面では、拡張性を意識して作ってあるようですね。