トップC-Tips > lint(ソースコード静的解析ツール)

lint(ソースコード静的解析ツール)

1.lint(ソースコード静的解析ツール)

lintの存在は知っていたが、これまで使った記憶はない。 今回、ポータビリティ・チェックのためにちょっと使ってみたい。 UNIX環境ならば、何もしなくても lintコマンドが使えるかも知れないが、 Windowsパソコンではなさそう。

大きなプログラムをチェックするわけではないので、なるべく手軽な方がいい。 ネットをちょっと検索した限りでは、見つからない。 インストール済みのMinGWのbinにもそれらしいものは見つからない。 一からMinGWをインストールすればあるかも知れない。

2.splint

splint(Secure Programming Lint) を Win32 Installationページに従ってインストールしてみよう。

まず、splint-3.1.1.win32.zipをダウンロードして、c:\splint-3.1.1 に展開した。 多くのツールをインストールする場合には、どこか一つの親ディレクトリにまとめる方がいいだろう。

環境変数として PATH に c:\splint-3.1.1\bin を追加した。更に LARCH_PATH − .;c:\splint-3.1.1\lib および LCLIMPORTDIR − c:\splint-3.1.1\imports を新規登録した。 INCLUDE はすでに設定されていた。

以上、インストール自体は非常に簡単で即座に終わった。早速、動かしてみると、 下のように、ヘッダファイルが引っかかってしまった。

このエラーをネット検索してみた。 いくつかページが見つかったが、簡単な対策は見つからない。 少し、腰を据えて splint の使い方を調べる必要がある。

c:\sys>splint  mcxx\mc00.c
Splint 3.1.1 --- 12 April 2003

C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\INCLUDE\crtdefs.h(478,27)

    : Parse Error: Suspect missing struct or union keyword: __int64 :
    long int. (For help on parse errors, see splint -help parseerrors.)
*** Cannot continue.

splintでググってみると、このページが見つかった。 このページによると、とりあえずは次のオプションを使えば動きそうだ。

c:\sys>splint +skip-sys-headers +single-include +posixlib mcxx\mc00.c

個々のオプションの意味は後ほど調べるとして、これでソースファイル mcxx\mc00.c のチェックが行えるようになった。

まず、次の警告が目についた。

mcxx/lib0.c: (in function isJp)
mcxx/lib0.c(8,25): Return value type boolean does not match declared type int:
                      (c >= 0x81 && c <= 0x9F) || (c >= 0xE0 && c <= 0xFC)
  To make bool and int types equivalent, use +boolint.

mcxx/lib0.c(54,23): Incompatible types for + (int, char): 20000 + *dt
  To make char and int types equivalent, use +charint.

+boolint、+charint でこの二つの警告は抑制できるが、それがベストだろうか? windows.h をインクルードすれば、BOOL型、TRUE、FALSE が使えるが、これは

typedef int BOOL;
#define FALSE 0
#define TRUE 1
と定義されているため、実質的には int 型に過ぎない。

オプションをコマンドパラメータで指定するのは面倒である。

そこで、 このページに従って、 以下29行をC:\splint-3.1.1\にsplintrc.txtという名前で保存した。

;====================
; * mode
;====================
-weak               ;   ゆるい
;-standard           ;   ややゆるい
;-checks             ;   ややきびしい
;-strict             ;   きびしい
 
;====================
; * include directory
;====================
-I.

;「C:\splint-3.1.1\」以外にインストールしたときは、下記2行のコメントを外してパスを指定する
;+larchpath    C:/PROGRA~1/splint-3.1.1/lib
;+lclimportdir C:/PROGRA~1/splint-3.1.1/imports

;====================
; * option (format)
;====================
-linelen 9999       ;   なるべく折り返さない
+forcehints         ;   毎回ヒントを出力する(デフォルトは初回のみ)
-showcolumn         ;   エラー位置は行数のみ出力(桁数を出力しない)

;+showsummary        ;   エラーの数一覧も出力

;====================
; * option (to ignore)
;====================

2.簡単な例題で使ってみよう。

小さなプログラムで少し使い慣れよう。モードは -weak ではなく -standard にしておく。 下のような簡単なプログラムでは オプション +skip-sys-headers +single-include +posixlib はなくてもいいようだ。

#include <stdio.h>

int main() {
    int data[10];
    printf("%d\n", sizeof(data)/sizeof(int));
    return 0;
}

このプログラムに対しては、下の警告が出た。

c:\sys>splint ptest\test5.c
Splint 3.1.1 --- 12 April 2003

ptest\test5.c: (in function main)
ptest\test5.c(6,26): Format argument 1 to printf (%d) expects int gets
    arbitrary unsigned integral type: sizeof((data)) / sizeof(int)
  To allow arbitrary integral types to match any integral type, use
  +matchanyintegral.
   ptest\test5.c(6,14): Corresponding format code

Finished checking --- 1 code warning

次のようにすれば、警告は出ないが、+matchanyintegral を追加して、 この警告を抑止することでいいだろう。

    printf("%d\n", (int)(sizeof(data)/sizeof(int)));
    printf("%u\n", (unsigned int)(sizeof(data)/sizeof(int)));
c:\sys>splint  +f C:/splint-3.1.1/splintrc.txt ptest/test5.c
Splint 3.1.1 --- 12 April 2003

Finished checking --- no warnings

通常、カレント・ディレクトリを c:\sys にするならば、splintrc.txt を c:\sys におけば ファイルの指定が次のように楽になる。

c:\sys>splint  +f splintrc.txt ptest/test5.c
Splint 3.1.1 --- 12 April 2003

Finished checking --- no warnings
bash-3.1$ splint +f splintrc.txt ptest/test5.c
Splint 3.1.1 --- 12 April 2003

Finished checking --- no warnings

3.例題: ハッシュ表

splintに触れて日が浅いため、適切な使い方がまだ分かっていない。 それを探るために、ハッシュ表プログラムを書いてみた。

決して分かりやすいプログラムではないが、警告が出ないようにするため、 hgetx関数にメモリ管理まで押し込んだ。

メモリ管理は外にだすべきであるが、その方法が分からなかった。 実用的なプログラムにはかけ離れているが、学習の足跡を残すことと、 この先で参考にする部分も少しはあると考え、記録として残す。

文字列の管理だけであるが、同時に複数のハッシュ表を管理している。 ただし、文字列自体は一元管理である。サイズをチェックして、足りなくなったら、拡張する予定であるが、 その部分はまだ実装していない。

// lib.h
typedef struct { 
    int key;    // text配列の添え字
    int sqn;	// sequence number(通し番号)
} Data;

typedef struct {
    int   size, nData;        // サイズ、登録数
    Data  data[0];
} Hash;

int hgetx(char *str, int id);
/*@dependent@*/ char *hgets(int ix);

// lib.c
#include <stdio.h>
#include <stdlib.h>
#include "lib.h"

#define InitialHashSize 3
//int   sizeText;               // サイズ
static int   nText = 1;         // 次の文字列登録位置
/*@only@*/static char  *text;//[10000];

static int hash(char *str, int size) {                 // 内部関数
    int n = 0, h = 0;
    while (str[n] != '\0') {
        h = (h * 137 + (int)(str[n++]&0xff)) % size;
    }
    return h;
}

static int hput0(char *str, Hash *pH) { 
    int n, h = hash(str, pH->size);
    for (n = 0; n < pH->size; n++) {
        int ix = (h + n) % pH->size;
        if (pH->data[ix].key == 0) {
            pH->data[ix].key = nText;
            pH->data[ix].sqn = pH->nData++;
	    if (text == NULL) text = calloc(1000, sizeof(char));
	    if (text == NULL) exit(EXIT_FAILURE);
            strncpy(&text[nText], str, strlen(str)+1);  // '\0'を含めてコピー
            nText += strlen(str) + 1;
            return ix;                // 新規登録
        } else if (strcmp(&text[pH->data[ix].key], str) == 0) {
            return ix;                // 登録済み
        }
    }
    return -1;
}
 
static int getIx(char *str, Hash *pH) { 
    int n, h = hash(str, pH->size);
    for (n = 0; n < pH->size; n++) {
        int ix = (h + n) % pH->size;
        if (pH->data[ix].key == 0) {
	    pH->nData++;
            return ix;                // 新規登録
        } else if (strcmp(&text[pH->data[ix].key], str) == 0) {
            return ix;
        }
    }
    return -1;
}
 
int hgetx(char *str, int id) {
    /*@only@*/static Hash *pH[10];
    if (pH[id] == NULL) {
        pH[id] = calloc(1, sizeof(Hash) + InitialHashSize*sizeof(Data));
        if (pH[id] == NULL) { 
            fprintf(stderr, "hgetx: alloc error\n");
            exit(EXIT_FAILURE);
        }
        pH[id]->size = InitialHashSize; // 初期サイズ
    } else if (pH[id]->nData * 2 > pH[id]->size) {
        int n;
        Hash *pNew = calloc(1, sizeof(Hash) + pH[id]->size * 2 * sizeof(Data));
        if (pNew == NULL) {
            fprintf(stderr, "hgetx: realloc error\n");
            exit(EXIT_FAILURE);
        }
        pNew->size = pH[id]->size * 2;  // 新しいサイズ
        for (n = 0; n < pH[id]->size; n++) {
	    int key = pH[id]->data[n].key;
            if (key > 0) {
                int ixOld = getIx(&text[key], pH[id]);
                int ixNew = getIx(&text[key], pNew);
                pNew->data[ixNew].key = key;
                pNew->data[ixNew].sqn = pH[id]->data[ixOld].sqn;
            }   // 格納場所が変わるのみで、key, sqn は引き継ぐ
        }
        free(pH[id]);
        pH[id] = pNew;
    }
    if (str == NULL) { 
        free(pH[id]); 
        pH[id] = NULL;
        return -1;
    } else {
        int ix = hput0(str, pH[id]);
        return ix >= 0 ? pH[id]->data[ix].key : ix;
    }
}

/*@dependent@*/char *hgets(int ix) { 
    return text+ix; 
}

// libtest.c
#include <stdio.h>
#include "lib.h"

int main() {
    printf("ix=%d %s\n", hgetx("abc", 0), hgets(hgetx("abc", 0)));
    printf("ix=%d\n", hgetx("12345", 0));
    printf("ix=%d\n", hgetx("abc", 0));
    printf("ix=%d\n", hgetx("abcd", 0));
    printf("ix=%d\n", hgetx("abced", 0));
    printf("ix=%d\n", hgetx("abcxx", 0));
    printf("ix=%d\n", hgetx("abc1", 0));
    printf("ix=%d\n", hgetx("abcd1", 0));
    printf("ix=%d\n", hgetx("abced1", 0));
    printf("ix=%d\n", hgetx("abcxx1", 0));
    return 0;
}

splintでのチェック結果およびMSVC(cl.exe)でのコンパイル並びに実行結果を下に示す。

c:\sys>splint mc\libtest.c mc\lib.c
Splint 3.1.1 --- 12 April 2003

Finished checking --- no warnings

c:\sys>cl -nologo mc\libtest.c mc\lib.c
libtest.c
lib.c
コードを生成中...

c:\sys>libtest
ix=1 abc
ix=5
ix=1
ix=11
ix=16
ix=22
ix=28
ix=33
ix=39
ix=46

今日、マニュアルを読んで、少し分かったのは、メモリ管理を外に出すには 次のようにすればいいのかも知れない。

メモリ獲得に失敗した場合、そこで exit するならば、/*@null@*/ はいらないのかも知れない。

/*@only@*/ /*@null@*/ Hash *halloc(size_t size);
/*@only@*/ /*@null@*/ Hash *hrealloc(/*@only@*/ /*@null@*/ /*@out@*/ Hash *Hash, size_t size);
void hfree(/*@only@*/ /*@out@*/ /*@null@*/ Hash *pHash);

outアノテーションは、割り当てられたメモリに未使用な部分があるかも知れないことを表すようだ。

まだ、よく理解できていないが、とりあえず、以上の憶測で調べてみよう。

下のプログラムはデフォルトモード(-standard)で警告が出なかった。

#include <stdio.h>
#include <stdlib.h>

typedef struct { 
    int key;    // text配列の添え字
    int sqn;	// sequence number(通し番号)
} Data;

typedef struct {
    int   size, nData;        // サイズ、登録数
    Data  data[0];
} Hash;

/*@only@*/ /*@null@*/ static Hash *halloc(size_t size) 
{
    return (Hash *)calloc(1, sizeof(Hash) + size * sizeof(Data));
}

/*@only@*/ /*@null@*/ static Hash *hrealloc(/*@only@*/ /*@null@*/ /*@out@*/ Hash *pHash, size_t size)
{
    Hash *pNew = calloc(1, sizeof(Hash) + size * sizeof(Data));
    // pHash のデータをpNewにコピーする
    free(pHash);
    return pNew;
}

void static hfree(/*@only@*/ /*@out@*/ /*@null@*/ Hash *pHash)
{
    free(pHash);
}

int main() {
    Hash *pHash = halloc(100);
    pHash = hrealloc(pHash, 200);
    hfree(pHash);
    return 0;
}

参考文献

[1] Splint Manual(Version 3.1.1)
[2] Splint翻訳